diff --git a/panel/src/games/chess/ChessBoard.tsx b/panel/src/games/chess/ChessBoard.tsx new file mode 100644 index 0000000..cd29f9b --- /dev/null +++ b/panel/src/games/chess/ChessBoard.tsx @@ -0,0 +1,133 @@ +import { useState } from "react"; +import type { GameUIProps } from "../registry"; + +interface Piece { + type: string; + color: "white" | "black"; +} + +interface ChessState { + board: (Piece | null)[][]; + currentTurn: "white" | "black"; + players: { white: string; black: string }; + moveHistory: string[]; + status: string; + winner: string | null; +} + +const PIECE_SYMBOLS: Record> = { + white: { king: "♔", queen: "♕", rook: "♖", bishop: "♗", knight: "♘", pawn: "♙" }, + black: { king: "♚", queen: "♛", rook: "♜", bishop: "♝", knight: "♞", pawn: "♟" }, +}; + +export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }: GameUIProps) { + const chess = state as ChessState; + const [selected, setSelected] = useState<[number, number] | null>(null); + + if (!chess?.board) { + return
Waiting for game to start...
; + } + + const myColor = chess.players.white === myPlayerId ? "white" : chess.players.black === myPlayerId ? "black" : null; + const isMyTurn = myColor === chess.currentTurn && !isSpectator; + + function handleSquareClick(row: number, col: number) { + if (isSpectator || !isMyTurn) return; + + if (selected) { + onAction({ type: "move", from: selected, to: [row, col] }); + setSelected(null); + } else { + const piece = chess.board[row][col]; + if (piece && piece.color === myColor) { + setSelected([row, col]); + } + } + } + + function handleForfeit() { + onAction({ type: "forfeit" }); + } + + const opponentId = myColor === "white" ? chess.players.black : chess.players.white; + const opponent = players.find(p => p.discordId === opponentId); + const me = players.find(p => p.discordId === myPlayerId); + + return ( +
+
+
+
+ {opponent?.username?.[0]?.toUpperCase() ?? "?"} +
+ {opponent?.username ?? "Opponent"} + · {myColor === "white" ? "Black" : "White"} +
+ +
+ {chess.board.map((row, r) => + row.map((piece, c) => { + const isLight = (r + c) % 2 === 0; + const isSelected = selected?.[0] === r && selected?.[1] === c; + return ( + + ); + }) + )} +
+ +
+
+ {me?.username?.[0]?.toUpperCase() ?? "?"} +
+ {me?.username ?? "You"} + · {myColor ?? "Spectator"} + {isMyTurn && ( + Your turn + )} +
+
+ +
+
+
Move History
+
+ {chess.moveHistory.length === 0 ? ( +
No moves yet
+ ) : ( +
+ {chess.moveHistory.map((move, i) => ( + + {i % 2 === 0 && {Math.floor(i / 2) + 1}. } + {move}{" "} + + ))} +
+ )} +
+
+ + {!isSpectator && chess.status === "playing" && ( + + )} +
+
+ ); +} diff --git a/panel/src/games/chess/index.ts b/panel/src/games/chess/index.ts new file mode 100644 index 0000000..bd8c168 --- /dev/null +++ b/panel/src/games/chess/index.ts @@ -0,0 +1,9 @@ +import { gameUIRegistry } from "../registry"; +import { ChessBoard } from "./ChessBoard"; + +gameUIRegistry.register({ + slug: "chess", + name: "Chess", + icon: "♟", + component: ChessBoard, +}); diff --git a/panel/src/games/registry.ts b/panel/src/games/registry.ts new file mode 100644 index 0000000..41c9001 --- /dev/null +++ b/panel/src/games/registry.ts @@ -0,0 +1,30 @@ +import type { ComponentType } from "react"; + +export interface GameUIProps { + state: any; + myPlayerId: string; + isSpectator: boolean; + onAction: (action: unknown) => void; + players: { discordId: string; username: string }[]; +} + +export interface GameUIPlugin { + slug: string; + name: string; + icon: string; + component: ComponentType; +} + +const plugins = new Map(); + +export const gameUIRegistry = { + register(plugin: GameUIPlugin) { + plugins.set(plugin.slug, plugin); + }, + get(slug: string): GameUIPlugin | undefined { + return plugins.get(slug); + }, + list(): GameUIPlugin[] { + return Array.from(plugins.values()); + }, +};