5
\$\begingroup\$

I'm reading "Code Complete" by McConnel and I'm practicing using object oriented programming to "reduce complexity". (By that McConnel means, among other things, that it should be very easy to read through any given tiny part of a project and know what is happening without thinking at all about the rest of the program). To practice, I have written a Python program that reproduces the known win-rate results for random-move tic tac toe games. The main file uses an instance of the below Game class to call the play_game method... the parameter to the method indicates how many games are to be played. I'm just curious if an experienced OOP/Python person could let me know if the classes below are cohesive and well built.

There are two other classes in the program, one is a Board class that knows everything about the state and rules of the board at each point of the game, and the other is a Statistics class that tracks and reports the results. I have also included the Board class. I suppose issues such as "do the rules on wins and ties belong in the Board class or the Game class" could go either way: Does the Board know when a win is on itself... or does the Game know when there is a win?

By the way, when I pass 1000000 to play_game to run one million games, the whole program does reproduce the exact results of a 58.5% win rate for X, a 28.8% win rate for O, and a 12.7% tie rate for random Tic Tac Toe. (These values are for X always moving first). I'd love some feedback on whether the below classes are cohesive and well built.

from board import Board


class Game:

    # makes Game class a singleton
    _singleton = None

    def __new__(cls, *args, **kwargs):
        if not cls._singleton:
            cls._singleton = super(Game, cls).__new__(cls, *args, **kwargs)
        return cls._singleton

    def __init__(self):
        self.b = None

    # responsible for playing the number of games requested from main.py
    def play_game(self, number_of_games):
        i = 1
        while i <= number_of_games:
            # create a fresh board object for the next game
            self.b = Board()
            # play one game
            game_in_progress = True
            while game_in_progress:
                game_in_progress = self.make_move()
            i = i + 1

    def make_move(self):
        # randomly select an available square
        position = self.b.select_random_square()
        # make a move
        self.b.board[position] = self.b.next_move
        # call game_over to see if the game is over
        if self.b.game_over():
            return False
        # update who moves next
        temp = self.b.next_move
        self.b.next_move = self.b.previous_move
        self.b.previous_move = temp
        return True

And here is the Board class...

import random
from statistics import Statistics


class Board(Statistics):

    end_of_game_report = False

    def __init__(self):
        # the following line causes the __init__ method from the Statistics class to still run
        Statistics.__init__(self)
        # game board
        self.board = [' '] * 9
        self.next_move = 'X'
        self.previous_move = 'O'
        self.all_possible_wins = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
        self.letter_dict = {'X': -1, 'O': 1}
        self.x_won = False
        self.o_won = False
        self.tie = False

    # Printing an instance of the class will display a standard
    # tic tac toe game image.
    def __str__(self):
        return "\n" + self.board[0] + "|" + self.board[1] + "|" + self.board[2] + "\n"+ self.board[3] + "|" + self.board[4] + "|" + self.board[5] + "\n" + self.board[6] + "|" + self.board[7] + "|" + self.board[8]

    @classmethod
    def turn_on_end_of_game_reporting(cls):
        cls.end_of_game_report = True

    # call turn_on_end_of_game_reporting from main.py to run this report method
    def end_of_game_reporter(self):
        if self.x_won is True:
            print(self)
            print('X won\n')
        elif self.o_won is True:
            print(self)
            print('O won\n')
        else:
            print(self)
            print('Its a tie\n')

    def get_available_squares(self):
        # creates a list containing the position of all open squares
        list_of_open_squares = []
        i = 0
        for j in self.board:
            if j == ' ':
                list_of_open_squares.append(i)
            i = i + 1
        return list_of_open_squares

    def select_random_square(self):
        list_of_open_squares = self.get_available_squares()
        # return a randomly selected available square
        return random.choice(list_of_open_squares)

    def game_won(self):
        for three_contiguous in self.all_possible_wins:
            row = 0
            for i in three_contiguous:
                if self.board[i] == ' ':
                    break
                row = row + self.letter_dict[self.board[i]]
            if row == -3 or row == 3:
                if row == -3:
                    self.x_won = True
                    self.track_game_outcomes('x_won')
                if row == 3:
                    self.o_won = True
                    self.track_game_outcomes('o_won')
                # self.brain.prep_dict_entry(three_contiguous, self.board, self.previous_move)
                if Board.end_of_game_report is True:
                    self.end_of_game_reporter()
                return True
        return False

    def game_tied(self):
        if ' ' not in self.board[:9]:
            self.tie = True
            self.track_game_outcomes('tie')
            if Board.end_of_game_report is True:
                self.end_of_game_reporter()
            return True
        return False

    def game_over(self):
        if self.game_won():
            return True
        elif self.game_tied():
            return True
        else:
            return False
\$\endgroup\$
3
  • \$\begingroup\$ I suggest that you include your Board class as well. \$\endgroup\$ Feb 9, 2019 at 18:02
  • \$\begingroup\$ @200_success Great! I went ahead and added the Board class. \$\endgroup\$
    – okcapp
    Feb 9, 2019 at 22:27
  • \$\begingroup\$ @200_succes Also, I reread my post and there was a typo: I said that the main file uses an instance of the Board class... but I meant to say that the main file uses an instance of the Game class. (...the Game class has boards). \$\endgroup\$
    – okcapp
    Feb 9, 2019 at 22:39

1 Answer 1

4
\$\begingroup\$

You got the result you wanted, so that is a good thing. However, I'm not convinced as to the build up of your classes and the responsibilities of them. A class should to me be self-contained, and have a defined set of responsibilities.

The Board class should only handle stuff directly related to the Board, that is initialisations, placing moves, reporting stuff like free spaces and status of the Board. It shouldn't do game logic like selecting a random move, and I'm a bit uncertain on the end of game reporter.

The Game class, could possible get a better name, would be the better place for me to place the logic of selecting the next position, and most likely the end of game reporter. Game shouldn't update the board directly either, that should have been a method of the Board. That way the game logic and game state would have been better separated.

In addition to that I've got some comments on the actual code, which in general is mostly nice and clean:

Game class

  • Why a singleton? – Is there any reason why your game class is a singleton? It doesn't keep tags across instances, and neither does it have any class methods illustrating the need for it be a singleton.
  • No need for i to count games – You could instead just count down on number_of_games, and check if larger than zero.
  • Why is game_in_progress linked to make_move() – To me it's unnatural that making a move updates whether the game is in progress or not. Expected return of make_move() would be whether the move was successful or not. I'd rather see something like:

    while number_of_games > 0:
      board = Board()
      while board.game_in_progress():
        available_moves = board.get_available_squares()
        board.next_move(random(available_moves))
    
    number_of_games -= 1
    
  • Next move should be in Board – Most of this logic should be in the Board class, with exception of the bits shown in previous code excerpt. This would also allow for safer moves, and easier handling of win/lose situations, as they can be calculated when the move is made.
  • Statistics should be done in this class – It seems a little strange for the Board class to keep statistics, as that belongs more to the one playing the game. Imagine a scenario where you would allow your code to test out various game strategies, like always placing in the first available space, or trying to avoid wins on the other player, or random placements (like you do currently). The statistics connected directly to the Board class would then have a hard time differentiating between the various options. It would be better placed where the games where actually played.

Board class

  • Don't test for True – In end_of_game_reporter() you test whether self.x_won is True, when a simpler self.x_won would suffice.
  • Extract similar bits of code – Also in end_of_game_reporter(), you do print(self) in all three blocks. Move this in front of the if-statements, and you've saved yourself three code lines, and it's easier to read.
  • Avoid repeated computations – For every time a move is made, you re-calculate the list_of_open_squares. You could have started out with a set of all available positions, and just remove the last move done. Easier to read, and faster.
  • Calculation of win – Various strategy for win calculations exists, and doing the brute force option is not optimal. One better way could be use a little bit of extra memory, and store the sum of each winning combinations. That is have variables containing the sum of each of the three rows, each of the three columns, and each of the two diagonals. When a move is made, you correct the sum of corresponding row, column and diagonals. If the absolute value of either sum equals 3, you've got a winner.
  • Tie game calculation – Using the set from before, if there are no more places available in the set of possible moves, and no one has won yet, it's a tie.
  • Simplify if true: return True – In game_over() you could simplify the code a lot as you return the value of the if statement:
    return self.game_won() or self.game_tied()
    
  • Changing state on end game reporting – By default no end of game reporting is on, if you turn it on, there is no way of turning it back off again. Having a method to set this state, like set_game_reports() accepting a boolean value, and possible move the test inside of end_of_game_reporter() would look nicer.

PS! I'm somewhat rusty in reviewing code, so there might be bits and pieces I've left out, and I've not provided a refactor solution but leaving that for you to work out. Hopefully some of this will help you develop even better code.

\$\endgroup\$

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.