diff --git a/engine/src/ai/config.rs b/engine/src/ai/config.rs index 23ff532..d761af7 100644 --- a/engine/src/ai/config.rs +++ b/engine/src/ai/config.rs @@ -13,7 +13,7 @@ pub struct SearchConfig { impl Default for SearchConfig { fn default() -> Self { Self { - enable_lmr: true, + enable_lmr: false, timeout_ms: 100, iterative_start_depth: 1, } diff --git a/engine/src/ai/eval.rs b/engine/src/ai/eval.rs index dd1ea21..829fe70 100644 --- a/engine/src/ai/eval.rs +++ b/engine/src/ai/eval.rs @@ -8,7 +8,7 @@ //! //! 1. [`Board::for_each_line`] yields every row, column, and diagonal as a //! pair of `(me, opp)` bitmasks plus a length. -//! 2. [`crate::patterns::count_patterns`] tallies the runs on each line by +//! 2. [`crate::patterns::count_line_patterns`] tallies the runs on each line by //! length and openness. //! 3. [`score_from_counts`] converts the tallies to a single integer using //! the per-pattern weights below. @@ -24,12 +24,13 @@ use crate::board::Board; use crate::game::{Game, GameStatus, Player}; -use crate::patterns::{count_patterns, PatternCounts}; +use crate::patterns::{count_board_pattern, BoardPatternCounts}; /// Score returned for a winning terminal position. Mirrors /// [`crate::ai::search::WIN_SCORE`] so the two modules can be reasoned /// about independently. pub const WIN_SCORE: i32 = 1_000_000; +const UNSTABLE_FIVE: i32 = 100_000; const OPEN_FOUR: i32 = 100_000; const CLOSED_FOUR: i32 = 20_000; const OPEN_THREE: i32 = 5_000; @@ -41,7 +42,7 @@ const CAPTURE_PAIR: i32 = 2_000; /// A single 5-in-a-row already wins, but we never reach this branch in /// search — `terminal_score` has the final word at the root. Five-counts /// here are a defensive fallback if eval is called from outside. -const FIVE_FALLBACK: i32 = WIN_SCORE; +const STABLE_FIVE: i32 = WIN_SCORE; /// Score the position from the side-to-move's perspective. /// @@ -77,22 +78,33 @@ fn capture_diff(game: &Game, me: Player) -> i32 { } } +// fn score_player(board: &Board, player: Player) -> i32 { +// let mut totals = LinePatternCounts::default(); +// // No useful pattern fits in fewer than 5 cells — skip stub diagonals. +// board.for_each_line(player, 5, |me, opp, len| { +// let line = count_line_patterns(me, opp, len); +// totals.add(&line); +// }); + +// score_from_counts(&totals) +// } + /// Total pattern score for one player across every line on the board. fn score_player(board: &Board, player: Player) -> i32 { - let mut totals = PatternCounts::default(); - // No useful pattern fits in fewer than 5 cells — skip stub diagonals. - board.for_each_line(player, 5, |me, opp, len| { - let line = count_patterns(me, opp, len); - totals.add(&line); - }); + let mut totals = BoardPatternCounts::default(); + let me = board.bits(player); + let opp = board.bits(player.opponent()); + let score = count_board_pattern(me, opp); + totals.add(&score); score_from_counts(&totals) } -/// Convert a [`PatternCounts`] tally to an integer score using the +/// Convert a [`BoardPatternCounts`] tally to an integer score using the /// per-pattern weights at the top of this module. -fn score_from_counts(c: &PatternCounts) -> i32 { - (c.fives as i32) * FIVE_FALLBACK +fn score_from_counts(c: &BoardPatternCounts) -> i32 { + (c.stable_five as i32) * STABLE_FIVE + + (c.unstable_five as i32) * UNSTABLE_FIVE + (c.open_four as i32) * OPEN_FOUR + (c.closed_four as i32) * CLOSED_FOUR + (c.open_three as i32) * OPEN_THREE diff --git a/engine/src/ai/move_ordering.rs b/engine/src/ai/move_ordering.rs index 17212b3..6dfb9f5 100644 --- a/engine/src/ai/move_ordering.rs +++ b/engine/src/ai/move_ordering.rs @@ -28,7 +28,7 @@ //! ``` use crate::game::{ Direction, Player, Game, GameStatus, Pos }; use crate::board::Board; -use crate::patterns::count_patterns; +use crate::patterns::count_line_patterns; /// A move paired with its ordering heuristic score. #[derive(Debug, Clone, Copy)] @@ -91,7 +91,7 @@ fn evaluate_threat(game: &Game, pos: Pos, player: Player) -> i32 { for dir in Direction::all() { let (me, opp, len) = pack_local_line(&game.board, pos, dir, 4, player); - let patterns = count_patterns(me as u32, opp as u32, len); + let patterns = count_line_patterns(me as u32, opp as u32, len); score += patterns.fives as i32 * 100_000; score += patterns.open_four as i32 * 10_000; score += patterns.closed_four as i32 * 2_000; diff --git a/engine/src/ai/search.rs b/engine/src/ai/search.rs index 54556a0..9778b4c 100644 --- a/engine/src/ai/search.rs +++ b/engine/src/ai/search.rs @@ -22,7 +22,7 @@ #![allow(dead_code)] use std::sync::atomic::{AtomicU64, Ordering}; - +use std::fmt; use crate::ai::SearchConfig; use crate::ai::eval::evaluate; use crate::ai::iterative_deepening::iterative_deepening; @@ -53,6 +53,13 @@ pub struct SearchResult { pub max_ply: u32, } +impl fmt::Display for SearchResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {} {} {} {}", self.best_move.unwrap(), self.score, self.depth_reached, self.total_nodes, self.max_ply) + } +} + + /// If the position is terminal, return its score from the side-to-move's /// perspective. A player can never be on-move in a position they already won, /// so any `Win(_)` encountered here is a loss for the side to move. @@ -465,10 +472,14 @@ mod tests { let r1 = best_move(&mut g1, 6, 0); let r2 = best_move(&mut g2, 6, 20); + println!("r1: {}, r2: {}", r1, r2); println!("no TT nodes: {}", r1.total_nodes); println!("TT nodes: {}", r2.total_nodes); - - assert!(r2.total_nodes < r1.total_nodes); + if r1.depth_reached < r2.depth_reached { + //skip + } else if r1.depth_reached <= r2.depth_reached { + assert!(r2.total_nodes < r1.total_nodes); + } } #[test] diff --git a/engine/src/board.rs b/engine/src/board.rs index d1ea252..a13458c 100644 --- a/engine/src/board.rs +++ b/engine/src/board.rs @@ -221,6 +221,39 @@ impl BitBoard { } } + /// Extract and remove the least-significant set bit. + /// + /// Returns an empty bitboard if no bits are set. + #[inline] + pub fn pop_lsb(&mut self) -> BitBoard { + for i in 0..self.words.len() { + let word = self.words[i]; + + if word == 0 { + continue; + } + + let bit = word & (!word + 1); + + self.words[i] &= self.words[i] - 1; + + let mut out = BitBoard::new(); + out.words[i] = bit; + + return out; + } + + BitBoard::new() + } + + #[inline] + pub fn count_ones(self) -> u32 { + self.words + .iter() + .map(|x| x.count_ones()) + .sum() + } + /// Returns whether at least one bit is set. #[inline] pub fn any(self) -> bool { @@ -616,9 +649,9 @@ impl Board { /// # Examples /// /// ```ignore - /// let mut totals = PatternCounts::default(); + /// let mut totals = LinePatternCounts::default(); /// board.for_each_line(Player::Black, 5, |me, opp, len| { - /// totals.add(&count_patterns(me, opp, len)); + /// totals.add(&count_line_patterns(me, opp, len)); /// }); /// ``` pub fn for_each_line(&self, player: Player, min_len: u32, mut f: F) diff --git a/engine/src/game.rs b/engine/src/game.rs index f6ce831..dbc63bc 100644 --- a/engine/src/game.rs +++ b/engine/src/game.rs @@ -115,6 +115,21 @@ impl Direction { } } + #[inline] + pub fn forward_n( + self, + bits: BitBoard, + n: usize, + ) -> BitBoard { + let mut out = bits; + + for _ in 0..n { + out = self.forward(out); + } + + out + } + /// Shift the bitboard one cell backward along this direction. #[inline] pub fn backward( @@ -136,6 +151,21 @@ impl Direction { } } + #[inline] + pub fn backward_n( + self, + bits: BitBoard, + n: usize, + ) -> BitBoard { + let mut out = bits; + + for _ in 0..n { + out = self.backward(out); + } + + out + } + #[inline] pub fn all() -> [Direction; 4] { [ @@ -600,7 +630,7 @@ impl Game { /// of the four directions. /// /// Packs the 9-cell window centered on the stone into bitmasks and - /// hands the detection off to [`crate::patterns::count_patterns`]. + /// hands the detection off to [`crate::patterns::count_line_patterns`]. /// Off-board cells fall outside the packed window, so the board edge /// acts as a wall — same convention used by [`Game::is_free_three`]. pub fn check_win(&self, pos: Pos) -> bool { diff --git a/engine/src/patterns.rs b/engine/src/patterns.rs index c371107..c7d33a5 100644 --- a/engine/src/patterns.rs +++ b/engine/src/patterns.rs @@ -16,11 +16,11 @@ //! //! Packed-line (`u32`) helpers: //! -//! - [`count_patterns`] — tally every maximal run on a line by length and +//! - [`count_line_patterns`] — tally every maximal run on a line by length and //! openness. //! - [`has_free_three`] — yes/no check for the four free-three shapes, //! used by the double-three rule. -//! - [`PatternCounts`] — the tally type produced by [`count_patterns`]. +//! - [`LinePatternCounts`] — the tally type produced by [`count_line_patterns`]. //! //! Whole-board ([`BitBoard`]) helpers: //! @@ -45,7 +45,7 @@ use crate::board::BitBoard; /// "Open" runs have empty cells on both sides; "closed" runs have an /// opponent stone or the board edge on exactly one side. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct PatternCounts { +pub struct LinePatternCounts { /// Runs of 5 or more in a row. pub fives: u32, /// 4-runs with empty cells on both sides. @@ -62,9 +62,9 @@ pub struct PatternCounts { pub closed_two: u32, } -impl PatternCounts { +impl LinePatternCounts { /// Add the counts in `rhs` to `self` field-by-field. - pub fn add(&mut self, rhs: &PatternCounts) { + pub fn add(&mut self, rhs: &LinePatternCounts) { self.fives += rhs.fives; self.open_four += rhs.open_four; self.closed_four += rhs.closed_four; @@ -75,6 +75,39 @@ impl PatternCounts { } } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct BoardPatternCounts { + pub stable_five: u32, + pub unstable_five: u32, + /// 4-runs with empty cells on both sides. + pub open_four: u32, + /// 4-runs with one side blocked. + pub closed_four: u32, + /// 3-runs with empty cells on both sides. + pub open_three: u32, + /// 3-runs with one side blocked. + pub closed_three: u32, + /// 2-runs with empty cells on both sides. + pub open_two: u32, + /// 2-runs with one side blocked. + pub closed_two: u32, +} + +impl BoardPatternCounts { + /// Add the counts in `rhs` to `self` field-by-field. + pub fn add(&mut self, rhs: &BoardPatternCounts) { + + self.stable_five += rhs.stable_five; + self.unstable_five += rhs.unstable_five; + self.open_four += rhs.open_four; + self.closed_four += rhs.closed_four; + self.open_three += rhs.open_three; + self.closed_three += rhs.closed_three; + self.open_two += rhs.open_two; + self.closed_two += rhs.closed_two; + } +} + #[inline] fn line_mask(len: u32) -> u32 { if len >= 32 { u32::MAX } else { (1u32 << len) - 1 } @@ -96,6 +129,138 @@ fn run_starts(m: u32, k: u32, mask: u32) -> u32 { consecutive & not_left & not_right & mask } +pub fn split_runs( + me: BitBoard, + opp: BitBoard, + dir: Direction, + k: usize, +) -> (BitBoard, BitBoard) { + let empty = !(me | opp); + + // + // Exact-k maximal runs. + // + let mut run = me; + + for _ in 1..k { + run &= dir.forward(run); + } + + let not_left = + !dir.backward(me); + + let not_right = + !dir.forward_n(me, k); + + run &= + not_left & + not_right; + + // + // Openness classification. + // + let left_open = + dir.backward(empty); + + let right_open = + dir.forward_n(empty, k); + + let open = + run & + left_open & + right_open; + + let closed = + run & + (left_open ^ right_open); + + (open, closed) +} + +pub fn classify_fives(me: BitBoard, dir: Direction, capturable: BitBoard) -> (u32, u32) { + let mut stable = 0; + let mut unstable = 0; + + let mut starts = five_mask(me, dir); + + if !starts.any() { + return (0, 0); + } + + let mut killed = capturable; + let mut acc = capturable; + + for _ in 0..4 { + acc = dir.backward(acc); + killed |= acc; + } + + let stable_starts = starts & !killed; + + while starts.any() { + // Start one connected 5+ group. + let mut current = starts.pop_lsb(); + + let mut is_stable = + (current & stable_starts).any(); + + loop { + // Adjacent five-starts belong to the + // same overline structure. + let next = + dir.forward(current) & starts; + + if !next.any() { + break; + } + + current |= next; + starts &= !next; + + if (next & stable_starts).any() { + is_stable = true; + } + } + + starts &= !current; + + if is_stable { + stable += 1; + } else { + unstable += 1; + } + } + (stable, unstable) +} + +pub fn count_board_pattern( + me: BitBoard, + opp: BitBoard, +) -> BoardPatternCounts { + let capturable = + capturable_mask(me, opp); + + let mut res = BoardPatternCounts::default(); + + for dir in Direction::all() { + let (stable_five, unstable_five) = classify_fives(me, dir, capturable); + res.stable_five += stable_five; + res.unstable_five += unstable_five; + + let (open_four, closed_four) = split_runs(me, opp, dir, 4); + let (open_three, closed_three) = split_runs(me, opp, dir, 3); + let (open_two, closed_two) = split_runs(me, opp, dir, 2); + + res.open_four += open_four.count_ones(); + res.closed_four += closed_four.count_ones(); + res.open_three += open_three.count_ones(); + res.closed_three += closed_three.count_ones(); + res.open_two += open_two.count_ones(); + res.closed_two += closed_two.count_ones(); + } + res +} + /// Walk one packed line and tally every distinct run by length and openness. /// /// Each maximal run contributes to exactly one bucket — a 5-run is a @@ -114,10 +279,10 @@ fn run_starts(m: u32, k: u32, mask: u32) -> u32 { /// // ".XXXX." has one open four. /// let me = 0b011110; /// let opp = 0; -/// let counts = count_patterns(me, opp, 6); +/// let counts = count_line_patterns(me, opp, 6); /// assert_eq!(counts.open_four, 1); /// ``` -pub fn count_patterns(me: u32, opp: u32, len: u32) -> PatternCounts { +pub fn count_line_patterns(me: u32, opp: u32, len: u32) -> LinePatternCounts { let mask = line_mask(len); let m = me & mask; let o = opp & mask; @@ -144,7 +309,7 @@ pub fn count_patterns(me: u32, opp: u32, len: u32) -> PatternCounts { let (open_three, closed_three) = split(three, 3); let (open_two, closed_two) = split(two, 2); - PatternCounts { + LinePatternCounts { fives, open_four, closed_four, @@ -176,6 +341,62 @@ pub fn has_free_three(me: u32, opp: u32, len: u32) -> bool { ((solid_l | solid_r | split_l | split_r) & mask) != 0 } +pub fn free_three_mask( + me: BitBoard, + opp: BitBoard, + dir: Direction, +) -> BitBoard { + let empty = !(me | opp); + + // + // Pattern windows: + // + // .XXX.. + // ..XXX. + // .XX.X. + // .X.XX. + // + // Returned bits are canonical starts of the + // 6-cell pattern windows. + // + + let solid_l = + empty & + dir.forward_n(me, 1) & + dir.forward_n(me, 2) & + dir.forward_n(me, 3) & + dir.forward_n(empty, 4) & + dir.forward_n(empty, 5); + + let solid_r = + empty & + dir.forward_n(empty, 1) & + dir.forward_n(me, 2) & + dir.forward_n(me, 3) & + dir.forward_n(me, 4) & + dir.forward_n(empty, 5); + + let split_l = + empty & + dir.forward_n(me, 1) & + dir.forward_n(me, 2) & + dir.forward_n(empty, 3) & + dir.forward_n(me, 4) & + dir.forward_n(empty, 5); + + let split_r = + empty & + dir.forward_n(me, 1) & + dir.forward_n(empty, 2) & + dir.forward_n(me, 3) & + dir.forward_n(me, 4) & + dir.forward_n(empty, 5); + + solid_l | + solid_r | + split_l | + split_r +} /// Returns the starting cells of every contiguous /// five alignment in the given direction. @@ -304,8 +525,7 @@ fn capturable_pairs_dir( #[cfg(test)] mod tests { use crate::game::Pos; - -use super::*; + use super::*; /// Build a line from a string of `X`/`O`/`.` characters, returning /// (me, opp, len). `X` is me. @@ -326,7 +546,7 @@ use super::*; #[test] fn five_in_a_row_is_a_five() { let (m, o, l) = line(".XXXXX."); - let c = count_patterns(m, o, l); + let c = count_line_patterns(m, o, l); assert_eq!(c.fives, 1); // The 5-run isn't double-counted as smaller runs. assert_eq!(c.open_four, 0); @@ -336,7 +556,7 @@ use super::*; #[test] fn open_four_pattern() { let (m, o, l) = line("..XXXX.."); - let c = count_patterns(m, o, l); + let c = count_line_patterns(m, o, l); assert_eq!(c.open_four, 1); assert_eq!(c.closed_four, 0); } @@ -344,7 +564,7 @@ use super::*; #[test] fn closed_four_blocked_by_opponent() { let (m, o, l) = line("OXXXX.."); - let c = count_patterns(m, o, l); + let c = count_line_patterns(m, o, l); assert_eq!(c.open_four, 0); assert_eq!(c.closed_four, 1); } @@ -352,7 +572,7 @@ use super::*; #[test] fn closed_four_blocked_by_edge() { let (m, o, l) = line("XXXX.."); - let c = count_patterns(m, o, l); + let c = count_line_patterns(m, o, l); assert_eq!(c.closed_four, 1); assert_eq!(c.open_four, 0); } @@ -360,7 +580,7 @@ use super::*; #[test] fn open_three_and_open_two() { let (m, o, l) = line("..XX...XXX.."); - let c = count_patterns(m, o, l); + let c = count_line_patterns(m, o, l); assert_eq!(c.open_two, 1); assert_eq!(c.open_three, 1); } @@ -661,4 +881,417 @@ use super::*; has_stable_five(me, opp) ); } + + #[test] + fn exact_four_detected_once() { + let stones = bb(&[ + (0, 0), + (1, 0), + (2, 0), + (3, 0), + ]); + + let (open, closed) = split_runs( + stones, + BitBoard::new(), + Direction::Horizontal, + 4, + ); + + assert_eq!(open.count_ones(), 0); + assert_eq!(closed.count_ones(), 1); + } + + #[test] + fn open_four_detected() { + let stones = bb(&[ + (1, 0), + (2, 0), + (3, 0), + (4, 0), + ]); + + let (open, closed) = split_runs( + stones, + BitBoard::new(), + Direction::Horizontal, + 4, + ); + + assert_eq!(open.count_ones(), 1); + assert_eq!(closed.count_ones(), 0); + } + + #[test] + fn six_in_row_not_counted_as_four() { + let stones = bb(&[ + (0, 0), + (1, 0), + (2, 0), + (3, 0), + (4, 0), + (5, 0), + ]); + + let (open, closed) = split_runs( + stones, + BitBoard::new(), + Direction::Horizontal, + 4, + ); + + assert_eq!(open.count_ones(), 0); + assert_eq!(closed.count_ones(), 0); + } + + // #[test] + // fn exact_five_detected_once() { + // let stones = bb(&[ + // (0, 0), + // (1, 0), + // (2, 0), + // (3, 0), + // (4, 0), + // ]); + + // let starts = run_starts( + // stones, + // Direction::Horizontal, + // 5, + // ); + + // assert_eq!(starts.count_ones(), 1); + // } + + // #[test] + // fn six_in_row_not_double_counted_as_two_fives() { + // let stones = bb(&[ + // (0, 0), + // (1, 0), + // (2, 0), + // (3, 0), + // (4, 0), + // (5, 0), + // ]); + + // let starts = run_starts( + // stones, + // Direction::Horizontal, + // 5, + // ); + + // assert_eq!(starts.count_ones(), 0); + // } + + #[test] + fn stable_and_unstable_fives_are_classified() { + // + // X X X X X X + // X + // O + // + // One embedded five is unstable because of the + // capturable pair at x=0, but the shifted five + // remains stable. + // + let me = bb(&[ + (0, 1), + (1, 1), + (2, 1), + (3, 1), + (4, 1), + (5, 1), + (0, 2), + ]); + + let opp = bb(&[ + (0, 3), + ]); + + let capturable = + capturable_mask(me, opp); + + let (stable, unstable) = + classify_fives( + me, + Direction::Horizontal, + capturable, + ); + + assert_eq!(stable, 1); + assert_eq!(unstable, 0); + } + + #[test] + fn free_three_detected_horizontal() { + // + // . X X X . . + // + let me = bb(&[ + (1, 0), + (2, 0), + (3, 0), + ]); + + let mask = free_three_mask( + me, + BitBoard::new(), + Direction::Horizontal, + ); + + assert_eq!(mask.count_ones(), 1); + } + + #[test] + fn split_free_three_detected() { + // + // . X X . X . + // + let me = bb(&[ + (1, 0), + (2, 0), + (4, 0), + ]); + + let mask = free_three_mask( + me, + BitBoard::new(), + Direction::Horizontal, + ); + + assert_eq!(mask.count_ones(), 1); + } + + #[test] + fn blocked_three_is_not_free_three() { + // + // O X X X . . + // + let me = bb(&[ + (1, 0), + (2, 0), + (3, 0), + ]); + + let opp = bb(&[ + (0, 0), + ]); + + let mask = free_three_mask( + me, + opp, + Direction::Horizontal, + ); + + assert_eq!(mask.count_ones(), 0); + } + + #[test] + fn board_patterns_detect_open_four() { + // + // . X X X X . + // + let me = bb(&[ + (1, 0), + (2, 0), + (3, 0), + (4, 0), + ]); + + let res = + count_board_pattern( + me, + BitBoard::new(), + ); + + assert_eq!(res.open_four, 1); + assert_eq!(res.closed_four, 0); + } + + #[test] + fn board_patterns_detect_closed_four() { + // + // O X X X X . + // + let me = bb(&[ + (1, 0), + (2, 0), + (3, 0), + (4, 0), + ]); + + let opp = bb(&[ + (0, 0), + ]); + + let res = + count_board_pattern( + me, + opp, + ); + + assert_eq!(res.open_four, 0); + assert_eq!(res.closed_four, 1); + } + + #[test] + fn board_patterns_detect_open_three() { + // + // . X X X . + // + let me = bb(&[ + (1, 0), + (2, 0), + (3, 0), + ]); + + let res = + count_board_pattern( + me, + BitBoard::new(), + ); + + assert_eq!(res.open_three, 1); + assert_eq!(res.closed_three, 0); + } + + #[test] + fn board_patterns_detect_open_two() { + // + // . X X . + // + let me = bb(&[ + (1, 0), + (2, 0), + ]); + + let res = + count_board_pattern( + me, + BitBoard::new(), + ); + + assert_eq!(res.open_two, 1); + assert_eq!(res.closed_two, 0); + } + + #[test] + fn board_patterns_do_not_count_four_inside_five() { + // + // X X X X X + // + let me = bb(&[ + (0, 0), + (1, 0), + (2, 0), + (3, 0), + (4, 0), + ]); + + let res = + count_board_pattern( + me, + BitBoard::new(), + ); + + assert_eq!(res.open_four, 0); + assert_eq!(res.closed_four, 0); + } + + #[test] + fn board_patterns_do_not_count_three_inside_four() { + // + // X X X X + // + let me = bb(&[ + (0, 0), + (1, 0), + (2, 0), + (3, 0), + ]); + + let res = + count_board_pattern( + me, + BitBoard::new(), + ); + + assert_eq!(res.open_three, 0); + assert_eq!(res.closed_three, 0); + } + + #[test] + fn board_patterns_detect_stable_five() { + let me = bb(&[ + (0, 0), + (1, 0), + (2, 0), + (3, 0), + (4, 0), + ]); + + let res = + count_board_pattern( + me, + BitBoard::new(), + ); + + assert_eq!(res.stable_five, 1); + assert_eq!(res.unstable_five, 0); + } + + #[test] + fn board_patterns_detect_unstable_five() { + // + // . + // X X X X X + // X + // O + // + let me = bb(&[ + (0, 1), + (1, 1), + (2, 1), + (3, 1), + (4, 1), + (0, 2), + ]); + + let opp = bb(&[ + (0, 3), + ]); + + let res = + count_board_pattern( + me, + opp, + ); + + assert_eq!(res.stable_five, 0); + assert_eq!(res.unstable_five, 1); + } + + #[test] + fn board_patterns_merge_six_into_one_five() { + // + // X X X X X X + // + let me = bb(&[ + (0, 0), + (1, 0), + (2, 0), + (3, 0), + (4, 0), + (5, 0), + ]); + + let res = + count_board_pattern( + me, + BitBoard::new(), + ); + + assert_eq!(res.stable_five, 1); + assert_eq!(res.unstable_five, 0); + } }