refactor(games): rework room lifecycle events and remove chess plugin
Consolidate room leave/delete event handling into RoomManager emitter, remove redundant PLAYER_LEFT publishes from GameServer, and delete the chess game plugin (board, types, tests) in favor of the new plugin architecture. Add per-module CLAUDE.md files for leveling, guild-settings, feature-flags, db, api, and panel to improve agent navigability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,129 +0,0 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { chessPlugin } from "./plugin";
|
||||
import { gameRegistry } from "../registry";
|
||||
|
||||
const PLAYER_WHITE = "player1";
|
||||
const PLAYER_BLACK = "player2";
|
||||
|
||||
describe("chessPlugin", () => {
|
||||
describe("metadata", () => {
|
||||
it("should have correct slug and player counts", () => {
|
||||
expect(chessPlugin.slug).toBe("chess");
|
||||
expect(chessPlugin.minPlayers).toBe(2);
|
||||
expect(chessPlugin.maxPlayers).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createInitialState", () => {
|
||||
it("should create initial FEN position", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
expect(state.fen).toBe("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
|
||||
expect(state.players.white).toBe(PLAYER_WHITE);
|
||||
expect(state.players.black).toBe(PLAYER_BLACK);
|
||||
expect(state.moveHistory).toEqual([]);
|
||||
expect(state.status).toBe("playing");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleAction — move", () => {
|
||||
it("should allow a legal pawn move", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
const result = chessPlugin.handleAction(state, { type: "move", from: "e2", to: "e4" }, PLAYER_WHITE);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.state.fen).not.toBe(state.fen);
|
||||
expect(result.state.moveHistory).toEqual(["e4"]);
|
||||
}
|
||||
});
|
||||
|
||||
it("should reject move when it is not the player's turn", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
const result = chessPlugin.handleAction(state, { type: "move", from: "e7", to: "e5" }, PLAYER_BLACK);
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("should reject an illegal move", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
const result = chessPlugin.handleAction(state, { type: "move", from: "e2", to: "e5" }, PLAYER_WHITE);
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("should reject a non-player's move", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
const result = chessPlugin.handleAction(state, { type: "move", from: "e2", to: "e4" }, "random_player");
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("should detect checkmate", () => {
|
||||
// Scholar's mate: 1.e4 e5 2.Bc4 Nc6 3.Qh5 Nf6 4.Qxf7#
|
||||
let state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
const moves = [
|
||||
{ player: PLAYER_WHITE, from: "e2", to: "e4" },
|
||||
{ player: PLAYER_BLACK, from: "e7", to: "e5" },
|
||||
{ player: PLAYER_WHITE, from: "f1", to: "c4" },
|
||||
{ player: PLAYER_BLACK, from: "b8", to: "c6" },
|
||||
{ player: PLAYER_WHITE, from: "d1", to: "h5" },
|
||||
{ player: PLAYER_BLACK, from: "g8", to: "f6" },
|
||||
{ player: PLAYER_WHITE, from: "h5", to: "f7" },
|
||||
];
|
||||
for (const m of moves) {
|
||||
const result = chessPlugin.handleAction(state, { type: "move", from: m.from, to: m.to }, m.player);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) state = result.state;
|
||||
}
|
||||
expect(state.status).toBe("checkmate");
|
||||
expect(state.winner).toBe(PLAYER_WHITE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleAction — forfeit", () => {
|
||||
it("should end the game with the other player as winner", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
const result = chessPlugin.handleAction(state, { type: "forfeit" }, PLAYER_WHITE);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.state.status).toBe("forfeit");
|
||||
expect(result.state.winner).toBe(PLAYER_BLACK);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPlayerView", () => {
|
||||
it("should return full state", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
const view = chessPlugin.getPlayerView(state, PLAYER_WHITE);
|
||||
expect(view).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isGameOver", () => {
|
||||
it("should return null for ongoing game", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
expect(chessPlugin.isGameOver!(state)).toBeNull();
|
||||
});
|
||||
|
||||
it("should return winner for forfeit", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
state.status = "forfeit";
|
||||
state.winner = PLAYER_BLACK;
|
||||
expect(chessPlugin.isGameOver!(state)).toEqual({ winner: PLAYER_BLACK, reason: "forfeit" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("onPlayerDisconnect", () => {
|
||||
it("should forfeit the disconnected player", () => {
|
||||
const state = chessPlugin.createInitialState([PLAYER_WHITE, PLAYER_BLACK]);
|
||||
const result = chessPlugin.onPlayerDisconnect!(state, PLAYER_WHITE);
|
||||
expect(result.status).toBe("forfeit");
|
||||
expect(result.winner).toBe(PLAYER_BLACK);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chess registration", () => {
|
||||
it("should register and retrieve from gameRegistry", () => {
|
||||
gameRegistry.register(chessPlugin);
|
||||
expect(gameRegistry.get("chess")).toBe(chessPlugin);
|
||||
expect(gameRegistry.list()).toContain(chessPlugin);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,114 +0,0 @@
|
||||
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 };
|
||||
},
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export interface ChessState {
|
||||
fen: string;
|
||||
players: { white: string; black: string };
|
||||
moveHistory: string[];
|
||||
status: "playing" | "checkmate" | "stalemate" | "draw" | "forfeit";
|
||||
winner: string | null;
|
||||
}
|
||||
|
||||
export type ChessAction =
|
||||
| { type: "move"; from: string; to: string; promotion?: string }
|
||||
| { type: "forfeit" };
|
||||
Reference in New Issue
Block a user