# Chess CLI — Agent Guide

## Project Overview

Terminal chess game in Python. Play against Stockfish with real-time eval and post-game analysis.

## Stack

- **Python 3.10+** with **python-chess** and **textual** (TUI framework)
- **uv** for package management, **hatchling** build backend
- **Stockfish** must be on PATH (`brew install stockfish`)
- Entry point: `chess-cli = "chess_cli.main:main"`

## Running

```bash
uv sync                                           # install deps
uv run chess-cli                                  # interactive menu
uv run chess-cli play --color white --difficulty beginner
uv run chess-cli analyze game.pgn --depth 18
uv run chess-cli --version
```

## Project Structure

```
chess_cli/
├── main.py              # CLI entry, argparse, launches Textual app
├── board_render.py      # Legacy ANSI rendering (kept for analysis.py color constants)
├── engine.py            # Stockfish wrapper: difficulty presets, eval, move generation
├── game.py              # GameState: board, move history, PGN, undo (model)
├── analysis.py          # Post-game analysis: CP loss, classification, report
└── tui/
    ├── __init__.py      # Exports ChessApp
    ├── app.py           # ChessApp(App) — screen routing, CLI arg dispatch
    ├── screens/
    │   ├── menu.py      # MenuScreen — color/difficulty selection
    │   ├── game.py      # GameScreen — board, eval, moves, engine workers
    │   └── analysis.py  # AnalysisScreen — progress bar, results display
    ├── widgets/
    │   ├── board.py     # BoardWidget — Rich-rendered chess board
    │   ├── eval_bar.py  # EvalBarWidget — vertical eval bar
    │   ├── move_list.py # MoveListWidget — scrollable two-column notation
    │   ├── player_info.py # PlayerInfoWidget — name, captured pieces
    │   └── promotion.py # PromotionModal — piece selection dialog
    └── styles/
        └── app.tcss     # Global Textual CSS
```

## Architecture Rules

### Module Boundaries
- **game.py** and **engine.py** have no cross-dependencies — they are independent
- **analysis.py** depends only on board_render (for ANSI color constants)
- **main.py** is minimal — argparse + launches `ChessApp`
- **tui/** contains all UI code: screens orchestrate widgets, widgets are independent
- **tui/screens/game.py** imports from game.py, engine.py, and all widgets — it is the primary orchestrator
- **engine.py** is lazily imported inside functions/workers (not at module top level) to avoid Stockfish initialization when not needed

### Threading (Textual Workers)
- All Stockfish calls (get_move, get_evaluation, analyze_game) run in `@work(thread=True)` workers
- Workers use `board.copy()` to avoid racing with the main thread
- Workers use `self.app.call_from_thread()` to update UI state on the main thread
- Engine workers use `exclusive=True, group="engine"` to prevent concurrent engine access
- `on_unmount` cancels the engine worker group before calling `engine.quit()`

### Rendering
- All render functions return strings, never print directly. Caller prints.
- All ANSI escape codes are defined in `board_render.py` as module-level constants. No other module defines ANSI codes — they import from board_render.
- Board cells are 3 characters wide (space-piece-space) for square proportions.

### Evaluation Perspective
- Eval is **always stored and passed as White's perspective** internally.
- Display layer negates for Black when rendering: `display_eval = eval_score if perspective == chess.WHITE else -eval_score`
- CP loss in analysis flips based on which side moved.
- Never store or pass eval from a relative/side-to-move perspective.

### State Management
- `GameState` is the single source of truth for game state.
- Parallel tracking: `move_history` (chess.Move objects) and `san_history` (SAN strings). Keep both in sync.
- SAN must be generated **before** pushing the move (`board.san(move)` then `board.push(move)`).
- Undo removes 2 half-moves (player + engine) when it's the player's turn, 1 when it's the engine's turn.

### Engine Integration
- Stockfish difficulty uses `UCI_LimitStrength` + `UCI_Elo`. Minimum ELO for Stockfish 18 is 1320.
- Difficulty presets: 20 levels from Newcomer(1320) to Supreme(3100) in ~100 ELO steps, plus Maximum(no limit).
- Engine calls wrapped in try/except for `EngineTerminatedError` — game continues without eval if engine crashes.
- Always call `engine.quit()` in a `finally` block to avoid hanging processes.

### Move Input
- Castling normalized: `0-0` -> `O-O`, `0-0-0` -> `O-O-O` before parsing.
- Promotion: if pawn-to-8th-rank fails parse, detect and prompt for piece (default Queen).
- Validation delegated to python-chess (`parse_san`), catching `InvalidMoveError`, `IllegalMoveError`, `AmbiguousMoveError`.

## Difficulty Presets (Stockfish 18)

| Preset | ELO |
|--------|-----|
| Newcomer | 1320 |
| Beginner | 1400 |
| Novice | 1500 |
| Amateur | 1600 |
| Casual | 1700 |
| Intermediate | 1800 |
| Proficient | 1900 |
| Skilled | 2000 |
| Advanced | 2100 |
| Strong | 2200 |
| Expert | 2300 |
| Candidate | 2400 |
| Master | 2500 |
| Veteran | 2600 |
| Grandmaster | 2700 |
| Champion | 2800 |
| Elite | 2900 |
| Legendary | 3000 |
| Supreme | 3100 |
| Maximum | Full strength |

## Analysis Classification

| Classification | CP Loss Threshold | Color |
|---------------|-------------------|-------|
| Best | 0 | Green |
| Excellent | < 10 | Green |
| Good | < 30 | White |
| Inaccuracy | 30-80 | Yellow |
| Mistake | 80-200 | Orange |
| Blunder | > 200 | Red |

Accuracy formula: `max(0, 100 - ACPL / 3)`

## Common Gotchas

- **SAN before push**: Always call `board.san(move)` before `board.push(move)` — SAN generation needs the pre-move board state.
- **Perspective inversion**: Eval is always White's perspective. Negate for Black display.
- **Parallel histories**: `move_history` and `san_history` must stay in sync through all operations (push, pop, undo).
- **Late engine import**: `EnginePlayer` is imported inside functions, not at module level. Don't assume it's available at parse time. Use `TYPE_CHECKING` guard for type hints.
- **PGN objects**: `to_pgn()` returns `chess.pgn.Game`, not a string. Use `str()` to convert.
- **Engine lifecycle**: Stockfish process must be explicitly quit. Leaked processes hang.

## In-Game Commands

`help`, `moves`, `undo`, `resign`, `draw`, `eval`, `pgn`, `save [file]`, `quit`

## No Tests

No test suite exists yet. Manual testing via `uv run chess-cli`.
