caro5 / shared /bot /BitBoard.ts
Pedro de Carvalho
Add bot
4d74f52
Raw
History Blame Contribute Delete
8.5 kB
import { isBoardCoordInBounds, serializeCoord } from '../board.ts'
import type { Cell, SerializedBitBoard } from '../types.ts'
import type { PlayerColor } from '../dataset/types.ts'
import { getWinMasks, type WinDirection } from './WinMasks.ts'
const PLAYERS = 2
export class BitBoard {
readonly boardSize: number
readonly cellCount: number
readonly wordCount: number
readonly words: Uint32Array
occupiedCount: number
constructor(boardSize: number, words?: Uint32Array | readonly number[], _occupiedCount?: number) {
this.boardSize = boardSize
this.cellCount = boardSize * boardSize
this.wordCount = Math.ceil(this.cellCount / 32)
const expectedWords = this.wordCount * PLAYERS
this.words = words ? new Uint32Array(words) : new Uint32Array(expectedWords)
if (this.words.length !== expectedWords) {
throw new Error(`BitBoard expected ${expectedWords} words, got ${this.words.length}`)
}
sanitizePaddingBits(this.words, this.wordCount, this.cellCount)
this.occupiedCount = countOccupied(this.words, this.wordCount)
}
static empty(boardSize: number): BitBoard {
return new BitBoard(boardSize)
}
static deserialize(data: SerializedBitBoard): BitBoard {
return new BitBoard(data.boardSize, data.words, data.occupiedCount)
}
static fromBoard(board: Map<string, Cell> | Record<string, Cell>, boardSize: number): BitBoard {
const bitBoard = new BitBoard(boardSize)
const entries = board instanceof Map ? board.entries() : Object.entries(board)
for (const [key, cell] of entries) {
const [x, y] = key.split(':').map(Number)
const player = Number(cell.playerId)
if ((player !== 0 && player !== 1) || !isBoardCoordInBounds(x, y, boardSize)) continue
bitBoard.place(x, y, player)
}
return bitBoard
}
clone(): BitBoard {
return new BitBoard(this.boardSize, this.words, this.occupiedCount)
}
serialize(): SerializedBitBoard {
return {
boardSize: this.boardSize,
words: Array.from(this.words),
occupiedCount: this.occupiedCount,
}
}
has(x: number, y: number): boolean {
if (!isBoardCoordInBounds(x, y, this.boardSize)) return false
const { chunk, bit } = this.bitAt(x, y)
return ((this.words[chunk] | this.words[this.wordCount + chunk]) & bit) !== 0
}
hasPlayer(x: number, y: number, player: PlayerColor): boolean {
if (!isBoardCoordInBounds(x, y, this.boardSize)) return false
const { chunk, bit } = this.bitAt(x, y)
return (this.words[this.offset(player) + chunk] & bit) !== 0
}
playerAt(x: number, y: number): PlayerColor | null {
if (this.hasPlayer(x, y, 0)) return 0
if (this.hasPlayer(x, y, 1)) return 1
return null
}
getCell(x: number, y: number, symbols: readonly [string, string], timestamp = 0): Cell | undefined {
const player = this.playerAt(x, y)
if (player === null) return undefined
return { playerId: String(player), symbol: symbols[player], timestamp }
}
place(x: number, y: number, player: PlayerColor): void {
if (!isBoardCoordInBounds(x, y, this.boardSize)) {
throw new Error(`BitBoard move out of bounds: ${x}:${y}`)
}
if (this.has(x, y)) {
throw new Error(`BitBoard move is occupied: ${x}:${y}`)
}
const { chunk, bit } = this.bitAt(x, y)
this.words[this.offset(player) + chunk] |= bit
this.occupiedCount++
}
remove(x: number, y: number): void {
if (!isBoardCoordInBounds(x, y, this.boardSize) || !this.has(x, y)) return
const { chunk, bit } = this.bitAt(x, y)
this.words[chunk] &= ~bit
this.words[this.wordCount + chunk] &= ~bit
this.occupiedCount--
}
clear(): void {
this.words.fill(0)
this.occupiedCount = 0
}
withMove(x: number, y: number, player: PlayerColor): BitBoard {
const next = this.clone()
next.place(x, y, player)
return next
}
swapPlayers(): void {
for (let i = 0; i < this.wordCount; i++) {
const tmp = this.words[i]
this.words[i] = this.words[this.wordCount + i]
this.words[this.wordCount + i] = tmp
}
}
forEachCell(fn: (x: number, y: number, player: PlayerColor) => void): void {
for (let y = 0; y < this.boardSize; y++) {
for (let x = 0; x < this.boardSize; x++) {
const player = this.playerAt(x, y)
if (player !== null) fn(x, y, player)
}
}
}
entries(symbols: readonly [string, string], timestamp = 0): [string, Cell][] {
const entries: [string, Cell][] = []
this.forEachCell((x, y, player) => {
entries.push([serializeCoord(x, y), { playerId: String(player), symbol: symbols[player], timestamp }])
})
return entries
}
toMap(symbols: readonly [string, string], timestamp = 0): Map<string, Cell> {
return new Map(this.entries(symbols, timestamp))
}
key(): string {
return `${this.boardSize}:${Array.from(this.words).map(word => word.toString(16)).join(':')}`
}
private offset(player: PlayerColor): number {
return player * this.wordCount
}
private bitAt(x: number, y: number): { chunk: number; bit: number } {
const index = y * this.boardSize + x
return { chunk: index >>> 5, bit: 1 << (index & 31) }
}
}
export function bitBoardWinResult(
board: BitBoard,
x: number,
y: number,
player: PlayerColor,
noOverlines?: boolean,
): { won: boolean; cells: { x: number; y: number }[] } {
if (!isBoardCoordInBounds(x, y, board.boardSize)) return { won: false, cells: [] }
const index = y * board.boardSize + x
const offset = player * board.wordCount
const masks = getWinMasks(board.boardSize).byCellIndex[index]
for (const mask of masks) {
if (!matchesMask(board.words, offset, mask.words)) continue
const result = validateWinDirection(board, x, y, player, mask.direction, noOverlines)
if (result.won) return result
}
return { won: false, cells: [] }
}
export function bitBoardCheckWin(
board: BitBoard,
x: number,
y: number,
player: PlayerColor,
noOverlines?: boolean,
): boolean {
return bitBoardWinResult(board, x, y, player, noOverlines).won
}
export function bitBoardToMap(board: BitBoard, symbols: [string, string]): Map<string, Cell> {
return board.toMap(symbols)
}
function collectDirection(
board: BitBoard,
x: number,
y: number,
player: PlayerColor,
dx: number,
dy: number,
): { cells: { x: number; y: number }[]; end: { x: number; y: number } } {
const cells: { x: number; y: number }[] = []
let cx = x + dx
let cy = y + dy
while (board.hasPlayer(cx, cy, player)) {
cells.push({ x: cx, y: cy })
cx += dx
cy += dy
}
return { cells, end: { x: cx, y: cy } }
}
function validateWinDirection(
board: BitBoard,
x: number,
y: number,
player: PlayerColor,
direction: WinDirection,
noOverlines?: boolean,
): { won: boolean; cells: { x: number; y: number }[] } {
const [dx, dy] = direction
const cells: { x: number; y: number }[] = [{ x, y }]
const pos = collectDirection(board, x, y, player, dx, dy)
const neg = collectDirection(board, x, y, player, -dx, -dy)
const count = 1 + pos.cells.length + neg.cells.length
if (count < 5) return { won: false, cells: [] }
if (noOverlines && count > 5) return { won: false, cells: [] }
const bothBlocked = board.has(pos.end.x, pos.end.y) && board.has(neg.end.x, neg.end.y)
if (bothBlocked) return { won: false, cells: [] }
return { won: true, cells: cells.concat(pos.cells, neg.cells) }
}
function matchesMask(words: Uint32Array, offset: number, maskWords: Uint32Array): boolean {
for (let i = 0; i < maskWords.length; i++) {
const mask = maskWords[i]
if (mask === 0) continue
if (((words[offset + i] & mask) >>> 0) !== mask) return false
}
return true
}
function sanitizePaddingBits(words: Uint32Array, wordCount: number, cellCount: number): void {
const validBits = cellCount & 31
if (validBits === 0) return
const mask = 2 ** validBits - 1
for (let player = 0; player < PLAYERS; player++) {
const index = player * wordCount + wordCount - 1
const next = words[index] & mask
if (words[index] !== next) words[index] = next
}
}
function countOccupied(words: Uint32Array, wordCount: number): number {
let count = 0
for (let i = 0; i < wordCount; i++) {
count += popCount((words[i] | words[wordCount + i]) >>> 0)
}
return count
}
function popCount(value: number): number {
let count = 0
let word = value >>> 0
while (word !== 0) {
word &= word - 1
count++
}
return count
}