Tic-Tac-Toe Gauntlet
A terminal tic-tac-toe game that gets harder the more you win. Beat the random Rookie to unlock the rule-based Contender, then the unbeatable Minimax Champion. Every game and move is streamed into a local DuckDB database.
Built with Python + uv + Pydantic + Textual + DuckDB.
Quickstart
No clone, no database setup — the database is created on first run.
uvx ttt-gauntlet play # try it without installing anything
Or install it:
pip install ttt-gauntlet
ttt-gauntlet play # play the gauntlet (interactive TUI)
ttt-gauntlet stats # see your record
(uvx ships with uv; pipx run ttt-gauntlet play
works the same way.)
How it plays
- You are X and always move first.
- Win to advance to the next tier. Lose or draw to replay the tier.
- Progress is remembered across sessions —
ttt-gauntlet playresumes just past your best win. Force a tier withttt-gauntlet play --level 3. - A live stats panel sits to the right of the board — record, current/best win streak, average moves per win, win rate per tier, and a trend of your recent results — refreshed after every game.
Controls: click a square (or navigate with the keyboard), N to continue after
a game, Q to quit.
The difficulty ladder
| Level | Tier | Engine |
|---|---|---|
| 1 | Rookie | Random legal move |
| 2 | Contender | Heuristic: win → block → center → corner → edge |
| 3 | Champion | Minimax — optimal, cannot be beaten |
Architecture
The rules engine and the AI are pure and UI-agnostic, so the same logic backs the game, the opponents, and the tests.
src/ttt/
models.py # Pydantic models (Board, GameEvent, …)
engine/game.py # pure tic-tac-toe rules (single source of truth)
opponents/ # Opponent abstraction + random / heuristic / minimax + ladder
data/db.py # DuckDB connection + GameRepository
tui/app.py # Textual UI (thin: renders + dispatches only)
cli.py # Typer commands: play / stats
Data model
game_events is an append-only event stream — the source of truth for
everything that happens (game_started / move_made / game_ended). The
games and moves tables are projections written alongside it to keep common
queries simple.
The database lives at $XDG_DATA_HOME/ttt/ttt.duckdb (or
~/.local/share/ttt/ttt.duckdb). Override with TTT_DB_PATH.
Roadmap
Planned features are written up as one-file specs — these are the units of work fed to the OpenHands PR automation.
Development
Requires uv.
git clone https://github.com/cmacdonald0514/tictactoe.git
cd tictactoe
uv sync # install dependencies
uv run ttt-gauntlet play # run from the working tree
uv run pytest # unit tests (engine, opponents, persistence)
uv run ruff check # lint
uv run ruff format # format
Releasing
Bump version in pyproject.toml and merge to master. The release workflow
compares that version against the previous commit and publishes to PyPI via
Trusted Publishing when it changes;
merges that leave the version alone publish nothing.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ttt_gauntlet-0.2.0.tar.gz.
File metadata
- Download URL: ttt_gauntlet-0.2.0.tar.gz
- Upload date:
- Size: 35.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a870efedc84d6a83f631306a5ae82f3e32f72c0385ef7389b0ec9886a591b5a5
|
|
| MD5 |
d4e8df555acf558b5a22b4032bf49490
|
|
| BLAKE2b-256 |
d9db4eced8590a981cc66c99430dc4d6260f897c3392a0621bc78d358f00985c
|
Provenance
The following attestation bundles were made for ttt_gauntlet-0.2.0.tar.gz:
Publisher:
release.yml on cmacdonald0514/tictactoe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ttt_gauntlet-0.2.0.tar.gz -
Subject digest:
a870efedc84d6a83f631306a5ae82f3e32f72c0385ef7389b0ec9886a591b5a5 - Sigstore transparency entry: 2300967628
- Sigstore integration time:
-
Permalink:
cmacdonald0514/tictactoe@04dc7109dfaae250baef4b26586cef8edbdc6b07 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/cmacdonald0514
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@04dc7109dfaae250baef4b26586cef8edbdc6b07 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ttt_gauntlet-0.2.0-py3-none-any.whl.
File metadata
- Download URL: ttt_gauntlet-0.2.0-py3-none-any.whl
- Upload date:
- Size: 18.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
84086aa4f2c78b3776ba7352078955616b792769523efed5cf2ea4ec37a4dfec
|
|
| MD5 |
b2e34d01a6704e5a7c3df3eac2f4c584
|
|
| BLAKE2b-256 |
008411e92230ec6e2a9d9bccba566ec339a50a38d5ea30fad96e7c52a7149c0e
|
Provenance
The following attestation bundles were made for ttt_gauntlet-0.2.0-py3-none-any.whl:
Publisher:
release.yml on cmacdonald0514/tictactoe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ttt_gauntlet-0.2.0-py3-none-any.whl -
Subject digest:
84086aa4f2c78b3776ba7352078955616b792769523efed5cf2ea4ec37a4dfec - Sigstore transparency entry: 2300967883
- Sigstore integration time:
-
Permalink:
cmacdonald0514/tictactoe@04dc7109dfaae250baef4b26586cef8edbdc6b07 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/cmacdonald0514
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@04dc7109dfaae250baef4b26586cef8edbdc6b07 -
Trigger Event:
push
-
Statement type: