diff options
Diffstat (limited to 'src/bench.rs')
| -rw-r--r-- | src/bench.rs | 90 |
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)); +} |
