feat: add time controls, correspondence games, and tournament mode
**Zeitmodi (Blitz/Rapid):** - 5, 10, 30 Min Zeitkontrolle bei Spielerstellung - Server-seitige Uhren: Zeit wird bei jedem Zug abgezogen - Timeout-Erkennung via claimTimeout Socket-Event - Schachuhr-Anzeige in GamePage (rot bei < 30s) - Turnierspiele erhalten automatisch Zeitkontrolle **Tagespartien (Correspondence):** - Neue DB-Tabelle correspondence_game - REST API: erstellen, beitreten, Zug machen, aufgeben - Seite /correspondence: Liste aktiver Tagespartien - Seite /correspondence/[code]: Spielen mit interaktivem Brett - Einladungslink teilen, Gegner tritt via Link bei **Turniermodus (Round-Robin):** - Neue DB-Tabellen: tournament, tournament_player, tournament_round - Round-Robin Paarungen mit Circle-Methode - Turnier erstellen: /tournament/create - Turnierliste: /tournament - Turnierseite /tournament/[code]: Standings, Runden, Spiele - Automatische Aktivisierung der nächsten Runde - Turnierspiele werden als normale activeGames erstellt - Ergebnisse aktualisieren Punkte automatisch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
85
server/src/controllers/correspondence.controller.ts
Normal file
85
server/src/controllers/correspondence.controller.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { Request, Response } from "express";
|
||||
import CorrespondenceModel from "../db/models/correspondence.model.js";
|
||||
import { Chess } from "chess.js";
|
||||
|
||||
export const createCorrespondenceGame = async (req: Request, res: Response) => {
|
||||
if (!req.session.user?.id || typeof req.session.user.id === "string") { res.status(401).end(); return; }
|
||||
try {
|
||||
const game = await CorrespondenceModel.create(req.session.user.id as number, req.session.user.name!, req.body.daysPerMove || 3);
|
||||
res.status(201).json(game);
|
||||
} catch (e) { console.error(e); res.status(500).end(); }
|
||||
};
|
||||
|
||||
export const getMyCorrespondenceGames = async (req: Request, res: Response) => {
|
||||
if (!req.session.user?.id || typeof req.session.user.id === "string") { res.status(401).end(); return; }
|
||||
try {
|
||||
const games = await CorrespondenceModel.findByUserId(req.session.user.id as number);
|
||||
res.status(200).json(games);
|
||||
} catch (e) { res.status(500).end(); }
|
||||
};
|
||||
|
||||
export const getCorrespondenceGame = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const game = await CorrespondenceModel.findByCode(req.params.code);
|
||||
if (!game) { res.status(404).end(); return; }
|
||||
res.status(200).json(game);
|
||||
} catch (e) { res.status(500).end(); }
|
||||
};
|
||||
|
||||
export const joinCorrespondenceGame = async (req: Request, res: Response) => {
|
||||
if (!req.session.user?.id || typeof req.session.user.id === "string") { res.status(401).end(); return; }
|
||||
try {
|
||||
const game = await CorrespondenceModel.join(req.params.code, req.session.user.id as number, req.session.user.name!);
|
||||
if (!game) { res.status(400).json({ message: "Spiel nicht verfügbar." }); return; }
|
||||
res.status(200).json(game);
|
||||
} catch (e) { res.status(500).end(); }
|
||||
};
|
||||
|
||||
export const makeCorrespondenceMove = async (req: Request, res: Response) => {
|
||||
if (!req.session.user?.id || typeof req.session.user.id === "string") { res.status(401).end(); return; }
|
||||
try {
|
||||
const { from, to, promotion } = req.body;
|
||||
const game = await CorrespondenceModel.findByCode(req.params.code);
|
||||
if (!game || game.winner || game.endReason) { res.status(400).json({ message: "Spiel beendet." }); return; }
|
||||
if (!game.black?.id) { res.status(400).json({ message: "Kein Gegner." }); return; }
|
||||
|
||||
const chess = new Chess();
|
||||
if (game.pgn) chess.loadPgn(game.pgn);
|
||||
const turn = chess.turn();
|
||||
const userId = req.session.user.id as number;
|
||||
const isWhite = game.white?.id === userId;
|
||||
const isBlack = game.black?.id === userId;
|
||||
|
||||
if (!isWhite && !isBlack) { res.status(403).end(); return; }
|
||||
if ((turn === "w" && !isWhite) || (turn === "b" && !isBlack)) {
|
||||
res.status(403).json({ message: "Nicht dein Zug." }); return;
|
||||
}
|
||||
|
||||
const move = chess.move({ from, to, promotion: promotion || "q" });
|
||||
if (!move) { res.status(400).json({ message: "Ungültiger Zug." }); return; }
|
||||
|
||||
let winner: string | undefined, endReason: string | undefined;
|
||||
if (chess.isGameOver()) {
|
||||
if (chess.isCheckmate()) { endReason = "checkmate"; winner = turn === "w" ? "white" : "black"; }
|
||||
else if (chess.isStalemate()) { endReason = "stalemate"; winner = "draw"; }
|
||||
else if (chess.isThreefoldRepetition()) { endReason = "repetition"; winner = "draw"; }
|
||||
else if (chess.isInsufficientMaterial()) { endReason = "insufficient"; winner = "draw"; }
|
||||
else { endReason = "draw"; winner = "draw"; }
|
||||
}
|
||||
|
||||
const updated = await CorrespondenceModel.applyMove(req.params.code, chess.pgn(), winner, endReason);
|
||||
res.status(200).json(updated);
|
||||
} catch (e) { console.error(e); res.status(500).end(); }
|
||||
};
|
||||
|
||||
export const resignCorrespondenceGame = async (req: Request, res: Response) => {
|
||||
if (!req.session.user?.id || typeof req.session.user.id === "string") { res.status(401).end(); return; }
|
||||
try {
|
||||
const game = await CorrespondenceModel.findByCode(req.params.code);
|
||||
if (!game || game.endReason) { res.status(400).end(); return; }
|
||||
const userId = req.session.user.id as number;
|
||||
if (game.white?.id !== userId && game.black?.id !== userId) { res.status(403).end(); return; }
|
||||
const updated = await CorrespondenceModel.resign(req.params.code, userId);
|
||||
res.status(200).json(updated);
|
||||
} catch (e) { res.status(500).end(); }
|
||||
};
|
||||
@@ -78,11 +78,15 @@ export const createGame = async (req: Request, res: Response) => {
|
||||
connected: false
|
||||
};
|
||||
const unlisted: boolean = req.body.unlisted ?? false;
|
||||
const timeControl: number | undefined = req.body.timeControl ? parseInt(req.body.timeControl) : undefined;
|
||||
const game: Game = {
|
||||
code: nanoid(6),
|
||||
unlisted,
|
||||
host: user,
|
||||
pgn: ""
|
||||
pgn: "",
|
||||
timeControl: timeControl || undefined,
|
||||
whiteTimeMs: timeControl ? timeControl * 60 * 1000 : undefined,
|
||||
blackTimeMs: timeControl ? timeControl * 60 * 1000 : undefined
|
||||
};
|
||||
if (req.body.side === "white") {
|
||||
game.white = user;
|
||||
|
||||
82
server/src/controllers/tournament.controller.ts
Normal file
82
server/src/controllers/tournament.controller.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { Request, Response } from "express";
|
||||
import TournamentModel from "../db/models/tournament.model.js";
|
||||
import { activeGames } from "../db/models/game.model.js";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { Game } from "@michess/types";
|
||||
|
||||
export const createTournament = async (req: Request, res: Response) => {
|
||||
if (!req.session.user?.id || typeof req.session.user.id === "string") { res.status(401).end(); return; }
|
||||
const { name, timeControl, maxPlayers } = req.body;
|
||||
if (!name) { res.status(400).json({ message: "Name erforderlich." }); return; }
|
||||
try {
|
||||
const tournament = await TournamentModel.createTournament(
|
||||
req.session.user.id as number, req.session.user.name!, name,
|
||||
timeControl ? parseInt(timeControl) : undefined, maxPlayers ? parseInt(maxPlayers) : 8
|
||||
);
|
||||
await TournamentModel.joinTournament(tournament.code!, req.session.user.id as number, req.session.user.name!);
|
||||
const updated = await TournamentModel.findByCode(tournament.code!);
|
||||
res.status(201).json(updated);
|
||||
} catch (e) { console.error(e); res.status(500).end(); }
|
||||
};
|
||||
|
||||
export const getTournaments = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const tournaments = await TournamentModel.findAll();
|
||||
res.status(200).json(tournaments);
|
||||
} catch (e) { res.status(500).end(); }
|
||||
};
|
||||
|
||||
export const getTournament = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tournament = await TournamentModel.findByCode(req.params.code);
|
||||
if (!tournament) { res.status(404).end(); return; }
|
||||
res.status(200).json(tournament);
|
||||
} catch (e) { res.status(500).end(); }
|
||||
};
|
||||
|
||||
export const joinTournament = async (req: Request, res: Response) => {
|
||||
if (!req.session.user?.id || typeof req.session.user.id === "string") { res.status(401).end(); return; }
|
||||
try {
|
||||
const result = await TournamentModel.joinTournament(req.params.code, req.session.user.id as number, req.session.user.name!);
|
||||
if (!result) { res.status(400).json({ message: "Turnier nicht verfügbar." }); return; }
|
||||
const updated = await TournamentModel.findByCode(req.params.code);
|
||||
res.status(200).json(updated);
|
||||
} catch (e) { res.status(500).end(); }
|
||||
};
|
||||
|
||||
export const startTournament = async (req: Request, res: Response) => {
|
||||
if (!req.session.user?.id || typeof req.session.user.id === "string") { res.status(401).end(); return; }
|
||||
try {
|
||||
const tournament = await TournamentModel.findByCode(req.params.code);
|
||||
if (!tournament) { res.status(404).end(); return; }
|
||||
if (tournament.hostId !== req.session.user.id) { res.status(403).end(); return; }
|
||||
if (tournament.status !== "waiting") { res.status(400).json({ message: "Turnier bereits gestartet." }); return; }
|
||||
if (!tournament.players || tournament.players.length < 2) { res.status(400).json({ message: "Mindestens 2 Spieler erforderlich." }); return; }
|
||||
|
||||
const allRounds = await TournamentModel.startTournament(tournament.id!);
|
||||
if (!allRounds) { res.status(500).end(); return; }
|
||||
|
||||
// Create active games for round 1
|
||||
const round1 = allRounds[0];
|
||||
for (const roundRow of round1) {
|
||||
const code = nanoid(6);
|
||||
const tc = tournament.timeControl;
|
||||
const row = roundRow as any;
|
||||
const whiteUser = { id: row.white_id, name: row.white_name, connected: false };
|
||||
const blackUser = { id: row.black_id, name: row.black_name, connected: false };
|
||||
const game: Game = {
|
||||
code, unlisted: true, host: whiteUser,
|
||||
white: whiteUser, black: blackUser, pgn: "",
|
||||
startedAt: Date.now(),
|
||||
timeControl: tc || undefined,
|
||||
whiteTimeMs: tc ? tc * 60 * 1000 : undefined,
|
||||
blackTimeMs: tc ? tc * 60 * 1000 : undefined
|
||||
};
|
||||
activeGames.push(game);
|
||||
await TournamentModel.setRoundGameCode(row.id, code);
|
||||
}
|
||||
|
||||
const updated = await TournamentModel.findByCode(req.params.code);
|
||||
res.status(200).json(updated);
|
||||
} catch (e) { console.error(e); res.status(500).end(); }
|
||||
};
|
||||
Reference in New Issue
Block a user