aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CLAUDE.md4
-rw-r--r--README.md4
-rw-r--r--src/main.rs32
3 files changed, 5 insertions, 35 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index e9b236b..6064586 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -19,7 +19,7 @@ The toolchain comes from a Nix flake (`flake.nix`, loaded via direnv `.envrc` â†
### Benchmarking and profiling the search
-- Train: `cargo run --release -- train [generations] [games] [out-dir] [--from <weights>]` (defaults 50, 500, `weights/`, which is gitignored). It starts from `--from` if given, otherwise from the newest `gen-NNN.bin` in the out dir, otherwise from random weights. Then play it with `cargo run --release -- neural weights/latest.bin [sheep|tiger]`.
+- Train: `cargo run --release -- train [generations] [games] [out-dir] [--from <weights>]` (defaults 50, 500, `weights/`, which is gitignored). It starts from `--from` if given, otherwise from the newest `gen-NNN.bin` in the out dir, otherwise from random weights. Then play it with `cargo run --release -- -S human -T weights/latest.bin` (or the other way round).
- Bench: `cargo run --profile profiling -- bench [depth]` (default depth 6; 9 takes about 0.6 s) searches six fixed positions to a fixed depth without the TUI and prints the move, score and time for each. The positions come from `ClaudePlayer` playing itself to depth 4, so they are deterministic. An optimisation that only makes the search faster must leave every move and score unchanged.
- Counters: add `--features search-stats` to print nodes, nodes/s, cache hit/overwrite rates and the share of cutoffs on the first move. They are thread-local, bumped with the `count!` macro in `player.rs` (also used in `board.rs`), and compile to nothing without the feature. Run `cargo test` with and without the feature.
- The `profiling` Cargo profile is release plus debug info. The dev shell has `perf` and `samply`. As a non-root user, `perf record` needs `-m 16` (a small ring buffer) with `--call-graph dwarf`, otherwise mmap fails on the `perf_event_mlock_kb` limit: `perf record -e cycles:u -m 16 --call-graph dwarf target/profiling/tigers-and-sheep-rs bench 9`.
@@ -42,7 +42,7 @@ The toolchain comes from a Nix flake (`flake.nix`, loaded via direnv `.envrc` â†
- `src/network.rs` — `Network`, a small MLP (52 inputs → 64 ReLU → 32 ReLU → tanh, positive favours tigers) with hand-written backprop and an `Adam` optimiser, all in one flat `params` Vec at the `W1`/`B1`/… offsets. Inputs are only the raw position: a bit per point for tigers and for sheep, sheep in hand / 20, and whether tigers are to move. `forward` adds up the layer-1 weight rows of the set bits rather than multiplying through the zeros. `gradient` (used by `train_batch`) and `loss` run on rayon's pool in fixed-size chunks (`GRAD_CHUNK` = 32, `LOSS_CHUNK` = 1024) summed in order, so the result doesn't depend on the number of threads (`training_is_the_same_on_any_number_of_threads`); Adam's step stays serial. Also `Rng` (xorshift64*) and `Sample` (board, turn, target). File format (`save`/`load`): `TSNN`, then version, inputs, hidden1, hidden2 as little-endian u32s, then every param as a little-endian f32; changing the layer sizes makes old files fail to load.
- `src/neural-player.rs` — `NeuralPlayer`, a `MinimaxPlayer` like `ClaudePlayer` whose `evaluate` is a `Network`.
- `src/train.rs` — self-play training. Each generation plays `games_per_generation` games of the net against itself at a fixed depth (default 3) across all cores (`parallel`, on rayon's pool, which the training steps share; `RAYON_NUM_THREADS` limits it). Moves are random for the first `random_plies`, and with chance `epsilon` after that. A game is a draw on a threefold repetition (`play` keeps the earlier boards and passes them to `choose`, so the search sees them too), or on reaching `ply_cap`, a backstop for long games that never repeat. Each position is labelled with a TD(λ) target worked backwards from the result (±1, or 0) through the searched scores. A position where a random move was played is labelled with its own search score instead, which cuts the chain there. Without that cut, the sheep's random moves (which usually hang a sheep) taught the net that tigers nearly always win. Self-play still ends mostly in tiger wins, because of those random moves, so judge progress by the match against `ClaudePlayer` (`match_games` = 100). The net then trains on the last `buffer_generations` generations, each position shown in a random one of the board's 8 symmetries (`GameBoard::transformed`). With `balance_outcomes` (the default), `balanced` builds each epoch from as many positions of sheep-win games as of tiger-win games: their mean, so the epoch stays the same size. Tiger-win positions are subsampled and sheep-win ones repeated; draws are kept as they are. The output shows the mean error on tiger-win (T) and sheep-win (S) positions separately, before and after training, and how long the training part took (`train s`). On generations whose number is a multiple of `match_every` (default 10, `--match-every`), it then plays matches against the generation that last played them (N - `match_every`, loaded from its `gen-NNN.bin`, or the network the run started from if that file isn't there) and against `ClaudePlayer`, which is only a yardstick and never a training signal; other generations show `-` in those columns. Each generation is saved as `out_dir/gen-NNN.bin` and `latest.bin`, and a rerun carries on from the newest `gen-NNN.bin`. `Config::from` (`--from`) starts from any weights file instead. Generation numbers still follow on from the out dir. The Adam state and the sample buffer are not saved, so they start empty either way.
-- `src/main.rs` — the command line, parsed with clap's derive API (`Cli`, `Command`, `PlayArgs`, `TrainArgs`). With no command, or `play`, it plays a game in the TUI and prints the result; `--sheep`/`--tiger` take `human`, `claude` or a weights file (`parse_player` → `PlayerKind`, turned into a `Box<dyn Player>` by `make_player`), and `--time` is each computer player's time limit in seconds. `neural <weights> [sheep|tiger]` is shorthand for you against a `NeuralPlayer`. `bench [depth]` runs the benchmark. `train [generations] [games] [out dir]` trains a network (use `--release`); every other `train::Config` field is a `--long-option` whose default is read from `Config::default()`, so keep `TrainArgs` and its `From` impl in step with `Config`.
+- `src/main.rs` — the command line, parsed with clap's derive API (`Cli`, `Command`, `PlayArgs`, `TrainArgs`). With no command, or `play`, it plays a game in the TUI and prints the result; `--sheep`/`--tiger` take `human`, `claude` or a weights file (`parse_player` → `PlayerKind`, turned into a `Box<dyn Player>` by `make_player`), and `--time` is each computer player's time limit in seconds. `bench [depth]` runs the benchmark. `train [generations] [games] [out dir]` trains a network (use `--release`); every other `train::Config` field is a `--long-option` whose default is read from `Config::default()`, so keep `TrainArgs` and its `From` impl in step with `Config`.
Notation (`pos_name`, `impl Display for Move` in board.rs): files `a`–`e` are x 0–4, ranks are `5 - y` (y = 0 is drawn at the top as rank 5). Moves are `sc3` (place), `sc3c2` (sheep move), `ta5b4` (tiger step), `ta5xc3` (tiger jump).
diff --git a/README.md b/README.md
index e97a987..a9566c9 100644
--- a/README.md
+++ b/README.md
@@ -76,10 +76,10 @@ carries on from the newest generation in the folder. `--from` starts from any we
The other training settings (search depth, ε, λ, epochs and so on) are options too; see
`train --help`.
-Then play against it, as sheep (the default) or as tigers:
+Then play against it by giving its weights file as one of the players:
```bash
-cargo run --release -- neural weights/latest.bin tiger
+cargo run --release -- --sheep human --tiger weights/latest.bin
```
## Development
diff --git a/src/main.rs b/src/main.rs
index 5237100..9cd8e4c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
use std::{path::PathBuf, time::Duration};
-use clap::{Args, Parser, Subcommand, ValueEnum};
+use clap::{Args, Parser, Subcommand};
use crate::{board::Outcome, claude_player::ClaudePlayer, game::GameHandler, local_player::LocalPlayer, network::Network, neural_player::NeuralPlayer, player::Player};
@@ -35,18 +35,6 @@ enum Command
{
/// Play a game in the terminal (the default)
Play(PlayArgs),
- /// Play against a trained network
- Neural
- {
- /// Weights file written by `train`
- weights: PathBuf,
- /// The side you play
- #[arg(value_enum, default_value_t = Side::Sheep)]
- side: Side,
- /// Seconds the network may think per move
- #[arg(short, long, default_value_t = 2.0)]
- time: f64,
- },
/// Time the computer player's search on fixed positions, without the TUI
Bench
{
@@ -93,13 +81,6 @@ fn parse_player(s: &str) -> Result<PlayerKind, String>
}
}
-#[derive(Clone, Copy, ValueEnum)]
-enum Side
-{
- Sheep,
- Tiger,
-}
-
#[derive(Args)]
struct TrainArgs
{
@@ -190,17 +171,6 @@ fn main() -> std::io::Result<()>
Ok(())
}
Some(Command::Train(args)) => train::run(&args.into()),
- Some(Command::Neural { weights, side, time }) =>
- {
- let human = PlayerKind::Human;
- let computer = PlayerKind::Neural(weights);
- let (sheep, tiger) = match side
- {
- Side::Sheep => (human, computer),
- Side::Tiger => (computer, human),
- };
- play(PlayArgs { sheep, tiger, time })
- }
Some(Command::Play(args)) => play(args),
None => play(cli.play),
}