Update server.js
This commit is contained in:
@@ -2,10 +2,12 @@ import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import express from "express";
|
||||
|
||||
import { ChessEngine } from "./shared/chess-engine.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const PUBLIC_DIR = path.join(__dirname, "public");
|
||||
const SHARED_DIR = path.join(__dirname, "shared");
|
||||
const PORT = Number(process.env.PORT || 3015);
|
||||
@@ -13,465 +15,387 @@ const PORT = Number(process.env.PORT || 3015);
|
||||
const app = express();
|
||||
app.disable("x-powered-by");
|
||||
app.use(express.json({ limit: "32kb" }));
|
||||
|
||||
app.use(express.static(PUBLIC_DIR, { extensions: ["html"], maxAge: "1h" }));
|
||||
app.use("/shared", express.static(SHARED_DIR, { maxAge: "1h" }));
|
||||
|
||||
const MAX_SPECTATORS = 20;
|
||||
const MAX_CHAT = 100;
|
||||
|
||||
const TIME_CONTROLS = {
|
||||
none: { key: "none", label: "Без часов", initial: 0, increment: 0 },
|
||||
"1+0": { key: "1+0", label: "1 + 0 • Bullet", initial: 60, increment: 0 },
|
||||
"3+0": { key: "3+0", label: "3 + 0 • Blitz", initial: 180, increment: 0 },
|
||||
"3+2": { key: "3+2", label: "3 + 2 • Blitz", initial: 180, increment: 2 },
|
||||
"5+0": { key: "5+0", label: "5 + 0 • Blitz", initial: 300, increment: 0 },
|
||||
"5+3": { key: "5+3", label: "5 + 3 • Blitz", initial: 300, increment: 3 },
|
||||
"10+0": { key: "10+0", label: "10 + 0 • Rapid", initial: 600, increment: 0 },
|
||||
"15+10": { key: "15+10", label: "15 + 10 • Rapid", initial: 900, increment: 10 },
|
||||
"30+0": { key: "30+0", label: "30 + 0 • Classical", initial: 1800, increment: 0 },
|
||||
"30+20": { key: "30+20", label: "30 + 20 • Classical", initial: 1800, increment: 20 }
|
||||
none: { key: "none", label: "Без часов", initial: 0, increment: 0 },
|
||||
"1+0": { key: "1+0", label: "1 + 0 • Bullet", initial: 60, increment: 0 },
|
||||
"3+0": { key: "3+0", label: "3 + 0 • Blitz", initial: 180, increment: 0 },
|
||||
"3+2": { key: "3+2", label: "3 + 2 • Blitz", initial: 180, increment: 2 },
|
||||
"5+0": { key: "5+0", label: "5 + 0 • Blitz", initial: 300, increment: 0 },
|
||||
"5+3": { key: "5+3", label: "5 + 3 • Blitz", initial: 300, increment: 3 },
|
||||
"10+0": { key: "10+0", label: "10 + 0 • Rapid", initial: 600, increment: 0 },
|
||||
"15+10": { key: "15+10", label: "15 + 10 • Rapid", initial: 900, increment: 10 },
|
||||
"30+0": { key: "30+0", label: "30 + 0 • Classical", initial: 1800, increment: 0 },
|
||||
"30+20": { key: "30+20", label: "30 + 20 • Classical", initial: 1800, increment: 20 },
|
||||
};
|
||||
|
||||
const parties = new Map();
|
||||
|
||||
const safeName = (value) => {
|
||||
const name = String(value ?? "").trim().replace(/[<>]/g, "").slice(0, 24);
|
||||
return name || "Guest";
|
||||
};
|
||||
function safeName(name) {
|
||||
const value = typeof name === "string" ? name.trim() : "";
|
||||
return value ? value.replace(/[<>]/g, "").slice(0, 24) : "Guest";
|
||||
}
|
||||
|
||||
const normalizeCode = (value) => String(value ?? "").trim().toUpperCase();
|
||||
function normalizeCode(code) {
|
||||
return String(code || "").trim().toUpperCase();
|
||||
}
|
||||
|
||||
const clientId = () => crypto.randomUUID();
|
||||
function getTimeControl(key) {
|
||||
return TIME_CONTROLS[key] || TIME_CONTROLS["10+0"];
|
||||
}
|
||||
|
||||
function partyCode() {
|
||||
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
let code;
|
||||
do {
|
||||
code = Array.from({ length: 6 }, () =>
|
||||
alphabet[crypto.randomInt(0, alphabet.length)]
|
||||
).join("");
|
||||
} while (parties.has(code));
|
||||
return code;
|
||||
function createClientId() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
function createPartyCode() {
|
||||
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
let code;
|
||||
do {
|
||||
code = "";
|
||||
for (let i = 0; i < 6; i++) code += alphabet[crypto.randomInt(0, alphabet.length)];
|
||||
} while (parties.has(code));
|
||||
return code;
|
||||
}
|
||||
|
||||
function getParty(code) {
|
||||
const party = parties.get(normalizeCode(code));
|
||||
if (!party) throw new Error("Комната не найдена.");
|
||||
return party;
|
||||
const party = parties.get(normalizeCode(code));
|
||||
if (!party) throw new Error("Party not found.");
|
||||
return party;
|
||||
}
|
||||
|
||||
function participant(party, id) {
|
||||
return party.participants.get(String(id || "")) || null;
|
||||
function getParticipant(party, clientId) {
|
||||
return (clientId && party.participants.get(clientId)) || null;
|
||||
}
|
||||
|
||||
function player(party, role) {
|
||||
for (const p of party.participants.values()) {
|
||||
if (p.role === role) return p;
|
||||
}
|
||||
return null;
|
||||
function getPlayer(party, role) {
|
||||
for (const participant of party.participants.values()) {
|
||||
if (participant.role === role) return participant;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function roleColor(role) {
|
||||
return role === "white" ? "w" : "b";
|
||||
function getOpponentRole(role) { return role === "white" ? "black" : "white"; }
|
||||
function roleToColor(role) { return role === "white" ? "w" : "b"; }
|
||||
function colorToRole(color) { return color === "w" ? "white" : "black"; }
|
||||
|
||||
function countSpectators(party) {
|
||||
let count = 0;
|
||||
for (const participant of party.participants.values()) {
|
||||
if (participant.role === "spectator") count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function colorRole(color) {
|
||||
return color === "w" ? "white" : "black";
|
||||
}
|
||||
|
||||
function makeClocks(tc) {
|
||||
return {
|
||||
white: tc.initial,
|
||||
black: tc.initial,
|
||||
running: false,
|
||||
lastTick: Date.now()
|
||||
};
|
||||
function createClocks(timeControl) {
|
||||
return { white: timeControl.initial, black: timeControl.initial, running: false, lastTick: Date.now(), turn: "w" };
|
||||
}
|
||||
|
||||
function updateClock(party) {
|
||||
if (!party.clocks.running || !party.timeControl.initial || party.gameOverReason) return;
|
||||
const clocks = party.clocks;
|
||||
if (!clocks || party.timeControl.initial <= 0 || !clocks.running || party.gameOverReason) return;
|
||||
|
||||
const now = Date.now();
|
||||
const elapsed = Math.max(0, (now - party.clocks.lastTick) / 1000);
|
||||
const side = colorRole(party.engine.state.turn);
|
||||
const now = Date.now();
|
||||
const elapsed = (now - clocks.lastTick) / 1000;
|
||||
if (elapsed <= 0) return;
|
||||
|
||||
party.clocks[side] = Math.max(0, party.clocks[side] - elapsed);
|
||||
party.clocks.lastTick = now;
|
||||
const role = colorToRole(party.engine.state.turn);
|
||||
clocks[role] = Math.max(0, clocks[role] - elapsed);
|
||||
clocks.lastTick = now;
|
||||
clocks.turn = party.engine.state.turn;
|
||||
|
||||
if (party.clocks[side] <= 0) {
|
||||
party.clocks.running = false;
|
||||
party.gameOverReason = "timeout";
|
||||
party.winner = side === "w" ? "b" : "w";
|
||||
}
|
||||
if (clocks[role] <= 0) {
|
||||
setGameOver(party, "timeout", roleToColor(getOpponentRole(role)));
|
||||
}
|
||||
}
|
||||
|
||||
function startClockIfReady(party) {
|
||||
if (!party.timeControl.initial || party.gameOverReason) return;
|
||||
if (!player(party, "white") || !player(party, "black")) return;
|
||||
if (party.timeControl.initial <= 0 || party.gameOverReason || !clocksCanRun(party)) return;
|
||||
party.clocks.running = true;
|
||||
party.clocks.lastTick = Date.now();
|
||||
party.clocks.turn = party.engine.state.turn;
|
||||
}
|
||||
|
||||
party.clocks.running = true;
|
||||
party.clocks.lastTick = Date.now();
|
||||
function clocksCanRun(party) {
|
||||
return Boolean(party.clocks && !party.gameOverReason && getPlayer(party, "white") && getPlayer(party, "black"));
|
||||
}
|
||||
|
||||
function applyMoveClock(party, movingRole) {
|
||||
if (!party.timeControl.initial || party.gameOverReason) return;
|
||||
|
||||
updateClock(party);
|
||||
if (party.gameOverReason) return;
|
||||
|
||||
party.clocks[movingRole] += party.timeControl.increment;
|
||||
party.clocks.lastTick = Date.now();
|
||||
party.clocks.running = true;
|
||||
if (party.timeControl.initial <= 0) return;
|
||||
updateClock(party);
|
||||
if (party.gameOverReason) return;
|
||||
party.clocks[movingRole] += party.timeControl.increment;
|
||||
party.clocks.turn = party.engine.state.turn;
|
||||
party.clocks.lastTick = Date.now();
|
||||
party.clocks.running = true;
|
||||
}
|
||||
|
||||
function resetGame(party) {
|
||||
party.engine = new ChessEngine();
|
||||
party.clocks = makeClocks(party.timeControl);
|
||||
party.gameOverReason = null;
|
||||
party.winner = null;
|
||||
party.drawOffer = null;
|
||||
party.gameNumber += 1;
|
||||
startClockIfReady(party);
|
||||
function getClockSnapshot(party) {
|
||||
updateClock(party);
|
||||
return {
|
||||
white: Math.max(0, Number(party.clocks.white) || 0),
|
||||
black: Math.max(0, Number(party.clocks.black) || 0),
|
||||
running: Boolean(party.clocks.running),
|
||||
turn: party.engine.state.turn,
|
||||
};
|
||||
}
|
||||
|
||||
function serializeParticipant(p) {
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
role: p.role,
|
||||
connected: Boolean(p.connected)
|
||||
};
|
||||
function getEngineStatus(party) {
|
||||
return party.engine.state.status || ChessEngine.evaluateStatus(party.engine.state);
|
||||
}
|
||||
|
||||
function serializeParty(party, me = null) {
|
||||
updateClock(party);
|
||||
|
||||
const players = { white: null, black: null };
|
||||
const spectators = [];
|
||||
|
||||
for (const p of party.participants.values()) {
|
||||
if (p.role === "white") players.white = serializeParticipant(p);
|
||||
else if (p.role === "black") players.black = serializeParticipant(p);
|
||||
else spectators.push(serializeParticipant(p));
|
||||
}
|
||||
|
||||
const you = participant(party, me);
|
||||
|
||||
return {
|
||||
code: party.code,
|
||||
gameNumber: party.gameNumber,
|
||||
game: party.engine.getSnapshot(),
|
||||
players,
|
||||
spectators,
|
||||
spectatorCount: spectators.length,
|
||||
maxSpectators: MAX_SPECTATORS,
|
||||
timeControl: party.timeControl,
|
||||
clocks: {
|
||||
white: Math.max(0, party.clocks.white),
|
||||
black: Math.max(0, party.clocks.black),
|
||||
running: Boolean(party.clocks.running),
|
||||
turn: party.engine.state.turn
|
||||
},
|
||||
gameOverReason: party.gameOverReason || null,
|
||||
winner: party.winner || null,
|
||||
drawOffer: party.drawOffer
|
||||
? { from: party.drawOffer.from, name: party.drawOffer.name }
|
||||
: null,
|
||||
chat: party.chat.slice(-MAX_CHAT),
|
||||
you: you ? { id: you.id, name: you.name, role: you.role } : null
|
||||
};
|
||||
function setGameOver(party, reason, winner = null) {
|
||||
if (party.gameOverReason) return;
|
||||
party.gameOverReason = reason;
|
||||
party.winner = winner;
|
||||
party.clocks.running = false;
|
||||
broadcastParty(party);
|
||||
}
|
||||
|
||||
function send(res, event, data) {
|
||||
try {
|
||||
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
} catch {}
|
||||
function updateGameResult(party) {
|
||||
if (party.gameOverReason) return;
|
||||
const status = getEngineStatus(party);
|
||||
if (!status) return;
|
||||
if (status.phase === "checkmate") {
|
||||
setGameOver(party, "checkmate", party.engine.state.turn === "w" ? "b" : "w");
|
||||
} else if (status.phase === "draw") {
|
||||
setGameOver(party, "draw", null);
|
||||
}
|
||||
}
|
||||
|
||||
function broadcast(party) {
|
||||
for (const c of party.sse) {
|
||||
send(c.res, "party", serializeParty(party, c.id));
|
||||
}
|
||||
function serializeParticipant(participant) {
|
||||
return { id: participant.id, name: participant.name, role: participant.role, connected: participant.connected };
|
||||
}
|
||||
|
||||
function createParty(name, timeControl) {
|
||||
const tc = TIME_CONTROLS[timeControl] || TIME_CONTROLS["10+0"];
|
||||
const code = partyCode();
|
||||
const id = clientId();
|
||||
function serializeParty(party, clientId = null) {
|
||||
updateClock(party);
|
||||
const players = { white: null, black: null };
|
||||
const spectators = [];
|
||||
|
||||
const party = {
|
||||
code,
|
||||
engine: new ChessEngine(),
|
||||
timeControl: tc,
|
||||
clocks: makeClocks(tc),
|
||||
gameOverReason: null,
|
||||
winner: null,
|
||||
drawOffer: null,
|
||||
gameNumber: 1,
|
||||
participants: new Map(),
|
||||
sse: new Set(),
|
||||
chat: []
|
||||
};
|
||||
for (const participant of party.participants.values()) {
|
||||
if (participant.role === "white") players.white = serializeParticipant(participant);
|
||||
if (participant.role === "black") players.black = serializeParticipant(participant);
|
||||
if (participant.role === "spectator") spectators.push(serializeParticipant(participant));
|
||||
}
|
||||
|
||||
party.participants.set(id, {
|
||||
id,
|
||||
name: safeName(name),
|
||||
role: "white",
|
||||
connected: true
|
||||
});
|
||||
|
||||
parties.set(code, party);
|
||||
return { party, id };
|
||||
const you = getParticipant(party, clientId);
|
||||
return {
|
||||
code: party.code,
|
||||
game: party.engine.getSnapshot(),
|
||||
players,
|
||||
spectators,
|
||||
spectatorCount: spectators.length,
|
||||
maxSpectators: MAX_SPECTATORS,
|
||||
timeControl: party.timeControl,
|
||||
clocks: getClockSnapshot(party),
|
||||
gameOverReason: party.gameOverReason || null,
|
||||
winner: party.winner || null,
|
||||
you: you ? { id: you.id, name: you.name, role: you.role } : null,
|
||||
};
|
||||
}
|
||||
|
||||
function cleanup(party) {
|
||||
const active = [...party.participants.values()].some(p => p.connected);
|
||||
if (!active && party.sse.size === 0) parties.delete(party.code);
|
||||
function sendSSE(res, event, data) {
|
||||
try { res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } catch { }
|
||||
}
|
||||
|
||||
function broadcastParty(party) {
|
||||
if (!party.sseClients.size) return;
|
||||
for (const client of party.sseClients) {
|
||||
sendSSE(client.res, "party", serializeParty(party, client.id));
|
||||
}
|
||||
}
|
||||
|
||||
function addSSEClient(party, clientId, res) {
|
||||
const client = { id: clientId, res };
|
||||
party.sseClients.add(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
function removeSSEClient(party, client) { party.sseClients.delete(client); }
|
||||
|
||||
function disconnectParticipant(party, clientId) {
|
||||
const participant = getParticipant(party, clientId);
|
||||
if (!participant) return;
|
||||
participant.connected = false;
|
||||
broadcastParty(party);
|
||||
}
|
||||
|
||||
function cleanupEmptyParty(party) {
|
||||
const hasConnectedPlayer = [...party.participants.values()].some(p => p.role !== "spectator" && p.connected);
|
||||
const hasConnectedSpectator = [...party.participants.values()].some(p => p.role === "spectator" && p.connected);
|
||||
if (!hasConnectedPlayer && !hasConnectedSpectator && party.sseClients.size === 0) parties.delete(party.code);
|
||||
}
|
||||
|
||||
app.post("/api/party/create", (req, res) => {
|
||||
try {
|
||||
const { party, id } = createParty(req.body?.name, req.body?.timeControl);
|
||||
res.json({ ok: true, party: serializeParty(party, id) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message });
|
||||
}
|
||||
try {
|
||||
const name = safeName(req.body?.name);
|
||||
const timeControl = getTimeControl(req.body?.timeControl);
|
||||
const code = createPartyCode();
|
||||
const clientId = createClientId();
|
||||
const engine = new ChessEngine();
|
||||
|
||||
const party = {
|
||||
code, engine, timeControl,
|
||||
clocks: createClocks(timeControl),
|
||||
gameOverReason: null, winner: null,
|
||||
participants: new Map(), sseClients: new Set(),
|
||||
};
|
||||
|
||||
party.participants.set(clientId, { id: clientId, name, role: "white", connected: true });
|
||||
parties.set(code, party);
|
||||
res.json({ ok: true, party: serializeParty(party, clientId) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message || "Unable to create party." });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/party/join", (req, res) => {
|
||||
try {
|
||||
const code = normalizeCode(req.body?.partyCode);
|
||||
const party = getParty(code);
|
||||
const name = safeName(req.body?.name);
|
||||
let id = String(req.body?.clientId || "").trim();
|
||||
let p = participant(party, id);
|
||||
try {
|
||||
const code = normalizeCode(req.body?.partyCode);
|
||||
const name = safeName(req.body?.name);
|
||||
const party = getParty(code);
|
||||
let clientId = String(req.body?.clientId || "").trim();
|
||||
let participant = getParticipant(party, clientId);
|
||||
|
||||
if (p) {
|
||||
p.name = name;
|
||||
p.connected = true;
|
||||
} else {
|
||||
id = clientId();
|
||||
let role = "spectator";
|
||||
if (!player(party, "white")) role = "white";
|
||||
else if (!player(party, "black")) role = "black";
|
||||
else if (party.participants.size >= MAX_SPECTATORS + 2) {
|
||||
return res.status(403).json({ ok: false, error: "Лимит зрителей достигнут." });
|
||||
}
|
||||
if (participant) {
|
||||
participant.name = name;
|
||||
participant.connected = true;
|
||||
} else {
|
||||
clientId = createClientId();
|
||||
let role = "spectator";
|
||||
if (!getPlayer(party, "white")) role = "white";
|
||||
else if (!getPlayer(party, "black")) role = "black";
|
||||
else if (countSpectators(party) >= MAX_SPECTATORS) return res.status(403).json({ ok: false, error: "Spectator limit reached." });
|
||||
|
||||
p = { id, name, role, connected: true };
|
||||
party.participants.set(id, p);
|
||||
participant = { id: clientId, name, role, connected: true };
|
||||
party.participants.set(clientId, participant);
|
||||
}
|
||||
|
||||
startClockIfReady(party);
|
||||
broadcastParty(party);
|
||||
res.json({ ok: true, party: serializeParty(party, clientId) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message || "Unable to join party." });
|
||||
}
|
||||
|
||||
startClockIfReady(party);
|
||||
broadcast(party);
|
||||
res.json({ ok: true, party: serializeParty(party, id) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/party/move", (req, res) => {
|
||||
try {
|
||||
const party = getParty(req.body?.partyCode);
|
||||
const p = participant(party, req.body?.clientId);
|
||||
try {
|
||||
const { partyCode, clientId, from, to, promotion } = req.body || {};
|
||||
const party = getParty(partyCode);
|
||||
const participant = getParticipant(party, clientId);
|
||||
|
||||
if (!p || !["white", "black"].includes(p.role)) {
|
||||
return res.status(403).json({ ok: false, error: "Вы не игрок этой партии." });
|
||||
if (!participant) return res.status(403).json({ ok: false, error: "Player session not found." });
|
||||
if (participant.role !== "white" && participant.role !== "black") return res.status(403).json({ ok: false, error: "Spectators cannot make moves." });
|
||||
if (party.gameOverReason) return res.status(400).json({ ok: false, error: "Game is already over.", party: serializeParty(party, clientId) });
|
||||
|
||||
const playerColor = roleToColor(participant.role);
|
||||
if (party.engine.state.turn !== playerColor) return res.status(400).json({ ok: false, error: "It is not your turn." });
|
||||
|
||||
updateClock(party);
|
||||
if (party.gameOverReason) return res.status(400).json({ ok: false, error: "Time has expired.", party: serializeParty(party, clientId) });
|
||||
|
||||
const result = party.engine.makeMove({ from, to, promotion: promotion || null });
|
||||
if (!result?.ok) return res.status(400).json({ ok: false, error: result?.error || "Illegal move." });
|
||||
|
||||
applyMoveClock(party, participant.role);
|
||||
updateGameResult(party);
|
||||
broadcastParty(party);
|
||||
res.json({ ok: true, party: serializeParty(party, clientId) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message || "Move failed." });
|
||||
}
|
||||
|
||||
updateClock(party);
|
||||
if (party.gameOverReason) {
|
||||
return res.status(400).json({
|
||||
ok: false,
|
||||
error: "Игра уже завершена.",
|
||||
party: serializeParty(party, p.id)
|
||||
});
|
||||
}
|
||||
|
||||
const color = roleColor(p.role);
|
||||
if (party.engine.state.turn !== color) {
|
||||
return res.status(400).json({ ok: false, error: "Сейчас ход соперника." });
|
||||
}
|
||||
|
||||
const result = party.engine.makeMove({
|
||||
from: req.body?.from,
|
||||
to: req.body?.to,
|
||||
promotion: req.body?.promotion || "q"
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return res.status(400).json({ ok: false, error: result.error });
|
||||
}
|
||||
|
||||
applyMoveClock(party, p.role);
|
||||
|
||||
const status = party.engine.getStatus();
|
||||
if (status.phase === "checkmate") {
|
||||
party.gameOverReason = "checkmate";
|
||||
party.winner = party.engine.state.turn === "w" ? "b" : "w";
|
||||
party.clocks.running = false;
|
||||
} else if (status.phase === "draw") {
|
||||
party.gameOverReason = "draw";
|
||||
party.winner = null;
|
||||
party.clocks.running = false;
|
||||
}
|
||||
|
||||
party.drawOffer = null;
|
||||
broadcast(party);
|
||||
res.json({ ok: true, party: serializeParty(party, p.id) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/party/rematch", (req, res) => {
|
||||
try {
|
||||
const party = getParty(req.body?.partyCode);
|
||||
const p = participant(party, req.body?.clientId);
|
||||
if (!p) return res.status(403).json({ ok: false, error: "Сессия не найдена." });
|
||||
|
||||
resetGame(party);
|
||||
broadcast(party);
|
||||
res.json({ ok: true, party: serializeParty(party, p.id) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/party/resign", (req, res) => {
|
||||
try {
|
||||
const party = getParty(req.body?.partyCode);
|
||||
const p = participant(party, req.body?.clientId);
|
||||
if (!p || !["white", "black"].includes(p.role)) {
|
||||
return res.status(403).json({ ok: false, error: "Вы не игрок." });
|
||||
}
|
||||
if (party.gameOverReason) return res.json({ ok: true, party: serializeParty(party, p.id) });
|
||||
|
||||
party.gameOverReason = "resign";
|
||||
party.winner = roleColor(p.role) === "w" ? "b" : "w";
|
||||
party.clocks.running = false;
|
||||
party.drawOffer = null;
|
||||
broadcast(party);
|
||||
res.json({ ok: true, party: serializeParty(party, p.id) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/party/draw", (req, res) => {
|
||||
try {
|
||||
const party = getParty(req.body?.partyCode);
|
||||
const p = participant(party, req.body?.clientId);
|
||||
if (!p || !["white", "black"].includes(p.role)) {
|
||||
return res.status(403).json({ ok: false, error: "Вы не игрок." });
|
||||
}
|
||||
if (party.gameOverReason) return res.json({ ok: true, party: serializeParty(party, p.id) });
|
||||
|
||||
if (party.drawOffer && party.drawOffer.from !== p.id) {
|
||||
party.gameOverReason = "draw";
|
||||
party.winner = null;
|
||||
party.drawOffer = null;
|
||||
party.clocks.running = false;
|
||||
} else {
|
||||
party.drawOffer = { from: p.id, name: p.name };
|
||||
}
|
||||
|
||||
broadcast(party);
|
||||
res.json({ ok: true, party: serializeParty(party, p.id) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/party/chat", (req, res) => {
|
||||
try {
|
||||
const party = getParty(req.body?.partyCode);
|
||||
const p = participant(party, req.body?.clientId);
|
||||
if (!p) return res.status(403).json({ ok: false, error: "Сессия не найдена." });
|
||||
|
||||
const text = String(req.body?.text ?? "").trim().replace(/[<>]/g, "").slice(0, 300);
|
||||
if (!text) return res.status(400).json({ ok: false, error: "Пустое сообщение." });
|
||||
|
||||
party.chat.push({
|
||||
id: crypto.randomUUID(),
|
||||
name: p.name,
|
||||
text,
|
||||
at: Date.now()
|
||||
});
|
||||
if (party.chat.length > MAX_CHAT) party.chat.splice(0, party.chat.length - MAX_CHAT);
|
||||
|
||||
broadcast(party);
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/party/events", (req, res) => {
|
||||
try {
|
||||
const party = getParty(req.query?.partyCode);
|
||||
const id = String(req.query?.clientId || "").trim();
|
||||
if (!participant(party, id)) return res.status(403).end();
|
||||
try {
|
||||
const partyCode = normalizeCode(req.query?.partyCode);
|
||||
const clientId = String(req.query?.clientId || "").trim();
|
||||
const party = getParty(partyCode);
|
||||
const participant = getParticipant(party, clientId);
|
||||
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
res.flushHeaders?.();
|
||||
if (!participant) return res.status(403).end();
|
||||
|
||||
const client = { id, res };
|
||||
party.sse.add(client);
|
||||
participant(party, id).connected = true;
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
if (typeof res.flushHeaders === "function") res.flushHeaders();
|
||||
|
||||
send(res, "party", serializeParty(party, id));
|
||||
broadcast(party);
|
||||
const client = addSSEClient(party, clientId, res);
|
||||
participant.connected = true;
|
||||
sendSSE(res, "party", serializeParty(party, clientId));
|
||||
broadcastParty(party);
|
||||
|
||||
const heartbeat = setInterval(() => {
|
||||
try { res.write(": ping\n\n"); } catch { clearInterval(heartbeat); }
|
||||
}, 15000);
|
||||
const heartbeat = setInterval(() => {
|
||||
try { res.write(": ping\n\n"); } catch { clearInterval(heartbeat); }
|
||||
}, 15000);
|
||||
|
||||
req.on("close", () => {
|
||||
clearInterval(heartbeat);
|
||||
party.sse.delete(client);
|
||||
const p = participant(party, id);
|
||||
if (p) p.connected = false;
|
||||
broadcast(party);
|
||||
cleanup(party);
|
||||
});
|
||||
} catch {
|
||||
res.status(404).end();
|
||||
}
|
||||
req.on("close", () => {
|
||||
clearInterval(heartbeat);
|
||||
removeSSEClient(party, client);
|
||||
disconnectParticipant(party, clientId);
|
||||
cleanupEmptyParty(party);
|
||||
});
|
||||
} catch {
|
||||
res.status(404).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/party/leave", (req, res) => {
|
||||
try {
|
||||
const party = getParty(req.body?.partyCode);
|
||||
const p = participant(party, req.body?.clientId);
|
||||
if (p) {
|
||||
if (p.role === "spectator") party.participants.delete(p.id);
|
||||
else p.connected = false;
|
||||
try {
|
||||
const party = getParty(req.body?.partyCode);
|
||||
const clientId = String(req.body?.clientId || "").trim();
|
||||
const participant = getParticipant(party, clientId);
|
||||
|
||||
if (!participant) return res.json({ ok: true });
|
||||
|
||||
if (participant.role === "spectator") {
|
||||
party.participants.delete(clientId);
|
||||
} else {
|
||||
participant.connected = false;
|
||||
}
|
||||
|
||||
broadcastParty(party);
|
||||
cleanupEmptyParty(party);
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message || "Unable to leave party." });
|
||||
}
|
||||
broadcast(party);
|
||||
cleanup(party);
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
res.status(400).json({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
for (const party of parties.values()) {
|
||||
const before = party.gameOverReason;
|
||||
updateClock(party);
|
||||
if (!before && party.gameOverReason) broadcast(party);
|
||||
}
|
||||
}, 250);
|
||||
const clockTimer = setInterval(() => {
|
||||
for (const party of parties.values()) {
|
||||
if (party.timeControl.initial <= 0 || !party.clocks.running || party.gameOverReason) continue;
|
||||
const beforeWhite = party.clocks.white;
|
||||
const beforeBlack = party.clocks.black;
|
||||
updateClock(party);
|
||||
if (beforeWhite !== party.clocks.white || beforeBlack !== party.clocks.black) {
|
||||
broadcastParty(party);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
app.get("*", (req, res) => {
|
||||
res.sendFile(path.join(PUBLIC_DIR, "index.html"));
|
||||
});
|
||||
function shutdown() {
|
||||
clearInterval(clockTimer);
|
||||
for (const party of parties.values()) {
|
||||
for (const client of party.sseClients) { try { client.res.end(); } catch {} }
|
||||
party.sseClients.clear();
|
||||
}
|
||||
parties.clear();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
app.get("*", (req, res) => res.sendFile(path.join(PUBLIC_DIR, "index.html")));
|
||||
|
||||
app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`Chess Party running on port ${PORT}`);
|
||||
console.log(`Chess server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user