Imported from jfindlay/tetratile (
AGENTS.md). Install upstream withnpx skills add jfindlay/tetratile. Copyright stays with the author.
Agent Guidelines for Tetratile Project
Project Overview
Tetratile is a tetromino tessellation game built with Python and tkinter.
The implementation is transparently mathematical by design: every
significant type and operation maps directly to a named mathematical
concept. The full mathematical treatment is in docs/mathematics.rst.
Mathematical Design Principles
These principles govern all design decisions in the codebase. Code that deviates from them should be treated as a defect, not a style choice.
The Transform Group
The game operates on a finite rectangular sublattice
:math:\mathcal{B} \subset \mathbb{Z}^2 (the board) with y-up Cartesian
orientation (y=0 at the bottom). Valid piece moves are the
lattice-stabilising discrete versors (motors) of the plane-based
geometric algebra :math:Cl(2,0,1) — see ### On Geometric Algebra
below and docs/mathematics.rst. A motor unifies translation and
rotation under one composition law (the versor sandwich).
The concrete realisation of this versor group, retained as a bridge to
standard group theory and to the code's types, is the semidirect product
:math:G = \mathbb{Z}^2 \rtimes C_4, where :math:\mathbb{Z}^2 is the
integer translation group and :math:C_4 \cong \mathbb{Z}/4\mathbb{Z} is
the cyclic group of quarter-turn rotations. Reflections are excluded —
they produce physically distinct pieces (S ≠ Z, L ≠ J) — so the valid
rotation group is :math:C_4, not the full dihedral group :math:D_4.
This is why the game has 7 one-sided tetrominoes rather than 5 free
ones.
Eigentransformations
An eigentransformation is an atomic generator of :math:G — an
irreducible move from which all compound moves are composed. The type
alias type EigenTransformation = Translation | Rotation names this
concept directly.
Translation(dx, dy): an element of :math:\mathbb{Z}^2.dx > 0is rightward;dy > 0is upward. Gravity isTranslation(0, -1)— explicitly negative :math:y, consistent with the y-up convention.Rotation(steps): an element of :math:C_4.steps=+1is CW;steps=-1is CCW.
The operations move_left_max, move_right_max, and full_drop
are derived (orbit suprema), not generators. They belong in
InputHandler, not in EigenTransformation.
Polyominoes
A polyomino of ordinal :math:n is a connected finite subset of
:math:n unit cells of :math:\mathbb{Z}^2. In code:
Polyomino.squares: frozenset[Square]— afrozensetcaptures the correct set semantics (unordered, no duplicates, immutable).Square(x, y)is aNamedTupleidentifying a unit cell by its lower-left corner.Polyomino.ordinal=len(squares);ordinal == 4↔ tetromino.
The Board as an Occupancy Map
The locked-piece state is a partial function
:math:\mathcal{G}: \mathcal{B} \rightharpoonup \text{PieceName},
encoded as dict[Square, str]. Presence of a key = occupied; absence
= empty. The active piece is tracked separately in
TetraTile.piece and is never written to :math:\mathcal{G}.
Value Semantics
Polyomino.translate() and Polyomino.rotate() return new
Polyomino instances (or None if blocked). They do not mutate.
This reflects the group action: applying a group element to a piece yields
a new state. Value semantics eliminates copy.deepcopy.
Functional Boundary Kicks
Rotation kicks are algebraically derived from the rotated piece's
bounding box versus the grid domain — analogous to a covariant derivative.
No precomputed state-pair tables. The _boundary_kicks generator yields
minimal corrective Translation values in priority order (in-place,
horizontal, vertical, corner), at most four candidates.
Half-Integer Origins
Pieces whose geometric centre of symmetry is a half-integer point (Z, S,
I, O) store origin = (Decimal('-0.5'), Decimal('-0.5')) in local
coordinates. Decimal arithmetic preserves these values exactly
through all four rotation states, giving exact rotation with no
truncation drift.
Separation of Concerns
Grid (occupancy map, pure game state) has no Tkinter dependency and is
fully unit-testable. Board (Tkinter canvas) is a pure rendering
surface with no game logic.
Coequal Input Frontends
HumanInputHandler and AgentInputHandler are structurally identical
subclasses of InputHandler. All movement methods call
TetraTile.move_piece() with an EigenTransformation value
(a Translation or Rotation); the state guard
(GameState.running) lives there. Neither frontend has
privileged access to the game.
N-Dimensional Generalization
The design anticipates N-dimensional polyhypercube games, with the
algebra :math:Cl(N,0,1) as the uniform framework. Translation
becomes an N-vector (a product of null-bivector translators);
Rotation gains a plane: tuple[int,int] parameter that selects a
Euclidean bivector :math:e_{ij} — one of the :math:\binom{N}{2}
rotation planes. The rotation formula is already in the N-dimensional
form, with the single bivector :math:e_{12} (plane (0, 1))
hardcoded for 2D. _boundary_kicks extends to all N axes. The
concrete-realisation rotation group is the proper rotation subgroup
:math:B_N^+ of the hyperoctahedral group :math:B_N.
On Geometric Algebra (GA / Clifford Algebra)
Discrete plane-based geometric algebra (PGA) — the lattice-stabilising
versor subgroup of :math:Cl(N,0,1) — is the primary mathematical
framework, chosen deliberately for unification and pedagogy. A single
motor encodes both translation and rotation; one composition law (the
versor sandwich) governs every move; the construction generalises
uniformly to N dimensions. The semidirect product
:math:\mathbb{Z}^N \rtimes B_N^+ and the rotation matrices are retained
as bridges to standard group theory and to the code's
Translation / Rotation types — the same group in older language.
The earlier "set aside" verdict was reversed. The continuity objection
(rotors live in continuous :math:\mathrm{Spin}(N)) is answered by the
word discrete: only the finite order-4 rotors and integer translators
are used. The translation-overhead objection was overstated — plane-based
PGA adds a single null generator :math:e_0, not a heavy embedding, and
translations become native. The integer rotor :math:U = 1 + e_{ij}
(divided by :math:\lVert U \rVert^2 = 2) is lattice-exact: the
:math:\sqrt 2 cancels in every sandwich, so rotation needs no floating
point or Decimal. Conformal GA remains unnecessary; it would matter
only for a continuous-physics extension. Full treatment in
docs/mathematics.rst; the code refactor is planned in
docs/PLAN.md.
Docstring Standards
All docstrings follow this format:
"""Succinct one-line summary.
Optional paragraph with more details. Can span multiple lines.
:param param: Description of parameter.
:param param: Description of another parameter.
:returns: Description of return value.
:raises ExceptionType: Description of when this exception is raised.
:attr attr: Description of attribute.
:yields: Description of yielded value.
"""
"""
**Rules:**
1. Succinct summary string as first line (ends with period)
2. Optional detailed paragraph or bullet points
3. Every named code block (package, module, class, function) requires a docstring
4. Public classes/functions: document attributes, parameters, returns, and raises
5. Omit typing in docstrings - code is fully type annotated
6. Use sphinx rST shorthand markup:
- `:param name:` - function/method parameters
- `:returns:` - return value
- `:raises ExceptionType:` - exceptions
- `:attr name:` - class attributes
- `:yields:` - generator yield value
- Cross-references: ``:class:`ClassName` ``, ``:meth:`method` ``
- Code literals: ```code``` for code/names
- Parameters with backticks: ```param```
- Math (rST latex): ``$\mathbb Z^{\mathrm dim}$``
## Type Annotations
- No ``typing.Any`` - use specific types
- Use composition over inheritance for tkinter classes (e.g., StringVar wrappers)
- Use TypedDict for dict structures with known keys
- Use dataclass for data containers
## Code Style
- Line length: 128 characters
- Target Python version: 3.12+
- Use dataclasses where appropriate
- Avoid `isinstance()` checks for enum members - use `match/case`
## Dependencies
- **Runtime**: pydantic>=2.0
- **Build**: hatchling, hatch-vcs
- **Dev**: pytest, pytest-cov, pytest-mock, pyfakefs, ruff, mypy
## Build & Test Commands
```bash
# Install all deps including dev
uv sync
# Build + test + check_type + check_lint
uvx tox -m check
# Auto-fix imports and formatting
uvx tox -e fix_format
# Run individual tox environments
uvx tox -e build # build wheel
uvx tox -e test # run all tests with coverage
uvx tox -e check_type # mypy type checking
uvx tox -e check_lint # ruff lint + format check
# Build package wheel
uv build --wheel
Project Structure
tetratile/
├── docs/
│ ├── mathematics.rst # Mathematical treatise
│ └── BACKLOG.md # Deferred issues and known gaps
├── src/tetratile/
│ ├── __init__.py # Core types, Polyomino, Grid, Board, TetraTile
│ ├── __main__.py # CLI entry point
│ ├── agent.py # Agent ABC, Action enum, RandomAgent
│ ├── agent_runner.py # AgentRunner: wires Agent to TetraTile
│ ├── config.py # Configuration with Pydantic models
│ ├── config_ui.py # Preferences dialog
│ ├── event_log.py # Event logging
│ ├── input_agent.py # AgentInputHandler (coequal agent frontend)
│ ├── input_handler.py # InputHandler base class with concrete defaults
│ ├── input_human.py # HumanInputHandler (coequal human frontend)
│ ├── log_viewer.py # Log viewer widget
│ ├── output.py # OutputHandler, PrintObserver, AgentOutputHandler
│ └── py.typed # PEP 561 marker for type checkers
├── tests/
│ ├── unit/
│ │ └── test_tetratile.py # Unit tests
│ └── integration/
│ ├── conftest.py
│ ├── test_board.py
│ ├── test_config.py
│ ├── test_event_logging.py
│ ├── test_game_flow.py
│ ├── test_observation.py
│ ├── test_rotation_edge.py
│ ├── test_rotation_free.py
│ ├── test_row_removal.py
│ ├── test_srs_rotation.py # boundary kick and rotation tests
│ └── test_translation.py
├── .coveragerc # Coverage configuration (omit patterns, fail_under)
├── pyproject.toml # Project configuration (tox, ruff, mypy, pytest)
└── README.md # User documentation
Key Classes
- TetraTile: Main game window, event loop, and game controller
- Board: Tkinter canvas — pure rendering surface (no game state);
render(grid, active, locked_dirty=False)uses a targeted delta strategy (O(piece.ordinal) per tick); full repaint only whenlocked_dirty=True - Grid: Locked-piece occupancy map as
dict[Square, str]— pure game state (no rendering) - Polyomino: Immutable frozen dataclass;
squares: frozenset[Square];translate/rotatereturn new instances (value semantics);rotateacceptskick: bool - Colors: Immutable
NamedTuple(normal, light, dark)— rendering colors for a piece - TetrominoType: Enum of the 7 one-sided tetromino definitions
- TetrominoData: Frozen dataclass with tetromino spawn geometry
- InputHandler: Coequal base class for human and agent input
- Agent: Pure decision function
GameObservation → Action - AgentRunner: Owns game + agent; drives the action loop; returns
GameResult - GameResult: Frozen dataclass — final stats, terminal observation, step count
- OutputHandler: Push-notification observer interface
Tetromino Enumeration
Tetrominoes are defined as:
TetrominoData— frozen dataclass with immutable spawn-state geometryTetrominoType— enum of all 7 one-sided tetromino typestetrominoes— tuple of instantiatedPolyominoobjects
Configuration
- Pydantic models validate config at runtime
- JSON file format for persistent config
- CLI arguments override config file
- Defaults in Pydantic Field definitions
Version Management
- Version derived from git tags via hatch-vcs
- Runtime version via
importlib.metadata.version() - Fallback version: "0.0" (only if git unavailable)