9
\$\begingroup\$

I'm studying Object-oriented Programming and decided to apply some things doing an Object-oriented Tic-Tac-Toe.
I wanted to know if you have some hints of what to improve and why!

class Board:
    """Represents one board to a Tic-Tac-Toe game."""

    def __init__(self):
        """Initializes a new board.
        A board is a dictionary which the key is the position in the board
        and the value can be 'X', 'O' or ' ' (representing an empty position
        in the board.)"""
        self.board = {
                "TL": " ", "TM": " ", "TR": " ",
                "ML": " ", "MM": " ", "MR": " ",
                "BL": " ", "BM": " ", "BR": " "}

    def print_board(self):
        """Prints the board."""
        print(self.board["TL"] + "|" + self.board["TM"] \
            + "|" + self.board["TR"] + "|")
        print("-+" * 3)
        print(self.board["ML"] + "|" + self.board["MM"] \
            + "|" + self.board["MR"] + "|")
        print("-+" * 3)
        print(self.board["BL"] + "|" + self.board["BM"] \
            + "|" + self.board["BR"] + "|")

    def _is_valid_move(self, position):
        if self.board[position] is " ":
            return True
        return False

    def change_board(self, position, type):
        """Receive a position and if the player is 'X' or 'O'.
        Checks if the position is valid, modifies the board and returns the modified board.
        Returns None if the move is not valid."""
        if self._is_valid_move(position):
            self.board[position] = type
            return self.board
        return None

    def is_winner(self, player):
        """Returns True if the player won and False otherwise."""
        if self.board["TL"] == player.type and self.board["TM"] == player.type and self.board["TR"] == player.type or \
        self.board["ML"] == player.type and self.board["MM"] == player.type and self.board["MR"] == player.type or \
        self.board["BL"] == player.type and self.board["BM"] == player.type and self.board["BR"] == player.type or \
        self.board["TL"] == player.type and self.board["ML"] == player.type and self.board["BL"] == player.type or \
        self.board["TM"] == player.type and self.board["MM"] == player.type and self.board["BM"] == player.type or \
        self.board["TR"] == player.type and self.board["MR"] == player.type and self.board["BR"] == player.type or \
        self.board["TL"] == player.type and self.board["MM"] == player.type and self.board["BR"] == player.type or \
        self.board["BL"] == player.type and self.board["MM"] == player.type and self.board["TR"] == player.type:
            return True
        return False


class Player:
    """Represents one player."""
    def __init__(self, type):
        """Initializes a player with type 'X' or 'O'."""
        self.type = type

    def __str__(self):
        return "Player {}".format(self.type)


class Game:
    """Represents a Tic-Tac-Toe game.
    The game defines player 1 always playing with 'X'."""
    def __init__(self):
        """Initilize 2 Players and one Board."""
        self.player1 = Player("X")
        self.player2 = Player("O")
        self.board = Board()

    def print_valid_entries(self):
        """Prints the valid inputs to play the game."""
        print("""
            TL - top left    | TM - top middle    | TR - top right
            ML - middle left | MM - center        | MR - middle right
            BL - bottom left | BM - bottom middle | BR - bottom right""")

    def printing_board(self):
        """Prints the board."""
        self.board.print_board()

    def change_turn(self, player):
        """Changes the player turn.
        Receives a player and returns the other."""
        if player is self.player1:
            return self.player2
        else:
            return self.player1

    def won_game(self, player):
        """Returns True if the player won the game, False otherwise."""
        return self.board.is_winner(player)

    def modify_board(self, position, type):
        """Receives position and player type ('X' or 'O').
        Returns modified board if position was valid.
        Asks to player try a different position otherwise."""
        if self.board.change_board(position, type) is not None:
            return self.board.change_board(position, type)
        else:
            position = input("Not available position. Please, try again: ")
            return self.board.change_board(position, type)


def play():
    game = Game()
    game.print_valid_entries()
    player = game.player1
    num = 9
    while num > 0:
        num -= 1
        game.printing_board()
        position = input("{} turn, what's your move? ".format(player))
        game.modify_board(position, player.type)
        if game.won_game(player):
            print("{} is the Winner!".format(player))
            break
        else:
            player = game.change_turn(player)
    if num == 0:
        print("Game over! It's a tie!")


if __name__ == "__main__":
    play()

\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

Code readability and style

I would recommend you have a look at PEP 8, which is Python's official style guide.

Let's introduce you to f-strings -

To create an f-string, prefix the string with the letter “ f ”. The string itself can be formatted in much the same way that you would with str.format(). f-strings provide a concise and convenient way to embed python expressions inside string literals for formatting.

So, I would write these three statements -

# rest of the code
def __str__(self):
    return "Player {}".format(self.type)
# rest of the code

position = input("{} turn, what's your move? ".format(player))
# rest of the code
print("{} is the Winner!".format(player))

Like this -

# rest of the code
def __str__(self):
    return f"Player {self.type}"
# rest of the code    

position = input(f"{player} turn, what's your move? ")
# rest of the code
print(f"{player} is the Winner!")

See how concise it can get?


From PEP 8 -

PEP 257 describes good docstring conventions. Note that most importantly, the """ that ends a multiline docstring should be on a line by itself -

"""Return a foobang

Optional plotz says to frobnicate the bizbaz first.
"""

For one liner docstrings, please keep the closing """ on the same line.

So, for example, this -

"""Receives position and player type ('X' or 'O').
Returns modified board if position was valid.
Asks to player try a different position otherwise."""

Should actually be written as -

"""Receives position and player type ('X' or 'O').
Returns modified board if position was valid.
Asks to player try a different position otherwise.
"""

Also, since you have descriptively named functions, you don't need those unnecessary comments explaining what your function does. For example, this does not need a comment -

def printing_board(self):
    """Prints the board."""
    self.board.print_board()

We know you're printing the board; it says in the function itself - def printing_board(self).

Also, good use of the if '__name__' == __'main__': guard. Most people don't even attempt to use it.


Note that the trailing \ solutions are not recommended by PEP 8. One reason is that if space is added by mistake after a \ it might not show in your editor, and the code becomes syntactically incorrect.

The PEP changed at https://hg.python.org/peps/rev/7a48207aaab6 to explicitly discourage backslashes.

The preferred way of wrapping long lines is by using Python's implied line continuation inside parentheses, brackets, and braces. Long lines can be broken over multiple lines by wrapping expressions in parentheses. These should be used in preference to using a backslash for line continuation.

Another thing is that, here (for example) -

if self.board["TL"] == player.type and self.board["TM"] == player.type and self.board["TR"] == player.type or \
self.board["ML"] == player.type and self.board["MM"] == player.type and self.board["MR"] == player.type or \
self.board["BL"] == player.type and self.board["BM"] == player.type and self.board["BR"] == player.type or \
self.board["TL"] == player.type and self.board["ML"] == player.type and self.board["BL"] == player.type or \
self.board["TM"] == player.type and self.board["MM"] == player.type and self.board["BM"] == player.type or \
self.board["TR"] == player.type and self.board["MR"] == player.type and self.board["BR"] == player.type or \
self.board["TL"] == player.type and self.board["MM"] == player.type and self.board["BR"] == player.type or \
self.board["BL"] == player.type and self.board["MM"] == player.type and self.board["TR"] == player.type:

the lines are too long. According to PEP 8 -

Limit all lines to a maximum of 79 characters.

Therefore, these statements could alternatively be written as -

def is_winner(self, player):
    player_type = player.type
    runs = [
        # horizontal
        ["TL", "TM", "TR"],
        ["ML", "MM", "MR"],
        ["BL", "BM", "BR"],
        # vertical
        ["TL", "ML", "BL"],
        ["TM", "MM", "BM"],
        ["TR", "MR", "BR"],
        # diagonal
        ["TL", "MM", "BR"],
        ["BL", "MM", "TR"]
    ]
    for a, b, c in runs:
        if self.board[a] == self.board[b] == self.board[c] == player_type:
            return True
    return False

Overall, in terms of code readability and style, this is what you need to improve. You should make your code more PEP 8 compliant.

Hope this helps!

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Thank you!! That was really helpful, I'll look PEP 8 now, with more attention. \$\endgroup\$ Commented Jun 17, 2019 at 5:50
  • 1
    \$\begingroup\$ So, I was changing my code using those hints but the last one didn't work in here. Now, it doesn't access the value player.type correctly, so 'X O X' is a win point. I tried self.board["TL"] and self.board["TM"] and self.board["TR"] == player.type too, but it didn't work as well. Any idea of what happened?? \$\endgroup\$ Commented Jun 17, 2019 at 12:28
  • 1
    \$\begingroup\$ @AndressaCabistani - I have edited my answer to make some changes. This should definitely work. Sorry for any confusion :( \$\endgroup\$
    – Justin
    Commented Jun 17, 2019 at 15:49
  • 1
    \$\begingroup\$ Now it works perfectly!! \$\endgroup\$ Commented Jun 17, 2019 at 16:09
  • 1
    \$\begingroup\$ @AndressaCabistani - I'm glad it helped you :) \$\endgroup\$
    – Justin
    Commented Jun 17, 2019 at 16:09
3
\$\begingroup\$

You can use list comprehension as well as the built-in all() and any() methods to make your is_winner() method more concise and readable:

def is_winner(self, player):
    """Returns True if the player won and False otherwise."""
    row1 = [self.board[val] for val in ["TL","TM","TR"]]
    row2 = [self.board[val] for val in ["ML","MM","MR"]]
    row3 = [self.board[val] for val in ["BL","BM","BR"]]
    col1 = [self.board[val] for val in ["TL","ML","BL"]]
    col2 = [self.board[val] for val in ["TM","MM","BM"]]
    col3 = [self.board[val] for val in ["TR","MR","BR"]]
    dia1 = [self.board[val] for val in ["TL","MM","BR"]]
    dia2 = [self.board[val] for val in ["TR","MM","BL"]]
    wins = [row1, row2, row3, col1, col2, col3, dia1, dia2]

    isPlayer = lambda cell: cell == player.type
    isWinner = lambda seq: all(map(isPlayer, seq))
    if any(map(isWinner, wins)):
        ''' Returns true if any possible win is a win '''
        return True
    else:
        return False

The code to get all of the possible win combinations could have been made more concise if the board was represented by a 2D array rather than a dict as you have it; but the explicit nature of assigning the values of each row still has its benefits.


Messed around and made a more DRY way of building the wins array, but the nested list comprehension is arguably less readable/understandable. Nonetheless, here it is just for fun:

wins = [[self.board[cell] for cell in seq] 
        for seq in [ 
            ["TL","TM","TR"], # Row 1
            ["ML","MM","MR"], # Row 2
            ["BL","BM","BR"], # Row 3
            ["TL","ML","BL"], # Col 1
            ["TM","MM","BM"], # Col 2
            ["TR","MR","BR"], # Col 3
            ["TL","MM","BR"], # Dia 1
            ["TR","MM","BL"]  # Dia 2
        ]   
]
\$\endgroup\$

Your Answer

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

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