Some checks failed
Deploy to Production / test (push) Failing after 35s
Backend: - Fix session never being attached to ws.data at upgrade time - Add GameServer class: connection registry, per-connection room tracking, automatic room cleanup on disconnect via ws.data.rooms - Replace ws-handler.ts with typed event-driven architecture using mitt - Remove redundant subscription tracking from RoomManager - Add JOIN_RESULT with player/spectator lists replacing error-as-control-flow - Add SESSION_REPLACED for multi-tab same-account detection - Add FILL_ROOM command for admin solo testing (fills empty slots with host) - Fix dual-schema routing; remove game types from WsMessageSchema - Per-player personalized views sent directly after each action Chess plugin: - Allow same-player (solo) mode: skip color/turn ownership checks - Fix forfeit and disconnect handling in solo mode (winner: null) Frontend: - Click-to-move with legal move dots and last-move highlight - Auto-scroll move history, forfeit confirmation, turn-reactive board border - JOIN_RESULT initialises player/spectator lists immediately on join - Contextual connecting state, player slot cards in waiting room - Copy-invite button with Copied! flash, Back to Lobby CTA on finish - Session-replaced warning banner with Rejoin here action - Lobby passes preferAs intent through route state - Admin waiting room shows Start Solo Test button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
import { Chess } from "chess.js";
|
|
import type { GamePlugin, GameResult, GameOverResult } from "../types";
|
|
import type { ChessState, ChessAction } from "./types";
|
|
|
|
function getPlayerColor(state: ChessState, playerId: string): "white" | "black" | null {
|
|
if (state.players.white === playerId) return "white";
|
|
if (state.players.black === playerId) return "black";
|
|
return null;
|
|
}
|
|
|
|
function deriveStatus(game: Chess): ChessState["status"] {
|
|
if (game.isCheckmate()) return "checkmate";
|
|
if (game.isStalemate()) return "stalemate";
|
|
if (game.isDraw()) return "draw";
|
|
return "playing";
|
|
}
|
|
|
|
export const chessPlugin: GamePlugin<ChessState, ChessAction> = {
|
|
slug: "chess",
|
|
name: "Chess",
|
|
minPlayers: 2,
|
|
maxPlayers: 2,
|
|
|
|
createInitialState(players: string[]): ChessState {
|
|
const game = new Chess();
|
|
return {
|
|
fen: game.fen(),
|
|
players: { white: players[0]!, black: players[1]! },
|
|
moveHistory: [],
|
|
status: "playing",
|
|
winner: null,
|
|
};
|
|
},
|
|
|
|
handleAction(state: ChessState, action: ChessAction, playerId: string): GameResult<ChessState> {
|
|
if (state.status !== "playing") {
|
|
return { ok: false, error: "Game is already over" };
|
|
}
|
|
|
|
const solo = state.players.white === state.players.black;
|
|
|
|
if (action.type === "forfeit") {
|
|
if (solo) return { ok: true, state: { ...state, status: "forfeit", winner: null } };
|
|
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 game = new Chess(state.fen);
|
|
const turn = game.turn() === "w" ? "white" : "black";
|
|
|
|
if (!solo) {
|
|
const playerColor = getPlayerColor(state, playerId);
|
|
if (!playerColor) return { ok: false, error: "You are not a player in this game" };
|
|
if (playerColor !== turn) return { ok: false, error: "It is not your turn" };
|
|
}
|
|
|
|
let move;
|
|
try {
|
|
move = game.move({
|
|
from: action.from,
|
|
to: action.to,
|
|
promotion: action.promotion ?? "q",
|
|
});
|
|
} catch {
|
|
return { ok: false, error: "Invalid move" };
|
|
}
|
|
|
|
if (!move) return { ok: false, error: "Invalid move" };
|
|
|
|
const status = deriveStatus(game);
|
|
const winner = status === "checkmate"
|
|
? (turn === "white" ? state.players.white : state.players.black)
|
|
: null;
|
|
|
|
return {
|
|
ok: true,
|
|
state: {
|
|
...state,
|
|
fen: game.fen(),
|
|
moveHistory: [...state.moveHistory, move.san],
|
|
status,
|
|
winner,
|
|
},
|
|
};
|
|
}
|
|
|
|
return { ok: false, error: "Unknown action type" };
|
|
},
|
|
|
|
getPlayerView(state: ChessState, playerId: string): ChessState {
|
|
const playerColor = getPlayerColor(state, playerId);
|
|
if (!playerColor) return state;
|
|
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 {
|
|
if (state.players.white === state.players.black) {
|
|
return { ...state, status: "forfeit", winner: null };
|
|
}
|
|
const color = getPlayerColor(state, playerId);
|
|
if (!color) return state;
|
|
const winner = color === "white" ? state.players.black : state.players.white;
|
|
return { ...state, status: "forfeit", winner };
|
|
},
|
|
};
|