summaryrefslogtreecommitdiff
path: root/src/bench.rs
diff options
context:
space:
mode:
authorDylan <boss@tehbox.org>2026-10-23 19:40:00 +1300
committerDylan <boss@tehbox.org>2026-10-23 19:41:07 +1300
commit8b6323efba32da9e3651ce882c02a6e7a6671f23 (patch)
tree3b01f4d47176379df2aadbef0ffc621a2d9d6186 /src/bench.rs
parent50028fc708ef0cfea9fd589c90e07b413ecb570f (diff)
downloadtigers-and-sheep-8b6323efba32da9e3651ce882c02a6e7a6671f23.tar.gz
tigers-and-sheep-8b6323efba32da9e3651ce882c02a6e7a6671f23.zip
feat: Optimisation
Added benchmarking capabilities
Diffstat (limited to 'src/bench.rs')
-rw-r--r--src/bench.rs90
1 files changed, 90 insertions, 0 deletions
diff --git a/src/bench.rs b/src/bench.rs
new file mode 100644
index 0000000..ad6d63f
--- /dev/null
+++ b/src/bench.rs
@@ -0,0 +1,90 @@
+use std::time::{Duration, Instant};
+
+use crate::{board::{GameBoard, Turn}, claude_player::ClaudePlayer, player::{MinimaxPlayer, Player, SearchReport, SearchStats}};
+
+/// Positions from each phase of the game, as (label, plies of random play from the start).
+const POSITIONS: [(&str, usize); 6] = [
+ ("opening", 0),
+ ("tiger reply", 1),
+ ("mid-placement", 20),
+ ("end of placement", 38),
+ ("early movement", 45),
+ ("movement", 60),
+];
+
+/// The position after `plies` moves of `ClaudePlayer` playing itself to depth 4, which is
+/// deterministic and plays a lot more like a real game than random moves do.
+fn find_position(plies: usize) -> (GameBoard, Turn)
+{
+ let mut player = ClaudePlayer::new(Duration::from_secs(1 << 20));
+ player.max_depth = 4;
+ let mut board = GameBoard::default();
+ let mut turn = Turn::Sheep;
+ for _ in 0..plies
+ {
+ assert!(!board.check_win(turn), "the self-play game ended before ply {plies}");
+ board.make_move_unchecked(player.pick_move(board, turn).expect("a move"));
+ turn = turn.next_turn();
+ }
+ (board, turn)
+}
+
+/// Search each of `POSITIONS` to `depth` with a fresh `ClaudePlayer` and print what it found and how long it took.
+/// The depth caps the search instead of the time limit, so runs do the same work and can be compared.
+/// Build with `--features search-stats` to also print node counts (which slows the search a little).
+pub fn run(depth: usize)
+{
+ println!("{:<17} {:<6} {:>5} {:>9} {:<8} {:>8}", "position", "turn", "depth", "time ms", "move", "score");
+ let mut total_time = Duration::ZERO;
+ let mut total_stats = SearchStats::default();
+ for (label, plies) in POSITIONS
+ {
+ let (board, turn) = find_position(plies);
+ let mut player = ClaudePlayer::new(Duration::from_secs(1 << 20));
+ player.max_depth = depth;
+ SearchStats::take();
+ let start = Instant::now();
+ let m = player.pick_move(board, turn).expect("a move");
+ let time = start.elapsed();
+ let stats = SearchStats::take();
+ total_time += time;
+ add(&mut total_stats, &stats);
+
+ let SearchReport { mv, depth, score } = player.last_search().expect("a report");
+ assert_eq!(mv, m);
+ println!("{label:<17} {:<6} {depth:>5} {:>9.1} {:<8} {score:>8.1}",
+ format!("{turn:?}"), time.as_secs_f64() * 1000.0, mv.to_string());
+ print_stats(&stats, time);
+ }
+ println!("{:<17} {:<6} {:>5} {:>9.1}", "total", "", "", total_time.as_secs_f64() * 1000.0);
+ print_stats(&total_stats, total_time);
+}
+
+fn add(total: &mut SearchStats, s: &SearchStats)
+{
+ total.nodes += s.nodes;
+ total.leaves += s.leaves;
+ total.terminal += s.terminal;
+ total.cache_probes += s.cache_probes;
+ total.cache_hits += s.cache_hits;
+ total.cache_returns += s.cache_returns;
+ total.cache_overwrites += s.cache_overwrites;
+ total.cutoffs += s.cutoffs;
+ total.first_move_cutoffs += s.first_move_cutoffs;
+ total.move_gens += s.move_gens;
+}
+
+fn print_stats(s: &SearchStats, time: Duration)
+{
+ if !cfg!(feature = "search-stats")
+ {
+ return;
+ }
+ let pct = |a: u64, b: u64| if b == 0 { 0.0 } else { 100.0 * a as f64 / b as f64 };
+ println!(" {} nodes ({:.2} M/s), {} leaves, {} terminal, {:.2} move gens/node",
+ s.nodes, s.nodes as f64 / time.as_secs_f64() / 1e6, s.leaves, s.terminal,
+ s.move_gens as f64 / s.nodes.max(1) as f64);
+ println!(" cache: {} probes, {:.1}% hit, {:.1}% of hits settle the score, {} overwrites; cutoffs: {}, {:.1}% on the first move",
+ s.cache_probes, pct(s.cache_hits, s.cache_probes), pct(s.cache_returns, s.cache_hits), s.cache_overwrites,
+ s.cutoffs, pct(s.first_move_cutoffs, s.cutoffs));
+}