1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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));
}
|