pub fn check_winner(bb: &Bitboard) -> WinStatusExpand description
Determine winner. The last player to move (the one with more pieces on the board) is credited with the win.
Examples found in repository?
examples/selfplay_export.rs (line 78)
71fn play_game(seed: u64, iterations: u32) -> (Vec<PendingRow>, WinStatus) {
72 let mut bb = Bitboard::EMPTY;
73 let mut rows = Vec::new();
74 let mut ply = 0u32;
75
76 loop {
77 if has_winning_line(&bb) {
78 return (rows, check_winner(&bb));
79 }
80 let legal = generate_legal_moves(&bb);
81 if legal.is_empty() {
82 // No legal moves: the side to move loses (see Global
83 // Constraints — this is a decisive result, never a draw).
84 let loser = current_player(&bb).unwrap();
85 let winner = if loser == 0 {
86 WinStatus::Player1Wins
87 } else {
88 WinStatus::Player0Wins
89 };
90 return (rows, winner);
91 }
92
93 let side_to_move = current_player(&bb).unwrap();
94 // use_transposition_table MUST be false here: with it on (the
95 // engine's actual default), root moves that canonicalize to the
96 // same child are merged onto one shared node and reported under a
97 // single arbitrary move, silently dropping every other legal move
98 // that led there — worst exactly at shallow plies (the empty
99 // board's 64 legal moves collapse to 3), which every self-play
100 // game passes through. Verified in mcts.rs's
101 // root_move_visits_default_config_collapses_symmetric_root_moves
102 // test — this is not a hypothetical concern, it was caught by
103 // Opus review of the PR that added root_move_visits and is
104 // exactly what this exporter must avoid to produce a faithful
105 // per-legal-move policy target.
106 let mut engine = MCTSEngine::new(MCTSConfig {
107 max_iterations: iterations,
108 seed: Some(seed.wrapping_add(ply as u64)),
109 use_transposition_table: false,
110 ..Default::default()
111 });
112 let (best_move, _) = engine.search(&bb).expect("legal moves exist");
113 let policy: Vec<(u8, u8, u32)> = engine
114 .root_move_visits()
115 .into_iter()
116 .map(|(mv, visits)| (mv.shape, mv.position, visits))
117 .collect();
118
119 rows.push(PendingRow {
120 ply,
121 qfen: State::new(bb).to_qfen(),
122 side_to_move,
123 policy,
124 });
125
126 bb = apply_move(&bb, &best_move);
127 ply += 1;
128 }
129}More examples
examples/depth_sweep.rs (line 39)
21fn main() {
22 let mut frontier: HashMap<[u8; 16], (Bitboard, u64)> = HashMap::new();
23 frontier.insert(Bitboard::EMPTY.to_le_bytes(), (Bitboard::EMPTY, 1));
24
25 println!(
26 "depth,canonical_states,raw_boards,p0_wins_raw,p1_wins_raw,with_legal_moves,mean_orbit,min_orbit,max_orbit,elapsed_s"
27 );
28
29 for depth in 1..=16u32 {
30 let started = Instant::now();
31 let mut next: HashMap<[u8; 16], (Bitboard, u64)> = HashMap::new();
32 let mut p0_wins_raw: u64 = 0;
33 let mut p1_wins_raw: u64 = 0;
34
35 for (bb, mult) in frontier.values() {
36 for mv in generate_legal_moves(bb) {
37 let child = apply_move(bb, &mv);
38 if has_winning_line(&child) {
39 match check_winner(&child) {
40 WinStatus::Player0Wins => p0_wins_raw += mult,
41 WinStatus::Player1Wins => p1_wins_raw += mult,
42 WinStatus::NoWin => unreachable!(),
43 }
44 continue;
45 }
46 let canon = SymmetryHandler::find_canonical(&child);
47 let key = canon.to_le_bytes();
48 let entry = next.entry(key).or_insert((canon, 0));
49 entry.1 += mult;
50 }
51 }
52 frontier = next;
53
54 let canonical_states = frontier.len();
55 let raw_boards: u64 = frontier.values().map(|(_, m)| *m).sum();
56 let with_moves = frontier
57 .values()
58 .filter(|(bb, _)| !generate_legal_moves(bb).is_empty())
59 .count();
60
61 let mut orbit_sizes: Vec<usize> = frontier
62 .values()
63 .map(|(bb, _)| SymmetryHandler::orbit_size(bb))
64 .collect();
65 orbit_sizes.sort_unstable();
66 let mean = if orbit_sizes.is_empty() {
67 0.0
68 } else {
69 orbit_sizes.iter().sum::<usize>() as f64 / orbit_sizes.len() as f64
70 };
71 let min = orbit_sizes.first().copied().unwrap_or(0);
72 let max = orbit_sizes.last().copied().unwrap_or(0);
73
74 // Per-depth orbit-size histogram, written alongside the summary line.
75 let mut hist: BTreeMap<usize, u64> = BTreeMap::new();
76 for bb in frontier.values() {
77 *hist.entry(SymmetryHandler::orbit_size(&bb.0)).or_insert(0) += 1;
78 }
79 let hist_str: Vec<String> = hist.iter().map(|(s, c)| format!("{s}:{c}")).collect();
80
81 println!(
82 "{depth},{canonical_states},{raw_boards},{p0_wins_raw},{p1_wins_raw},{with_moves},{mean:.2},{min},{max},{:.3}",
83 started.elapsed().as_secs_f64()
84 );
85 eprintln!(" depth {depth} orbit histogram: {}", hist_str.join(" "));
86
87 if canonical_states == 0 {
88 eprintln!("frontier empty at depth {depth} — every line of play has ended by here");
89 break;
90 }
91 if with_moves == 0 {
92 eprintln!("all depth-{depth} states are dead ends (no legal moves) — game always over by depth {}", depth + 1);
93 }
94 }
95}