use an OR in a spot where it will help some tiny amount, set up doc

This commit is contained in:
jackjohn7 2025-11-05 21:29:08 -06:00
parent 4e74d926b0
commit 08e0b11db9
9 changed files with 13632 additions and 5 deletions

BIN
design/inc/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

387
design/othello.typ Normal file
View file

@ -0,0 +1,387 @@
#let position(..rows) = {
rows.pos()
}
#let othello(pos, labels: true, cell-size: 30pt) = {
let rows = pos
// Colors
let board-color = rgb("#2d8659")
let line-color = rgb("#1a5c3a")
let white-piece = rgb("#f0f0f0")
let black-piece = rgb("#1a1a1a")
let label-color = rgb("#000000")
let flip-color = rgb("#d32f2f")
let mask-color = rgb("#000000").transparentize(60%)
// Calculate total size
let board-size = cell-size * 8
let label-offset = if labels { 15pt } else { 0pt }
let total-size = board-size + label-offset * 2
let piece-radius = cell-size * 0.4
let move-radius = cell-size * 0.25
box(
width: total-size,
height: total-size,
{
place(
dx: label-offset,
dy: label-offset,
// Draw board background
rect(
width: board-size,
height: board-size,
fill: board-color,
stroke: line-color + 2pt
)
)
// Draw grid lines
for i in range(1, 8) {
let offset = cell-size * i
// Vertical lines
place(
dx: label-offset + offset,
dy: label-offset,
line(
start: (0pt, 0pt),
end: (0pt, board-size),
stroke: line-color + 1pt
)
)
// Horizontal lines
place(
dx: label-offset,
dy: label-offset + offset,
line(
start: (0pt, 0pt),
end: (board-size, 0pt),
stroke: line-color + 1pt
)
)
}
// Draw pieces and markers
for (row-idx, row) in rows.enumerate() {
for (col-idx, cell) in row.clusters().enumerate() {
// Regular pieces
if cell == "B" or cell == "W" {
let x = label-offset + col-idx * cell-size + cell-size / 2 - piece-radius
let y = label-offset + row-idx * cell-size + cell-size / 2 - piece-radius
let piece-color = if cell == "B" { black-piece } else { white-piece }
let piece-stroke = if cell == "W" { line-color + 0.5pt } else { none }
place(
dx: x,
dy: y,
circle(
radius: piece-radius,
fill: piece-color,
stroke: piece-stroke
)
)
}
// Pieces to be flipped
else if cell == "b" or cell == "w" {
let x = label-offset + col-idx * cell-size + cell-size / 2 - piece-radius
let y = label-offset + row-idx * cell-size + cell-size / 2 - piece-radius
let piece-color = if cell == "b" { black-piece } else { white-piece }
place(
dx: x,
dy: y,
circle(
radius: piece-radius,
fill: piece-color,
stroke: flip-color + 2pt
)
)
}
// Available moves
else if cell == "0" or cell == "1" {
let x = label-offset + col-idx * cell-size + cell-size / 2 - move-radius
let y = label-offset + row-idx * cell-size + cell-size / 2 - move-radius
let move-color = if cell == "0" {
black-piece.transparentize(50%)
} else {
white-piece.transparentize(50%)
}
place(
dx: x,
dy: y,
circle(
radius: move-radius,
fill: move-color,
stroke: none
)
)
}
// Masked squares
else if cell == "X" {
let x = label-offset + col-idx * cell-size
let y = label-offset + row-idx * cell-size
place(
dx: x,
dy: y,
rect(
width: cell-size,
height: cell-size,
fill: mask-color,
stroke: none
)
)
}
}
}
// Draw labels
if labels {
let files = ("a", "b", "c", "d", "e", "f", "g", "h")
let ranks = ("8", "7", "6", "5", "4", "3", "2", "1").rev()
// File labels (bottom)
for (i, file) in files.enumerate() {
let x = label-offset + i * cell-size + cell-size / 2
place(
dx: x,
dy: board-size + label-offset + 3pt,
align(center, text(size: 10pt, fill: label-color, weight: "bold", file))
)
}
// Rank labels (left)
for (i, rank) in ranks.enumerate() {
let y = label-offset + i * cell-size + cell-size / 2
place(
dx: 3pt,
dy: y,
align(horizon, text(size: 10pt, fill: label-color, weight: "bold", rank))
)
}
}
}
)
}
#let othello-legend(cell-size: 20pt) = {
let board-color = rgb("#2d8659")
let line-color = rgb("#1a5c3a")
let white-piece = rgb("#f0f0f0")
let black-piece = rgb("#1a1a1a")
let flip-color = rgb("#d32f2f")
let mask-color = rgb("#000000").transparentize(60%)
let piece-radius = cell-size * 0.4
let move-radius = cell-size * 0.25
let legend-item(symbol, description) = {
grid(
columns: (cell-size + 10pt, 1fr),
align: (center + horizon, left + horizon),
gutter: 10pt,
// Symbol cell
box(
width: cell-size,
height: cell-size,
{
place(
rect(
width: cell-size,
height: cell-size,
fill: board-color,
stroke: line-color + 1pt
)
)
symbol
}
),
// Description
text(size: 10pt, description)
)
}
stack(
dir: ttb,
spacing: 6pt,
legend-item(
place(
dx: cell-size / 2 - piece-radius,
dy: cell-size / 2 - piece-radius,
circle(radius: piece-radius, fill: black-piece)
),
[Black disc]
),
legend-item(
place(
dx: cell-size / 2 - piece-radius,
dy: cell-size / 2 - piece-radius,
circle(radius: piece-radius, fill: white-piece, stroke: line-color + 0.5pt)
),
[White disc]
),
legend-item(
place(
dx: cell-size / 2 - piece-radius,
dy: cell-size / 2 - piece-radius,
circle(radius: piece-radius, fill: black-piece, stroke: flip-color + 2pt)
),
[Black disc to be flipped]
),
legend-item(
place(
dx: cell-size / 2 - piece-radius,
dy: cell-size / 2 - piece-radius,
circle(radius: piece-radius, fill: white-piece, stroke: flip-color + 2pt)
),
[White disc to be flipped]
),
legend-item(
place(
dx: cell-size / 2 - move-radius,
dy: cell-size / 2 - move-radius,
circle(radius: move-radius, fill: black-piece.transparentize(50%))
),
[Available move for black]
),
legend-item(
place(
dx: cell-size / 2 - move-radius,
dy: cell-size / 2 - move-radius,
circle(radius: move-radius, fill: white-piece.transparentize(50%))
),
[Available move for white]
),
legend-item(
place(
rect(width: cell-size, height: cell-size, fill: mask-color)
),
[Masked square]
),
legend-item(
[],
[Empty square]
),
)
}
#let shift-indicator(values, cell-size: 30pt) = {
assert(values.len() == 9, message: "shift-indicator requires exactly 9 values")
let board-color = rgb("#2d8659")
let line-color = rgb("#1a5c3a")
let white-piece = rgb("#f0f0f0")
let black-piece = rgb("#1a1a1a")
let grid-size = cell-size * 3
let piece-radius = cell-size * 0.4
box(
width: grid-size,
height: grid-size,
{
// Draw background
place(
rect(
width: grid-size,
height: grid-size,
fill: board-color,
stroke: line-color + 2pt
)
)
// Draw grid lines
for i in range(1, 3) {
let offset = cell-size * i
// Vertical lines
place(
dx: offset,
line(
start: (0pt, 0pt),
end: (0pt, grid-size),
stroke: line-color + 1pt
)
)
// Horizontal lines
place(
dy: offset,
line(
start: (0pt, 0pt),
end: (grid-size, 0pt),
stroke: line-color + 1pt
)
)
}
// Draw cells
for row in range(3) {
for col in range(3) {
let idx = row * 3 + col
let value = values.at(idx)
let x = col * cell-size
let y = row * cell-size
if value == 0 {
// Half-white, half-black disc
let center-x = x + cell-size / 2
let center-y = y + cell-size / 2
// Draw full black circle
place(
dx: center-x - piece-radius,
dy: center-y - piece-radius,
circle(radius: piece-radius, fill: black-piece, stroke: none)
)
// Draw white half-circle using clipping
place(
dx: center-x,
dy: center-y - piece-radius,
box(
width: piece-radius,
height: piece-radius * 2,
clip: true,
place(
dx: -piece-radius,
circle(radius: piece-radius, fill: white-piece, stroke: none)
)
)
)
// Draw circle outline
place(
dx: center-x - piece-radius,
dy: center-y - piece-radius,
circle(radius: piece-radius, fill: none, stroke: line-color + 0.5pt)
)
} else {
// Arrow and number
let arrow = if value > 0 { "«" } else { "»" }
let num = str(calc.abs(value))
place(
dx: x,
dy: y,
box(
width: cell-size,
height: cell-size,
align(
center + horizon,
text(fill: white, weight: "bold", size: 10pt, arrow + " " + num)
)
)
)
}
}
}
}
)
}

6
design/references.bib Normal file
View file

@ -0,0 +1,6 @@
@book{2020_molecular_biology_principles_of_genome_function_craig,
title={Molecular biology: principles of genome function},
author={Craig, Nancy and Green, Rachel and Cohen-Fix, Orna and Greider, Carol and Storz, Gisela and Wolberger, Cynthia},
year={2020},
publisher={Oxford University Press}
}

12669
design/report.pdf Normal file

File diff suppressed because it is too large Load diff

431
design/report.typ Normal file
View file

@ -0,0 +1,431 @@
#import "@preview/efter-plugget:0.1.1"
#import "othello.typ": position, othello, othello-legend, shift-indicator
#import "@preview/lovelace:0.3.0": pseudocode-list
#import "@preview/algorithmic:1.0.6"
#import algorithmic: style-algorithm, algorithm-figure
// library for visualizing registers and memory
#import "@preview/rivet:0.3.0": schema
// hallon is an optional library for subfigures.
#import "@preview/hallon:0.1.2": subfigure
// cellpress is an optional library for Cell Press table style.
#import "@preview/cellpress-unofficial:0.1.0" as cellpress: toprule, midrule, bottomrule
// smartaref is an optional library for handling consecutive references.
#import "@preview/smartaref:0.1.0": cref, Cref
#show: efter-plugget.template.with(
logo: image("inc/logo.png"),
title: [Othello Done Fast],
subtitle: [Squeezing out maximum performance using better datastructures],
page-header-title: ("Othello"),
course-name: ("Artificial Intelligence"),
course-code: "CSC-4753",
authors: "Jack Branch",
)
#show: cellpress.style-table
#let bitarray(arr) = {
grid(columns: arr.len(),
..arr.filter(a => a != "").map(a => rect[
#text(str(a))
])
)
}
// #quote(
// block: true,
// attribution: [anonymous],
// )[
// #emph["Chemistry is all around us."]
// ]
Some of the datastructures and algorithms in use for this assignment are a bit unintuitive, so I figured I would spend the time thoroughly explaining each.
= Introduction
In standard Othello, you have an eight by eight board of squares. Each square can contain a disc that is either black or white. How might you represent this structure in memory? This question and the implications of its answers is what this document will explore.
I will make references to Rust structures and types while using pseudocode for all algorithms.
== A Naive Solution
A common naive approach would be to represent it as an array of characters where a `b` character represents a black disc, a `w` character represents a white disc, and an empty space represents a square with no disc at all.
```rust
struct NaiveBoard {
board: [[char; 8]; 8] // in C, char[8][8]
}
```
One may observe that each ordinary `char` is represented as a single byte (unsigned 8-bit integer) and thus this structure would take a total of 64 bytes in memory. While this spacial efficiency problem is egregious, I'd argue it's the least of its concerns.
The much larger problem with this datastructure is its impact on runtime performance of algorithms in terms of pure speed. While they _technically_ have the same time complexity in Big $O$, Big $O$ does not tell the full story here. This will be explained in further detail in the next section.
== The Optimal Solution
One may observe that each square in our board ultimately exists in one of three states at a time. These states are _has a black disc_, _has a white disc_, and _has no disc at all_. We could encode this information in just two bits! The first bit can encode whether or not there is a black disc while the second encodes whether or not there is a white disc. Naturally, if both bits are 0, then there is no disc at all.
So we'd have `00` for no disc, `10` for a black disc, and `01` for a white disc. `11` is not a valid state, so you would want to ensure your program cannot produce it. To create a full Othello board, you'll want to simply associate two unsigned 64-bit integers (one for black and one for white). I will hereafter refer to this structure as a _BitBoard_. An awesome thing about this representation is that we can derive certain properties using only bitwise operations.
For example, we can compute _all_ of the empty spaces by computing the negation of the bitwise `OR` of the two boards. To do the same with the naive solution, would require that you loop through the datastructure. Since the dimensions of the structure are constant, that algorithm would have constant time complexity. They may be equivalent in a sense, but they still perform quite differently. The naive requires repeated randomly array-accessed string comparisons while our new algorithm is, depending on CPU architecture, executing only one or two instructions total.
Perhaps that operation is too simple to be fully representative. In the following chapters, I will demonstrate that same general bitwise approach can be had for all of the required algorithms.
#figure(caption: "Starting Position",
othello(position(
"........",
"........",
"...0....",
"..0WB...",
"...BW0..",
"....0...",
"........",
"........"
), cell-size: 16pt))
#let bitboard-p1 = yaml("./structures/bitboard-p1.yaml")
#let doc = schema.load(bitboard-p1)
At the bit level, the structure of a single sub-board is as follows: #footnote("Text is a bit small. May have to zoom.")
#figure(caption: "Layout for a single sub-board",
schema.render(doc)) <sub-board-layout>
Now an entire game's board would simply be two of these. Ideally allocated contiguously in memory as part of one structure. My BitBoard is represented by the following rust code:
```rust
// `Board` is referred to as "sub-board" in this paper.
pub type Board = u64;
pub struct BitBoard {
pub boards: [Board; 2],
}
```
== A Note on Notation
I will use boards with descending rank and ascending file with the following legend. The starting board and black's first available moves will appear as follows:
#grid(
columns: (auto, 1fr),
gutter: 30pt,
align: (center, left),
// Board
othello(position(
"........",
"........",
"...0....",
"..0WB...",
"...BW0..",
"....0...",
"........",
"........"
)),
// Legend
othello-legend()
)
= Board Operations
== Generating Moves
In Othello, the only legal moves are ones that _flank_ your opponent. In other words, you have to place a disc that pinches one or more of your opponent's pieces.
In simple terms, the algorithm is as follows:
#pseudocode-list[
+ keep a running sub-board of available moves
+ *for each* disc of the current player's discs
+ *for each* direction
+ keep a running sub-board of opponent squares in this direction
+ *while* the next square in that direction is has an opponent disc
+ add to running sub-board of opponent squares
+ *end*
+ *if* next square in direction is empty
+ add next empty square to running sub-board of moves
+ *end*
+ *end*
+ *end*
]
=== The Optimal Solution
To generate moves, we need to generate a sub-board (unsigned 64-bit integer) where bits are excited if and only if the current player can place a disc there.
Now, one might scratch their head wondering how they gain anything with this datastructure if you need to go disc by disc and shift them over repeatedly to determine whether or not there is an available move. Thankfully, because of the structure of the BitBoard, we can avoid this process entirely!
Now, since my structure happens to be Big Endian #footnote("It would be equally viable in Little Endian, but you would need to reverse directions of shifts."), to compute a sub-board with every piece moved to the right, we can shift the entire board to the left by one. Observe that in our diagram <sub-board-layout>, *b1* is to the _left_ of *a1* despite it being to the _right_ in the board visually
#footnote[An advantage of a Little Endian implementation is that the shift logic would be slightly more intuitive. However, in a Big Endian structure, the sub-board containing only *a1* is defined as $1$ which just seems natural to my human brain.].
With that in mind, we can just think of all the different values that correspond to particular adjacencies. To go up, trivially, we shift left by 8. To go up and to the right, we shift once more. To go up and to the left, we shift by one less. Going down, we mirror the shifts to go up but swap the diagonals. To go to the left, we shift to the right once. Thus, we have the following shift constants:
#figure(caption: "Shift constants",
shift-indicator((7, 8, 9, 1, 0, -1, -9, -8, -7))
)
Consider a black disc on *h1*. If we shift it to the right, where does it end up? Observe what happens:
#grid(
columns: (auto, auto),
gutter: 70pt,
align: (center, left),
figure(caption: [*h1*], othello(cell-size: 20pt, position(
"........",
"........",
"........",
"........",
"........",
"........",
"........",
".......B"
)))
,
// Board shifted once over
figure(caption: [*h1 << 1*], othello(cell-size: 20pt, position(
"........",
"........",
"........",
"........",
"........",
"........",
"B.......",
"........"
))),
)
Therefore, it is prudent that we prevent wrap-around when moving horizontally or diagonally. We can do this by masking out the *a* file when moving left (visually) and masking out the *h* file when moving right.
#grid(
columns: (auto, auto, auto),
align: (center, left),
othello(cell-size: 16pt, position(
"........",
"........",
"........",
"........",
"........",
"........",
"........",
".......B"
))
,
figure(caption: [*!h*], othello(cell-size: 16pt, position(
".......X",
".......X",
".......X",
".......X",
".......X",
".......X",
".......X",
".......X"
)))
,
// Board shifted once over
figure(caption: [*(h1 & !h) << 1*], othello(cell-size: 16pt, position(
"........",
"........",
"........",
"........",
"........",
"........",
"........",
"........"
))),
)
Thus, we can associate directional shifts with masks. With those pieces in mind, let's go through the algorithm to find left-flanks in the starting position for Black
#footnote[A left flank involves placing a disc to the right of an enemy disc].
#grid(
columns: (auto, auto, auto),
align: (center, left),
othello(cell-size: 14pt, position(
"........",
"........",
"........",
"...WB...",
"...BW...",
"........",
"........",
"........"
))
,
figure(caption: [(B & !h) << 1], othello(cell-size: 14pt, position(
".......X",
".......X",
".......X",
".....B.X",
"....B..X",
".......X",
".......X",
".......X"
))),
figure(caption: [*((P & !h) << 1) & O* #footnote[$P$ is the current team playing, and $O$ is the opponent.]], othello(cell-size: 14pt, position(
"........",
"........",
"........",
"........",
"....W...",
"........",
"........",
"........"
))),
)
The result above is only the first pass when computing left flanks. You would want to run the same computation again six more times with the previous result as input instead of $B$.
However, since there are no more discs to the right, we'd end up with the next seven passes will end up being effectively no-ops. Let's call this sub-board $A_r$.
Once we've finished looking seven squares to the right, we want to add #footnote[*XOR* is slightly faster and equivalent in this case.] empty squares to the right of the squares we've computed to our total sub-board of available moves $M$.
#figure(caption: [*$((A_r << 1) & E)$*], othello(cell-size: 30pt, position(
"........",
"........",
"........",
"........",
".....0..",
"........",
"........",
"........"
))),
Thus, we have determined that our sub-board $M$ contains *f5*. Okay, with that out of the way, let's generalize the algorithm for each direction.
#show: style-algorithm
#algorithm-figure(
"Move Generation",
vstroke: .5pt + luma(200),
{
import algorithmic: *
Comment[Define the directional shift constants]
Assign[$D$][${ 9, 8, 7, 1, -1, -7, -8, -9 }$]
Comment[Define the masks ($F_a$ includes all but $F_a$)]
Assign[$F_a$][0x7F7F7F7F7F7F7F7F]
Assign[$F_h$][0xFEFEFEFEFEFEFEFE]
Assign[all][0xFEFEFEFEFEFEFEFE]
Assign[$M$][${
F_a,
"all",
F_h,
F_a,
F_h,
F_a,
"all",
F_h,
}$]
LineBreak
Comment[A helper function that shifts in the appropriate direction depending on the magnitude]
Procedure(
"directional_shift",
("magnitude", "board"),
{
If($"magnitude" >= 0 $, {
Return[$"board" << "magnitude"$]
})
Else({
Return[$"board" >> "magnitude"$]
})
},
)
LineBreak
Procedure(
"Available",
("P", "O"),
{
Comment[Define the board of empty squares]
Assign[$E$][$!(P | O)$]
LineBreak
Assign[$"move"$][$0$]
Assign[$i$][$0$]
While(
$i < 8$,
{
Assign([d], [$D[i]$])
Assign([m], [$M[i]$])
Assign([$A$], FnInline[directional_shift][$(d, P amp m)$])
Assign([$A$], FnInline[directional_shift][$(d, A amp m)$])
Assign([$A$], FnInline[directional_shift][$(d, A amp m)$])
Assign([$A$], FnInline[directional_shift][$(d, A amp m)$])
Assign([$A$], FnInline[directional_shift][$(d, A amp m)$])
Assign([$A$], FnInline[directional_shift][$(d, A amp m)$])
Assign([$A$], FnInline[directional_shift][$(d, A amp m)$])
Assign([$"move"$], $"move" | A$)
Assign([$i$], $i + 1$)
},
)
Return[$"move"$]
},
)
}
)
If the algorithm is performed correctly, you will end up with the following board of legal moves:
#figure(caption: [Starting moves for Black], othello(cell-size: 30pt, position(
"........",
"........",
"...0....",
"..0.....",
".....0..",
"....0...",
"........",
"........"
)))
=== Implementation in Rust
```rust
const NOT_A_FILE: Board = 0x7F7F7F7F7F7F7F7F;
const NOT_H_FILE: Board = 0xFEFEFEFEFEFEFEFE;
const SHIFT_MASK_COMBOS: [(i8, Board); 8] = [
(9, NOT_A_FILE), // 9 (up right)
(8, Board::MAX), // 8 (up)
(7, NOT_H_FILE), // 7 (up left)
(1, NOT_A_FILE), // 1 (right)
(-1, NOT_H_FILE), // -1 (left)
(-7, NOT_A_FILE), // -7 (down right)
(-8, Board::MAX), // -8 (down)
(-9, NOT_H_FILE), // -9 (down left)
];
impl BitBoard {
pub fn available(&self, current_team: Team) -> Board {
let empties = !(self.boards[0] | self.boards[1]);
let mut moves = 0;
let (team, opponent) = (
self.boards[current_team as usize],
self.boards[current_team.next() as usize],
);
for (shift, mask) in SHIFT_MASK_COMBOS {
let mut sub_move = shift_in_direction(shift, team & mask) & opponent;
sub_move |= shift_in_direction(shift, sub_move & mask) & opponent;
sub_move |= shift_in_direction(shift, sub_move & mask) & opponent;
sub_move |= shift_in_direction(shift, sub_move & mask) & opponent;
sub_move |= shift_in_direction(shift, sub_move & mask) & opponent;
sub_move |= shift_in_direction(shift, sub_move & mask) & opponent;
sub_move = shift_in_direction(shift, sub_move & mask) & empties;
moves |= sub_move;
}
moves
}
}
```
=== Benchmarks
== Playing Moves
=== Benchmarks

View file

@ -0,0 +1,135 @@
structures:
main:
bits: 64
ranges:
63:
name: h8
description: top-right corner
62:
name: g8
61:
name: f8
60:
name: e8
59:
name: d8
58:
name: c8
57:
name: b8
56:
name: a8
description: top-left corner
55:
name: h7
54:
name: g7
53:
name: f7
52:
name: e7
51:
name: d7
50:
name: c7
49:
name: b7
48:
name: a7
47:
name: h6
46:
name: g6
45:
name: f6
44:
name: e6
43:
name: d6
42:
name: c6
41:
name: b6
40:
name: a6
39:
name: h5
38:
name: g5
37:
name: f5
36:
name: e5
35:
name: d5
34:
name: c5
33:
name: b5
32:
name: a5
31:
name: h4
30:
name: g4
29:
name: f4
28:
name: e4
27:
name: d4
26:
name: c4
25:
name: b4
24:
name: a4
23:
name: h3
22:
name: g3
21:
name: f3
20:
name: e3
19:
name: d3
18:
name: c3
17:
name: b3
16:
name: a3
15:
name: h2
14:
name: g2
13:
name: f2
12:
name: e2
11:
name: d2
10:
name: c2
9:
name: b2
8:
name: a2
7:
name: h1
6:
name: g1
5:
name: f1
4:
name: e1
3:
name: d1
2:
name: c1
1:
name: b1
0:
name: a1
description: bottom-left corner

View file

@ -11,7 +11,7 @@
devShells."x86_64-linux".default = pkgs.mkShell {
buildInputs = with pkgs; [
cargo rustc rustfmt clippy rust-analyzer glibc
cargo rustc rustfmt clippy rust-analyzer glibc typst tinymist
];
nativeBuildInputs = [ pkgs.pkg-config ];
env.RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";

View file

@ -144,6 +144,7 @@ impl Display for BitBoard {
}
}
#[inline]
fn shift_in_direction(magnitude: i8, value: Board) -> Board {
if magnitude >= 0 {
let (result, _) = value.overflowing_shl(magnitude.unsigned_abs() as u32);
@ -374,10 +375,8 @@ impl BitBoard {
}
}
self.boards[current_team_idx] += flips;
self.boards[current_team_idx] |= flips | play;
self.boards[current_team.next() as usize] ^= flips;
self.boards[current_team_idx] += play;
}
/// Compute the score (B, W) by counting the excited bits in each board.

View file

@ -50,7 +50,7 @@ impl Game {
#[cfg(test)]
mod tests {
use super::*;
use crate::board::{squares::*, view::View};
use crate::board::squares::*;
#[test]
fn play_switches_team() {
let mut game = Game::default();