xo
A Tic-tac-toe CLI game and library written in Python.
The library is written in a modular way. Its overall design consists of 4 decoupled components:
- A Tic-tac-toe board data structure,
xo.board. - An arbiter for analyzing the state of a board,
xo.arbiter. - A game engine to implement and enforce the Tic-tac-toe game logic,
xo.game. - And finally, an AI for finding excellent moves,
xo.ai.
The board
>>> from xo.board import Board
>>> from xo.token import isempty
>>> board = Board.fromstring('..x.o')
>>> print(board)
..x.o....
>>> print(board.toascii()) # doctest: +NORMALIZE_WHITESPACE
| | x
---+---+---
| o |
---+---+---
| |
>>> board[1, 3]
'x'
>>> board[3, 3] = 'x'
>>> print(board)
..x.o...x
>>> for r, c, piece in board:
... if isempty(piece):
... print('{}, {}'.format(r, c))
...
1, 1
1, 2
2, 1
2, 3
3, 1
3, 2
The board isn't concerned with whether or not a given layout can be reached in an actual Tic-tac-toe game. Hence, the following is perfectly legal:
>>> board = Board.fromstring('xxxxxxxxo')
>>> print(board)
xxxxxxxxo
The arbiter is concerned about that though and can detect such invalid board layouts.
The arbiter
>>> from xo import arbiter
>>> from xo.board import Board
>>> arbiter.outcome(Board.fromstring(), 'x')
{'status': 'in-progress', 'piece_counts': {'xs': 0, 'os': 0, 'es': 9}}
>>> arbiter.outcome(Board.fromstring('xxxoo'), 'o')
{'status': 'gameover', 'reason': 'loser', 'details': [{'where': 'row', 'index': 1, 'positions': [(1, 1), (1, 2), (1, 3)]}], 'piece_counts': {'xs': 3, 'os': 2, 'es': 4}}
>>> arbiter.outcome(Board.fromstring('xxxxxxxxo'), 'x')
{'status': 'invalid', 'reason': 'too-many-moves-ahead', 'piece_counts': {'xs': 8, 'os': 1, 'es': 0}}
The game engine
Enforcer of the game rules.
>>> from xo.game import Game
>>> game = Game()
>>> game.start('x')
>>> game.moveto(1, 1)
{'name': 'next-turn', 'last_move': {'r': 1, 'c': 1, 'token': 'x'}}
>>> game.moveto(1, 1)
{'name': 'invalid-move', 'reason': 'occupied'}
>>> game.moveto(0, 0)
{'name': 'invalid-move', 'reason': 'out-of-bounds'}
>>> game.moveto(2, 2)
{'name': 'next-turn', 'last_move': {'r': 2, 'c': 2, 'token': 'o'}}
>>> game.moveto(3, 1)
{'name': 'next-turn', 'last_move': {'r': 3, 'c': 1, 'token': 'x'}}
>>> print(game.board.toascii()) # doctest: +NORMALIZE_WHITESPACE
x | |
---+---+---
| o |
---+---+---
x | |
>>> game.moveto(3, 3)
{'name': 'next-turn', 'last_move': {'r': 3, 'c': 3, 'token': 'o'}}
>>> game.moveto(2, 1)
{'name': 'gameover', 'reason': 'winner', 'last_move': {'r': 2, 'c': 1, 'token': 'x'}, 'details': [{'where': 'column', 'index': 1, 'positions': [(1, 1), (2, 1), (3, 1)]}]}
>>> game.moveto(1, 3)
Traceback (most recent call last):
...
xo.error.IllegalStateError: gameover
>>> # start a new game
>>> game.restart()
>>> # since x won, it would be x's turn to play
>>> # if the game was squashed then it would have been o's turn to play
>>> game.moveto(1, 1)
{'name': 'next-turn', 'last_move': {'r': 1, 'c': 1, 'token': 'x'}}
>>> print(game.board.toascii()) # doctest: +NORMALIZE_WHITESPACE
x | |
---+---+---
| |
---+---+---
| |
The AI
No Tic-tac-toe library is complete without an AI that can play a perfect game of Tic-tac-toe.
>>> from xo import ai
>>> from xo.board import Board
>>> ai.evaluate(Board.fromstring('xo.xo.'), 'x')
MinimaxResult(score=26, depth=1, positions=[(3, 1)])
>>> ai.evaluate(Board.fromstring('xo.xo.'), 'o')
MinimaxResult(score=26, depth=1, positions=[(3, 2)])
>>> ai.evaluate(Board.fromstring('x.o'), 'x')
MinimaxResult(score=18, depth=5, positions=[(2, 1), (3, 1), (3, 3)])
Finally, xo.cli brings it all together in its implementation of the command-line Tic-tac-toe game. It's interesting to see how easy it becomes to implement the game so be sure to check it out.
Note: An extensive suite of tests is also available that can help you better understand how each component is supposed to work.
Installation
Install it using pip, or your favourite Python package manager:
pip install xo
You would now have access to an executable called xo. Type
xo
to start playing immediately.
Usage
For help, type
xo -h
By default xo is configured for a human player to play with x and a computer player to play with o. However, this can be easily changed to allow any of the other 3 possibilities:
# Computer vs Human
xo -x computer -o human
# Human vs Human
xo -x human -o human
xo -o human # since x defaults to human
# Computer vs Computer
xo -x computer -o computer
xo -x computer # since o defaults to computer
You can also change who plays first. By default it's the x player.
# Let o play first
xo -f o
Finally, when letting the computers battle it out you can specify the number of times you want them to play each other. By default they play 50 rounds.
xo -x computer -r 5
# .....
#
# Game statistics
# ---------------
# Total games played: 5 (2.438 secs)
# Number of times x won: 0
# Number of times o won: 0
# Number of squashed games: 5
Development
You'll need Nix with flakes enabled.
git clone git@github.com:dwayne/xo-python.git
cd xo-python
nix develop
You're now all set to begin development. Some common tasks are available through make:
| Command | What it does |
|---|---|
make build |
Build the sdist and wheel into dist/ |
make check |
Build the package, run the tests and doctests, and check metadata |
make check-all |
Run make check, then the tests on the other supported Python versions |
make clean |
Remove build artifacts |
Testing
Tests are written using the unittest unit testing framework.
Run everything with make check, or run tests directly:
python -m unittest
Run a specific test module.
python -m unittest tests.test_arbiter
Run a specific test case.
python -m unittest tests.test_arbiter.GameoverPositionsTestCase
Run a specific test method.
python -m unittest tests.test_arbiter.GameoverPositionsTestCase.test_when_x_wins
Credits
Thanks to Patrick Henry Winston for clarifying the Minimax algorithm. His video on the topic was a joy to watch.
Metadata
Release files for xo 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xo-1.1.0.tar.gz | 10.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xo-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 23.0 kB
Release files / xo-1.1.0.tar.gz
| Download URL | xo-1.1.0.tar.gz |
|---|---|
| Size | 10.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f70893cce7b3945814cbb371decf56a62d315c45bf696b7246fa318e3cf9ce88
|
|
BLAKE2b-256 checksum How to use checksums |
2bbf6657426a811c4a2a94410a33f5af4523c1e6a78093a0e9cb4701727cb7b7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Pop!_OS","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / xo-1.1.0-py3-none-any.whl
| Download URL | xo-1.1.0-py3-none-any.whl |
|---|---|
| Size | 12.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
54d364099bc50f65d95bbf417bd909bc4bf82c1f2af5f3f2a18c0819adb3ceb7
|
|
BLAKE2b-256 checksum How to use checksums |
3051049d1fce6ded0e81fd36f85d40419873b5062b2ffa408c346282896ae664
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Pop!_OS","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|