import type { GamePlugin, GameResult, GameOverResult } from "../types"; import type { ChessState, ChessAction, Piece, PieceColor, PieceType } from "./types"; const BACK_ROW: PieceType[] = ["rook", "knight", "bishop", "queen", "king", "bishop", "knight", "rook"]; function createStartingBoard(): (Piece | null)[][] { const board: (Piece | null)[][] = Array.from({ length: 8 }, () => Array(8).fill(null)); for (let col = 0; col < 8; col++) { board[0][col] = { type: BACK_ROW[col], color: "black" }; board[1][col] = { type: "pawn", color: "black" }; board[6][col] = { type: "pawn", color: "white" }; board[7][col] = { type: BACK_ROW[col], color: "white" }; } return board; } function inBounds(row: number, col: number): boolean { return row >= 0 && row < 8 && col >= 0 && col < 8; } function getPlayerColor(state: ChessState, playerId: string): PieceColor | null { if (state.players.white === playerId) return "white"; if (state.players.black === playerId) return "black"; return null; } function isValidMove(board: (Piece | null)[][], from: [number, number], to: [number, number], piece: Piece): boolean { const [fromRow, fromCol] = from; const [toRow, toCol] = to; const target = board[toRow][toCol]; if (target && target.color === piece.color) return false; const rowDiff = toRow - fromRow; const colDiff = toCol - fromCol; const absRow = Math.abs(rowDiff); const absCol = Math.abs(colDiff); switch (piece.type) { case "pawn": { const direction = piece.color === "white" ? -1 : 1; const startRow = piece.color === "white" ? 6 : 1; if (colDiff === 0 && rowDiff === direction && !target) return true; if (colDiff === 0 && rowDiff === 2 * direction && fromRow === startRow && !target && !board[fromRow + direction][fromCol]) return true; if (absCol === 1 && rowDiff === direction && target) return true; return false; } case "rook": if (fromRow !== toRow && fromCol !== toCol) return false; return isPathClear(board, from, to); case "knight": return (absRow === 2 && absCol === 1) || (absRow === 1 && absCol === 2); case "bishop": if (absRow !== absCol) return false; return isPathClear(board, from, to); case "queen": if (fromRow !== toRow && fromCol !== toCol && absRow !== absCol) return false; return isPathClear(board, from, to); case "king": return absRow <= 1 && absCol <= 1; default: return false; } } function isPathClear(board: (Piece | null)[][], from: [number, number], to: [number, number]): boolean { const [fromRow, fromCol] = from; const [toRow, toCol] = to; const rowStep = Math.sign(toRow - fromRow); const colStep = Math.sign(toCol - fromCol); let row = fromRow + rowStep; let col = fromCol + colStep; while (row !== toRow || col !== toCol) { if (board[row][col]) return false; row += rowStep; col += colStep; } return true; } function toAlgebraic(from: [number, number], to: [number, number], piece: Piece, captured: boolean): string { const files = "abcdefgh"; const prefix = piece.type === "pawn" ? "" : piece.type[0].toUpperCase(); const cap = captured ? "x" : ""; const fromStr = piece.type === "pawn" && captured ? files[from[1]] : ""; return `${prefix}${fromStr}${cap}${files[to[1]]}${8 - to[0]}`; } export const chessPlugin: GamePlugin = { slug: "chess", name: "Chess", minPlayers: 2, maxPlayers: 2, createInitialState(players: string[]): ChessState { return { board: createStartingBoard(), currentTurn: "white", players: { white: players[0], black: players[1] }, moveHistory: [], status: "playing", winner: null, }; }, handleAction(state: ChessState, action: ChessAction, playerId: string): GameResult { if (state.status !== "playing") { return { ok: false, error: "Game is already over" }; } if (action.type === "forfeit") { const color = getPlayerColor(state, playerId); if (!color) return { ok: false, error: "You are not a player in this game" }; const winner = color === "white" ? state.players.black : state.players.white; return { ok: true, state: { ...state, status: "forfeit", winner } }; } if (action.type === "move") { const { from, to } = action; if (!inBounds(from[0], from[1]) || !inBounds(to[0], to[1])) { return { ok: false, error: "Coordinates out of bounds" }; } const piece = state.board[from[0]][from[1]]; if (!piece) return { ok: false, error: "No piece at source square" }; const playerColor = getPlayerColor(state, playerId); if (!playerColor) return { ok: false, error: "You are not a player in this game" }; if (playerColor !== state.currentTurn) return { ok: false, error: "It is not your turn" }; if (piece.color !== playerColor) return { ok: false, error: "That is not your piece" }; if (!isValidMove(state.board, from, to, piece)) return { ok: false, error: "Invalid move" }; const newBoard = state.board.map(row => [...row]); const captured = newBoard[to[0]][to[1]]; newBoard[to[0]][to[1]] = piece; newBoard[from[0]][from[1]] = null; const notation = toAlgebraic(from, to, piece, captured !== null); const nextTurn: PieceColor = state.currentTurn === "white" ? "black" : "white"; return { ok: true, state: { ...state, board: newBoard, currentTurn: nextTurn, moveHistory: [...state.moveHistory, notation] }, }; } return { ok: false, error: "Unknown action type" }; }, getPlayerView(state: ChessState, _playerId: string): ChessState { return state; }, getSpectatorView(state: ChessState): ChessState { return state; }, isGameOver(state: ChessState): GameOverResult | null { if (state.status === "playing") return null; return { winner: state.winner, reason: state.status }; }, onPlayerDisconnect(state: ChessState, playerId: string): ChessState { const color = getPlayerColor(state, playerId); if (!color) return state; const winner = color === "white" ? state.players.black : state.players.white; return { ...state, status: "forfeit", winner }; }, };