3
\$\begingroup\$

So iam making a game using OOP in python however is this the best practise when defining your objects? are you meant to define them all at the end of the script like I've done, or are you meant to define them after each class you make? Thanks in advance and any other improvements would be much appreciated!

import random    # Import random module to use for computer random move
import os   # Import system from os module to be able to clear the console

# The following function print the current board to the command window.
def print_board(board):
    print("\n")
    print(" |", board[0], "|", board[1], "|", board[2], " |     1 | 2 | 3")
    print(" |", "--+---+---", "|")
    print(" |", board[3], "|", board[4], "|", board[5], " |     4 | 5 | 6")
    print(" |", "--+---+---", "|")
    print(" |", board[6], "|", board[7], "|", board[8], " |     7 | 8 | 9")

# This function takes the existing board, position input from player,
# marker type (either x or o) and returns the updated board based on the arguments.
def update_board(board, position, marker):
    board[position-1] = marker
    print_board(board)
    return board

# Add any other functions below for your program

# Function to allow the user to select the game mode they wish to play either,
# Player vs Player, Player vs Computer or their Special equivalent.
def game_selection():
    while True:
        print("\tWELCOME TO TIC - TAC - TOE!")
        game_mode = input("""\nSELECT YOUR GAME MODE FROM BELOW!
 - Player Vs Player - Type 'PvP'
 - Player vs Computer - Type 'PvC'
Type your desired game mode here: """).strip()
        if game_mode in ('PvP', 'pvp', 'PVP'):
            pVp.instructions()
            return pVp.play_game()    # Calls method for Player vs Player
        elif game_mode in ('PvC', 'pvc', 'PVP'):
            pVc.instructions()
            return pVc.play_game()    # Calls method for Player vs Computer
        else:
            print("\n-- INVALID GAME MODE Type 'PvP' or 'PvC' --")

# This function is called whenever the initial game ends,and give the user the
# option to play another game or to exit out the console.
def play_again():
    while True:
        play_another = input("\nThank you for playing! \
Would you like to play again (Type 'yes') or enter any key to exit!: ").lower()
        if not play_another == "yes":
            exit(0)    # Execute a successful exit
        return None

# Define a class for Player
class Player:
    # Player class has instance variables for their counter,and if they have won (True/False)
    def __init__(self, counter):
        self.has_won = False
        self.counter = counter

    # Method to allow player objects to move and select a unoccupied position on the board,
    # from 1-9,along with error checking/validation of there input e.g.input is a integer.
    def move(self):
        while True:
            try:
                player_input = int(input(f"\nWhere do you want to place your {self.counter}?(1-9): "))
            except ValueError:
                print("\nInvalid Input not an number,Try Again.")
                continue
            if player_input in range(1, 10) and board[player_input - 1] == " ":
                update_board(board, player_input, self.counter)
                return self.game_state(self.counter)
            else:
                print("\nPlease enter a position between 1-9 ,which is also empty.")

    # Method which allows the board to be reset back to empty, and to reset the has_won
    # value back to 'False'. So the user can choose to play a fresh match.
    def reset(self):
        global board
        board = [" " for move in range(9)]
        self.has_won = False

    # Method allows a player to move an existing counter to any location on the board
    def move_exisiting_piece(self):
        if self.counter not in board:    # Check if the player has a counter on the board
            return None

        while True:    # Ask user if they want to move an exisiting piece or not
            answer = input("\nDo you want to move an existing piece (yes/no): ").lower()
            if answer == "yes":
                break
            if answer == "no":
                return None

        while True:    # Loop which asks user to choose the counter they want to move
            try:
                location = int(input("\nWhat's the location of the your counter"
                                     " you want to move (1-9): "))
            except ValueError:
                print("\nYou did not enter a valid location on the board!")
            if location in range(1, 10) and board[location - 1] == self.counter:
                board[location - 1] = " "
                while True:    # Loop to ask user where to move the exisiting counter
                    try:
                        move_location = int(input("\nWhere on the board is the new location"
                                                  " you want to place the piece? (1-9): "))
                    except ValueError:
                        print("\nNot a valid location,Try Again.")
                    if move_location in range(1, 10) and board[move_location - 1] == " ":
                        update_board(board, move_location, self.counter)
                        return self.game_state(self.counter)
                    else:
                        print("\nThis location is not avaliable to place your piece.")
            else:
                print(f"\nYou do not have a {self.counter} in this location! Try Again.")

    # This method is used to check the game state after every indivdual move to check if
    # the game is a draw, a win or to continue playing.
    def game_state(self, counter):
        horizontal_win = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
        vertical_win = [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
        diagnol_win = [[1, 5, 9], [3, 5, 7]]

        for x, y, z in horizontal_win:  # For loop to check for horizontal win
            if board[x-1] == board[y-1] == board[z-1] != " ":
                print("\nPlayer with counter " + board[x-1] + " has won!")
                self.has_won = True
                return 1

        for x, y, z in vertical_win:    # For loop to check for vertical win
            if board[x-1] == board[y-1] == board[z-1] != " ":
                print("\nPlayer with counter " + board[x-1] + " has won!")
                self.has_won = True
                return 1

        for x, y, z in diagnol_win:     # For loop to check for diagnol win
            if board[x-1] == board[y-1] == board[z-1] != " ":
                print("\nPlayer with counter " + board[x-1] + " has won!")
                self.has_won = True
                return 1

        # Use a for loop to check if board is full and there is no winner (draw)
        if len([c for c in board if c in ('X', 'O')]) == 9:
            print("\nTHIS GAME IS A DRAW! NOBODY WINS!")
            self.has_won = True
            return 1
        return 0    # Return 0 if no win/draw is found

# Class for the computer, which inherited values and methods from the Player class
class Computer(Player):
    def __init__(self, counter):
        Player.__init__(self, counter)

    # Method to allow computer to select the optimal position to place a counter
    def cpu_move(self):
        # Create a 2D array of all possible combinations to play
        winning_combos = [[0, 1, 2], [0, 2, 1], [2, 1, 0], [3, 4, 5],
                          [3, 5, 4], [4, 5, 3], [6, 7, 8], [6, 8, 7],
                          [7, 8, 6], [0, 3, 6], [0, 6, 3], [3, 6, 0],
                          [1, 4, 7], [1, 7, 4], [4, 7, 1], [2, 5, 8],
                          [2, 8, 5], [5, 8, 2], [0, 4, 8], [0, 8, 4],
                          [8, 4, 0], [2, 4, 6], [2, 6, 4], [6, 4, 2]]

        # For each value in each sub array of win_combos we check for a winning position
        # for the computer.If there is none then we check if the human player has a
        # winning turn next, if so we place a 'O' in that position
        for x, y, z in winning_combos:
            if board[4] == " ":
                print("\nThe COMPUTER has placed a 'O' in position 5")
                update_board(board, 5, self.counter)
                return self.game_state(self.counter)
            elif board[x] == "O" and board[y] == "O" and board[z] == " ":
                print("\nThe COMPUTER has placed a 'O' in position " + str(z+1))
                update_board(board, z+1, self.counter)
                return self.game_state(self.counter)
            elif board[x] == "X" and board[y] == "X" and board[z] == " ":
                print("\nThe COMPUTER has placed a 'O' in position " + str(z+1))
                update_board(board, z+1, self.counter)
                return self.game_state(self.counter)

        # If non of the operations above work then get the computer to select a
        # random position on the board to place a 'O'.
        while True:
            move = random.randint(1, 9)
            if board[move-1] == " ":
                print("\nThe COMPUTER has placed a 'O' in position " + str(move))
                update_board(board, move, self.counter)
                return self.game_state(self.counter)

# Create Game class that takes argument for the who each player is and what game mode to play
class Game:
    def __init__(self,player1 ,player2, mode):
        self.player1 = player1
        self.player2 = player2
        self.mode = mode
        
    def instructions(self):
        os.system("cls")
        print(f"\tWELCOME TO {self.player1} VS {self.player2}!")
        print(f"""\nRULES: \n1. {self.player1} you're COUNTER 'X' and {self.player2} is COUNTER 'O'.
2. The first player to get 3 of their counters in a row,
   (up, down, across, or diagonally) is the winner.
3. When all 9 squares are full, the game is over. 
4. If no player has 3 marks in a row, the game ends in a tie.
5. In this mode you also have the option to move an existing counter of your own!""")
    
    # Method which plays the oppropriate game mode depending on 
    def play_game(self):
        # Print out the initial board to the console
        print_board(board) 

        if self.mode == "PVP":
            while True:
                print("\n -PLAYER ONE'S TURN (X)-")
                if player1.move_exisiting_piece() is None:
                    player1.move()
                if player1.has_won:    # If player 1 wins, reset the board and has_won values
                    player1.reset()
                    player2.reset()
                    return None

                print("\n -PLAYER TWO'S TURN (O)-")
                if player2.move_exisiting_piece() is None:
                    player2.move()
                if player2.has_won:    # If player 2 wins, reset the board and has_won values
                    player1.reset()
                    player2.reset()
                    return None
                
        elif self.mode == "PVC":
            while True:
                print("\n -PLAYER ONE'S TURN (X)-")
                if player1.move_exisiting_piece() is None:
                    player1.move()
                if player1.has_won:    # If player 1 wins, reset the board and has_won values
                    player1.reset()
                    computer.reset()
                    return None

                computer.cpu_move()
                if computer.has_won:   # If computer wins, reset the board and has_won values
                    player1.reset()
                    computer.reset()
                    return None

# Program main starts from here

# Global variable for the board
board = [" " for move in range(9)]

# Define the require objects
player1 = Player("X")    # Player 1 object

player2 = Player("O")    # Player 2 object

computer = Computer("O")    # Computer player object

pVp = Game("PLAYER 1", "PLAYER 2", "PVP")   # Player vs Player game mode object

pVc = Game("PLAYER 1", "COMPUTER", "PVC")   # Player vs Computer game mode object

# If this is the main file execute the Tic Tac Toe Game
if __name__ == "__main__":
    while True:    # Run a loop to let user select game mode / play again
        game_selection()
        play_again()
```
\$\endgroup\$
2
  • \$\begingroup\$ @M_ so here was my answer, if any question let me know, if it helped please remember to Accted the answer and upvote \$\endgroup\$ Jan 26, 2021 at 14:01
  • \$\begingroup\$ I added a final review, is actually pretty good overall. Remember to accept the answer so it can be removed from the unanswered queue. \$\endgroup\$ Jan 26, 2021 at 14:34

1 Answer 1

0
\$\begingroup\$
  1. Everything in Python is an Object ---> Is everything an object in Python like Ruby?

So what you refer seems to be the Variable declaration ---> Python Variable Declaration.

"There is no such thing as "variable declaration" or "variable initialization" in Python."

However is normally called define a variable or "assignment".

  1. Where you define the variable depends of the context. There are some best prectices for instance:

A. Define the Variables Close to the function call.

B. Define Constant and Global Variable all in Upper Case.

C. The variable are written in a snake_case style --> Snake case See also --> What is the naming convention in Python for variable and function names? .


Consideration of your code

  1. In your case, define the variables as you did at the bottom is actually ok, And I tell you more. Because you used `if name' == 'main' shield, is very common to do this:

    # If this is the main file execute the Tic Tac Toe Game
     if __name__ == "__main__":
         board = [" " for move in range(9)]
         player1 = Player("X")    
         player2 = Player("O")  
         computer = Computer("O")   
         pVp = Game("PLAYER 1", "PLAYER 2", "PVP")  
         pVc = Game("PLAYER 1", "COMPUTER", "PVC")  
         while True:    # Run a loop to let user select game mode / play again
             game_selection()
             play_again()
    
  2. To many Comments. The comments should be added only when is needed to make further clarification, for instance player1 = Player("X") # Player 1 object is very obvious what's is going on, hence a comment is not really needed here.Is important also for maintainability, imagine if you need to change the code, there is too much 'comment noise' around.

  3. Use DocStrings for function instead of comment.

Not only looks better in most of the IDE or Editor (because of the highlighting) but it looks more professional. But probably the most important aspect is that if How you officially document your code in Python. IDE and other tool use it, (like when you run the help function).

Final review

import random   
import os  


def print_board(board):
    """The following function print the current board to the command window."""
    print("\n")
    print(" |", board[0], "|", board[1], "|", board[2], " |     1 | 2 | 3")
    print(" |", "--+---+---", "|")
    print(" |", board[3], "|", board[4], "|", board[5], " |     4 | 5 | 6")
    print(" |", "--+---+---", "|")
    print(" |", board[6], "|", board[7], "|", board[8], " |     7 | 8 | 9")


def update_board(board, position, marker):
    """This function takes the existing board, position input from player,
    marker type (either x or o) and returns the updated board based on the arguments."""
    board[position-1] = marker
    print_board(board)
    return board




def game_selection():
    print("\n--- WELCOME TO TIC - TAC - TOE! ---")
    
    while True:
        game_mode = input("""\nSELECT YOUR GAME MODE FROM BELOW!
 - Player Vs Player - Type 'PvP'
 - Player vs Computer - Type 'PvC'
Type your desired game mode here: """).strip()
        if game_mode in ('PvP', 'pvp', 'PVP'):
            pVp.instructions()
            return pVp.play_game()   
        elif game_mode in ('PvC', 'pvc', 'PVP'):
            pVc.instructions()
            return pVc.play_game()   
        else:
            print("\n-- INVALID GAME MODE Type 'PvP' or 'PvC' --")


def play_again():
    """This function is called whenever the initial game ends,and give the user the
option to play another game or to exit out the console."""
    while True:
        play_another = input("\nThank you for playing! \
Would you like to play again (Type 'yes') or enter any key to exit!: ").lower()
        if not play_another == "yes":
            exit(0)    # Execute a successful exit
        os.system("cls")
        return None


class Player:
    """Player class has instance variables for their counter,and if they have won (True/False)"""
    def __init__(self, counter):
        self.has_won = False
        self.counter = counter


    def move(self): # NOTE: 
        """"Method to allow player objects to move and select a unoccupied position on the board,
            from 1-9,along with error checking/validation of there input e.g.input is a integer."""
        while True:
            try:
                player_input = int(input(f"\nWhere do you want to place your {self.counter}?(1-9): "))
            except ValueError:
                print("\nInvalid Input not an number,Try Again.")
                continue
            if player_input in range(1, 10) and board[player_input - 1] == " ":
                update_board(board, player_input, self.counter)
                return self.game_state(self.counter)
            else:
                print("\nPlease enter a position between 1-9 ,which is also empty.")

    
    def reset(self):
        """Method which allows the board to be reset back to empty, and to reset the has_won
        value back to 'False'. So the user can choose to play a fresh match."""
        global board
        board = [" " for move in range(9)]
        self.has_won = False

 
    def move_exisiting_piece(self):  # NOTE:  move an existing counter to any location on the board
        if self.counter not in board:    # NOTE: Check if the player has a counter on the board
            return None

        while True:    
            answer = input("\nDo you want to move an existing piece (yes/no): ").lower()
            if answer == "yes":
                break
            if answer == "no":
                return None

        while True:  
            try:
                location = int(input("\nWhat's the location of the your counter"
                                     " you want to move (1-9): "))
            except ValueError:
                print("\nYou did not enter a valid location on the board!")
            if location in range(1, 10) and board[location - 1] == self.counter:
                board[location - 1] = " "
                while True:    # Loop to ask user where to move the exisiting counter
                    try:
                        move_location = int(input("\nWhere on the board is the new location"
                                                  " you want to place the piece? (1-9): "))
                    except ValueError:
                        print("\nNot a valid location,Try Again.")
                    if move_location in range(1, 10) and board[move_location - 1] == " ":
                        update_board(board, move_location, self.counter)
                        return self.game_state(self.counter)
                    else:
                        print("\nThis location is not avaliable to place your piece.")
            else:
                print(f"\nYou do not have a {self.counter} in this location! Try Again.")

    
    def game_state(self, counter):
        """Used to check the game state after every indivdual move to check if
            the game is a draw, a win or to continue playing."""
        horizontal_win = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
        vertical_win = [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
        diagnol_win = [[1, 5, 9], [3, 5, 7]]

        for x, y, z in horizontal_win:  # NOTE: Check for horizontal win
            if board[x-1] == board[y-1] == board[z-1] != " ":
                print("\nPlayer with counter " + board[x-1] + " has won!")
                self.has_won = True
                return 1

        for x, y, z in vertical_win:    # NOTE: Check for vertical win
            if board[x-1] == board[y-1] == board[z-1] != " ":
                print("\nPlayer with counter " + board[x-1] + " has won!")
                self.has_won = True
                return 1

        for x, y, z in diagnol_win:     # NOTE: Check for diagnol win
            if board[x-1] == board[y-1] == board[z-1] != " ":
                print("\nPlayer with counter " + board[x-1] + " has won!")
                self.has_won = True
                return 1

        if len([c for c in board if c in ('X', 'O')]) == 9: # FAQ: Check if board is full an there is no winner (draw)
            print("\nTHIS GAME IS A DRAW! NOBODY WINS!")
            self.has_won = True
            return 1
        return 0    # Return 0 if no win/draw is found


class Computer(Player):
    """Class for the computer, which inherited values and methods from the Player class"""
    def __init__(self, counter):
        Player.__init__(self, counter)

    def cpu_move(self): # NOTE: allow computer to select the optimal position to place a counter
        # Create a 2D array of all possible combinations to play
        winning_combos = [[0, 1, 2], [0, 2, 1], [2, 1, 0], [3, 4, 5],
                          [3, 5, 4], [4, 5, 3], [6, 7, 8], [6, 8, 7],
                          [7, 8, 6], [0, 3, 6], [0, 6, 3], [3, 6, 0],
                          [1, 4, 7], [1, 7, 4], [4, 7, 1], [2, 5, 8],
                          [2, 8, 5], [5, 8, 2], [0, 4, 8], [0, 8, 4],
                          [8, 4, 0], [2, 4, 6], [2, 6, 4], [6, 4, 2]]

        # NOTE: For each value in each sub array of win_combos we check for a winning position
        # for the computer.If there is none then we check if the human player has a
        # winning turn next, if so we place a 'O' in that position
        for x, y, z in winning_combos:
            if board[4] == " ":
                print("\nThe COMPUTER has placed a 'O' in position 5")
                update_board(board, 5, self.counter)
                return self.game_state(self.counter)
            elif board[x] == "O" and board[y] == "O" and board[z] == " ":
                print("\nThe COMPUTER has placed a 'O' in position " + str(z+1))
                update_board(board, z+1, self.counter)
                return self.game_state(self.counter)
            elif board[x] == "X" and board[y] == "X" and board[z] == " ":
                print("\nThe COMPUTER has placed a 'O' in position " + str(z+1))
                update_board(board, z+1, self.counter)
                return self.game_state(self.counter)

        # If non of the operations above work then get the computer to select a
        # random position on the board to place a 'O'.
        while True:
            move = random.randint(1, 9)
            if board[move-1] == " ":
                print("\nThe COMPUTER has placed a 'O' in position " + str(move))
                update_board(board, move, self.counter)
                return self.game_state(self.counter)


class Game(Player):
    def __init__(self,player1 ,player2, mode):
        self.player1 = player1
        self.player2 = player2
        self.mode = mode

    def instructions(self):
        """Method to output the instructions (depending on the gamemode) to the user"""
        os.system("cls")
        print(f"\tWELCOME TO {self.player1} VS {self.player2}!")
        print(f"""\nRULES: \n1. {self.player1} you're COUNTER 'X' and {self.player2} is COUNTER 'O'.
2. The first player to get 3 of their counters in a row,
   (up, down, across, or diagonally) is the winner.
3. When all 9 squares are full, the game is over. 
4. If no player has 3 marks in a row, the game ends in a tie.
5. In this mode you also have the option to move an existing counter of your own!""")
    
    # 
    def play_game(self):
        """Method which plays the oppropriate game mode depending on the selected game mode"""
        print_board(board) 

        while True:
            print("\n -PLAYER ONE'S TURN (X)-")
            if player1.move_exisiting_piece() is None:
                player1.move()
            if player1.has_won:    # If player 1 wins, reset the board and has_won values
                player1.reset()
                player2.reset()
                return None
            if self.mode == "PVP":    # If the mode slected is PvP then execute player 2 move
                print("\n -PLAYER TWO'S TURN (O)-")
                if player2.move_exisiting_piece() is None:
                    player2.move()
                if player2.has_won:    # If player 2 wins, reset the board and has_won values
                    player1.reset()
                    player2.reset()
                    return None
            elif self.mode == "PVC":    # If the mode slected is PvP then execute computer move
                computer.cpu_move()
                if computer.has_won:   # If computer wins, reset the board and has_won values
                    player1.reset()
                    computer.reset()
                    return None


if __name__ == "__main__":
    BOARD = [" " for move in range(9)]
    player1 = Player("X")   
    player2 = Player("O")   
    computer = Computer("O") 
    pVp = Game("PLAYER 1", "PLAYER 2", "PVP")   
    pVc = Game("PLAYER 1", "COMPUTER", "PVC")   

    while True:   
        game_selection()
        play_again()
\$\endgroup\$
6
  • \$\begingroup\$ Thanks alot,ive fixed it now \$\endgroup\$
    – Mj _
    Jan 26, 2021 at 14:10
  • \$\begingroup\$ yes, come back in 10.15 minute, I'm adding more stuff, is that I'm busy, Also remember to accetp the answer pls \$\endgroup\$ Jan 26, 2021 at 14:16
  • \$\begingroup\$ So i just need to use doc strings in my classes, and also have 2 blank spaces between each class? \$\endgroup\$
    – Mj _
    Jan 26, 2021 at 15:49
  • \$\begingroup\$ Yes in general the code is pretty good, can be improved? Probably a bit but honestly if I try to improve the whole logic, it may instead be so clustered that is less readable (which is worst). Regarding the 2 spaces, this is mandatory however it is reccomended in the PEP 8 which is the writing convention for Python. \$\endgroup\$ Jan 26, 2021 at 15:56
  • \$\begingroup\$ To be honest, I probably added 2 spaces unconsciously, is it because I study PEP 8? Nope, I never read it!. What I suggest you is to use Pycharm Comunity edition (is free ;) ) Why? Well Pycharm gives you some little error underline your code in case you don't follow the PEP 8 you can turn it off. but this is how I learn how to write pretty Good Python code, it is so annoyinh that you clean it up so now I do it Automaticallly. I suggest you to give it a try \$\endgroup\$ Jan 26, 2021 at 16:09

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.