Skip to content

Commit ea56fce

Browse files
committed
Bitboard improvements
1 parent 4befd04 commit ea56fce

File tree

3 files changed

+109
-101
lines changed

3 files changed

+109
-101
lines changed

gomori/src/board.rs

+7-4
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub struct PlayCardCalculation<'a> {
4646
/// This struct ties together the board and its diff, to prevent any possible mixups
4747
board: &'a Board,
4848
pub(crate) diff: Diff,
49-
/// The cards that were won by this play,
49+
/// The cards that were won as a result of playing this card
5050
pub cards_won: CardsSet,
5151
/// Should another card be played?
5252
pub combo: bool,
@@ -132,10 +132,11 @@ impl Board {
132132
// placed card. If there is a line of 4 cards, it must be cards of this
133133
// suit.
134134
let cards_of_same_suit = self.bitboards[card.suit as usize]
135-
.recenter_to(flipped.center())
136135
.insert(i, j)
137136
.difference(flipped);
138-
cards_of_same_suit.detect_central_lines().remove(i, j)
137+
cards_of_same_suit
138+
.lines_going_through_point(i, j)
139+
.remove(i, j)
139140
};
140141

141142
let cards_won = {
@@ -234,6 +235,7 @@ impl Board {
234235
false
235236
}
236237

238+
/// Returns all the coordinates that are valid places to play the given card.
237239
pub fn locations_for_card(&self, card: Card) -> BitBoard {
238240
// Create a BitBoard with 1 in every location where any card could be played
239241
// so that it is not out of bounds.
@@ -243,7 +245,8 @@ impl Board {
243245
i_max,
244246
j_max,
245247
} = self.playable_area();
246-
let (center_i, center_j) = self.bitboards[0].center();
248+
let center_i = (i_min + i_max) / 2;
249+
let center_j = (j_min + j_max) / 2;
247250
let mut bitboard = BitBoard::empty_board_centered_at(center_i, center_j)
248251
.insert_area(i_min, j_min, i_max, j_max);
249252

gomori/src/board/bitboard.rs

+95-90
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ pub struct BitBoard {
7878
/// How do (i, j) coordinates map to bits in the board?
7979
/// (i, j) is represented as the bit number (i * 7 + j), counted from
8080
/// the least significant bit. So if you lay out a number like
81-
/// 0b0000000000000011111111111111111111111111111111111 in blocks of 7
81+
/// 0b1100000000000011111111111111111111111111111111111 in blocks of 7
8282
/// from least significant to most significant bit
8383
/// (which is also what the Debug impl does) like so:
8484
///
@@ -89,7 +89,7 @@ pub struct BitBoard {
8989
/// 1 1 1 1 1 1 1
9090
/// 1 1 1 1 1 1 1
9191
/// 0 0 0 0 0 0 0
92-
/// 0 0 0 0 0 0 0
92+
/// 0 0 0 0 0 1 1
9393
/// ```
9494
/// then this 2D array effectively has a coordinate system that has i going from the
9595
/// top (0) to the bottom (6), and j going from the left (0) to the right (6).
@@ -176,111 +176,52 @@ impl BitBoard {
176176
(self.bits & BOARD_MASK).count_ones()
177177
}
178178

179-
pub(crate) fn center(self) -> (i8, i8) {
180-
let (offset_i, offset_j) = self.offset();
181-
(offset_i + 3, offset_j + 3)
182-
}
183-
184-
pub(crate) fn recenter_to(self, new_center: (i8, i8)) -> BitBoard {
185-
let (offset_i, offset_j) = self.offset();
186-
let (new_offset_i, new_offset_j) = (new_center.0 - 3, new_center.1 - 3);
187-
debug_assert!((offset_i - new_offset_i).abs() < 4);
188-
debug_assert!((offset_j - new_offset_j).abs() < 4);
189-
190-
let board_bits = self.bits & BOARD_MASK;
191-
192-
// Imagine the 49 board bits like this (top left is lowest bit, bottom right highest, and
193-
// i selects the row, j the column):
194-
//
195-
// . . . . . . .
196-
// . . . . . . .
197-
// . . . . 1 . .
198-
// . . 1 x . 1 .
199-
// . . . . 1 n .
200-
// . . . . . . .
201-
// . . . . . . .
202-
//
203-
// The center is marked with x, cards are marked with 1 (since they are 1s in the bitset),
204-
// and the new center is marked with n. We need to shift n to land on x.
205-
// It's important to realize that there will never be any wraparound in the sense that a
206-
// 1 lands on the other side of the board, as long as the new center is a valid center for
207-
// a bitboard that contains the same cards.
208-
209-
let diff = (offset_i - new_offset_i) * 7 + (offset_j - new_offset_j);
210-
let board_bits_shifted = if diff > 0 {
211-
// Since the new center has lower coordinates, the local coordinates will have
212-
// higher coordinates => shift left
213-
board_bits << diff
214-
} else {
215-
board_bits >> diff.abs()
216-
};
217-
// No ones should have gotten lost
218-
debug_assert_eq!(
219-
board_bits.count_ones(),
220-
(board_bits_shifted & BOARD_MASK).count_ones()
221-
);
222-
let offset_bits = encode_offset(new_offset_i, new_offset_j);
223-
Self {
224-
bits: offset_bits | board_bits_shifted,
225-
}
226-
}
227-
228-
pub(crate) fn shift_lossy(self, new_center: (i8, i8)) -> BitBoard {
229-
assert!(new_center.0 >= -52);
230-
assert!(new_center.1 >= -52);
231-
assert!(new_center.0 <= 52);
232-
assert!(new_center.1 <= 52);
233-
234-
let (offset_i, offset_j) = self.offset();
235-
let (new_offset_i, new_offset_j) = (new_center.0 - 3, new_center.1 - 3);
236-
let (diff_i, diff_j) = (new_offset_i - offset_i, new_offset_j - offset_j);
237-
let mask_i = SHIFT_MASK_I[(diff_i + 7).clamp(0, 14) as usize];
238-
let mask_j = SHIFT_MASK_J[(diff_j + 7).clamp(0, 14) as usize];
239-
let valid_bits = self.bits & mask_i & mask_j;
240-
let shift_by = diff_i * 7 + diff_j;
241-
let bits_shifted = if shift_by > 0 {
242-
valid_bits >> shift_by
243-
} else {
244-
valid_bits << shift_by.abs()
245-
};
246-
let offset_bits = encode_offset(new_offset_i, new_offset_j);
247-
Self {
248-
bits: offset_bits | bits_shifted,
249-
}
250-
}
251-
252-
/// Compute the difference to another `BitBoard`.
253-
///
254-
/// Both boards must be centered around the same point, otherwise this
255-
/// function will panic.
179+
/// Computes the difference to another `BitBoard`.
256180
#[must_use]
257181
pub fn difference(self, other: BitBoard) -> BitBoard {
258-
assert_eq!(self.bits & IJ_MASK, other.bits & IJ_MASK);
182+
// Recenter the other bitboard to our center, which may lose some coordinates.
183+
// But that's fine - since these coordinates are not representable in our bitboard,
184+
// they can't be contained in our bitboard anyway.
259185
Self {
260-
bits: self.bits & !(other.bits & BOARD_MASK),
186+
bits: self.bits & !other.board_bits_shifted_to_offset_lossy(self.offset()),
261187
}
262188
}
263189

190+
/// Checks whether there are any horizontal, vertical or diagonal lines of length 4
191+
/// passing through the specified point (in a 7 x 7 area centered on the point).
192+
///
193+
/// Any lines that are found are returned in a new `BitBoard`. The result is therefore
194+
/// a subset of the input.
195+
///
196+
/// Only valid for point coordinates in the range `[-52, 52]`.
264197
#[must_use]
265-
pub(crate) fn detect_central_lines(self) -> BitBoard {
198+
pub fn lines_going_through_point(self, point_i: i8, point_j: i8) -> BitBoard {
199+
debug_assert!(point_i >= -52);
200+
debug_assert!(point_j >= -52);
201+
debug_assert!(point_i <= 52);
202+
debug_assert!(point_j <= 52);
203+
204+
let offset = (point_i - 3, point_j - 3);
205+
// The bits we may lose by shifting to the new center are exactly those that are
206+
// more than 3 fields away from the point. They don't count anyway.
207+
let bits_centered = self.board_bits_shifted_to_offset_lossy(offset);
208+
266209
let mut line_bits = 0;
267210
// These patterns are lines on the 7x7 board - horizontal, vertical, and two diagonal.
268-
// They already are zero outside of BOARD_MASK, so self.bits & pattern is the same as
269-
// (self.bits & BOARD_MASK) & pattern.
270211
for pattern in [
271212
0xfe00000u64,
272213
0x204081020408u64,
273214
0x1010101010101u64,
274215
0x41041041040u64,
275216
] {
276-
let pattern_intersect = self.bits & pattern;
217+
let pattern_intersect = bits_centered & pattern;
277218
debug_assert!(pattern_intersect.count_ones() <= 4);
278219
if pattern_intersect.count_ones() == 4 {
279220
line_bits |= pattern_intersect;
280221
}
281222
}
282223
Self {
283-
bits: (self.bits & IJ_MASK) | line_bits,
224+
bits: encode_offset(offset.0, offset.1) | line_bits,
284225
}
285226
}
286227

@@ -303,6 +244,25 @@ impl BitBoard {
303244
fn offset(self) -> (i8, i8) {
304245
decode_offset(self.bits)
305246
}
247+
248+
fn board_bits_shifted_to_offset_lossy(self, new_offset: (i8, i8)) -> u64 {
249+
debug_assert!(new_offset.0 >= -55);
250+
debug_assert!(new_offset.1 >= -55);
251+
debug_assert!(new_offset.0 <= 49);
252+
debug_assert!(new_offset.1 <= 49);
253+
254+
let (offset_i, offset_j) = self.offset();
255+
let (diff_i, diff_j) = (new_offset.0 - offset_i, new_offset.1 - offset_j);
256+
let mask_i = SHIFT_MASK_I[(diff_i + 7).clamp(0, 14) as usize];
257+
let mask_j = SHIFT_MASK_J[(diff_j + 7).clamp(0, 14) as usize];
258+
let valid_bits = self.bits & mask_i & mask_j;
259+
let shift_by = diff_i * 7 + diff_j;
260+
if shift_by > 0 {
261+
valid_bits >> shift_by.min(63)
262+
} else {
263+
valid_bits << shift_by.abs().min(63)
264+
}
265+
}
306266
}
307267

308268
fn decode_offset(bits: u64) -> (i8, i8) {
@@ -386,6 +346,24 @@ mod python {
386346
fn py_is_empty(&self) -> bool {
387347
self.is_empty()
388348
}
349+
350+
#[pyo3(name = "difference")]
351+
fn py_difference(&self, other: BitBoard) -> BitBoard {
352+
self.difference(other)
353+
}
354+
355+
#[pyo3(name = "lines_going_through_point")]
356+
fn py_lines_going_through_point(&self, point_i: i8, point_j: i8) -> BitBoard {
357+
self.lines_going_through_point(point_i, point_j)
358+
}
359+
360+
fn __len__(&self) -> usize {
361+
self.num_entries() as usize
362+
}
363+
364+
fn __bool__(&self) -> bool {
365+
!self.is_empty()
366+
}
389367
}
390368
}
391369

@@ -405,20 +383,47 @@ mod tests {
405383
}
406384

407385
#[test]
408-
fn recenter() {
386+
fn shift_far() {
409387
let bb = BitBoard::empty_board_centered_at(12, 30)
388+
.insert(11, 32)
410389
.insert(12, 30)
411390
.insert(12, 33)
412391
.insert(15, 30);
413-
assert_eq!(bb.bits, bb.recenter_to((15, 33)).recenter_to((12, 30)).bits);
392+
// None of the coordinates on the board are representable with that offset.
393+
let bits_shifted = bb.board_bits_shifted_to_offset_lossy((0, 0));
394+
assert_eq!(bits_shifted, 0);
414395
}
415396

416397
#[test]
417398
fn shift() {
418399
let bb = BitBoard::empty_board_centered_at(12, 30)
419-
.insert(12, 30)
400+
.insert(11, 32)
401+
.insert(12, 31)
420402
.insert(12, 33)
421403
.insert(15, 30);
422-
assert_eq!(bb.bits, bb.shift_lossy((15, 33)).shift_lossy((12, 30)).bits);
404+
let (offset_i, offset_j) = (11, 31);
405+
let bits_shifted = bb.board_bits_shifted_to_offset_lossy((offset_i, offset_j));
406+
let bb_shifted = BitBoard {
407+
bits: bits_shifted | encode_offset(offset_i, offset_j),
408+
};
409+
let coordinates_after_shift = Vec::from_iter(bb_shifted);
410+
assert_eq!(coordinates_after_shift, vec![(11, 32), (12, 31), (12, 33)]);
411+
}
412+
413+
#[test]
414+
fn detect_line() {
415+
let bb = BitBoard::empty_board_centered_at(10, 10)
416+
.insert(8, 11)
417+
.insert(11, 11)
418+
.insert(12, 11)
419+
.insert(13, 11);
420+
assert_eq!(
421+
Vec::from_iter(bb.lines_going_through_point(11, 11)),
422+
Vec::from_iter(bb)
423+
);
424+
assert_eq!(
425+
Vec::from_iter(bb.lines_going_through_point(9, 9)),
426+
Vec::new()
427+
);
423428
}
424429
}

gomori/src/cards.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,22 @@ impl Card {
106106

107107
/// The error type for the [`FromStr`] instance of [`Card`].
108108
#[derive(Clone, Copy, Debug)]
109-
pub enum InvalidCardErr {
109+
pub enum CardFromStrErr {
110110
LessThanTwoChars,
111111
MoreThanTwoChars,
112112
InvalidRank,
113113
InvalidSuit,
114114
}
115115

116116
impl FromStr for Card {
117-
type Err = InvalidCardErr;
117+
type Err = CardFromStrErr;
118118

119119
fn from_str(s: &str) -> Result<Self, Self::Err> {
120120
let mut chars = s.chars();
121-
let rank_char = chars.next().ok_or(InvalidCardErr::LessThanTwoChars)?;
122-
let suit_char = chars.next().ok_or(InvalidCardErr::LessThanTwoChars)?;
121+
let rank_char = chars.next().ok_or(CardFromStrErr::LessThanTwoChars)?;
122+
let suit_char = chars.next().ok_or(CardFromStrErr::LessThanTwoChars)?;
123123
if chars.next().is_some() {
124-
return Err(InvalidCardErr::MoreThanTwoChars);
124+
return Err(CardFromStrErr::MoreThanTwoChars);
125125
}
126126
let rank = match rank_char {
127127
'2' => Rank::Two,
@@ -137,14 +137,14 @@ impl FromStr for Card {
137137
'Q' => Rank::Queen,
138138
'K' => Rank::King,
139139
'A' => Rank::Ace,
140-
_ => return Err(InvalidCardErr::InvalidRank),
140+
_ => return Err(CardFromStrErr::InvalidRank),
141141
};
142142
let suit = match suit_char {
143143
'♦' => Suit::Diamond,
144144
'♥' => Suit::Heart,
145145
'♠' => Suit::Spade,
146146
'♣' => Suit::Club,
147-
_ => return Err(InvalidCardErr::InvalidSuit),
147+
_ => return Err(CardFromStrErr::InvalidSuit),
148148
};
149149
Ok(Card { rank, suit })
150150
}

0 commit comments

Comments
 (0)