Skip to content

Commit b467d9a

Browse files
committed
Add BitBoard::threes_in_a_row()
Also implement some nice-to-have traits on existing iterators.
1 parent 81e2d37 commit b467d9a

File tree

2 files changed

+169
-12
lines changed

2 files changed

+169
-12
lines changed

gomori/src/board/bitboard.rs

+154-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::fmt::{self, Debug};
1+
use std::{
2+
fmt::{self, Debug},
3+
iter::FusedIterator,
4+
};
25

36
const I_SHIFT: u8 = 49 + 7;
47
const J_SHIFT: u8 = 49;
@@ -51,7 +54,7 @@ const OFFSET_MASK: u64 = 0x7ffe000000000000;
5154
/// Every valid board would fit in a 4 x 4 area, so why 7 x 7? One reason is that with a 7 x 7 board,
5255
/// we can be sure that not only the board itself can be represented, but also the next card, as long as it is in the board's [playable area](crate::Board::playable_area).
5356
#[cfg_attr(feature = "python", pyo3::pyclass)]
54-
#[derive(Clone, Copy)]
57+
#[derive(Clone, Copy, PartialEq, Eq)]
5558
pub struct BitBoard {
5659
/// The low 49 bits are the board itself (7x7)
5760
/// The next highest 7 bits are the j offset.
@@ -168,6 +171,31 @@ impl BitBoard {
168171
}
169172
}
170173

174+
/// Returns all lines on the field consisting of at least three points.
175+
///
176+
/// An exception are the diagonals in the far four corners of the bitboard, for example:
177+
///
178+
/// ```text
179+
/// 0 0 0 0 0 0 0
180+
/// 0 0 0 0 0 0 0
181+
/// 0 0 0 0 0 0 0
182+
/// 0 0 0 1 0 0 0
183+
/// 0 0 0 0 0 0 1
184+
/// 0 0 0 0 0 1 0
185+
/// 0 0 0 0 1 0 0
186+
/// ```
187+
///
188+
/// They are not included, since the fourth field for those diagonals is outside the
189+
/// playable area of the board.
190+
#[must_use]
191+
pub fn threes_in_a_row(self) -> ThreesInARowIter {
192+
ThreesInARowIter {
193+
bitboard: self,
194+
n: 0,
195+
orientation: LineOrientation::IRow,
196+
}
197+
}
198+
171199
pub fn is_empty(self) -> bool {
172200
(self.bits & BOARD_MASK) == 0
173201
}
@@ -310,6 +338,68 @@ fn shift_2d_lossy(bits: u64, (delta_i, delta_j): (i8, i8)) -> u64 {
310338
}
311339
}
312340

341+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
342+
pub enum LineOrientation {
343+
/// A line of coordinates that all share the same `i` value.
344+
IRow,
345+
/// A line of coordinates that all share the same `j` value.
346+
JRow,
347+
/// A diagonal where `i` and `j` increase.
348+
Diagonal,
349+
/// A diagonal where `i` increases while `j` decreases.
350+
Antidiagonal,
351+
}
352+
353+
/// Iterator returned by [`BitBoard::threes_in_a_row()`].
354+
pub struct ThreesInARowIter {
355+
bitboard: BitBoard,
356+
// Bitset of the local i coordinates whose rows have at least 3 bits set
357+
n: i8,
358+
orientation: LineOrientation,
359+
}
360+
361+
impl Iterator for ThreesInARowIter {
362+
type Item = (LineOrientation, BitBoard);
363+
364+
fn next(&mut self) -> Option<Self::Item> {
365+
while self.n < 7 {
366+
let current_orientation = self.orientation;
367+
let mask = match current_orientation {
368+
LineOrientation::IRow => {
369+
let mask = 0b1111111u64 << (self.n * 7);
370+
(self.orientation, self.n) = (LineOrientation::JRow, self.n);
371+
mask
372+
}
373+
LineOrientation::JRow => {
374+
let mask = 0b1000000100000010000001000000100000010000001u64 << self.n;
375+
(self.orientation, self.n) = (LineOrientation::Diagonal, self.n);
376+
mask
377+
}
378+
LineOrientation::Diagonal => {
379+
let mask = shift_2d_lossy(0x1010101010101u64, (3 - self.n, 0));
380+
(self.orientation, self.n) = (LineOrientation::Antidiagonal, self.n);
381+
mask
382+
}
383+
LineOrientation::Antidiagonal => {
384+
let mask = shift_2d_lossy(0x41041041040u64, (self.n - 3, 0));
385+
(self.orientation, self.n) = (LineOrientation::IRow, self.n + 1);
386+
mask
387+
}
388+
};
389+
let intersection = self.bitboard.bits & mask;
390+
if intersection.count_ones() >= 3 {
391+
let bb = BitBoard {
392+
bits: self.bitboard.bits & OFFSET_MASK | intersection,
393+
};
394+
return Some((current_orientation, bb));
395+
}
396+
}
397+
None
398+
}
399+
}
400+
401+
impl FusedIterator for ThreesInARowIter {}
402+
313403
impl std::ops::BitAnd for BitBoard {
314404
type Output = Self;
315405

@@ -366,18 +456,25 @@ impl std::ops::BitXorAssign for BitBoard {
366456

367457
impl Debug for BitBoard {
368458
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
369-
let digits = format!("{:049b}", self.bits & BOARD_MASK);
370-
let mut s = String::with_capacity(49 * 2);
371-
for (idx, c) in digits.chars().rev().enumerate() {
372-
s.push(c);
373-
if idx % 7 == 6 {
374-
s.push('\n');
375-
} else {
376-
s.push(' ');
377-
}
459+
write!(f, "{}", print_bits(self.bits))
460+
}
461+
}
462+
463+
// Prints the bitset as a 2D array, least significant bit first,
464+
// such that the local coordinate (0, 0) is in the top left corner,
465+
// and i is the vertical and j the horizontal coordinate.
466+
fn print_bits(bits: u64) -> String {
467+
let digits = format!("{:049b}", bits & BOARD_MASK);
468+
let mut s = String::with_capacity(49 * 2);
469+
for (idx, c) in digits.chars().rev().enumerate() {
470+
s.push(c);
471+
if idx % 7 == 6 {
472+
s.push('\n');
473+
} else {
474+
s.push(' ');
378475
}
379-
write!(f, "{}", s)
380476
}
477+
s
381478
}
382479

383480
/// Iterator produced by [`BitBoard::into_iter()`].
@@ -410,8 +507,21 @@ impl Iterator for BitBoardIter {
410507
Some((offset_i + idx / 7, offset_j + idx % 7))
411508
}
412509
}
510+
511+
fn size_hint(&self) -> (usize, Option<usize>) {
512+
let size = self.bitboard.num_entries() as usize;
513+
(size, Some(size))
514+
}
515+
}
516+
517+
impl ExactSizeIterator for BitBoardIter {
518+
fn len(&self) -> usize {
519+
self.bitboard.num_entries() as usize
520+
}
413521
}
414522

523+
impl FusedIterator for BitBoardIter {}
524+
415525
#[cfg(feature = "python")]
416526
mod python {
417527
use pyo3::pymethods;
@@ -537,4 +647,36 @@ mod tests {
537647
Vec::new()
538648
);
539649
}
650+
651+
#[test]
652+
fn threes_in_a_row() {
653+
let bb_1 = BitBoard::empty_board_centered_at((-20, -10));
654+
assert_eq!(bb_1.threes_in_a_row().count(), 0);
655+
let bb_2 = BitBoard::empty_board_centered_at((-20, -10))
656+
.insert(-20, -10)
657+
.insert(-21, -10)
658+
.insert(-21, -9)
659+
.insert(-20, -9);
660+
assert_eq!(bb_2.threes_in_a_row().count(), 0);
661+
let bb_3 = BitBoard::empty_board_centered_at((-20, -10))
662+
.insert(-20, -9)
663+
.insert(-20, -10)
664+
.insert(-20, -12);
665+
assert_eq!(
666+
Vec::from_iter(bb_3.threes_in_a_row()),
667+
vec![(LineOrientation::IRow, bb_3)]
668+
);
669+
assert_eq!(
670+
Vec::from_iter(bb_3.insert(-21, -9).threes_in_a_row()),
671+
vec![(LineOrientation::IRow, bb_3)]
672+
);
673+
let bb_4 = BitBoard::empty_board_centered_at((-20, -10))
674+
.insert(-22, -10)
675+
.insert(-21, -9)
676+
.insert(-20, -8);
677+
assert_eq!(
678+
Vec::from_iter(bb_4.threes_in_a_row()),
679+
vec![(LineOrientation::Diagonal, bb_4)]
680+
);
681+
}
540682
}

gomori/src/cards_set.rs

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::iter::FusedIterator;
2+
13
use crate::Card;
24

35
/// A compact set of [`Card`]s.
@@ -180,8 +182,21 @@ impl Iterator for CardsSetIter {
180182
Some(Card::from_index(card_idx))
181183
}
182184
}
185+
186+
fn size_hint(&self) -> (usize, Option<usize>) {
187+
let size = self.bits.count_ones() as usize;
188+
(size, Some(size))
189+
}
183190
}
184191

192+
impl ExactSizeIterator for CardsSetIter {
193+
fn len(&self) -> usize {
194+
self.bits.count_ones() as usize
195+
}
196+
}
197+
198+
impl FusedIterator for CardsSetIter {}
199+
185200
#[cfg(feature = "python")]
186201
mod python {
187202
use pyo3::pymethods;

0 commit comments

Comments
 (0)