Skip to content

Tic-Tac-Toe

Let’s Play a Game

Small games like Tic-Tac-Toe have a very easy ruleset and you can implement them in a Python program with what you already know. Since this is a rather long task, instead of letting you figure out all the details by yourself, you will be given some guidance.

Preparations

Start by creating a separate folder tic_tac_toe for your project.

We will need the file constants.py inside to note down some constants to make our program a bit more readable. Further, a __main__.py will hold the central part of our program.

Some Things never Change

In the constants.py you will have to define the following:

  • Assign some symbols to PLAYER_X and PLAYER_O respectively
    • These are used to identify the players on the board later, so using letters makes sense here
    • Please use the exact names for these constants, some other code later depends on it.

Other Peoples’ Code

One major hurdle would be how to represent the current state of the board. You have 9 cells which could either hold the symbol of PLAYER_O or PLAYER_X or none of both.

Luckily you already found some code that might help you online, which you are allowed to use. It seems to boil down to some mathematical shenannigans that allow you to encode the state of your board into an integer. While you do not need to understand all its intricacies, you will have to figure out how to use it.

The Code you found

from constants import PLAYER_O, PLAYER_X

# === Module internal constants ===
# They are only needed for the internal workings of the module
_COLUMNS_PER_ROW = 3
_BITS_PER_QUARTER_BYTE = 2
_ENCODING = {  # How to map a cell state to a quarter-byte
    PLAYER_X: 0b01,
    PLAYER_O: 0b10
}
_DECODING = {  # The reverse mapping for the ENCODING
    value: key for key, value in _ENCODING.items()
}

# === Module internal functions ===
# They are only here as helpers
# for the internal working of the module
def _offset(row, column):
    quarter_bytes = row * _COLUMNS_PER_ROW + column
    return quarter_bytes * _BITS_PER_QUARTER_BYTE

def _print_state(board_state):
    print(f"{board_state:032b}")

def _clear_cell(board_state, row, colum):
    bitmask = ~(0b11 << _offset(row, column))
    return board_state & bitmask

# === Module public functions ===
# You may import/ use them as you see fit.
def create_empty_board():
    """Create an empty 3×3 board.

    Returns:
        The state of an empty board encoded as an integer
    """
    return 0

def set_player(board_state, row, column, player):
    """Set a cell of a board to be occupied by a player.

    This does not check if the cell is already occupied.
    Providing incorrect values for any of the arguments may have unexpected results.

    Args:
        board_state: An integer encoding the board state before the change.
        row: The row of the cell to be set. Must be in the interval (0…2).
        column: The column of the cell to be set. Must be in the interval (0…2).
        player: The player which occupies the cell, either `PLAYER_X`, `PLAYER_O` or `None`.
    Returns:
        The board state encoded as integer after the change was made.
    """
    if player is None:
        return _clear_cell(board_state, row, column)

    bitmask = _ENCODING[player] << _offset(row, column)
    return board_state | bitmask

def get_player(board_state, row, column):
    """Get the player who occupies a given cell.

    Providing incorrect values for any of the arguments may have unexpected results.

    Args:
        board_state: An integer encoding the board state from which to extract the player.
        row: The row of the cell to be gotten. Must be in the interval (0…2).
        column: The column of the cell to be gotten. Must be in the interval (0…2).
    Returns:
        The player which occupies the cell, either `PLAYER_X`, `PLAYER_O` or `None`.
    """
    flags = (board_state >> _offset(row, column)) & 0b11
    return _DECODING.get(flags, None)

def is_occupied(board_state, row, column):
    """Checks if a cell is occuoied by a player.

    Providing incorrect values for any of the arguments may have unexpected results.

    Args:
        board_state: An integer encoding the board state which to check.
        row: The row of the cell to be checked. Must be in the interval (0…2).
        column: The column of the cell to be checked. Must be in the interval (0…2).
    Returns:
        `True` if a player occupies the field, `False` otherwise.
    """
    return bool(get_player(board_state, row, column))

Copy the new-found code into a separate file board.py. Consider which of the provided functions you will need in your program and import them into __main__.py.

Bookkeeping

During the game you will need to keep track of some information. In __main__.py, create the appropriate variables for the following data. Consider which data types and initial values they might have.

  • The board_state which encodes which fields are already taken and by whom
  • The current_player who is about to make a move.
  • The winner, as soon as there is one

Little Helpers

You will need to repeatedly do certain things, so it might be a good idea to write some functions for those. If any of those functions becomes to unwieldy consider to split it into smaller pieces or move it to its own file. Don’t forget to add some documantation,this can also help with getting to grips with the problem at hand.

You can try out each of the functions in isolation to test if they do what they are supposed to.

Print the current state of the board.

It could look something like this:

 X |   | O 
---+---+---
   |   | X 
---+---+---
 O |   | X 

Note that you already have some utility functions to extract the current player at a given position.


  • Parameters: board_state
  • Returns: (Nothing)

Switch from one player to the next. Also print some nice text to inform the players who is next.

Note that you aleady have some constants for each of the players, make good use of them!


  • Parameters: current_player
  • Returns: The next player

Ask the current player to select a row / column respectively which can be used to specify the cell where the player wants to place their mark. Make sure that the inputs are integers in the intervals (0…2).


  • Parameters: (Nothing)
  • Returns: The row / column selected by the player as integer.

Check the board if one of the following conditions are fulfilled (in the given order):

  • One player has occupied a complete row, column or diagonal
    • That player would then be the winner
  • All fields are occupied
    • No more moves are possible, resulting in a draw
    • Add a constant to represent a DRAW, since None would be used to represent that the game is not yet over

  • Parameters: board_state
  • Returns: The winning player, DRAW or None if the game is not over yet

Tying it all Together

With all those building pieces, you can now implement a whole game in the __main__.py. Here is a suggested program flow to give you some inspiration. (Of course you may chose your own approach if you desire to do so.)

Kroki

Rematch

After some rounds, your fellow players have come up with some additional suggestions:

  • Run the game in a loop so it automatically starts a new game once the old one is over
  • Keep a score of
    • How many games have been played
    • How often each player won
  • Print the wins/total games ratio in percent for each player

Closing Remarks

Simple games like theese are a good way to train programming. The rules are not overburdening and well-known so you can go directly to problem-solving. Breaking these problems down into step-by-step solutions and considering how to represent the game situations in a program is a very good exercise for training to think in the more strict ruleset that programming imposes.

It is strongly recommended to revisit this exercise once you have learned more. This first implementation is still very much guided and does not take advantage of all the possible features Python offers to you. Once you get to know more language features you will likely find new ways to represent the board and structure the program flow.