|
1 |
| -use std::fmt::{self, Debug}; |
| 1 | +use std::{ |
| 2 | + fmt::{self, Debug}, |
| 3 | + iter::FusedIterator, |
| 4 | +}; |
2 | 5 |
|
3 | 6 | const I_SHIFT: u8 = 49 + 7;
|
4 | 7 | const J_SHIFT: u8 = 49;
|
@@ -51,7 +54,7 @@ const OFFSET_MASK: u64 = 0x7ffe000000000000;
|
51 | 54 | /// 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,
|
52 | 55 | /// 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).
|
53 | 56 | #[cfg_attr(feature = "python", pyo3::pyclass)]
|
54 |
| -#[derive(Clone, Copy)] |
| 57 | +#[derive(Clone, Copy, PartialEq, Eq)] |
55 | 58 | pub struct BitBoard {
|
56 | 59 | /// The low 49 bits are the board itself (7x7)
|
57 | 60 | /// The next highest 7 bits are the j offset.
|
@@ -168,6 +171,31 @@ impl BitBoard {
|
168 | 171 | }
|
169 | 172 | }
|
170 | 173 |
|
| 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 | + |
171 | 199 | pub fn is_empty(self) -> bool {
|
172 | 200 | (self.bits & BOARD_MASK) == 0
|
173 | 201 | }
|
@@ -310,6 +338,68 @@ fn shift_2d_lossy(bits: u64, (delta_i, delta_j): (i8, i8)) -> u64 {
|
310 | 338 | }
|
311 | 339 | }
|
312 | 340 |
|
| 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 | + |
313 | 403 | impl std::ops::BitAnd for BitBoard {
|
314 | 404 | type Output = Self;
|
315 | 405 |
|
@@ -366,18 +456,25 @@ impl std::ops::BitXorAssign for BitBoard {
|
366 | 456 |
|
367 | 457 | impl Debug for BitBoard {
|
368 | 458 | 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(' '); |
378 | 475 | }
|
379 |
| - write!(f, "{}", s) |
380 | 476 | }
|
| 477 | + s |
381 | 478 | }
|
382 | 479 |
|
383 | 480 | /// Iterator produced by [`BitBoard::into_iter()`].
|
@@ -410,8 +507,21 @@ impl Iterator for BitBoardIter {
|
410 | 507 | Some((offset_i + idx / 7, offset_j + idx % 7))
|
411 | 508 | }
|
412 | 509 | }
|
| 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 | + } |
413 | 521 | }
|
414 | 522 |
|
| 523 | +impl FusedIterator for BitBoardIter {} |
| 524 | + |
415 | 525 | #[cfg(feature = "python")]
|
416 | 526 | mod python {
|
417 | 527 | use pyo3::pymethods;
|
@@ -537,4 +647,36 @@ mod tests {
|
537 | 647 | Vec::new()
|
538 | 648 | );
|
539 | 649 | }
|
| 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 | + } |
540 | 682 | }
|
0 commit comments