xo

A Tic-tac-toe CLI game and library.


Keywords
tic-tac-toe, tic, tac, toe, noughts, crosses
License
MIT
Install
pip install xo==1.0.0

Documentation

xo

A Python CLI game and library for Tic-tac-toe.

The library is written in a modular way. Its overall design consists of 4 decoupled components:

  1. A Tic-tac-toe board data structure, xo.board.
  2. An arbiter for analyzing the state of a board, xo.arbiter.
  3. A game engine to implement and enforce the Tic-tac-toe game logic, xo.game.
  4. And finally, an AI for finding excellent moves, xo.ai.

The board

>>> from xo.board import isempty, Board

>>> board = Board.fromstring('..x.o')
>>> print(board)
..x.o....

>>> print(board.toascii())
   |   | 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')
{
  'piece_counts': {'os': 0, 'xs': 0, 'es': 9},
  'status': 'in-progress'
}

>>> arbiter.outcome(Board.fromstring('xxxoo'), 'o')
{
  'piece_counts': {'os': 2, 'xs': 3, 'es': 4},
  'details': [
    {'index': 1, 'positions': [(1, 1), (1, 2), (1, 3)], 'where': 'row'}
  ],
  'status': 'gameover',
  'reason': 'loser'
}

>>> arbiter.outcome(Board.fromstring('xxxxxxxxo'), 'x')
{
  'piece_counts': {'os': 1, 'xs': 8, 'es': 0},
  'status': 'invalid',
  'reason': 'too-many-moves-ahead'
}

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': {'token': 'x', 'r': 1, 'c': 1}
}
>>> 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': {'token': 'o', 'r': 2, 'c': 2}
}
>>> game.moveto(3, 1)
{
  'name': 'next-turn',
  'last_move': {'token': 'x', 'r': 3, 'c': 1}
}
>>> print(game.board.toascii())
 x |   |
---+---+---
   | o |
---+---+---
 x |   |

>>> game.moveto(3, 3)
{
  'name': 'next-turn',
  'last_move': {'token': 'o', 'r': 3, 'c': 3}
}
>>> game.moveto(2, 1)
{
  'name': 'gameover',
  'reason': 'winner',
  'last_move': {'token': 'x', 'r': 2, 'c': 1},
  'details': [{'index': 1, 'positions': [(1, 1), (2, 1), (3, 1)], 'where': 'column'}]
}

>>> game.moveto(1, 3)
...
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)
>>> print(game.board.toascii())
 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 install xo

You would now have access to an executable called xo. Type

$ xo

to starting 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

Get the source code.

$ git clone git@github.com:dwayne/xo-python.git

Create a virtual environment and activate it.

$ cd xo-python
$ pyvenv venv
$ . venv/bin/activate

Then, upgrade pip and setuptools and install the development dependencies.

(venv) $ pip install -U pip setuptools
(venv) $ pip install -r requirements-dev.txt

You're now all set to begin development.

Testing

Tests are written using the unittest unit testing framework.

Run all tests.

(venv) $ python -m unittest

Run a specific test module.

(venv) $ python -m unittest tests.test_arbiter

Run a specific test case.

(venv) $ python -m unittest tests.test_arbiter.GameoverPositionsTestCase

Run a specific test method.

(venv) $ 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.

Copyright

Copyright (c) 2016 Dwayne Crooks. See LICENSE for further details.