Skip to content

Commit

Permalink
Trying to find a better BitBoard API
Browse files Browse the repository at this point in the history
  • Loading branch information
nnmm committed Jul 21, 2024
1 parent a514b36 commit e99b4af
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 45 deletions.
8 changes: 3 additions & 5 deletions gomori/src/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,18 @@ mod compact_field;
pub use bbox::*;
pub use bitboard::*;
pub use compact_field::*;
#[cfg(feature = "python")]
use pyo3::pyclass;

pub const BOARD_SIZE: i8 = 4;

use std::ops::Deref;

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

pub const BOARD_SIZE: i8 = 4;

/// Represents a board with at least one card on it.
//
// Because after the first move, there is at least one card on it,
// the minimum and maximum coordinates always exist.
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3::pyclass)]
#[derive(Clone, Debug)]
pub struct Board {
/// There is exactly one entry in this list for every field with at least one card on it.
Expand Down
22 changes: 18 additions & 4 deletions gomori/src/board/bbox.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
#[cfg(feature = "python")]
use pyo3::pyclass;

/// A 2D area represented by a min + max coordinate pair.
///
/// The two coordinates form an _inclusive_ 2D range, i.e. unlike in a
/// half-open range, it's possible for a point with `i == i_max`
/// to be contained in the area.
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
#[derive(Clone, Copy, Debug)]
pub struct BoundingBox {
pub i_min: i8,
Expand Down Expand Up @@ -47,6 +44,7 @@ impl BoundingBox {
})
}

/// Expands the bounding box to cover point `(i, j)`.
pub fn update(&mut self, i: i8, j: i8) {
self.i_min = self.i_min.min(i);
self.i_max = self.i_max.max(i);
Expand All @@ -62,9 +60,25 @@ mod python {
use super::*;
#[pymethods]
impl BoundingBox {
#[new]
#[pyo3(signature = (*, i_min, j_min, i_max, j_max))]
fn py_new(i_min: i8, j_min: i8, i_max: i8, j_max: i8) -> Self {
Self {
i_min,
j_min,
i_max,
j_max,
}
}

#[pyo3(name = "contains")]
fn py_contains(&self, i: i8, j: i8) -> bool {
self.contains(i, j)
}

#[pyo3(name = "update")]
fn py_update(&mut self, i: i8, j: i8) {
self.update(i, j)
}
}
}
129 changes: 112 additions & 17 deletions gomori/src/board/bitboard.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
use std::fmt::{self, Debug};

#[cfg(feature = "python")]
use pyo3::pyclass;

static I_SHIFT: u8 = 49 + 7;
static J_SHIFT: u8 = 49;
static BOARD_MASK: u64 = 0x1ffffffffffff;
static IJ_MASK: u64 = 0x7ffe000000000000;

// A mask for the bits that do not get "shifted out" when changing the offset's i value.
// The index into this array is (offset_i_new - offset_i + 7), clamped to (0, 14).
static SHIFT_MASK_I: [u64; 15] = [
0b0000000000000000000000000000000000000000000000000,
0b0000000000000000000000000000000000000000001111111,
0b0000000000000000000000000000000000011111111111111,
0b0000000000000000000000000000111111111111111111111,
0b0000000000000000000001111111111111111111111111111,
0b0000000000000011111111111111111111111111111111111,
0b0000000111111111111111111111111111111111111111111,
0b1111111111111111111111111111111111111111111111111,
0b1111111111111111111111111111111111111111110000000,
0b1111111111111111111111111111111111100000000000000,
0b1111111111111111111111111111000000000000000000000,
0b1111111111111111111110000000000000000000000000000,
0b1111111111111100000000000000000000000000000000000,
0b1111111000000000000000000000000000000000000000000,
0b0000000000000000000000000000000000000000000000000,
];

// A mask for the bits that do not get "shifted out" when changing the offset's j value.
// The index into this array is (offset_j_new - offset_j + 7), clamped to (0, 14).
static SHIFT_MASK_J: [u64; 15] = [
0b0000000000000000000000000000000000000000000000000,
0b0000001000000100000010000001000000100000010000001,
0b0000011000001100000110000011000001100000110000011,
0b0000111000011100001110000111000011100001110000111,
0b0001111000111100011110001111000111100011110001111,
0b0011111001111100111110011111001111100111110011111,
0b0111111011111101111110111111011111101111110111111,
0b1111111111111111111111111111111111111111111111111,
0b1111110111111011111101111110111111011111101111110,
0b1111100111110011111001111100111110011111001111100,
0b1111000111100011110001111000111100011110001111000,
0b1110000111000011100001110000111000011100001110000,
0b1100000110000011000001100000110000011000001100000,
0b1000000100000010000001000000100000010000001000000,
0b0000000000000000000000000000000000000000000000000,
];

/// A [`Copy`] board representation that stores only a single
/// bit per field.
///
Expand All @@ -18,7 +55,7 @@ static IJ_MASK: u64 = 0x7ffe000000000000;
/// means of its [`IntoIterator`] instance.
///
/// Note that its "mutating" methods return a new object instead of really mutating.
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3::pyclass)]
#[derive(Clone, Copy)]
pub struct BitBoard {
/// The low 49 bits are the board itself (7x7)
Expand All @@ -37,6 +74,24 @@ pub struct BitBoard {
/// and all the numbers in [0, 63i8] start with the bits 00.
/// Therefore, compression works by removing the highest bit,
/// and adding it back when reading.
///
/// How do (i, j) coordinates map to bits in the board?
/// (i, j) is represented as the bit number (i * 7 + j), counted from
/// the least significant bit. So if you lay out a number like
/// 0b0000000000000011111111111111111111111111111111111 in blocks of 7
/// (which is also what the Debug impl does) like so:
///
/// ```text
/// 0 0 0 0 0 0 0
/// 0 0 0 0 0 0 0
/// 1 1 1 1 1 1 1
/// 1 1 1 1 1 1 1
/// 1 1 1 1 1 1 1
/// 1 1 1 1 1 1 1
/// 1 1 1 1 1 1 1
/// ```
/// then this 2D array effectively has a coordinate system that has i going from the
/// bottom (0) to the top (6), and j going from the right (0) to the left (6).
bits: u64,
}

Expand All @@ -55,10 +110,8 @@ impl BitBoard {
// all the cards, it will fit within the 7x7 area.
let offset_i = i - 3;
let offset_j = j - 3;
let offset_i_bits = u64::from(offset_i as u8 & 0b01111111u8) << I_SHIFT;
let offset_j_bits = u64::from(offset_j as u8 & 0b01111111u8) << J_SHIFT;
Self {
bits: offset_i_bits | offset_j_bits,
bits: encode_offset(offset_i, offset_j),
}
}

Expand Down Expand Up @@ -165,10 +218,33 @@ impl BitBoard {
board_bits.count_ones(),
(board_bits_shifted & BOARD_MASK).count_ones()
);
let offset_i_bits = u64::from(new_offset_i as u8 & 0b01111111u8) << I_SHIFT;
let offset_j_bits = u64::from(new_offset_j as u8 & 0b01111111u8) << J_SHIFT;
let offset_bits = encode_offset(new_offset_i, new_offset_j);
Self {
bits: offset_i_bits | offset_j_bits | board_bits_shifted,
bits: offset_bits | board_bits_shifted,
}
}

pub(crate) fn shift_lossy(self, new_center: (i8, i8)) -> BitBoard {
assert!(new_center.0 >= -52);
assert!(new_center.1 >= -52);
assert!(new_center.0 <= 52);
assert!(new_center.1 <= 52);

let (offset_i, offset_j) = self.offset();
let (new_offset_i, new_offset_j) = (new_center.0 - 3, new_center.1 - 3);
let (diff_i, diff_j) = (new_offset_i - offset_i, new_offset_j - offset_j);
let mask_i = SHIFT_MASK_I[(diff_i + 7).clamp(0, 14) as usize];
let mask_j = SHIFT_MASK_J[(diff_j + 7).clamp(0, 14) as usize];
let valid_bits = self.bits & mask_i & mask_j;
let shift_by = diff_i * 7 + diff_j;
let bits_shifted = if shift_by > 0 {
valid_bits >> shift_by
} else {
valid_bits << shift_by.abs()
};
let offset_bits = encode_offset(new_offset_i, new_offset_j);
Self {
bits: offset_bits | bits_shifted,
}
}

Expand Down Expand Up @@ -224,16 +300,26 @@ impl BitBoard {
}

fn offset(self) -> (i8, i8) {
// The highest bit of i_compressed is garbage and needs
// to be replaced with the second-highest bit.
let offset_i_compressed = 0b01111111i8 & (self.bits >> I_SHIFT) as i8;
let offset_i = offset_i_compressed | ((offset_i_compressed & 0b01000000i8) << 1);
let offset_j_compressed = 0b01111111i8 & (self.bits >> J_SHIFT) as i8;
let offset_j = offset_j_compressed | ((offset_j_compressed & 0b01000000i8) << 1);
(offset_i, offset_j)
decode_offset(self.bits)
}
}

fn decode_offset(bits: u64) -> (i8, i8) {
// The highest bit of i_compressed is garbage and needs
// to be replaced with the second-highest bit.
let offset_i_compressed = 0b01111111i8 & (bits >> I_SHIFT) as i8;
let offset_i = offset_i_compressed | ((offset_i_compressed & 0b01000000i8) << 1);
let offset_j_compressed = 0b01111111i8 & (bits >> J_SHIFT) as i8;
let offset_j = offset_j_compressed | ((offset_j_compressed & 0b01000000i8) << 1);
(offset_i, offset_j)
}

fn encode_offset(offset_i: i8, offset_j: i8) -> u64 {
let offset_i_bits = u64::from(offset_i as u8 & 0b01111111u8) << I_SHIFT;
let offset_j_bits = u64::from(offset_j as u8 & 0b01111111u8) << J_SHIFT;
offset_i_bits | offset_j_bits
}

impl Debug for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let digits = format!("{:049b}", self.bits & BOARD_MASK);
Expand Down Expand Up @@ -325,4 +411,13 @@ mod tests {
.insert(15, 30);
assert_eq!(bb.bits, bb.recenter_to((15, 33)).recenter_to((12, 30)).bits);
}

#[test]
fn shift() {
let bb = BitBoard::empty_board_centered_at(12, 30)
.insert(12, 30)
.insert(12, 33)
.insert(15, 30);
assert_eq!(bb.bits, bb.shift_lossy((15, 33)).shift_lossy((12, 30)).bits);
}
}
9 changes: 3 additions & 6 deletions gomori/src/board/compact_field.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
#[cfg(feature = "python")]
use pyo3::pyclass;

use crate::helpers::bitset_traits;
use crate::{Card, Field, Rank, Suit};

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

/// Iterator for a [`CardsSet`] that returns cards by ascending rank.
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3::pyclass)]
#[derive(Clone, Copy, Debug)]
pub struct CardsSetIter {
bits: u64,
Expand Down
8 changes: 3 additions & 5 deletions gomori/src/cards.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
use std::str::FromStr;

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

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

#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3::pyclass)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(u8)]
pub enum Suit {
Expand All @@ -26,7 +24,7 @@ pub enum Suit {
Club,
}

#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3::pyclass)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(u8)]
pub enum Rank {
Expand Down
10 changes: 4 additions & 6 deletions gomori/src/protocol_types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use std::collections::BTreeSet;

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

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

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

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

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

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

Expand Down
4 changes: 2 additions & 2 deletions judge/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ use tracing_subscriber::util::SubscriberInitExt;

#[derive(Parser)]
struct Args {
/// Path to the executable for player 1
/// Path to the config JSON file for player 1
player_1_config: PathBuf,

/// Path to the executable for player 2
/// Path to the config JSON file for player 2
player_2_config: PathBuf,

/// How many games to play
Expand Down

0 comments on commit e99b4af

Please sign in to comment.