4
\$\begingroup\$

I am learning and trying to implement the best practice for snake and ladder game.

Rules of the game

  1. The board will have 100 cells numbered from 1 to 100.
  2. The game will have a six sided dice numbered from 1 to 6 and will always give a random number on rolling it.
  3. Each player has a piece which is initially kept outside the board (i.e., at position 0).
  4. Each player rolls the dice when their turn comes. Based on the dice value, the player moves their piece forward that number of cells. Ex: If the dice value is 5 and the piece is at position 21, the player will put their piece at position 26 now (21+5).
  5. A player wins if it exactly reaches the position 100 and the game ends there.
  6. After the dice roll, if a piece is supposed to move outside position 100, it does not move.
  7. The board also contains some snakes and ladders. Each snake will have its head at some number and its tail at a smaller number. Whenever a piece ends up at a position with the head of the snake, the piece should go down to the position of the tail of that snake.
  8. Each ladder will have its start position at some number and end position at a larger number. Whenever a piece ends up at a position with the start of the ladder, the piece should go up to the position of the end of that ladder.
  9. There could be another snake/ladder at the tail of the snake or the end position of the ladder and the piece should go up/down accordingly.

Assumptions

  • There won’t be a snake at 100.
  • There won’t be multiple snakes/ladders at the same start/head point.
  • It is possible to reach 100, i.e., it is possible to win the game.
  • Snakes and Ladders do not form an infinite loop.
class Snake:
    def __init__(self, start, end):
        self.start = start
        self.end = end
class Ladder:
    def __init__(self, start, end):
        self.start = start
        self.end = end
class Board:
    def __init__(self, size = 100):
        self.size = size
        self.snake_list = []
        self.ladder_list = []

    def add_snake(self, snake):
        self.snake_list.append(snake)

    def add_ladder(self, ladder):
        self.snake_list.append(ladder)
class Dice:
    dice_count = 1
    @staticmethod
    def roll():
        return random.randint(1 * Dice.dice_count, 6 * Dice.dice_count)
class Player:
    def __init__(self, name):
        self.name = name
class PlayerPosition:
    def __init__(self, player, position):
        self.player = player
        self.position = position

    def update_position(self, new_position):
        self.position = new_position
class Game:
    def __init__(self, board):
        self.board = board
        self.players_position = []

    def add_players(self, player, position=0):
        player_position = PlayerPosition(player, position)
        self.players_position.append(player_position)


    def check_win_condition(self, position):
        if position == self.board.size:
            return True
        return False

    def check_for_snake(self, new_position):
        for snake in self.board.snake_list:
            start, end = snake.start, snake.end
            if start == new_position:
                return end

    def check_for_ladder(self, new_position):
        for ladder in self.board.ladder_list:
            start, end = ladder.start, ladder.end
            if start == new_position:
                return end

    def find_new_position(self, new_position):
        if self.check_for_snake(new_position):
            return self.check_for_snake(new_position)
        elif self.check_for_ladder(new_position):
            return self.check_for_ladder(new_position)
        else:
            return new_position


    def start(self):
        still_playing = len(self.players_position)
        while(still_playing):
            for player_position in self.players_position:
                dice_value = Dice.roll()
                current_position = player_position.position
                new_position = current_position + dice_value

                if new_position < self.board.size:
                    new_position = self.find_new_position(new_position)
                    player_position.update_position(new_position)
                    print(player_position.player.name, 'moved from', current_position, 'to', new_position)

                if self.check_win_condition(new_position):
                    print("player", player_position.player.name, "wins!")
                    player_position.update_position(new_position + 1)
                    still_playing -= 1
class GameRunner:
    @classmethod
    def run_game(cls):
        board = Board()
        s1 = Snake(62, 5)
        s2 = Snake(33, 6)
        s3 = Snake(49, 9)
        s4 = Snake(56, 53)
        s5 = Snake(98, 64)
        s6 = Snake(88, 16)
        s7 = Snake(93, 73)
        s8 = Snake(95, 75)

        l1 = Ladder(2,37)
        l2 = Ladder(27, 46)
        l3 = Ladder(10, 32)
        l4 = Ladder(51, 68)
        l5 = Ladder(61, 79)
        l6 = Ladder(65, 84)
        l7 = Ladder(71, 91)
        l8 = Ladder(81, 100)

        board = Board()
        board.add_ladder(l1)
        board.add_ladder(l2)
        board.add_ladder(l3)
        board.add_ladder(l4)
        board.add_ladder(l5)
        board.add_ladder(l6)
        board.add_ladder(l7)
        board.add_ladder(l8)

        board.add_snake(s1)
        board.add_snake(s2)
        board.add_snake(s3)
        board.add_snake(s4)
        board.add_snake(s5)
        board.add_snake(s6)
        board.add_snake(s7)
        board.add_snake(s8)

        player1 = Player("python")
        player2 = Player("java")
        player3 = Player("go")

        game = Game(board)

        game.add_players(player1)
        game.add_players(player2)
        game.add_players(player3)

        game.start()

GameRunner.run_game()

Please suggest places of improvements and corrections on this.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Unnecessary classes

The snake, ladder and dice classes are not at all useful. They can simply be replaced with a namedtuple or a dataclass.

Similarly, player and playerposition should both be a single class element. A player object should be responsible for keeping track of their position.

Verbosity

s1, s2, ... s8 and similary l1, l2, ... l8 are not really used. Keep a tuple of positions, and iterate over them, calling either add_ladder or add_snake accordingly.

Control flow

The position updates should happen at the player's end, and not the game. The print statement for when player position gets updated would happen inside player class. The game's object is only to control and validate moves.

There are no statements showing when a player encounters a ladder or a snake. just their position changes in that verbose print statement mesh.

Double execution

    if self.check_for_snake(new_position):
        return self.check_for_snake(new_position)
    elif self.check_for_ladder(new_position):
        return self.check_for_ladder(new_position)
    else:
        return new_position

You have the same function being called twice, twice. For a major set of the board cells, there is neither a snake, nor a ladder. Yet, you keep calling both the functions.

Alternatives

Dice roll

sum(random.choices(range(1, 7), k=dice_count))

Win condition

def check_win_condition(self, position):
    return position == self.board.size

Snake head or ladder base check

Keep an account using a set or tuple for board's snakes and ladder being added. Board should validate if a position has either snake's head or ladder's base there. You are currently iterating over all the snakes and ladders (twice, as mentioned above) for each position, whereas a lookup would be \$ O(1) \$.

\$\endgroup\$
1
  • \$\begingroup\$ Just too many irrelevant classes \$\endgroup\$ Oct 16, 2020 at 11:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.