0
\$\begingroup\$

I have built a project OOP Tic Tac Toe. I am beginner-intermediate developer. Can you tell me what I can improve, what I did wrong and what to look up to? This is my first OOP project. I'm happy that I built in looking for any reviews I struggled a little bit with play_the_game() function because I got some times that I have to write two times X computer not once but somehow I solve it.

Do you have any suggestions what to build next or what to add to this project and what I can say that for this type of project. I think it is better to use OOP because if it wasn't OOP I would use a lot of global variables. Some people told me that global are bad practice.

class Tic_Tac_Toe:
    def __init__(self,playerX):
        self.playerX = playerX
        self.winner = None
        self.board = ["-","-","-" ,
        "-","-","-",
        "-","-","-",]
        self.current_player = "X"
        self.game_over = False
    
    def display_board(self):
        print(f" {self.board[0]} | {self.board[1]} | {self.board[2]} ")
        print("-----------")
        print(f" {self.board[3]} | {self.board[4]} | {self.board[5]} ")
        print("-----------")
        print(f" {self.board[6]} | {self.board[7]} | {self.board[8]} ")
    
    def user_move(self):
        guess = int(input("Please enter a number between 0-8: "))
        if guess < 0 or guess > 8:
            print("Enter a number between 1-8")
        else:
            if self.board[guess] == "-":
                self.board[guess] = "X"
                print(f"You taken place--> {guess}")
            else:
                print("The place is occupied please choose another!")
                self.user_move()
    def computer_move(self):
        if self.current_player == "Y":
            if "-" in self.board:
                position = random.randint(0,8)
                if self.board[position] == "-":
                    self.board[position] = "Y"
                    print(f"Computer taken place--> {position}")
                else:
                    self.computer_move()

    
    def handle_turn(self):
        if self.current_player == "X":
            self.current_player = "Y"
        else:
            self.current_player = "Y"
    
    def check_if_tie(self):
        if "-" not in self.board and self.winner == None:
            print("Tie no one won!")
            print("Best of luck next time!")
            self.game_over = True

    def possible_horizontal_win(self):
        if self.board[0] == self.board[3] == self.board[6] != "-":
            self.game_over = True
            self.winner = self.board[0]
            if self.winner == "X":          
                print(f"The winner is -- > {self.playerX}")
            else:
                print(f"The winner is -- > {self.winner}")
        elif self.board[1] == self.board[4] == self.board[7] != "-":
            self.game_over = True
            self.winner = self.board[1]
            if self.winner == "X":          
                print(f"The winner is -- > {self.playerX}")
            else:
                print(f"The winner is -- > {self.winner}")
        elif self.board[2] == self.board[5] == self.board[8] != "-":
            self.game_over = True
            self.winner = self.board[2]
            if self.winner == "X":          
                print(f"The winner is -- > {self.playerX}")
            else:
                print(f"The winner is -- > {self.winner}")
    def possible_vertical_win(self):
        if self.board[0] == self.board[1] == self.board[2] != "-":
            self.game_over = True
            self.winner = self.board[0]
            if self.winner == "X":          
                print(f"The winner is -- > {self.playerX}")
            else:
                print(f"The winner is -- > {self.winner}")
        elif self.board[3] == self.board[4] == self.board[5] != "-":
            self.game_over = True
            self.winner = self.board[3]
            if self.winner == "X":          
                print(f"The winner is -- > {self.playerX}")
            else:
                print(f"The winner is -- > {self.winner}")
        elif self.board[6] == self.board[7] == self.board[8] != "-":
            self.game_over = True
            self.winner = self.board[6]
            if self.winner == "X":          
                print(f"The winner is -- > {self.playerX}")
            else:
                print(f"The winner is -- > {self.winner}")
    def possible_sideways_win(self):
        if self.board[0] == self.board[4] == self.board[8] != "-":
            self.game_over = True
            self.winner = self.board[0]
            if self.winner == "X":          
                print(f"The winner is -- > {self.playerX}")
            else:
                print(f"The winner is -- > {self.winner}")
        elif self.board[2] == self.board[4] == self.board[6] != "-":
            self.game_over = True
            self.winner = self.board[2]
            if self.winner == "X":          
                print(f"The winner is -- > {self.playerX}")
            else:
                print(f"The winner is -- > {self.winner}")


    def play_the_game(self):
        while self.game_over == False and self.winner == None:
            self.user_move()
            self.handle_turn()
            self.computer_move()
            self.possible_horizontal_win()
            self.possible_sideways_win()
            self.possible_vertical_win()
            self.check_if_tie()
            self.display_board()
        

tictactoe = Tic_Tac_Toe("Joelinton")
tictactoe.play_the_game()
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

You don't really use winner (only used to display Y (computer) wins) or current_player (only computer player checks this and never reverts from Y), which means you can remove these and handle_turn. If you'd like to track whose turn it is for two player game I'd suggest a boolean that can toggle (turn = !turn) each time. Instead of checking for winner check if game_over

Your check win functions can be reduced in length. A shorter version would be to create a list, join it together and check if they match XXX or YYY (for rows list slicing self.board[index:index+3], and columns/diagonals something like self.board[i:i+9:3]). Using a simple for loop you can do row/col checker using some simple math.

Minor thing, usually tic tac toe uses Xs and Os, but it doesn't matter that much.

Another potential improvement would be to track open spaces in a separate list, so that the computer will guaranteed generate random placement the first time. For example spots = [0, 1, 2, ...,8] after each move (pc and user) something like spots.remove(guess). Then computer can then guess random.choice(spots)

import random
class Tic_Tac_Toe:
  def __init__(self,playerX):
    self.playerX = playerX
    self.board = ["-","-","-" ,
    "-","-","-",
    "-","-","-",]
    self.game_over = False
    
  def display_board(self):
    print(f" {self.board[0]} | {self.board[1]} | {self.board[2]} ")
    print("-----------")
    print(f" {self.board[3]} | {self.board[4]} | {self.board[5]} ")
    print("-----------")
    print(f" {self.board[6]} | {self.board[7]} | {self.board[8]} ")
    
  def user_move(self):
    guess = int(input("Please enter a number between 0-8: "))
    if guess < 0 or guess > 8:
      print("Enter a number between 1-8")
    else:
      if self.board[guess] == "-":
        self.board[guess] = "X"
        print(f"You taken place--> {guess}")
      else:
        print("The place is occupied please choose another!")
        self.user_move()
  def computer_move(self):
    if "-" in self.board:
      position = random.randint(0,8)
      if self.board[position] == "-":
        self.board[position] = "Y"
        print(f"Computer taken place--> {position}")
      else:
        self.computer_move()
  
  def check_if_tie(self):
    if "-" not in self.board and self.game_over:
      print("Tie no one won!")
      print("Best of luck next time!")
      self.game_over = True
      
  def check_win(self):
    # horizontal/vertical check
    for i in range(3):
      if ''.join(self.board[i*3:(i+1)*3]) == 'XXX' or ''.join(self.board[i:i+9:3]) == 'XXX':
        print(f"The winner is -- > {self.playerX}")
        self.game_over = True
      elif ''.join(self.board[i*3:(i+1)*3]) == 'YYY' or ''.join(self.board[i:i+9:3]) == 'YYY':
        print("The winner is -- > Y")
        self.game_over = True
    # diag check
    if ''.join([self.board[0],self.board[4],self.board[8]]) == 'XXX' or ''.join([self.board[2],self.board[4],self.board[6]]) == 'XXX':
      print(f"The winner is -- > {self.playerX}")
      self.game_over = True
    elif ''.join([self.board[0],self.board[4],self.board[8]]) == 'YYY' or ''.join([self.board[2],self.board[4],self.board[6]]) == 'YYY':
      print("The winner is -- > Y")
      self.game_over = True

  def play_the_game(self):
    while self.game_over == False:
      self.user_move()
      self.computer_move()
      self.check_win()
      self.check_if_tie()
      self.display_board()

tictactoe = Tic_Tac_Toe("Joelinton")
tictactoe.play_the_game()
\$\endgroup\$
2
  • \$\begingroup\$ You use the slice self.board[i*3:(i+1)*3] for one direction, but then resort to self.board[i],self.board[i+3],self.board[i+6] for the other? Why not use a slice-with-stride for that too? ie, self.board[i:i+9:3] \$\endgroup\$
    – AJNeufeld
    Commented Jan 5, 2023 at 19:03
  • \$\begingroup\$ @AJNeufeld forgot about step part \$\endgroup\$
    – depperm
    Commented Jan 5, 2023 at 19:10
2
\$\begingroup\$

Global Variables

Using globals is not an inherently bad practice. They have their uses. For instance, we could easily add some globals to your code and actually improve it:

EMPTY = '-'
PLAYER = 'X'
COMPUTER = 'Y'

Now, instead of self.board[guess] == "-", you can write self.board[guess] == EMPTY, which actually makes the code clearer. Moreover, if you wish to change your "X's and Y's" game into a more traditionally "X's and O's" game, it would only take changing the third line to COMPUTER = 'O'. Want a blank cell instead of a dash? EMPTY = ' '! Using these named constants allows you to make a change in one place, instead of everywhere in your code, and hoping you haven't accidentally missed a spot.

PEP 8

The Style Guide for Python Code (aka PEP-8) has many recommendation you should follow.

Naming

Classes should be named with BumpyWords, not Mixed_Bumpy_Words, so your class name should be TicTacToe.

Variables and members should be snake_case, not mixedCase, so names like playerX should be avoided. Since it holds the name of the player, player_name would be more readable.

Indentation

When an expression is split over multiple lines, the continuation line(s) should be indented to the corresponding indentation of the sub-expression, eg:

        self.board = ["-", "-", "-",
                      "-", "-", "-",
                      "-", "-", "-"]

Although in this case, using the EMPTY global constant defined earlier, and list replication, would be clearer and shorter:

        self.board = [EMPTY] * 9

Blank Lines

You should have a blank line between functions. That is, you need to add a blank like before possible_vertical_win and possible_sideways_win.

Private members

Members which are not expected to be accessed from external to the class should be "flagged" with a leading underscore. Eg) self._player_name, self._board and self._game_over. This doesn't make them inaccessible, but IDE's will recognize the variables as "internal" and not suggest them as auto-completions. Linters can also detect and warn when accesses to internals are made.

Bug

        guess = int(input("Please enter a number between 0-8: "))
        if guess < 0 or guess > 8:
            print("Enter a number between 1-8")

Is the number supposed to be between 0-8 or 1-8? You're confusing the player!

Avoid Tail-Recursion

Many programming languages detect tail-recursion, and optimize it away as a loop. Python cannot and does not, as such using tail-recursion can cause unexpected stack-overflows. It is better to use an explicit loop:

    def user_move(self):
        while True:
            guess = int(input("Please enter a number between 0-8: "))
            if guess < 0 or guess > 8:
                print("Enter a number between 0-8")
            elif self.board[guess] != EMPTY:
                print("The place is occupied please choose another!")
            else:
                break

        self.board[guess] = "X"
        print(f"You taken place--> {guess}")

Using elif instead of else allows us to remove one level of indentation, which makes the 4-space indentation standard easier to obey.

The same change should be made to computer_move().

Bug

    def handle_turn(self):
        if self.current_player == "X":
            self.current_player = "Y"
        else:
            self.current_player = "Y"

This only ever sets self.current_player to "Y".

DRY -vs- WET

DRY stands for "Don't Repeat Yourself"; WET stands for "Write Everything Twice". You want to write DRY code.

You've rewritten very WET code in possible_horizontal_win, possible_vertical_win and possible_sideways_win.

Let's dry it up.

class TicTacToe:

    _LINES = [slice(0, 3, 1), slice(3, 6, 1), slice(6, 9, 1),    # rows
              slice(0, 9, 3), slice(1, 10, 3), slice(2, 11, 3),  # columns
              slice(2, 8, 2), slice(0, 12, 4)]                   # diagonals

    def _has_won(self, player):
        win = [player] * 3
        return any(self._board[line] == win for line in TicTacToe._LINES)

    def check_for_win(self):
        if self._has_won(PLAYER):
            self._game_over = True
            self._winner = PLAYER
            print(f"The winner is -- > {self._player_name}")
        elif self._has_won(COMPUTER):
            self._game_over = True
            self._winner = COMPUTER
            print(f"The winner is -- > {COMPUTER}")

What's going on here?

List slicing (like in depprem's answer) board[0:3:1] returns a "slice" of the board, as in [board[0], board[1], board[2]]. The 0:3:1 is shorthand for slice(0, 3, 1), but the shorthand only works within []. The _LINES member contains the 8 slices for the 8 possible winning lines.

The any(expr for value in container) statement evaluates expr for each value in the container, and returns True if expr ever evaluates to True.

expr in this case is self._board[line] == win, where line becomes each of the 8 slices, and win is [player] * 3, so would be [PLAYER, PLAYER, PLAYER] if _has_won() is called with the PLAYER or [COMPUTER, COMPUTER, COMPUTER] if called with COMPUTER.

Improved Code

import random

EMPTY = "-"
PLAYER = "X"
COMPUTER = "O"

class TicTacToe:

    _LINES = [slice(0, 3, 1), slice(3, 6, 1), slice(6, 9, 1),    # rows
              slice(0, 9, 3), slice(1, 10, 3), slice(2, 11, 3),  # columns
              slice(2, 8, 2), slice(0, 12, 4)]                   # diagonals

    def __init__(self, player_name):
        self._player_name = player_name
        self._board = [EMPTY] * 9
        self._winner = None
        self._game_over = False

    def _display_board(self):
        print(" {} | {} | {}\n"
              "-----------\n"
              " {} | {} | {}\n"
              "-----------\n"
              " {} | {} | {}\n".format(*self._board))

    def _user_move(self):
        while True:
            guess = int(input("Please enter a number between 0-8: "))
            if guess < 0 or guess > 8:
                print("Enter a number between 0-8")
            elif self._board[guess] != EMPTY:
                print("The place is occupied please choose another!")
            else:
                break

        self._board[guess] = PLAYER
        print(f"You taken place--> {guess}")

    def _computer_move(self):
        if EMPTY in self._board:
            while True:
                position = random.randint(0,8)
                if self._board[position] == EMPTY:
                    break
            self._board[position] = COMPUTER
            print(f"Computer taken place--> {position}")

    def _check_if_tie(self):
        if EMPTY not in self._board and self._winner == None:
            print("Tie no one won!")
            print("Best of luck next time!")
            self._game_over = True

    def _has_won(self, player):
        win = [player] * 3
        return any(self._board[line] == win for line in TicTacToe._LINES)

    def _check_for_win(self):
        if self._has_won(PLAYER):
            self._game_over = True
            self._winner = PLAYER
            print(f"The winner is -- > {self._player_name}")
        elif self._has_won(COMPUTER):
            self._game_over = True
            self._winner = COMPUTER
            print(f"The winner is -- > {COMPUTER}")

    def play_the_game(self):
        self._display_board()
        while not self._game_over:
            self._user_move()
            self._computer_move()
            self._check_for_win()
            self._check_if_tie()
            self._display_board()

if __name__ == '__main__':
    game = TicTacToe("Joelinton")
    game.play_the_game()

This code is somewhat better, but there is still much that can be improved. For one, like the original code, after the human player makes a winning move, the computer still takes an additional move, which shouldn't happen. This should be corrected. Left to student.

\$\endgroup\$

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