summaryrefslogtreecommitdiff
path: root/docs/players.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/players.md')
-rw-r--r--docs/players.md170
1 files changed, 170 insertions, 0 deletions
diff --git a/docs/players.md b/docs/players.md
new file mode 100644
index 0000000..7a5c145
--- /dev/null
+++ b/docs/players.md
@@ -0,0 +1,170 @@
+# Players
+
+A side in the game is a `Player`. The game coordinator doesn't know or care whether it's a human
+clicking, a search, or a network: it asks the player for a move, passes on input, and draws whatever
+the player wants to show. Both traits are in [`src/player.rs`](../src/player.rs).
+
+## The `Player` trait
+
+```rust
+pub trait Player
+{
+ fn name(&self) -> String;
+ fn update(&mut self, _board: GameBoard, _turn: Turn) {}
+ fn pick_move(&mut self, _board: GameBoard, _turn: Turn, _previous: &[GameBoard]) -> Option<Move> { None }
+ fn handle_input(&mut self, _input: Input, _board: GameBoard, _turn: Turn) -> Option<Move> { None }
+ fn highlights(&self) -> Highlights { Highlights::default() }
+ fn reset(&mut self) {}
+ fn render(&self, area: Rect, buf: &mut Buffer) { /* shows name() */ }
+ fn render_last_turn(&self, _area: Rect, _buf: &mut Buffer) {}
+}
+```
+
+Only `name` is required. Players are **polled, not blocking**: no method waits for anything, so the
+coordinator can keep redrawing and handling quit keys whatever kind of player is on move.
+
+| Method | When it's called | What to do |
+|--------|------------------|------------|
+| `name` | For panel titles and the result | A short name, e.g. `"Claude"` |
+| `update(board, turn)` | Before every redraw, while it's this player's turn | Remember the position if `render` needs it |
+| `pick_move(board, turn, previous)` | Once per loop, while it's this player's turn and the game isn't over | Computer players: search and return `Some(move)`. Interactive players: return `None`. `previous` is every earlier board in the game, oldest first, for spotting repetitions. |
+| `handle_input(input, board, turn)` | When the user clicks a board point (`Input::Click(pos)`) or presses a key (`Input::Key(event)`) during this player's turn | Return `Some(move)` once the input completes a move |
+| `highlights()` | When drawing the board during this player's turn | `Highlights { selected, targets }`: a selected piece and the points it can move to |
+| `reset()` | On **both** players after the user reverts to an earlier move | Drop any half-made selection and any comments about the last move. It is *not* called after a normal move, so a player clears its own selection when it returns a move. |
+| `render(area, buf)` | Every redraw, while it's this player's turn | Draw the player's panel. The default shows `name()`. |
+| `render_last_turn(area, buf)` | Every redraw during the *other* player's turn | Draw comments on the move this player just made. The default draws nothing. |
+
+Rules a player must follow:
+
+- **Only return legal moves.** The coordinator plays them with `make_move(..).unwrap()`, so an
+ illegal move panics. Moves from `board.get_moves(turn)` are always legal; `board.check_move(mv)`
+ checks any other.
+- `pick_move` is never called once the game is over, so `get_moves(turn)` is never empty there.
+- Input that arrives while a computer player is searching is handled after the search, so a long
+ `pick_move` delays quitting until it returns. Keep a time limit.
+
+`impl Widget for &dyn Player` makes every player a ratatui widget by forwarding to
+`Player::render`, which is how the coordinator draws the panel. (A blanket
+`impl<P: Player> Widget for &P` would break the orphan rule.)
+
+### Where each method shows up on screen
+
+```text
+┌ Board ───────────────────────────┬ Moves ─────┐
+│ highlights() of the player │ │
+│ to move are drawn here │ │
+├──────────────────────────────────┼────────────┤
+│ render() of the player to move │ render_last│
+│ │ _turn() of │
+│ │ the other │
+└────────── Sheep: name() ─────────┴────────────┘
+```
+
+## `LocalPlayer`: a human with a mouse
+
+[`src/local-player.rs`](../src/local-player.rs). It's the example of an interactive player:
+
+- `pick_move` keeps the default `None`, so the coordinator waits for input.
+- `handle_input` takes clicks. While sheep are in hand, a click on an empty point places a sheep.
+ Otherwise a click on one of its own pieces selects it and keeps its legal moves; a click on one of
+ their destinations plays that move; any other click clears the selection. Illegal clicks leave a
+ message in red, from `MoveError`'s `Display`.
+- `highlights` returns the selected piece and its destinations.
+- `update` stores the position so `render` can show the sheep counts and an instruction.
+- `render_last_turn` shows `Played <move>`.
+- `reset` clears the selection, message and last move.
+
+## The `MinimaxPlayer` trait
+
+Every computer player so far is a search with a different evaluation. `MinimaxPlayer` holds that
+search, and **a blanket impl makes every `MinimaxPlayer` a `Player`**:
+
+```rust
+impl<P: MinimaxPlayer> Player for P { … }
+```
+
+So a computer player implements `MinimaxPlayer` only, never `Player` directly (the two impls would
+conflict).
+
+```rust
+pub trait MinimaxPlayer
+{
+ const WIN: f32 = f32::INFINITY;
+ const DRAW: f32 = 0.0;
+
+ // Required
+ fn evaluate(&mut self, board: &GameBoard, turn: Turn) -> ScoreType;
+
+ // Optional settings
+ fn depth(&self) -> usize { MAX_DEPTH } // at most 64
+ fn time_limit(&self) -> Duration { Duration::from_secs(1) }
+ fn name(&self) -> String { "Computer".to_string() }
+
+ // Optional storage that speeds up the search
+ fn cache(&mut self) -> Option<&mut dyn ScoreCache> { None }
+ fn killers(&mut self) -> Option<&mut Killers> { None }
+
+ // Optional storage for the last-turn panel
+ fn set_last_search(&mut self, _report: Option<SearchReport>) {}
+ fn last_search(&self) -> Option<SearchReport> { None }
+
+ // The search itself; not meant to be overridden
+ fn minimax(…) -> Option<ScoreType>;
+ fn minimax_uncached(…) -> Option<(ScoreType, Option<Move>)>;
+}
+```
+
+| Item | Purpose |
+|------|---------|
+| `evaluate(board, turn)` | The score of a position where the search stops. Positive favours tigers, whichever side is to move; `turn` is there for evaluations that care. Return finite values: `±WIN` (infinity) is reserved for won games. If the player has a cache, give symmetric boards the same score (see [the search](search.md#symmetric-positions-share-entries)). |
+| `depth()` | The most plies to search, counting the move itself. Normally the time limit stops the search first. |
+| `time_limit()` | How long `pick_move` may take. It stops deepening after half of it. |
+| `cache()` | Return a `ScoreCache` (normally a `TranspositionTable`) to turn on the transposition table. |
+| `killers()` | Return a `Killers` to turn on killer-move ordering. |
+| `set_last_search` / `last_search` | Store and return the `SearchReport { mv, depth, score }` of the last `pick_move`, which the blanket impl's `render_last_turn` shows as "Played … / Searched to depth … / Score: …". |
+| `WIN`, `DRAW` | Scores of a won game and of a repetition |
+
+The blanket `Player` impl provides:
+
+- `pick_move`: iterative deepening with the time limit (see [The minimax search](search.md)).
+- `render`: the name and "Thinking…", which is on screen while the search runs.
+- `render_last_turn`: the search report, with the score to 5 significant figures, or "forced win for
+ tigers/sheep".
+- `reset`: forgets the search report.
+
+### Supporting types
+
+| Type | Purpose |
+|------|---------|
+| `ScoreType` | `f32` |
+| `SearchReport` | The move, depth and score of the last search |
+| `CacheEntry` | Bounds `lower..=upper` on a position's score and its best move |
+| `ScoreCache` | Trait for caches: `lookup`, `insert`, `clear`. Implemented by `TranspositionTable` and (for tests) `HashMap` |
+| `TranspositionTable` | Fixed-size cache: `new(bits)` gives `1 << bits` slots of ~24 bytes |
+| `Killers` | Two killer moves per remaining depth |
+| `SearchStats` | Counters filled in with the `search-stats` feature |
+| `MAX_DEPTH` | 64 |
+
+## `ClaudePlayer`
+
+[`src/claude-player.rs`](../src/claude-player.rs). A `MinimaxPlayer` with a 2^20-slot
+`TranspositionTable`, killers, and a hand-written evaluation from the tigers' side:
+
+```text
+100 × dead sheep five dead sheep win the game for the tigers
++20 × jumps available now sheep left hanging
+−15 × trapped tigers tigers with no move; trapping all of them wins for the sheep
+ +2 × tiger moves mobility in general, which the sheep win by squeezing to zero
+```
+
+`time_limit` and `max_depth` are public fields. `ClaudePlayer::new(time_limit)` makes one; the
+`bench` and `train` code set `max_depth` and a huge time limit to get fixed-depth, repeatable
+searches.
+
+## `NeuralPlayer`
+
+[`src/neural-player.rs`](../src/neural-player.rs). The same as `ClaudePlayer`, except that
+`evaluate` returns `self.net.forward(board, turn)`, the [network](training.md)'s output in `-1..1`.
+`NeuralPlayer::new(net, time_limit, table_bits)` lets training use a smaller table (2^16 slots),
+since it runs one player per thread. Forced wins are still `±WIN` from the search itself, far
+outside the network's range.