use an OR in a spot where it will help some tiny amount, set up doc
This commit is contained in:
parent
4e74d926b0
commit
08e0b11db9
9 changed files with 13632 additions and 5 deletions
BIN
design/inc/logo.png
Normal file
BIN
design/inc/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
387
design/othello.typ
Normal file
387
design/othello.typ
Normal 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
6
design/references.bib
Normal 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
12669
design/report.pdf
Normal file
File diff suppressed because it is too large
Load diff
431
design/report.typ
Normal file
431
design/report.typ
Normal 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
|
||||
135
design/structures/bitboard-p1.yaml
Normal file
135
design/structures/bitboard-p1.yaml
Normal 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
|
||||
|
|
@ -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}";
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue