4
\$\begingroup\$

So I started Python this week, and I never programmed a game before. Even though Tic Tac Toe is probably the simplest game ever, I had some challenges while coding the game rules. I split my project in 4 files : board.py, game.py, main.py, ui.py. I think my file content is self-explanatory enough that I don't need to explain the purpose of each of them.

board.py

import numpy as np

# The board is a zero-based index 2D array
# The X player is represented using 1
# The O player is represented using -1
# Empty cells are represented using 0


class Board:

    def __init__(self):
        self.__board = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])

    def isFull(self):
        boardShape = np.shape(self.__board)
        for rowIndex in xrange(boardShape[0]):
            for colIndex in xrange(boardShape[1]):
                if self.__board[rowIndex, colIndex] == 0:
                    return False
        return True

    def setCell(self, cell, player):
        #Will throw an exception if the cell is invalid
        self.__validateSetCell(cell)

        self.__board[cell] = player

    def isCellEmpty(self, cell):
        return self.__board[cell] == 0

    def toArray(self):
        return np.copy(self.__board)

    def __validateSetCell(self, cell):
        if not self.isCellEmpty(cell):
            raise ValueError("The field already has a value. Cell : " + str(cell))

        boardShape = np.shape(self.__board)
        if cell[0] >= boardShape[0] or cell[1] >= boardShape[1]:
            raise IndexError("The cell is out of bound. Cell : " + str(cell))

    def __len__(self):
        return len(self.__board)

    def __getitem__(self, item):
        return self.__board[item]

    def __iter__(self):
        return iter(self.__board)

game.py

from board import Board

class Game:
    # If the abs sum of a row (for example) equals 3, it means the row has 3 times the same token, so it's a win.
    SumToWin = 3

    winOnTopRow = staticmethod(lambda b: abs(b[0, 0] + b[0, 1] + b[0, 2]) == Game.SumToWin)
    winOnMiddleRow = staticmethod(lambda b: abs(b[1, 0] + b[1, 1] + b[1, 2]) == Game.SumToWin)
    winOnBottomRow = staticmethod(lambda b: abs(b[2, 0] + b[2, 1] + b[2, 2]) == Game.SumToWin)
    winOnLeftCol = staticmethod(lambda b: abs(b[0, 0] + b[1, 0] + b[2, 0]) == Game.SumToWin)
    winOnMiddleCol = staticmethod(lambda b: abs(b[0, 1] + b[1, 1] + b[2, 1]) == Game.SumToWin)
    winOnRightCol = staticmethod(lambda b: abs(b[0, 2] + b[1, 2] + b[2, 2]) == Game.SumToWin)
    winOnLeftToRightDiag = staticmethod(lambda b: abs(b[0, 0] + b[1, 1] + b[2, 2]) == Game.SumToWin)
    winOnRightToLeftDiag = staticmethod(lambda b: abs(b[0, 2] + b[1, 1] + b[2, 0]) == Game.SumToWin)

    __winningMoveValidator = {}
    __winningMoveValidator[(0, 0)] = lambda b: Game.winOnLeftCol(b) or Game.winOnTopRow(b) or Game.winOnLeftToRightDiag(b)
    __winningMoveValidator[(0, 1)] = lambda b: Game.winOnTopRow(b) or Game.winOnMiddleCol(b)
    __winningMoveValidator[(0, 2)] = lambda b: Game.winOnTopRow(b) or Game.winOnRightCol(b) or Game.winOnRightToLeftDiag(b)
    __winningMoveValidator[(1, 0)] = lambda b: Game.winOnMiddleRow(b) or Game.winOnLeftCol(b)
    __winningMoveValidator[(1, 1)] = lambda b: Game.winOnMiddleRow(b) or Game.winOnMiddleCol(b) or Game.winOnLeftToRightDiag(b) or Game.winOnRightToLeftDiag(b)
    __winningMoveValidator[(1, 2)] = lambda b: Game.winOnMiddleRow(b) or Game.winOnRightCol(b)
    __winningMoveValidator[(2, 0)] = lambda b: Game.winOnLeftCol(b) or Game.winOnBottomRow(b) or Game.winOnRightToLeftDiag(b)
    __winningMoveValidator[(2, 1)] = lambda b: Game.winOnMiddleCol(b) or Game.winOnBottomRow(b)
    __winningMoveValidator[(2, 2)] = lambda b: Game.winOnRightCol(b) or Game.winOnBottomRow(b) or Game.winOnLeftToRightDiag(b)

    XValue = 1
    OValue = -1

    def __init__(self):
        self.__currentPlayer = Game.XValue
        self.__board = Board()
        self.__winner = None

    def getBoardAsArray(self):
        return self.__board.toArray()

    def getCurrentPlayer(self):
        return self.__currentPlayer

    def playForCurrentPlayer(self, cell):

        if self.isOver():
            raise StandardError("The Game is over, you can't play anymore.")

        self.__board.setCell(cell, self.__currentPlayer)

        if self.__isWinningMove(cell):
            self.__winner = self.__currentPlayer
            return

        # X = 1, O = -1
        self.__currentPlayer = -self.__currentPlayer

    def isValidMove(self, cell):
        return self.__board.isCellEmpty(cell)

    def isOver(self):
        return self.__board.isFull() or self.__winner is not None

    def getWinner(self):
        return self.__winner

    def __isWinningMove(self, cell):
        return Game.__winningMoveValidator[cell](self.getBoardAsArray())

ui.py

import numpy as np
import re

userInputValidator = re.compile("[0-2],[0-2]")
boardIntToString = {0: ' ', 1: 'X', -1: 'O'}

def printInstructions():
    print("Welcome to Tic Tac Toe.")
    print("When the game asks for your input, refer to this table for help :")
    helpTable = "0,0 | 0,1 | 0,2\n"\
                "---------------\n"\
                "1,0 | 1,1 | 1,2\n" \
                "---------------\n" \
                "2,0 | 2,1 | 2,2\n"
    print(helpTable)
    print("Good luck.")
    print("-------------------------")

def requestValidPlayerMove(validatiionFunction):
    isValid = False
    move = None

    while not isValid:
        wantedMove = raw_input("Where do you want to play?  ")
        isValid = userInputValidator.match(wantedMove)

        if isValid:
            move = tuple(map(lambda i: int(i), wantedMove.split(',')))
            isValid = validatiionFunction(move)

        if not isValid:
            print("Invalid move, choose again.")

    return move

def printCurrentPlayer(player):
    print("It is " + boardIntToString[player] + "'s turn.")

def printGameOver(winner=None):
    if winner is None:
        print("Game over, no one won.")
    else:
        print("Player " + boardIntToString[winner] + " has won the game.")

def printBoard(board):
    string = "\n"
    string += boardIntToString[board[0, 0]] + " | " + boardIntToString[board[0,1]] + " | " + boardIntToString[board[0,2]] + "\n"
    string += "----------\n"
    string += boardIntToString[board[1,0]] + " | " + boardIntToString[board[1,1]] + " | " + boardIntToString[board[1,2]] + "\n"
    string += "----------\n"
    string += boardIntToString[board[2,0]] + " | " + boardIntToString[board[2,1]] + " | " + boardIntToString[board[2,2]] + "\n"

    print string

main.py

import ui
from game import Game

game = Game()
ui.printInstructions()

while not game.isOver():

    ui.printCurrentPlayer(game.getCurrentPlayer())
    ui.printBoard(game.getBoardAsArray())
    # The UI validates the input format, but we need to check the game state to see if move is valid, hence the lambda
    cellToPlay = ui.requestValidPlayerMove(lambda (row, col): game.isValidMove((row, col)))
    game.playForCurrentPlayer(cellToPlay)

ui.printBoard(game.getBoardAsArray())
# If there is no winner, ui expects None
ui.printGameOver(game.getWinner())

The main thing I dislike is how I validate if a move is a "winning" move, meaning if a move causes the game to end with a winner. The dictionary is pretty ugly and there is some code repetition. I feel like there is not pretty way to do this though. Obviously, I'd also like to know if I made any "noob" mistakes regarding Python standards.

\$\endgroup\$
1
\$\begingroup\$

Firstly, Python has a very well defined official style guide called PEP8, which you should follow. There are tools that are checking whether your code fits its requirements. For example, after I run flake8 tool on your code I've got:

$ python2.7 -m flake8 board.py game.py main.py ui.py
board.py:37:80: E501 line too long (82 > 79 characters)
game.py:3:1: E302 expected 2 blank lines, found 1
game.py:4:80: E501 line too long (113 > 79 characters)
game.py:7:80: E501 line too long (91 > 79 characters)
game.py:8:80: E501 line too long (94 > 79 characters)
game.py:9:80: E501 line too long (94 > 79 characters)
game.py:10:80: E501 line too long (92 > 79 characters)
game.py:11:80: E501 line too long (94 > 79 characters)
game.py:12:80: E501 line too long (93 > 79 characters)
game.py:13:80: E501 line too long (100 > 79 characters)
game.py:14:80: E501 line too long (100 > 79 characters)
game.py:17:80: E501 line too long (122 > 79 characters)
game.py:18:80: E501 line too long (92 > 79 characters)
game.py:19:80: E501 line too long (123 > 79 characters)
game.py:20:80: E501 line too long (93 > 79 characters)
game.py:21:80: E501 line too long (159 > 79 characters)
game.py:22:80: E501 line too long (94 > 79 characters)
game.py:23:80: E501 line too long (125 > 79 characters)
game.py:24:80: E501 line too long (95 > 79 characters)
game.py:25:80: E501 line too long (126 > 79 characters)
main.py:11:80: E501 line too long (118 > 79 characters)
main.py:12:80: E501 line too long (91 > 79 characters)
ui.py:1:1: F401 'np' imported but unused
ui.py:7:1: E302 expected 2 blank lines, found 1
ui.py:19:1: E302 expected 2 blank lines, found 1
ui.py:36:1: E302 expected 2 blank lines, found 1
ui.py:39:1: E302 expected 2 blank lines, found 1
ui.py:45:1: E302 expected 2 blank lines, found 1
ui.py:47:79: E231 missing whitespace after ','
ui.py:47:80: E501 line too long (128 > 79 characters)
ui.py:47:118: E231 missing whitespace after ','
ui.py:49:39: E231 missing whitespace after ','
ui.py:49:78: E231 missing whitespace after ','
ui.py:49:80: E501 line too long (127 > 79 characters)
ui.py:49:117: E231 missing whitespace after ','
ui.py:51:39: E231 missing whitespace after ','
ui.py:51:78: E231 missing whitespace after ','
ui.py:51:80: E501 line too long (127 > 79 characters)
ui.py:51:117: E231 missing whitespace after ','

You should keep your lines shorter, watch the whitespaces around operators and commas and so on.


board.py

# The board is a zero-based index 2D array
# The X player is represented using 1
# The O player is represented using -1
# Empty cells are represented using 0

class Board:
    # ...

We don't document classes in Python like that. Instead do:

class Board:
    """
    The board is a zero-based index 2D array
    The X player is represented using 1
    The O player is represented using -1
    Empty cells are represented using 0
    """
    # ...

So you can display your help using interactive console, for example:

$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import board
>>> help(board.Board)

Similarly, you document methods like this:

def setCell(self, cell, player):
    """Will throw an exception if the cell is invalid"""
    # ...

It's called docstring and is defined in PEP257.

I see that you try to use a lot of "private" variables, by enforcing a double underline before your variables. I'd suggest to drop that habit. This isn't Java, this isn't C++, in Python you usually don't try to hide insides of your class. There's nothing preventing user for accessing any part of the class so there's little point in trying to avoid it. You usually use a single underscore if you want to stress that a particular method or variable isn't really meant for an external use but is a part of the internal implementation that might change in future releases. Double underscores are used even less often. You can read about differences between these in here or by googling "single vs double underscore in Python".

I don't really see a point of using a np.array in a simple tic-tac-toe game. That sounds like an overkill. np library is useful mostly when you want to do some heavy computations on big amount of numbers. For a simple 9x9 grid using a regular list is more that enough.

 >>> l = [['_'] * 3 for l in range(3)]
 >>> l
 [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
 >>> l[0][1] = 'X'
 >>> l
 [['_', X, '_'], ['_', '_', '_'], ['_', '_', '_']]

game.py

Oh my, that looks scary. I think it isn't really the best idea to keep a dictionary of all posible positions in order to check whether the next move is a winning one. You should rather check whether adding a new value caused to a line (row, column o diagonal) to consist of same character (or sum to -3 or 3 if you decide to represent -1 as X and 1 as Y). Try to adapt this algorithm to Python.

For inspiration, here's one way to do it. Assuming you use Python list of lists instead of np.array, for row and column you don't need a loop, list comprehension is enough:

>>> board = [
...     ['X', '_', 'O'],
...     ['X', 'X', 'O'],
...     ['_', 'O', '_']
... ]
>>> 
>>> 
>>> x, y = 1, 1
>>> column = [board[i][x] for i in range(3)]
>>> row = [board[y][i] for i in range(3)]
>>> 
>>> print(column, row)
(['_', 'X', 'O'], ['X', 'X', 'O'])

Anti-diagonal and anti-diagonal consist both of three positions, so you can actually list these to check whether you're on one of them. If you are, you need to check it as well:

>>> board = [
...     ['X', '_', 'O'],
...     ['X', 'X', 'O'],
...     ['_', 'O', '_']
... ]
>>> 
>>> x, y = 1, 1
>>> 
>>> diagonal = []
>>> antidiagonal = []
>>> 
>>> on_diagonal = [(0, 0), (1, 1), (2, 2)]
>>> on_antidiagonal = [(0, 2), (1, 1), (2, 0)]
>>> 
>>> if (x, y) in on_diagonal:
...     diagonal = [board[i][j] for (i, j) in on_diagonal]
... 
>>> if (x, y) in on_antidiagonal:
...     antidiagonal = [board[i][j] for (i, j) in on_antidiagonal]
... 
>>> print(diagonal, antidiagonal)
(['X', 'X', '_'], ['O', 'X', '_'])

After you have these four list of three positions, you can translate '0' and 'X' to numbers and sum them:

>>> translate_table = {
...     'X': -1,
...     '_': 0,
...     'O': -1
... }
>>> l = ['X', '_', 'O']
>>> l = [translate_table[i] for i in l]
>>> l
[-1, 0, -1]

main.py

You often see in Python script a following pattern:

def main():
    # ...

if __name__ == '__main__':
    main()

You can move your code inside this main() function. It's not necessary but usually you'll have a much better performance, since Python handles better code placed inside functions that inside global space.


ui.py

Python handles multi line string literals so there's no need for that:

helpTable = "0,0 | 0,1 | 0,2\n"\
            "---------------\n"\
            "1,0 | 1,1 | 1,2\n" \
            "---------------\n" \
            "2,0 | 2,1 | 2,2\n"

You can just use

def printHelpTable():
    helpTable = """
0,0 | 0,1 | 0,2
---------------
1,0 | 1,1 | 1,2
---------------
2,0 | 2,1 | 2,2"""
    print(helpTable)

If you don't like lack of indentation, then there are two approaches you can take. One style is to group strings inside single parenthesis, so you don't have to use \ sign to mark breaking a line:

def printHelpTable():
    helpTable = (
        "0,0 | 0,1 | 0,2\n"
        "---------------\n"
        "1,0 | 1,1 | 1,2\n"
        "---------------\n"
        "2,0 | 2,1 | 2,2\n"
    )
    print(helpTable)

Alternatively you can use textwrap.dedent function from the standard library

import textwrap

def printHelpTable():
    helpTable = textwrap.dedent("""
        0,0 | 0,1 | 0,2
        ---------------
        1,0 | 1,1 | 1,2
        ---------------
        2,0 | 2,1 | 2,2
    """)
    print(helpTable)

This is an overkill:

tuple(map(lambda i: int(i), wantedMove.split(',')))

Avoid unnecessary uses of map and lambdas. You can convert your move like this:

>>> wantedMove = '2,1'
>>> tuple(int(x) for x in wantedMove.split(','))
(2, 1)

Even better, you don't even need to convert anything. You use raw_input(), which treats input as string. You can simply use input() instead to simply get a pair of numbers:

>>> a, b = input("Type your numbers: ")
Type your numbers: 2, 1
>>> a, b
(2, 1)
>>> type(a), type(b)
(<type 'int'>, <type 'int'>)

Read here.

Finally, printing a board written a bit differently:

board = [
    ['X', '_', 'O'],
    ['X', 'X', 'O'],
    ['_', 'O', '_']
]

for row_num, row in enumerate(board):
    print('|'.join(row))
    if row_num != 2:
        print('-----')
\$\endgroup\$
  • \$\begingroup\$ Your flake8 is expecting Python 3. If you were using a Python 2 version you wouldn't have the message "F821 undefined name 'xrange'". \$\endgroup\$ – Peilonrayz Aug 9 '17 at 17:16
  • \$\begingroup\$ @Peilonrayz good point, I updated the output. \$\endgroup\$ – Nykakin Aug 9 '17 at 17:26
  • \$\begingroup\$ If you don't mind me asking, when I check for PEP8 warnings in my IDE' it says all variable/function should be lower cased (not camelCased). Is this really a standard? \$\endgroup\$ – IEatBagels Aug 9 '17 at 18:03
  • \$\begingroup\$ @TopinFrassi, yep its in PEP8 document here. See here for a cheatsheet. \$\endgroup\$ – Nykakin Aug 9 '17 at 18:28
  • \$\begingroup\$ -1 for recommending using input on python2. There is a reason it was removed from python 3 (it is a code injection). \$\endgroup\$ – Oscar Smith Aug 15 '17 at 14:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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