noughts-crosses

alt text

Noughts and Crosses
(SWE Home Assignment)

This is a simple implementation of the game Noughts and Crosses (Tic Tac Toe) in Python. The game is played on a 3x3 grid and the first player to get 3 of their marks in a row (horizontally, vertically or diagonally) wins the game.

The original code was compiled to webassembly using Pyodide, a Python environment for the browser. An online demo of the game can be found here.


Requirements

Installation

To install Pygame, run the following command:

Set virtual environment (recommended):

python3 -m venv .venv
source .venv/bin/activate

Install requirements:

pip install -r requirements.txt

Run the game:

python3 src/main.py

How to play

The game is designed for 2 players. Player 1 is ‘X’ and Player 2 is ‘O’. Players take turns to place their mark on the grid by clicking on an empty cell. The game ends when one of the players gets 3 of their marks in a row or when there are no more empty cells left on the grid.

The gameplay is straightforward, as can be seen in the GIF below. The scoreboard keeps track of the number of wins for each player. The game can be reset at any time by clicking the “Reset” button.

Note that the reset button is hidden and apears during a tie, but can also be clicked during game.

Code Overview

The game consists of the following classes (if you are unable to view the diagram, please refer to the class_diagram.png file):

classDiagram
    class EventHandler {
        +EventHandler(game, board_renderer, score_renderer, reset_button)
        -game : NoughtsCrosses
        -board_renderer : BoardRenderer
        -score_renderer : ScoreRenderer
        -reset_button : pygame.Rect
    }
    class GameBoard {
        +GameBoard(game, board_size)
        -game : NoughtsCrosses
        -score_keeper : ScoreKeeper
        -board_renderer : BoardRenderer
        -score_renderer : ScoreRenderer
        -event_handler : EventHandler
        -screen : pygame.Surface
        -reset_button : pygame.Rect
        -board_size : tuple
        -screen_size : tuple
        -font_size : int
    }
    class Renderer {
        +Renderer(screen, board_size)
        -_screen : pygame.Surface
        -_board_size : tuple
        -_font_size : int
    }
    class BoardRenderer {
        +BoardRenderer(screen, board_size, grid_size)
        -_screen : pygame.Surface
        -_board_size : tuple
        -_grid_size : int
        -_board_lines : list
        -_o_img : pygame.Surface
        -_x_img : pygame.Surface
        -_small_font : pygame.Font
    }
    class ScoreKeeper {
        +ScoreKeeper()
        -x_wins : int
        -o_wins : int
    }
    class ScoreRenderer {
        +ScoreRenderer(screen, board_size, font_size)
        -_screen : pygame.Surface
        -_board_size : tuple
        -_font_size : int
        -_font : pygame.Font
        -_small_font : pygame.Font
    }
    class NoughtsCrosses {
        +NoughtsCrosses()
        -board : list
        -current_player : str
        -winning_combo : None
    }
    GameBoard --> EventHandler
    GameBoard --> BoardRenderer
    GameBoard --> ScoreRenderer
    GameBoard --> ScoreKeeper
    GameBoard --> NoughtsCrosses
    BoardRenderer --> Renderer
    ScoreRenderer --> Renderer

Error Handling

The program utilize Python try and except blocks to catch exceptions and handle errors. For example, the run function in the GameBoard class catches exceptions that occur during the game loop and prints an error message to the console. Inplace error handling was also included, such as raising ValueError when the game logic encounters an invalid move in the make_move method of the NoughtsCrosses class.

Where program flow could continued, a fallback procedure was implemented. For example, when the images for the ‘X’ and ‘O’ marks could not be loaded, the program would fallback to generate the figures using Pygame’s drawing functions.

alt text

Testing

The tester.py module contains unit tests for the classes in the renderer.py, gameboard.py, and event_handler.py files. These tests are designed to ensure that the game’s components are functioning as expected.

The tests are organized into several classes, each of which corresponds to a class in the game’s code. Each class contains a setUp method, which initializes the game, board size, and other necessary components for the tests.

To run the tests, execute the tester.py file. If all tests pass, the components of the game are working as expected. If any test fails, it indicates a problem with the corresponding component of the game.

Edge Cases

The game was tested with various edge cases to ensure that it behaves correctly in all scenarios. Some of the edge cases tested include:

alt text
alt text