Update chess/public/js/app.js

This commit is contained in:
2026-08-22 18:20:15 +00:00
parent 6ed96533b0
commit 039b64f9e5
+342 -89
View File
@@ -90,8 +90,11 @@ const elements = {
partyTimeControl: document.querySelector("#partyClockSelect"), partyTimeControl: document.querySelector("#partyClockSelect"),
startAiButton: document.querySelector("#startAiButton"), startAiButton: document.querySelector("#startAiButton"),
newGameToolbarButton: document.querySelector("#newGameToolbarButton"),
createPartyButton: document.querySelector("#createPartyButton"), createPartyButton: document.querySelector("#createPartyButton"),
newPartyGameButton: document.querySelector("#newPartyGameButton"),
partyCodeInput: document.querySelector("#partyCodeInput"), partyCodeInput: document.querySelector("#partyCodeInput"),
joinPartyButton: document.querySelector("#joinPartyButton"), joinPartyButton: document.querySelector("#joinPartyButton"),
leavePartyButton: document.querySelector("#leavePartyButton"), leavePartyButton: document.querySelector("#leavePartyButton"),
@@ -113,6 +116,11 @@ const elements = {
whiteClock: document.querySelector("#whiteClock"), whiteClock: document.querySelector("#whiteClock"),
blackClock: document.querySelector("#blackClock"), blackClock: document.querySelector("#blackClock"),
capturedByWhite: document.querySelector("#capturedByWhite"),
capturedByBlack: document.querySelector("#capturedByBlack"),
moveHistory: document.querySelector("#moveHistory"),
promotionDialog: document.querySelector("#promotionDialog"), promotionDialog: document.querySelector("#promotionDialog"),
promotionOptions: document.querySelector("#promotionOptions"), promotionOptions: document.querySelector("#promotionOptions"),
@@ -303,10 +311,9 @@ function showToast(message, type = "info") {
elements.toastHost.appendChild(toast); elements.toastHost.appendChild(toast);
window.setTimeout( window.setTimeout(() => {
() => toast.remove(), toast.remove();
3200 }, 3200);
);
} }
@@ -384,15 +391,13 @@ function renderBoard() {
const selectedMoves = const selectedMoves =
getLegalMovesForSelected(); getLegalMovesForSelected();
const moveTargets = new Map( const moveTargets = new Map();
selectedMoves.map(move => [
move.to,
move,
])
);
const orientation = for (const move of selectedMoves) {
appState.orientation; moveTargets.set(move.to, move);
}
const orientation = appState.orientation;
const xOrder = const xOrder =
orientation === "w" orientation === "w"
@@ -426,16 +431,24 @@ function renderBoard() {
document.createElement("button"); document.createElement("button");
square.type = "button"; square.type = "button";
/*
* ВАЖНО:
* Теперь цвет клетки задаётся непосредственно
* через .light / .dark.
*/
const isDark =
(x + y) % 2 === 0;
square.className = square.className =
`square ${ `square ${
(x + y) % 2 === 0 isDark ? "dark" : "light"
? "dark"
: "light"
}`; }`;
square.dataset.square = square.dataset.square =
squareName; squareName;
/* Selected */
if ( if (
appState.selectedSquare === appState.selectedSquare ===
squareName squareName
@@ -445,6 +458,7 @@ function renderBoard() {
); );
} }
/* Legal move */
const move = const move =
moveTargets.get(squareName); moveTargets.get(squareName);
@@ -456,6 +470,7 @@ function renderBoard() {
); );
} }
/* Last move */
if ( if (
lastMove && lastMove &&
( (
@@ -468,6 +483,7 @@ function renderBoard() {
); );
} }
/* Check */
if ( if (
piece && piece &&
piece.type === "K" && piece.type === "K" &&
@@ -478,6 +494,7 @@ function renderBoard() {
); );
} }
/* Piece */
if (piece) { if (piece) {
const pieceNode = const pieceNode =
document.createElement("span"); document.createElement("span");
@@ -494,15 +511,26 @@ function renderBoard() {
piece.color piece.color
][piece.type]; ][piece.type];
square.appendChild(pieceNode); pieceNode.setAttribute(
"aria-hidden",
"true"
);
square.appendChild(
pieceNode
);
} }
square.addEventListener( square.addEventListener(
"click", "click",
() => handleSquareClick(squareName) () => handleSquareClick(
squareName
)
); );
elements.board.appendChild(square); elements.board.appendChild(
square
);
} }
} }
} }
@@ -520,22 +548,22 @@ function formatStatus() {
const status = currentStatus(); const status = currentStatus();
if (status.phase === "checkmate") { if (status.phase === "checkmate") {
return "Checkmate"; return "Мат";
} }
if (status.phase === "draw") { if (status.phase === "draw") {
return "Draw"; return "Ничья";
} }
if (status.check) { if (status.check) {
return appState.engine.state.turn === "w" return appState.engine.state.turn === "w"
? "White is in check" ? "Белые под шахом"
: "Black is in check"; : "Чёрные под шахом";
} }
return appState.engine.state.turn === "w" return appState.engine.state.turn === "w"
? "White to move" ? "Ход белых"
: "Black to move"; : "Ход чёрных";
} }
@@ -546,7 +574,7 @@ function getGameOverMessage() {
if (!snapshot) { if (!snapshot) {
return ( return (
appState.gameOverMessage || appState.gameOverMessage ||
"Game over." "Игра окончена."
); );
} }
@@ -559,21 +587,21 @@ function getGameOverMessage() {
switch (reason) { switch (reason) {
case "timeout": case "timeout":
return winner === "w" return winner === "w"
? "White wins on time." ? "Белые победили по времени."
: "Black wins on time."; : "Чёрные победили по времени.";
case "checkmate": case "checkmate":
return winner === "w" return winner === "w"
? "White wins by checkmate." ? "Белые победили матом."
: "Black wins by checkmate."; : "Чёрные победили матом.";
case "draw": case "draw":
return "Draw."; return "Ничья.";
default: default:
return ( return (
appState.gameOverMessage || appState.gameOverMessage ||
"Game over." "Игра окончена."
); );
} }
} }
@@ -586,17 +614,17 @@ function getGameOverMessage() {
function renderRoster() { function renderRoster() {
if (appState.mode === "ai") { if (appState.mode === "ai") {
const human = const human =
appState.playerName || "You"; appState.playerName || "Вы";
const whiteName = const whiteName =
appState.playerColor === "w" appState.playerColor === "w"
? human ? human
: `Computer (${appState.aiLevel})`; : `Компьютер (${appState.aiLevel})`;
const blackName = const blackName =
appState.playerColor === "b" appState.playerColor === "b"
? human ? human
: `Computer (${appState.aiLevel})`; : `Компьютер (${appState.aiLevel})`;
if (elements.whitePlayerLabel) { if (elements.whitePlayerLabel) {
elements.whitePlayerLabel.textContent = elements.whitePlayerLabel.textContent =
@@ -620,7 +648,7 @@ function renderRoster() {
if (elements.spectatorList) { if (elements.spectatorList) {
elements.spectatorList.innerHTML = elements.spectatorList.innerHTML =
"<li>None in AI mode</li>"; "<li>В режиме AI зрителей нет</li>";
} }
if (elements.spectatorCount) { if (elements.spectatorCount) {
@@ -642,34 +670,38 @@ function renderRoster() {
? `${white.name}${ ? `${white.name}${
white.connected white.connected
? "" ? ""
: " · offline" : " · офлайн"
}` }`
: "Open seat"; : "Свободно";
const blackName = const blackName =
black black
? `${black.name}${ ? `${black.name}${
black.connected black.connected
? "" ? ""
: " · offline" : " · офлайн"
}` }`
: "Open seat"; : "Свободно";
elements.whitePlayerLabel && if (elements.whitePlayerLabel) {
(elements.whitePlayerLabel.textContent = elements.whitePlayerLabel.textContent =
whiteName); whiteName;
}
elements.blackPlayerLabel && if (elements.blackPlayerLabel) {
(elements.blackPlayerLabel.textContent = elements.blackPlayerLabel.textContent =
blackName); blackName;
}
elements.rosterWhite && if (elements.rosterWhite) {
(elements.rosterWhite.textContent = elements.rosterWhite.textContent =
whiteName); whiteName;
}
elements.rosterBlack && if (elements.rosterBlack) {
(elements.rosterBlack.textContent = elements.rosterBlack.textContent =
blackName); blackName;
}
const spectators = const spectators =
appState.partySnapshot?.spectators || []; appState.partySnapshot?.spectators || [];
@@ -685,7 +717,7 @@ function renderRoster() {
if (!spectators.length) { if (!spectators.length) {
elements.spectatorList.innerHTML = elements.spectatorList.innerHTML =
"<li>None yet</li>"; "<li>Пока никого нет</li>";
return; return;
} }
@@ -699,13 +731,149 @@ function renderRoster() {
)}${ )}${
spectator.connected spectator.connected
? "" ? ""
: " · offline" : " · офлайн"
}</li>` }</li>`
) )
.join(""); .join("");
} }
/* =========================================================
CAPTURED PIECES
========================================================= */
function renderCaptured() {
if (
!elements.capturedByWhite &&
!elements.capturedByBlack
) {
return;
}
/*
* Поддерживаем несколько возможных форматов
* истории движка, если они присутствуют.
*/
const history =
appState.engine.state.moveHistory ||
appState.engine.state.history ||
[];
const whiteCaptured = [];
const blackCaptured = [];
for (const move of history) {
if (!move) {
continue;
}
const captured =
move.captured ||
move.capturedPiece ||
null;
if (!captured) {
continue;
}
const color =
captured.color ||
(captured.type
? (move.color === "w" ? "b" : "w")
: null);
const type =
captured.type ||
(
typeof captured === "string"
? captured
: null
);
if (!type) {
continue;
}
const glyph =
PIECE_GLYPHS[color]?.[type];
if (!glyph) {
continue;
}
if (move.color === "w") {
whiteCaptured.push(glyph);
} else {
blackCaptured.push(glyph);
}
}
if (elements.capturedByWhite) {
elements.capturedByWhite.textContent =
whiteCaptured.join(" ");
}
if (elements.capturedByBlack) {
elements.capturedByBlack.textContent =
blackCaptured.join(" ");
}
}
/* =========================================================
MOVE HISTORY
========================================================= */
function renderMoveHistory() {
if (!elements.moveHistory) {
return;
}
const history =
appState.engine.state.moveHistory ||
appState.engine.state.history ||
[];
if (!Array.isArray(history) || !history.length) {
elements.moveHistory.innerHTML =
"<li class=\"empty-state\">Ходов пока нет</li>";
return;
}
elements.moveHistory.innerHTML =
history
.map((move, index) => {
if (typeof move === "string") {
return `<li>${escapeHtml(move)}</li>`;
}
const from =
move?.from || "";
const to =
move?.to || "";
const promotion =
move?.promotion
? `=${String(move.promotion).toUpperCase()}`
: "";
const text =
from && to
? `${from}${to}${promotion}`
: JSON.stringify(move);
return (
`<li>${escapeHtml(
`${index + 1}. ${text}`
)}</li>`
);
})
.join("");
}
/* ========================================================= /* =========================================================
PARTY SUMMARY PARTY SUMMARY
========================================================= */ ========================================================= */
@@ -717,7 +885,7 @@ function renderPartySummary() {
if (!appState.party) { if (!appState.party) {
elements.partySummary.textContent = elements.partySummary.textContent =
"Create a room to become White. The next player joins as Black. Up to 20 spectators can watch live."; "Создайте комнату, чтобы играть белыми. Следующий игрок присоединится за чёрных. До 20 зрителей.";
return; return;
} }
@@ -727,10 +895,10 @@ function renderPartySummary() {
const roleText = const roleText =
role === "white" role === "white"
? "playing as White" ? "вы играете белыми"
: role === "black" : role === "black"
? "playing as Black" ? "вы играете чёрными"
: "watching as a spectator"; : "вы зритель";
const timeLabel = const timeLabel =
appState.partySnapshot appState.partySnapshot
@@ -738,7 +906,7 @@ function renderPartySummary() {
"Без часов"; "Без часов";
elements.partySummary.textContent = elements.partySummary.textContent =
`Party ${appState.party.code} · ${roleText}. ${timeLabel}.`; `Комната ${appState.party.code} · ${roleText} · ${timeLabel}.`;
} }
@@ -782,17 +950,21 @@ function renderClocks() {
if (!enabled) { if (!enabled) {
if (elements.whiteClock) { if (elements.whiteClock) {
elements.whiteClock.textContent = "∞"; elements.whiteClock.textContent = "∞";
elements.whiteClock.classList.remove( elements.whiteClock.classList.remove(
"active", "active",
"low" "low",
"danger"
); );
} }
if (elements.blackClock) { if (elements.blackClock) {
elements.blackClock.textContent = "∞"; elements.blackClock.textContent = "∞";
elements.blackClock.classList.remove( elements.blackClock.classList.remove(
"active", "active",
"low" "low",
"danger"
); );
} }
@@ -819,6 +991,11 @@ function renderClocks() {
"low", "low",
white <= 10 white <= 10
); );
elements.whiteClock.classList.toggle(
"danger",
white <= 5
);
} }
if (elements.blackClock) { if (elements.blackClock) {
@@ -835,6 +1012,11 @@ function renderClocks() {
"low", "low",
black <= 10 black <= 10
); );
elements.blackClock.classList.toggle(
"danger",
black <= 5
);
} }
} }
@@ -851,28 +1033,61 @@ function renderConnectionBadge() {
if (appState.mode === "ai") { if (appState.mode === "ai") {
elements.connectionBadge.textContent = elements.connectionBadge.textContent =
appState.isAiThinking appState.isAiThinking
? "Computer thinking" ? "Компьютер думает"
: `AI: ${appState.aiLevel}`; : `AI · ${appState.aiLevel}`;
elements.connectionBadge.className =
"status-chip connection-solo";
return; return;
} }
if (!appState.party) { if (!appState.party) {
elements.connectionBadge.textContent = elements.connectionBadge.textContent =
"Party idle"; "Party ожидание";
elements.connectionBadge.className =
"status-chip connection-solo";
return; return;
} }
const role = const role =
appState.party.role === "white" appState.party.role === "white"
? "White" ? "Белые"
: appState.party.role === "black" : appState.party.role === "black"
? "Black" ? "Чёрные"
: "Spectator"; : "Зритель";
elements.connectionBadge.textContent = elements.connectionBadge.textContent =
`Party ${appState.party.code} · ${role}`; `${appState.party.code} · ${role}`;
elements.connectionBadge.className =
"status-chip connection-party";
}
/* =========================================================
STATUS BADGE
========================================================= */
function renderTurnBadge() {
if (!elements.turnBadge) {
return;
}
const turn =
appState.engine.state.turn;
elements.turnBadge.classList.toggle(
"turn-white",
turn === "w"
);
elements.turnBadge.classList.toggle(
"turn-black",
turn === "b"
);
} }
@@ -886,8 +1101,12 @@ function render() {
renderPartySummary(); renderPartySummary();
renderClocks(); renderClocks();
renderConnectionBadge(); renderConnectionBadge();
renderTurnBadge();
renderCaptured();
renderMoveHistory();
const status = formatStatus(); const status =
formatStatus();
if (elements.turnBadge) { if (elements.turnBadge) {
elements.turnBadge.textContent = elements.turnBadge.textContent =
@@ -939,7 +1158,8 @@ function openPromotionDialog(moves) {
button.textContent = button.textContent =
PIECE_GLYPHS[ PIECE_GLYPHS[
move.color move.color ||
appState.engine.state.turn
][promotion]; ][promotion];
button.addEventListener( button.addEventListener(
@@ -1013,7 +1233,7 @@ async function submitMove(move) {
showToast( showToast(
response.error || response.error ||
"Move rejected.", "Ход отклонён.",
"error" "error"
); );
@@ -1039,7 +1259,7 @@ async function submitMove(move) {
if (!result.ok) { if (!result.ok) {
showToast( showToast(
result.error || result.error ||
"Illegal move.", "Недопустимый ход.",
"error" "error"
); );
@@ -1084,7 +1304,10 @@ function handleSquareClick(square) {
); );
if (matchingMoves.length === 1) { if (matchingMoves.length === 1) {
submitMove(matchingMoves[0]); submitMove(
matchingMoves[0]
);
return; return;
} }
@@ -1092,6 +1315,7 @@ function handleSquareClick(square) {
openPromotionDialog( openPromotionDialog(
matchingMoves matchingMoves
); );
return; return;
} }
} }
@@ -1111,6 +1335,7 @@ function handleSquareClick(square) {
: square; : square;
renderBoard(); renderBoard();
return; return;
} }
@@ -1304,8 +1529,8 @@ function updateAiClock() {
appState.gameOverMessage = appState.gameOverMessage =
color === "w" color === "w"
? "Black wins on time." ? "Чёрные победили по времени."
: "White wins on time."; : "Белые победили по времени.";
stopAiClock(); stopAiClock();
@@ -1368,6 +1593,7 @@ function resetLocalGame() {
new ChessEngine(); new ChessEngine();
appState.partySnapshot = null; appState.partySnapshot = null;
appState.selectedSquare = null;
appState.orientation = appState.orientation =
appState.playerColor; appState.playerColor;
@@ -1416,7 +1642,7 @@ async function postJson(url, body) {
ok: false, ok: false,
error: error:
`Server returned ${response.status} instead of JSON.`, `Сервер вернул ${response.status} вместо JSON.`,
}; };
} }
@@ -1428,7 +1654,7 @@ async function postJson(url, body) {
error: error:
error instanceof Error error instanceof Error
? error.message ? error.message
: "Network error.", : "Ошибка сети.",
}; };
} }
} }
@@ -1475,6 +1701,8 @@ function hydratePartyState(
partyPayload.game partyPayload.game
); );
appState.selectedSquare = null;
appState.gameOver = appState.gameOver =
Boolean( Boolean(
partyPayload.gameOverReason partyPayload.gameOverReason
@@ -1581,7 +1809,7 @@ function connectPartyStream() {
elements.connectionBadge elements.connectionBadge
) { ) {
elements.connectionBadge.textContent = elements.connectionBadge.textContent =
`Party ${code} · reconnecting`; `Party ${code} · переподключение`;
} }
}; };
@@ -1624,7 +1852,7 @@ async function createParty() {
if (!response.ok) { if (!response.ok) {
showToast( showToast(
response.error || response.error ||
"Unable to create party.", "Не удалось создать комнату.",
"error" "error"
); );
@@ -1644,7 +1872,7 @@ async function createParty() {
connectPartyStream(); connectPartyStream();
showToast( showToast(
`Party ${response.party.code} created.`, `Комната ${response.party.code} создана.`,
"success" "success"
); );
} }
@@ -1662,7 +1890,7 @@ async function joinParty(code) {
if (!normalizedCode) { if (!normalizedCode) {
showToast( showToast(
"Enter a party code first.", "Введите код комнаты.",
"error" "error"
); );
@@ -1674,7 +1902,7 @@ async function joinParty(code) {
normalizedCode normalizedCode
) { ) {
showToast( showToast(
`You are already in party ${normalizedCode}.` `Вы уже в комнате ${normalizedCode}.`
); );
return; return;
@@ -1706,7 +1934,7 @@ async function joinParty(code) {
if (!response.ok) { if (!response.ok) {
showToast( showToast(
response.error || response.error ||
"Unable to join party.", "Не удалось войти в комнату.",
"error" "error"
); );
@@ -1726,7 +1954,7 @@ async function joinParty(code) {
connectPartyStream(); connectPartyStream();
showToast( showToast(
`Joined party ${response.party.code}.`, `Вы вошли в комнату ${response.party.code}.`,
"success" "success"
); );
} }
@@ -1774,7 +2002,7 @@ async function leaveParty(
if (!silent) { if (!silent) {
showToast( showToast(
"Left the party room." "Вы покинули комнату."
); );
} }
} }
@@ -1802,12 +2030,12 @@ async function copyInviteLink() {
); );
showToast( showToast(
"Invite link copied.", "Ссылка скопирована.",
"success" "success"
); );
} catch { } catch {
showToast( showToast(
`Party code: ${appState.party.code}` `Код комнаты: ${appState.party.code}`
); );
} }
} }
@@ -1960,7 +2188,28 @@ function bindEvents() {
resetLocalGame(); resetLocalGame();
showToast( showToast(
"New AI game ready.", "Новая партия с ИИ готова.",
"success"
);
}
);
elements.newGameToolbarButton?.addEventListener(
"click",
async () => {
if (appState.mode === "party") {
showToast(
"Для новой партии создайте новую комнату."
);
return;
}
resetLocalGame();
showToast(
"Новая партия.",
"success" "success"
); );
} }
@@ -1973,6 +2222,12 @@ function bindEvents() {
); );
elements.newPartyGameButton?.addEventListener(
"click",
createParty
);
elements.joinPartyButton?.addEventListener( elements.joinPartyButton?.addEventListener(
"click", "click",
() => () =>
@@ -2113,5 +2368,3 @@ async function init() {
========================================================= */ ========================================================= */
init(); init();