caro5 / shared /GameManager.ts
Pedro de Carvalho
Add shared checks
ea1fe68
Raw
History Blame Contribute Delete
10.1 kB
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)
}