Spaces:
Runtime error
Runtime error
| import type { Cell, GameRules, SerializedBitBoard } from './types.ts' | |
| import { checkWin } from './checkWin.ts' | |
| import { BOARD_SIZE, isBoardCoordInBounds } from './board.ts' | |
| import { BitBoard } from './bot/BitBoard.ts' | |
| function generateId(): string { | |
| return Math.random().toString(36).substring(2, 8).toUpperCase() | |
| } | |
| export interface MoveRecord { | |
| x: number | |
| y: number | |
| player: number | |
| symbol: string | |
| } | |
| export interface Score { | |
| wins: number | |
| losses: number | |
| draws: number | |
| } | |
| export interface GameResult { | |
| winner: number | null | |
| winCells?: { x: number; y: number }[] | |
| } | |
| export type PlaceMoveResult = | |
| | { success: false; error: string } | |
| | { success: true; kind: 'continue' } | |
| | { success: true; kind: 'win'; winner: number; winCells: { x: number; y: number }[] } | |
| | { success: true; kind: 'draw' } | |
| | { success: true; kind: 'swap2_choice' } | |
| export interface GameExportData { | |
| id: string | |
| player1: string | |
| player2: string | |
| winner: number | null | |
| moves: MoveRecord[] | |
| createdAt: string | |
| } | |
| const DEFAULT_RULES: GameRules = { swap2: false, noOverlines: false } | |
| export class GameManager { | |
| private _board: BitBoard | |
| private _currentPlayer = 0 | |
| private _status: 'active' | 'win' | 'draw' = 'active' | |
| private _moveHistory: MoveRecord[] = [] | |
| private _playerNames: [string, string] | |
| private _playerSymbols: [string, string] | |
| private _scores: [Score, Score] | |
| private _gameId: string | |
| private _createdAt: string | |
| private _rules: GameRules | |
| private _boardSize: number | |
| private _phase: 'normal' | 'swap2_opening' | 'swap2_choice' | 'swap2_placement' | 'swap2_choice2' = 'normal' | |
| private _openingCount = 0 | |
| private _additionalCount = 0 | |
| private swapCellOwners(): void { | |
| this._board.swapPlayers() | |
| } | |
| constructor( | |
| player1Name?: string, | |
| player2Name?: string, | |
| symbols?: [string, string], | |
| rules?: GameRules, | |
| ) { | |
| this._rules = rules ?? { ...DEFAULT_RULES } | |
| this._boardSize = this._rules.boardSize ?? BOARD_SIZE | |
| this._board = BitBoard.empty(this._boardSize) | |
| this._playerNames = [player1Name ?? 'Player 1', player2Name ?? 'Player 2'] | |
| this._playerSymbols = symbols ?? ['X', 'O'] | |
| this._scores = [ | |
| { wins: 0, losses: 0, draws: 0 }, | |
| { wins: 0, losses: 0, draws: 0 }, | |
| ] | |
| this._gameId = generateId() | |
| this._createdAt = new Date().toISOString() | |
| if (this._rules.swap2) { | |
| this._phase = 'swap2_opening' | |
| } | |
| } | |
| placeMove(x: number, y: number): PlaceMoveResult { | |
| if (this._status !== 'active') { | |
| return { success: false, error: 'Game is not active' } | |
| } | |
| if (this._phase === 'swap2_choice' || this._phase === 'swap2_choice2') { | |
| return { success: false, error: 'Waiting for swap decision' } | |
| } | |
| if (!isBoardCoordInBounds(x, y, this._boardSize)) { | |
| return { success: false, error: 'Move out of bounds' } | |
| } | |
| if (this._board.has(x, y)) { | |
| return { success: false, error: 'Cell already occupied' } | |
| } | |
| let playerId: string | |
| let symbol: string | |
| if (this._phase === 'swap2_opening') { | |
| this._openingCount++ | |
| const isOpponentStone = this._openingCount === 3 | |
| playerId = isOpponentStone ? '1' : '0' | |
| symbol = isOpponentStone ? this._playerSymbols[1] : this._playerSymbols[0] | |
| } else if (this._phase === 'swap2_placement') { | |
| this._additionalCount++ | |
| // 1st additional stone is Player 2 ('1'), 2nd is Player 1 ('0') | |
| const isPlayer1Stone = this._additionalCount === 2 | |
| playerId = isPlayer1Stone ? '0' : '1' | |
| symbol = isPlayer1Stone ? this._playerSymbols[0] : this._playerSymbols[1] | |
| } else { | |
| playerId = String(this._currentPlayer) | |
| symbol = this._playerSymbols[this._currentPlayer] | |
| } | |
| this._board.place(x, y, Number(playerId) as 0 | 1) | |
| this._moveHistory.push({ | |
| x, | |
| y, | |
| player: Number(playerId), | |
| symbol, | |
| }) | |
| const { won, cells: winCells } = checkWin(this._board, x, y, playerId, this._rules.noOverlines) | |
| if (won) { | |
| this._status = 'win' | |
| const winner = this._phase === 'swap2_opening' ? 0 : this._currentPlayer | |
| this._scores[winner].wins++ | |
| this._scores[winner === 0 ? 1 : 0].losses++ | |
| this._phase = 'normal' | |
| return { success: true, kind: 'win', winner, winCells } | |
| } | |
| if (this._board.occupiedCount >= this._boardSize * this._boardSize) { | |
| this._status = 'draw' | |
| this._scores[0].draws++ | |
| this._scores[1].draws++ | |
| return { success: true, kind: 'draw' } | |
| } | |
| if (this._phase === 'swap2_opening') { | |
| if (this._openingCount >= 3) { | |
| this._phase = 'swap2_choice' | |
| return { success: true, kind: 'swap2_choice' } | |
| } | |
| return { success: true, kind: 'continue' } | |
| } | |
| if (this._phase === 'swap2_placement') { | |
| if (this._additionalCount >= 2) { | |
| this._phase = 'swap2_choice2' | |
| return { success: true, kind: 'swap2_choice' } | |
| } | |
| return { success: true, kind: 'continue' } | |
| } | |
| this._currentPlayer = this._currentPlayer === 0 ? 1 : 0 | |
| return { success: true, kind: 'continue' } | |
| } | |
| swapSides(): void { | |
| if (this._status !== 'active') return | |
| if (this._phase === 'swap2_choice' || this._phase === 'swap2_choice2') return | |
| ;[this._playerSymbols[0], this._playerSymbols[1]] = [this._playerSymbols[1], this._playerSymbols[0]] | |
| this.swapCellOwners() | |
| this._currentPlayer = this._currentPlayer === 0 ? 1 : 0 | |
| } | |
| chooseSwap(accept: boolean): void { | |
| if (this._phase !== 'swap2_choice') return | |
| this.chooseSwap2Decision(accept ? 'player1' : 'player2') | |
| } | |
| chooseSwap2Decision(decision: 'player1' | 'player2' | 'offer'): void { | |
| if (this._phase === 'swap2_choice') { | |
| if (decision === 'player1') { | |
| this._phase = 'normal' | |
| ;[this._playerSymbols[0], this._playerSymbols[1]] = [this._playerSymbols[1], this._playerSymbols[0]] | |
| this.swapCellOwners() | |
| this._currentPlayer = 0 | |
| } else if (decision === 'player2') { | |
| this._phase = 'normal' | |
| this._currentPlayer = 1 | |
| } else if (decision === 'offer') { | |
| this._phase = 'swap2_placement' | |
| this._additionalCount = 0 | |
| this._currentPlayer = 1 | |
| } | |
| } else if (this._phase === 'swap2_choice2') { | |
| this._phase = 'normal' | |
| if (decision === 'player1') { | |
| this._currentPlayer = 1 | |
| } else if (decision === 'player2') { | |
| ;[this._playerSymbols[0], this._playerSymbols[1]] = [this._playerSymbols[1], this._playerSymbols[0]] | |
| this.swapCellOwners() | |
| this._currentPlayer = 0 | |
| } | |
| } | |
| } | |
| getBoard(): ReadonlyMap<string, Cell> { | |
| return this._board.toMap(this._playerSymbols) | |
| } | |
| getBitBoard(): BitBoard { | |
| return this._board | |
| } | |
| getBoardSnapshot(): SerializedBitBoard { | |
| return this._board.serialize() | |
| } | |
| getCurrentPlayer(): number { | |
| return this._currentPlayer | |
| } | |
| getStatus(): 'active' | 'win' | 'draw' { | |
| return this._status | |
| } | |
| isGameOver(): boolean { | |
| return this._status === 'win' || this._status === 'draw' | |
| } | |
| getMoveHistory(): readonly MoveRecord[] { | |
| return this._moveHistory | |
| } | |
| getPlayerNames(): readonly [string, string] { | |
| return this._playerNames | |
| } | |
| getPlayerSymbols(): readonly [string, string] { | |
| return this._playerSymbols | |
| } | |
| setPlayerSymbol(playerIndex: number, symbol: string): void { | |
| if (playerIndex === 0 || playerIndex === 1) { | |
| this._playerSymbols[playerIndex] = symbol | |
| } | |
| } | |
| setPlayerName(playerIndex: number, name: string): void { | |
| if (playerIndex === 0 || playerIndex === 1) { | |
| this._playerNames[playerIndex] = name | |
| } | |
| } | |
| getScores(): [Score, Score] { | |
| return [ | |
| { ...this._scores[0] }, | |
| { ...this._scores[1] }, | |
| ] | |
| } | |
| getWinner(): number | null { | |
| if (this._status === 'win') { | |
| return this._currentPlayer | |
| } | |
| return null | |
| } | |
| getBoardSize(): number { | |
| return this._boardSize | |
| } | |
| getRules(): GameRules { | |
| return { ...this._rules } | |
| } | |
| getPhase(): 'normal' | 'swap2_opening' | 'swap2_choice' | 'swap2_placement' | 'swap2_choice2' { | |
| return this._phase | |
| } | |
| reset(): void { | |
| this._board = BitBoard.empty(this._boardSize) | |
| this._currentPlayer = 0 | |
| this._status = 'active' | |
| this._moveHistory = [] | |
| this._gameId = generateId() | |
| this._createdAt = new Date().toISOString() | |
| this._phase = this._rules.swap2 ? 'swap2_opening' : 'normal' | |
| this._openingCount = 0 | |
| this._additionalCount = 0 | |
| } | |
| resetWithNewPlayers( | |
| player1Name?: string, | |
| player2Name?: string, | |
| symbols?: [string, string], | |
| rules?: GameRules, | |
| ): void { | |
| if (rules) { | |
| this._rules = { ...rules } | |
| this._boardSize = rules.boardSize ?? BOARD_SIZE | |
| } | |
| this.reset() | |
| this._playerNames = [player1Name ?? 'Player 1', player2Name ?? 'Player 2'] | |
| this._playerSymbols = symbols ?? ['X', 'O'] | |
| } | |
| exportGame(): GameExportData { | |
| return { | |
| id: this._gameId, | |
| player1: this._playerNames[0], | |
| player2: this._playerNames[1], | |
| winner: this.getWinner(), | |
| moves: [...this._moveHistory], | |
| createdAt: this._createdAt, | |
| } | |
| } | |
| replaceBoard(board: Record<string, Cell> | SerializedBitBoard, currentPlayer: number, status: 'active' | 'win' | 'draw', moveHistory: MoveRecord[]): void { | |
| this._board = isSerializedBitBoard(board) ? BitBoard.deserialize(board) : BitBoard.fromBoard(board, this._boardSize) | |
| this._currentPlayer = currentPlayer | |
| this._status = status | |
| this._moveHistory = [...moveHistory] | |
| } | |
| addMove(x: number, y: number, player: number, symbol: string): void { | |
| this._board.place(x, y, player as 0 | 1) | |
| this._moveHistory.push({ x, y, player, symbol }) | |
| } | |
| setStatus(status: 'active' | 'win' | 'draw'): void { | |
| this._status = status | |
| } | |
| setCurrentPlayer(player: number): void { | |
| this._currentPlayer = player | |
| } | |
| get gameId(): string { | |
| return this._gameId | |
| } | |
| } | |
| function isSerializedBitBoard(board: Record<string, Cell> | SerializedBitBoard): board is SerializedBitBoard { | |
| return Array.isArray((board as SerializedBitBoard).words) | |
| } | |