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 | Record, 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 { 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 { 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 }