3
\$\begingroup\$

Simulate a tic-tac-toe game

Requirements

  • A player is picked at random as a first player.
  • The two players plays tic tac toe game.
  • The game ends when the board is full or either one of the player wins.
  • Return the board status and result after end of the game.

Player Class

class Player:
    def __init__(self, name, symbol):
        self.name = name
        self.symbol = symbol

    def pick_available_grid(self, available_position):
        random_index = random.choice(available_position.keys())
        return random_index

Board Class

class Board:
    def __init__(self):
        self.grid = [None]*9
        self.available_position = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}

    def update_board(self, index, symbol):
        self.grid[index-1] = symbol
        del self.available_position[index]

    def check_if_board_full(self):
        len(self.available_position) == 0

    def check_for_win_condition(self,symbol):
        if (self.grid[0]==symbol and self.grid[1]==symbol and self.grid[2]==symbol):
            return True
        if (self.grid[3]==symbol and self.grid[4]==symbol and self.grid[5]==symbol):
            return True
        if (self.grid[6]==symbol and self.grid[7]==symbol and self.grid[8]==symbol):
            return True

        #vertical grid check
        if (self.grid[0]==symbol and self.grid[3]==symbol and self.grid[6]==symbol):
            return True
        if (self.grid[1]==symbol and self.grid[4]==symbol and self.grid[7]==symbol):
            return True
        if (self.grid[2]==symbol and self.grid[5]==symbol and self.grid[8]==symbol):
            return True

        #diagonal grid check
        if (self.grid[0]==symbol and self.grid[4]==symbol and self.grid[8]==symbol):
            return True
        if (self.grid[2]==symbol and self.grid[4]==symbol and self.grid[6]==symbol):
            return True

Game Class

class Game:
    global SYMBOL
    SYMBOL = ['X', 'O']

    def __init__(self):
        self.board = Board()
        self.players = []
        self.result = None

    def set_players(self):
        player1 = Player('akanksha', SYMBOL[0])
        player2 = Player('akanksha', SYMBOL[1])
        self.players.append(player1)
        self.players.append(player2)

    def set_current_player(self, current_player=None):
        current_player = self.players[1] if current_player == self.players[0] else self.players[0]
        return current_player

    def set_result(self):
        if self.board.check_for_win_condition(self.players[0].symbol):
            self.result = self.players[0]
        elif self.board.check_for_win_condition(self.players[1].symbol):
            self.result = self.players[1]
        elif self.board.check_if_board_full():
            self.result = 'Draw'
        return self.result


    def play_game(self):
        current_player = self.set_current_player()
        while(not self.set_result()):
            available_position = self.board.available_position
            grid_position = current_player.pick_available_grid(available_position)
            self.board.update_board(grid_position, current_player.symbol)
            current_player = self.set_current_player(current_player)

    def end_game(self):
        print(self.board.grid)
        if self.result == 'Draw':
            print('This game is a draw')
        else:
            print('player', self.result.name, "with symbol", self.result.symbol, "wins")

    def start_game(self):
        self.set_players()
        self.play_game()
        self.set_result()
        self.end_game()

Main Program

class MainProgram:
    def main():
        print("Welcome to TicTacToe Game")
        game = Game()
        game.start_game()

    main()

I am new to oops, how can I improve this?

Questions:

  • Should I create a separate class for symbol?
  • Should check_for_win_condition be included in Game instead of Board class?
  • Should self.available_position be used or it should be method which computes available cells from grid
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

Honestly, for the symbol, I'd just go with using uppercase characters (X and O) since the pieces don't really have any special functionality at this stage, and it's good to keep some simplicity with earlier projects.

I'd put the win condition checking into the game class. If you want to reuse the board class in the future, it's a good idea for you to avoid having any implementation-specific code (i.e. functionality that's specific to Tic-Tac-Toe) in the class - when you're dealing with Object Orientied Programming, you have to consider SOLID principles.

In terms of self.available_positions, I'd forego it entirely. You already have a structure which keeps track of the board, so adding a second structure to keep track of available positions is largely redundant - because one is just the inverse of the other (i.e. self.available_positions are the null elements in self.grid), and duplication is wasting memory and can lead to error (it's also good practice to have a single source of truth).

In terms of code, it's a good start, but I have a few suggested modifications for the Board class:

class Board:
    def __init__(self):
        self.grid = {1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: }
        # Board starts with 9 available positions, and we'll decrement it by one.
        # I've changed to this from the self.available_position dictionary because you were
        # essentially maintaining two structures to maintain the progress of the game, 
        # and having a single integer that we decrement is easier.
        self.remaining_turns = 9

    def update_board(self, index, symbol):
        self.grid[index-1] = symbol
        # Decrease the number of available turns after placing a piece.
        self.remaining_turns -= 1
        # Probably worth checking for 0 remaining turns after performing this decrement.    

    def check_for_win_condition(self,symbol):
        # This if statement will be entered if any of the functions return true

        if check_diagonal(symbol) or check_horizontal(symbol) or check_vertical(symbol):
            return True

    def check_horizontal(self, symbol):
        # 0,3,6 refer to the starting elements in your vertical grid check, the additions
        # refer to the offsets to get to the other elements in the line
        for element in [0, 3, 6]:
            if self.grid[element]==self.grid[element+1]==self.grid[element+2]==symbol:
                return True

    def check_vertical(self, symbol):
        # 0,1,2 refer to the starting elements in your vertical grid check, the additions
        # refer to the offsets to get to the other elements in the line
        for element in [0, 1, 2]:
            if self.grid[element]==self.grid[element+3]==self.grid[element+6]==symbol:
                return True

    def check_diagonal(self, symbol):
        # You can chain equality checks
        if (self.grid[0]==self.grid[4]==self.grid[8]==symbol) or (self.grid[2]==self.grid[4]==self.grid[6]==symbol):
            return True

It's just something I typed up quickly, and I haven't had the opportunity to test it yet. My biggest issue was with the check_for_win_condition function - you were repeating a lot of the same checks (just altering the element reference for the grid).

In terms of language choice; Python is great - it's versatile, and it's intuitive - however, go with Python 3. Python 2 is end of life in 2020 - which means it will no longer have support from the Python developers/maintainers, but Python 3 is in active development, and it has a number of improvements and new features.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks for answering this. I need a clarification on one point: self.grid = {1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: } How are players going to find the available_position. Is it by looping through grid and picking up the keys which have empty string as value and then picking a random one among them. If it so then is it not better to have a available_position, it is a trade-off between time and space. \$\endgroup\$
    – Akanksha
    Oct 7, 2019 at 7:07
  • \$\begingroup\$ Basically, when the user says where they want to place their piece, you can run a small function to check whether the element at that index is an empty string (i.e. if self.grid == '': you can place the piece). If the element at the specified index isn’t an empty string, someone has already placed a piece there, so you should prompt the player to pick another place. \$\endgroup\$
    – OngGab
    Oct 7, 2019 at 11:29
  • \$\begingroup\$ Instead of initializing grid as dict won't list be a better data structure? \$\endgroup\$
    – Akanksha
    Oct 15, 2020 at 19:35

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.