Skip to content

Commit e99b4af

Browse files
committed
Trying to find a better BitBoard API
1 parent a514b36 commit e99b4af

File tree

7 files changed

+145
-45
lines changed

7 files changed

+145
-45
lines changed

gomori/src/board.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,18 @@ mod compact_field;
55
pub use bbox::*;
66
pub use bitboard::*;
77
pub use compact_field::*;
8-
#[cfg(feature = "python")]
9-
use pyo3::pyclass;
10-
11-
pub const BOARD_SIZE: i8 = 4;
128

139
use std::ops::Deref;
1410

1511
use crate::{Card, CardToPlace, Field, IllegalCardPlayed, Rank, Suit};
1612

13+
pub const BOARD_SIZE: i8 = 4;
14+
1715
/// Represents a board with at least one card on it.
1816
//
1917
// Because after the first move, there is at least one card on it,
2018
// the minimum and maximum coordinates always exist.
21-
#[cfg_attr(feature = "python", pyclass)]
19+
#[cfg_attr(feature = "python", pyo3::pyclass)]
2220
#[derive(Clone, Debug)]
2321
pub struct Board {
2422
/// There is exactly one entry in this list for every field with at least one card on it.

gomori/src/board/bbox.rs

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
#[cfg(feature = "python")]
2-
use pyo3::pyclass;
3-
41
/// A 2D area represented by a min + max coordinate pair.
52
///
63
/// The two coordinates form an _inclusive_ 2D range, i.e. unlike in a
74
/// half-open range, it's possible for a point with `i == i_max`
85
/// to be contained in the area.
9-
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
6+
#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
107
#[derive(Clone, Copy, Debug)]
118
pub struct BoundingBox {
129
pub i_min: i8,
@@ -47,6 +44,7 @@ impl BoundingBox {
4744
})
4845
}
4946

47+
/// Expands the bounding box to cover point `(i, j)`.
5048
pub fn update(&mut self, i: i8, j: i8) {
5149
self.i_min = self.i_min.min(i);
5250
self.i_max = self.i_max.max(i);
@@ -62,9 +60,25 @@ mod python {
6260
use super::*;
6361
#[pymethods]
6462
impl BoundingBox {
63+
#[new]
64+
#[pyo3(signature = (*, i_min, j_min, i_max, j_max))]
65+
fn py_new(i_min: i8, j_min: i8, i_max: i8, j_max: i8) -> Self {
66+
Self {
67+
i_min,
68+
j_min,
69+
i_max,
70+
j_max,
71+
}
72+
}
73+
6574
#[pyo3(name = "contains")]
6675
fn py_contains(&self, i: i8, j: i8) -> bool {
6776
self.contains(i, j)
6877
}
78+
79+
#[pyo3(name = "update")]
80+
fn py_update(&mut self, i: i8, j: i8) {
81+
self.update(i, j)
82+
}
6983
}
7084
}

gomori/src/board/bitboard.rs

+112-17
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,50 @@
11
use std::fmt::{self, Debug};
22

3-
#[cfg(feature = "python")]
4-
use pyo3::pyclass;
5-
63
static I_SHIFT: u8 = 49 + 7;
74
static J_SHIFT: u8 = 49;
85
static BOARD_MASK: u64 = 0x1ffffffffffff;
96
static IJ_MASK: u64 = 0x7ffe000000000000;
107

8+
// A mask for the bits that do not get "shifted out" when changing the offset's i value.
9+
// The index into this array is (offset_i_new - offset_i + 7), clamped to (0, 14).
10+
static SHIFT_MASK_I: [u64; 15] = [
11+
0b0000000000000000000000000000000000000000000000000,
12+
0b0000000000000000000000000000000000000000001111111,
13+
0b0000000000000000000000000000000000011111111111111,
14+
0b0000000000000000000000000000111111111111111111111,
15+
0b0000000000000000000001111111111111111111111111111,
16+
0b0000000000000011111111111111111111111111111111111,
17+
0b0000000111111111111111111111111111111111111111111,
18+
0b1111111111111111111111111111111111111111111111111,
19+
0b1111111111111111111111111111111111111111110000000,
20+
0b1111111111111111111111111111111111100000000000000,
21+
0b1111111111111111111111111111000000000000000000000,
22+
0b1111111111111111111110000000000000000000000000000,
23+
0b1111111111111100000000000000000000000000000000000,
24+
0b1111111000000000000000000000000000000000000000000,
25+
0b0000000000000000000000000000000000000000000000000,
26+
];
27+
28+
// A mask for the bits that do not get "shifted out" when changing the offset's j value.
29+
// The index into this array is (offset_j_new - offset_j + 7), clamped to (0, 14).
30+
static SHIFT_MASK_J: [u64; 15] = [
31+
0b0000000000000000000000000000000000000000000000000,
32+
0b0000001000000100000010000001000000100000010000001,
33+
0b0000011000001100000110000011000001100000110000011,
34+
0b0000111000011100001110000111000011100001110000111,
35+
0b0001111000111100011110001111000111100011110001111,
36+
0b0011111001111100111110011111001111100111110011111,
37+
0b0111111011111101111110111111011111101111110111111,
38+
0b1111111111111111111111111111111111111111111111111,
39+
0b1111110111111011111101111110111111011111101111110,
40+
0b1111100111110011111001111100111110011111001111100,
41+
0b1111000111100011110001111000111100011110001111000,
42+
0b1110000111000011100001110000111000011100001110000,
43+
0b1100000110000011000001100000110000011000001100000,
44+
0b1000000100000010000001000000100000010000001000000,
45+
0b0000000000000000000000000000000000000000000000000,
46+
];
47+
1148
/// A [`Copy`] board representation that stores only a single
1249
/// bit per field.
1350
///
@@ -18,7 +55,7 @@ static IJ_MASK: u64 = 0x7ffe000000000000;
1855
/// means of its [`IntoIterator`] instance.
1956
///
2057
/// Note that its "mutating" methods return a new object instead of really mutating.
21-
#[cfg_attr(feature = "python", pyclass)]
58+
#[cfg_attr(feature = "python", pyo3::pyclass)]
2259
#[derive(Clone, Copy)]
2360
pub struct BitBoard {
2461
/// The low 49 bits are the board itself (7x7)
@@ -37,6 +74,24 @@ pub struct BitBoard {
3774
/// and all the numbers in [0, 63i8] start with the bits 00.
3875
/// Therefore, compression works by removing the highest bit,
3976
/// and adding it back when reading.
77+
///
78+
/// How do (i, j) coordinates map to bits in the board?
79+
/// (i, j) is represented as the bit number (i * 7 + j), counted from
80+
/// the least significant bit. So if you lay out a number like
81+
/// 0b0000000000000011111111111111111111111111111111111 in blocks of 7
82+
/// (which is also what the Debug impl does) like so:
83+
///
84+
/// ```text
85+
/// 0 0 0 0 0 0 0
86+
/// 0 0 0 0 0 0 0
87+
/// 1 1 1 1 1 1 1
88+
/// 1 1 1 1 1 1 1
89+
/// 1 1 1 1 1 1 1
90+
/// 1 1 1 1 1 1 1
91+
/// 1 1 1 1 1 1 1
92+
/// ```
93+
/// then this 2D array effectively has a coordinate system that has i going from the
94+
/// bottom (0) to the top (6), and j going from the right (0) to the left (6).
4095
bits: u64,
4196
}
4297

@@ -55,10 +110,8 @@ impl BitBoard {
55110
// all the cards, it will fit within the 7x7 area.
56111
let offset_i = i - 3;
57112
let offset_j = j - 3;
58-
let offset_i_bits = u64::from(offset_i as u8 & 0b01111111u8) << I_SHIFT;
59-
let offset_j_bits = u64::from(offset_j as u8 & 0b01111111u8) << J_SHIFT;
60113
Self {
61-
bits: offset_i_bits | offset_j_bits,
114+
bits: encode_offset(offset_i, offset_j),
62115
}
63116
}
64117

@@ -165,10 +218,33 @@ impl BitBoard {
165218
board_bits.count_ones(),
166219
(board_bits_shifted & BOARD_MASK).count_ones()
167220
);
168-
let offset_i_bits = u64::from(new_offset_i as u8 & 0b01111111u8) << I_SHIFT;
169-
let offset_j_bits = u64::from(new_offset_j as u8 & 0b01111111u8) << J_SHIFT;
221+
let offset_bits = encode_offset(new_offset_i, new_offset_j);
170222
Self {
171-
bits: offset_i_bits | offset_j_bits | board_bits_shifted,
223+
bits: offset_bits | board_bits_shifted,
224+
}
225+
}
226+
227+
pub(crate) fn shift_lossy(self, new_center: (i8, i8)) -> BitBoard {
228+
assert!(new_center.0 >= -52);
229+
assert!(new_center.1 >= -52);
230+
assert!(new_center.0 <= 52);
231+
assert!(new_center.1 <= 52);
232+
233+
let (offset_i, offset_j) = self.offset();
234+
let (new_offset_i, new_offset_j) = (new_center.0 - 3, new_center.1 - 3);
235+
let (diff_i, diff_j) = (new_offset_i - offset_i, new_offset_j - offset_j);
236+
let mask_i = SHIFT_MASK_I[(diff_i + 7).clamp(0, 14) as usize];
237+
let mask_j = SHIFT_MASK_J[(diff_j + 7).clamp(0, 14) as usize];
238+
let valid_bits = self.bits & mask_i & mask_j;
239+
let shift_by = diff_i * 7 + diff_j;
240+
let bits_shifted = if shift_by > 0 {
241+
valid_bits >> shift_by
242+
} else {
243+
valid_bits << shift_by.abs()
244+
};
245+
let offset_bits = encode_offset(new_offset_i, new_offset_j);
246+
Self {
247+
bits: offset_bits | bits_shifted,
172248
}
173249
}
174250

@@ -224,16 +300,26 @@ impl BitBoard {
224300
}
225301

226302
fn offset(self) -> (i8, i8) {
227-
// The highest bit of i_compressed is garbage and needs
228-
// to be replaced with the second-highest bit.
229-
let offset_i_compressed = 0b01111111i8 & (self.bits >> I_SHIFT) as i8;
230-
let offset_i = offset_i_compressed | ((offset_i_compressed & 0b01000000i8) << 1);
231-
let offset_j_compressed = 0b01111111i8 & (self.bits >> J_SHIFT) as i8;
232-
let offset_j = offset_j_compressed | ((offset_j_compressed & 0b01000000i8) << 1);
233-
(offset_i, offset_j)
303+
decode_offset(self.bits)
234304
}
235305
}
236306

307+
fn decode_offset(bits: u64) -> (i8, i8) {
308+
// The highest bit of i_compressed is garbage and needs
309+
// to be replaced with the second-highest bit.
310+
let offset_i_compressed = 0b01111111i8 & (bits >> I_SHIFT) as i8;
311+
let offset_i = offset_i_compressed | ((offset_i_compressed & 0b01000000i8) << 1);
312+
let offset_j_compressed = 0b01111111i8 & (bits >> J_SHIFT) as i8;
313+
let offset_j = offset_j_compressed | ((offset_j_compressed & 0b01000000i8) << 1);
314+
(offset_i, offset_j)
315+
}
316+
317+
fn encode_offset(offset_i: i8, offset_j: i8) -> u64 {
318+
let offset_i_bits = u64::from(offset_i as u8 & 0b01111111u8) << I_SHIFT;
319+
let offset_j_bits = u64::from(offset_j as u8 & 0b01111111u8) << J_SHIFT;
320+
offset_i_bits | offset_j_bits
321+
}
322+
237323
impl Debug for BitBoard {
238324
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
239325
let digits = format!("{:049b}", self.bits & BOARD_MASK);
@@ -325,4 +411,13 @@ mod tests {
325411
.insert(15, 30);
326412
assert_eq!(bb.bits, bb.recenter_to((15, 33)).recenter_to((12, 30)).bits);
327413
}
414+
415+
#[test]
416+
fn shift() {
417+
let bb = BitBoard::empty_board_centered_at(12, 30)
418+
.insert(12, 30)
419+
.insert(12, 33)
420+
.insert(15, 30);
421+
assert_eq!(bb.bits, bb.shift_lossy((15, 33)).shift_lossy((12, 30)).bits);
422+
}
328423
}

gomori/src/board/compact_field.rs

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
#[cfg(feature = "python")]
2-
use pyo3::pyclass;
3-
41
use crate::helpers::bitset_traits;
52
use crate::{Card, Field, Rank, Suit};
63

@@ -16,7 +13,7 @@ const CLEAR_TOP_CARD_MASK: u64 = !(TOP_CARD_INDICATOR_BIT | TOP_CARD_MASK);
1613
/// facing up and down, because that doesn't matter for the game.
1714
///
1815
/// Note that its "mutating" methods return a new object instead of really mutating.
19-
#[cfg_attr(feature = "python", pyclass)]
16+
#[cfg_attr(feature = "python", pyo3::pyclass)]
2017
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2118
pub struct CompactField {
2219
/// The low 52 bits are a bitset of the hidden cards.
@@ -126,7 +123,7 @@ impl From<&Field> for CompactField {
126123
///
127124
/// Allows intersection/union/xor with other such sets via bitwise ops.
128125
/// Also implements [`IntoIterator`].
129-
#[cfg_attr(feature = "python", pyclass)]
126+
#[cfg_attr(feature = "python", pyo3::pyclass)]
130127
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131128
pub struct CardsSet {
132129
bits: u64,
@@ -184,7 +181,7 @@ impl IntoIterator for CardsSet {
184181
}
185182

186183
/// Iterator for a [`CardsSet`] that returns cards by ascending rank.
187-
#[cfg_attr(feature = "python", pyclass)]
184+
#[cfg_attr(feature = "python", pyo3::pyclass)]
188185
#[derive(Clone, Copy, Debug)]
189186
pub struct CardsSetIter {
190187
bits: u64,

gomori/src/cards.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
use std::str::FromStr;
22

3-
#[cfg(feature = "python")]
4-
use pyo3::pyclass;
53
use serde::{Deserialize, Serialize};
64

75
/// A playing card in a standard 52-card game.
8-
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
6+
#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
97
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
108
pub struct Card {
119
pub suit: Suit,
1210
pub rank: Rank,
1311
}
1412

15-
#[cfg_attr(feature = "python", pyclass)]
13+
#[cfg_attr(feature = "python", pyo3::pyclass)]
1614
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1715
#[repr(u8)]
1816
pub enum Suit {
@@ -26,7 +24,7 @@ pub enum Suit {
2624
Club,
2725
}
2826

29-
#[cfg_attr(feature = "python", pyclass)]
27+
#[cfg_attr(feature = "python", pyo3::pyclass)]
3028
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3129
#[repr(u8)]
3230
pub enum Rank {

gomori/src/protocol_types.rs

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use std::collections::BTreeSet;
22

3-
#[cfg(feature = "python")]
4-
use pyo3::pyclass;
53
use serde::{Deserialize, Serialize};
64

75
use crate::Card;
@@ -46,7 +44,7 @@ pub enum Request {
4644
#[derive(Clone, Debug, Serialize, Deserialize)]
4745
pub struct Okay();
4846

49-
#[cfg_attr(feature = "python", pyclass)]
47+
#[cfg_attr(feature = "python", pyo3::pyclass)]
5048
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
5149
#[serde(rename_all = "lowercase")]
5250
pub enum Color {
@@ -57,7 +55,7 @@ pub enum Color {
5755
}
5856

5957
/// A single field on the board, including coordinates.
60-
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
58+
#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
6159
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
6260
pub struct Field {
6361
/// The first coordinate.
@@ -73,7 +71,7 @@ pub struct Field {
7371
}
7472

7573
/// Specifies which card to play, and where.
76-
#[cfg_attr(feature = "python", pyclass)]
74+
#[cfg_attr(feature = "python", pyo3::pyclass)]
7775
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
7876
pub struct CardToPlace {
7977
pub card: Card,
@@ -88,7 +86,7 @@ pub struct CardToPlace {
8886
}
8987

9088
/// The cards to play in this turn, in order.
91-
#[cfg_attr(feature = "python", pyclass)]
89+
#[cfg_attr(feature = "python", pyo3::pyclass)]
9290
#[derive(Clone, Debug, Serialize, Deserialize)]
9391
pub struct PlayTurnResponse(pub Vec<CardToPlace>);
9492

judge/src/main.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use tracing_subscriber::util::SubscriberInitExt;
1111

1212
#[derive(Parser)]
1313
struct Args {
14-
/// Path to the executable for player 1
14+
/// Path to the config JSON file for player 1
1515
player_1_config: PathBuf,
1616

17-
/// Path to the executable for player 2
17+
/// Path to the config JSON file for player 2
1818
player_2_config: PathBuf,
1919

2020
/// How many games to play

0 commit comments

Comments
 (0)