Files
website-collection/chess/public/js/app.js
T

2048 lines
32 KiB
JavaScript
Raw Normal View History

2026-08-23 07:19:12 +00:00
import { ChessEngine } from "/shared/chess-engine.js";
2026-08-22 17:37:25 +00:00
2026-08-23 07:19:12 +00:00
const PIECES = {
2026-08-22 17:37:25 +00:00
2026-08-23 07:19:12 +00:00
w: {
k: "♔",
q: "♕",
r: "♖",
b: "♗",
n: "♘",
p: "♙"
},
2026-08-22 17:37:25 +00:00
2026-08-23 07:19:12 +00:00
b: {
k: "♚",
q: "♛",
r: "♜",
b: "♝",
n: "♞",
p: "♟"
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
};
const state = {
mode: "ai",
engine: new ChessEngine(),
orientation: "w",
selected: null,
legalMoves: [],
aiColor: "b",
aiThinking: false,
aiTimer: null,
gameStarted: false,
party: null,
partyClientId: null,
partyEvents: null,
promotionResolver: null,
clocks: {
white: 600,
black: 600,
running: false,
turn: "w"
},
clockTimer: null
};
const $ = id =>
document.getElementById(id);
const boardElement =
$("board");
const statusMessage =
$("statusMessage");
const turnBadge =
$("turnBadge");
const connectionBadge =
$("connectionBadge");
const modeBadge =
$("modeBadge");
const aiControls =
$("aiControls");
const partyControls =
$("partyControls");
const whitePlayerLabel =
$("whitePlayerLabel");
const blackPlayerLabel =
$("blackPlayerLabel");
const whiteClock =
$("whiteClock");
const blackClock =
$("blackClock");
const spectatorCount =
$("spectatorCount");
const spectatorList =
$("spectatorList");
const toastHost =
$("toastHost");
const promotionDialog =
$("promotionDialog");
const promotionOptions =
$("promotionOptions");
const files =
"abcdefgh";
function showToast(
message,
type = ""
) {
const element =
document.createElement("div");
element.className =
`toast ${type}`;
element.textContent =
message;
toastHost.appendChild(
element
);
setTimeout(
() => element.remove(),
3200
);
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
function setStatus(message) {
statusMessage.textContent =
message;
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
function colorName(color) {
return color === "w"
? "белых"
: "черных";
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
function getSquare(file, rank) {
return `${files[file]}${rank + 1}`;
2026-08-22 17:37:25 +00:00
}
2026-08-23 07:19:12 +00:00
function getCoordinates(
index
) {
if (state.orientation === "w") {
return {
file: index % 8,
rank: 7 - Math.floor(index / 8)
};
}
return {
file: 7 - index % 8,
rank: Math.floor(index / 8)
};
}
2026-08-22 17:37:25 +00:00
function renderBoard() {
2026-08-22 18:59:37 +00:00
2026-08-23 07:19:12 +00:00
boardElement.innerHTML = "";
2026-08-22 18:59:37 +00:00
2026-08-23 07:19:12 +00:00
const snapshot =
state.engine.state;
2026-08-22 18:59:37 +00:00
2026-08-23 07:19:12 +00:00
const status =
state.engine.getStatus();
let checkedKing = null;
if (
status.phase === "check"
) {
for (
const [squareName, piece]
of Object.entries(
snapshot.board
)
) {
if (
piece.type === "k" &&
piece.color === snapshot.turn
) {
checkedKing = squareName;
break;
2026-08-23 06:42:17 +00:00
}
}
2026-08-22 17:37:25 +00:00
}
2026-08-23 07:19:12 +00:00
const legalMap =
new Map();
2026-08-22 17:37:25 +00:00
2026-08-23 07:19:12 +00:00
for (
const move
of state.legalMoves
) {
2026-08-22 17:37:25 +00:00
2026-08-23 07:19:12 +00:00
legalMap.set(
move.to,
move
);
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
for (
let index = 0;
index < 64;
index++
) {
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
const {
file,
rank
} = getCoordinates(index);
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
const squareName =
getSquare(file, rank);
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
const element =
document.createElement("button");
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
element.type =
"button";
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
element.className =
"square";
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
if (
(file + rank) % 2 === 0
) {
element.classList.add(
"light"
);
} else {
element.classList.add(
"dark"
);
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
if (
state.selected === squareName
) {
element.classList.add(
"selected"
);
}
2026-08-22 17:37:25 +00:00
2026-08-23 07:19:12 +00:00
if (
snapshot.lastMove &&
(
snapshot.lastMove.from === squareName ||
snapshot.lastMove.to === squareName
)
) {
element.classList.add(
"last-move"
);
}
if (
checkedKing === squareName
) {
element.classList.add(
"check-square"
);
}
const legal =
legalMap.get(
squareName
);
if (legal) {
if (legal.capture) {
element.classList.add(
"capture-target"
);
} else {
element.classList.add(
"legal-target"
);
}
}
const piece =
snapshot.board[squareName];
if (piece) {
const span =
document.createElement("span");
span.className =
`piece ${
piece.color === "w"
? "white"
: "black"
}`;
span.textContent =
PIECES[
piece.color
][
piece.type
];
element.appendChild(
span
);
}
element.dataset.square =
squareName;
element.addEventListener(
"click",
() => handleSquareClick(squareName)
);
boardElement.appendChild(
element
);
}
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
function canUserMove() {
if (
!state.gameStarted
) {
return false;
}
const status =
state.engine.getStatus();
if (
status.phase === "checkmate" ||
status.phase === "draw"
) {
return false;
}
if (
state.aiThinking
) {
return false;
}
if (
state.mode === "party"
) {
if (!state.party) {
return false;
}
const role =
state.party.you?.role;
if (
role !== "white" &&
role !== "black"
) {
return false;
}
return (
state.engine.state.turn ===
(
role === "white"
? "w"
: "b"
)
);
}
return (
state.engine.state.turn !==
state.aiColor
);
}
function handleSquareClick(
squareName
) {
if (!canUserMove()) {
return;
}
const piece =
state.engine.getPiece(
squareName
);
if (!state.selected) {
if (
piece &&
piece.color ===
state.engine.state.turn
) {
selectSquare(
squareName
);
}
return;
}
if (
state.selected === squareName
) {
2026-08-23 06:42:17 +00:00
clearSelection();
2026-08-23 07:19:12 +00:00
2026-08-23 06:42:17 +00:00
return;
}
2026-08-23 07:19:12 +00:00
const move =
state.legalMoves.find(
item =>
item.to === squareName
);
if (move) {
if (
move.promotion
) {
openPromotion(
move,
promotion =>
performMove(
move.from,
move.to,
promotion
)
);
} else {
performMove(
move.from,
move.to,
null
);
}
2026-08-23 06:42:17 +00:00
return;
}
2026-08-23 07:19:12 +00:00
if (
piece &&
piece.color ===
state.engine.state.turn
) {
selectSquare(
squareName
);
return;
}
2026-08-23 06:42:17 +00:00
clearSelection();
2026-08-23 07:19:12 +00:00
}
function selectSquare(
squareName
) {
state.selected =
squareName;
state.legalMoves =
state.engine.movesFrom(
squareName
);
2026-08-23 06:42:17 +00:00
renderBoard();
}
2026-08-23 07:19:12 +00:00
function clearSelection() {
state.selected =
null;
state.legalMoves =
[];
renderBoard();
}
function openPromotion(
move,
callback
) {
promotionOptions.innerHTML =
"";
const color =
state.engine.state.turn;
for (
const type
of ["q", "r", "b", "n"]
) {
const button =
document.createElement("button");
button.type =
"button";
button.textContent =
PIECES[color][type];
button.addEventListener(
"click",
() => {
promotionDialog.classList.add(
"hidden"
);
state.promotionResolver =
null;
callback(type);
}
);
promotionOptions.appendChild(
button
);
}
promotionDialog.classList.remove(
"hidden"
);
}
function performMove(
from,
to,
promotion
) {
if (
state.mode === "party"
) {
performPartyMove(
from,
to,
promotion
);
2026-08-23 06:42:17 +00:00
return;
}
2026-08-23 07:19:12 +00:00
const result =
state.engine.makeMove({
from,
to,
promotion
});
if (!result.ok) {
showToast(
result.error,
"error"
);
clearSelection();
return;
}
clearSelection();
state.gameStarted =
true;
updateGameUI();
if (
result.status.phase ===
"checkmate" ||
result.status.phase ===
"draw"
) {
finishGame();
return;
}
if (
state.engine.state.turn ===
state.aiColor
) {
scheduleAiMove();
}
}
function scheduleAiMove() {
if (
state.aiThinking
) {
return;
}
state.aiThinking =
true;
setStatus(
"Компьютер думает..."
);
renderBoard();
clearTimeout(
state.aiTimer
);
state.aiTimer =
setTimeout(
() => {
try {
makeAiMove();
} finally {
state.aiThinking =
false;
}
},
getAiDelay()
);
}
function getAiDelay() {
const level =
$("aiLevelSelect").value;
if (level === "easy") {
return 350;
}
if (level === "hard") {
return 800;
}
return 550;
}
function makeAiMove() {
if (
state.mode !== "ai"
) {
return;
}
const status =
state.engine.getStatus();
if (
status.phase === "checkmate" ||
status.phase === "draw"
) {
return;
}
const legal =
state.engine.legalMoves(
state.aiColor
);
if (!legal.length) {
updateGameUI();
return;
}
const level =
$("aiLevelSelect").value;
let move;
if (level === "easy") {
move =
legal[
Math.floor(
Math.random() *
legal.length
)
];
} else {
move =
chooseAiMove(
legal
);
}
const result =
state.engine.makeMove({
from: move.from,
to: move.to,
promotion:
move.promotion || null
});
if (!result.ok) {
showToast(
"ИИ не смог выполнить ход.",
"error"
);
return;
}
updateGameUI();
if (
result.status.phase ===
"checkmate" ||
result.status.phase ===
"draw"
) {
finishGame();
}
}
function chooseAiMove(
moves
) {
let bestScore =
-Infinity;
let bestMoves = [];
for (const move of moves) {
let score =
Math.random() * 0.4;
if (move.capture) {
score += 3;
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
if (move.promotion) {
score += 8;
}
if (
move.to[1] === "4" ||
move.to[1] === "5"
) {
score += .15;
}
if (score > bestScore) {
bestScore =
score;
bestMoves = [
move
];
} else if (
Math.abs(
score - bestScore
) < .1
) {
bestMoves.push(
move
);
}
}
return bestMoves[
Math.floor(
Math.random() *
bestMoves.length
)
];
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
function finishGame() {
state.aiThinking =
false;
clearTimeout(
state.aiTimer
);
updateGameUI();
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
function updateGameUI() {
const status =
state.engine.getStatus();
const turn =
state.engine.state.turn;
turnBadge.textContent =
`Ход ${colorName(turn)}`;
turnBadge.className =
`status-chip ${
turn === "w"
? "turn-white"
: "turn-black"
}`;
if (
status.phase === "checkmate"
) {
setStatus(
`Мат. Победили ${colorName(status.winner)}.`
);
} else if (
status.phase === "draw"
) {
setStatus(
drawMessage(
status.reason
)
);
} else if (
status.phase === "check"
) {
setStatus(
`Шах — ход ${colorName(turn)}.`
);
} else if (
state.mode === "party"
) {
const role =
state.party?.you?.role;
if (
role === "spectator"
) {
setStatus(
`Ход ${colorName(turn)}. Вы зритель.`
);
} else {
const myColor =
role === "white"
? "w"
: "b";
setStatus(
myColor === turn
? "Ваш ход."
: "Ход соперника."
);
}
} else if (
state.gameStarted
) {
setStatus(
turn === state.aiColor
? "Ход компьютера."
: "Ваш ход."
);
} else {
setStatus(
"Новая игра готова."
);
}
renderBoard();
updateClocks();
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
function drawMessage(
reason
) {
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
switch (reason) {
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
case "stalemate":
return "Ничья — пат.";
case "50-move":
return "Ничья — правило 50 ходов.";
case "threefold":
return "Ничья — троекратное повторение.";
case "insufficient-material":
return "Ничья — недостаточно материала.";
default:
return "Ничья.";
2026-08-23 06:42:17 +00:00
}
}
2026-08-23 07:19:12 +00:00
function startNewAiGame() {
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
state.mode =
"ai";
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
state.engine =
new ChessEngine();
state.selected =
null;
state.legalMoves =
[];
state.gameStarted =
true;
state.aiThinking =
false;
clearTimeout(
state.aiTimer
);
const selectedColor =
$("playerColorSelect").value;
state.aiColor =
selectedColor === "w"
? "b"
: "w";
state.orientation =
selectedColor;
setupAiPlayers();
resetClockFromSelect(
$("aiClockSelect").value
);
updateGameUI();
if (
state.aiColor === "w"
) {
scheduleAiMove();
2026-08-23 06:42:17 +00:00
}
}
2026-08-23 07:19:12 +00:00
function setupAiPlayers() {
const name =
getPlayerName();
if (
state.aiColor === "b"
) {
whitePlayerLabel.textContent =
name;
blackPlayerLabel.textContent =
"Компьютер";
} else {
whitePlayerLabel.textContent =
"Компьютер";
blackPlayerLabel.textContent =
name;
}
}
function getPlayerName() {
const value =
$("playerNameInput")
.value
.trim();
return value || "Вы";
}
function resetClockFromSelect(
value
) {
const seconds =
parseClock(
value
);
state.clocks = {
white: seconds,
black: seconds,
running:
seconds > 0,
turn: "w"
2026-08-23 06:42:17 +00:00
};
2026-08-23 07:19:12 +00:00
updateClocks();
2026-08-23 06:42:17 +00:00
}
2026-08-23 07:19:12 +00:00
function parseClock(
value
) {
if (
value === "none"
) {
return 0;
}
const [minutes] =
value.split("+");
return (
Number(minutes) *
60
);
2026-08-22 17:37:25 +00:00
}
2026-08-23 07:19:12 +00:00
function updateClocks() {
const white =
state.clocks.white;
const black =
state.clocks.black;
whiteClock.textContent =
formatTime(white);
blackClock.textContent =
formatTime(black);
whiteClock.classList.toggle(
"active",
state.clocks.running &&
state.clocks.turn === "w"
);
blackClock.classList.toggle(
"active",
state.clocks.running &&
state.clocks.turn === "b"
);
whiteClock.classList.toggle(
"low",
white > 0 &&
white < 30
);
blackClock.classList.toggle(
"low",
black > 0 &&
black < 30
);
2026-08-22 17:37:25 +00:00
}
2026-08-23 07:19:12 +00:00
function formatTime(
seconds
) {
if (!seconds) {
return "—";
}
const value =
Math.max(
0,
Math.ceil(seconds)
);
const minutes =
Math.floor(
value / 60
);
const remaining =
value % 60;
return (
String(minutes)
.padStart(2, "0") +
":" +
String(remaining)
.padStart(2, "0")
);
}
function startClockTimer() {
clearInterval(
state.clockTimer
);
state.clockTimer =
setInterval(
() => {
if (
!state.clocks.running
) {
return;
}
if (
state.clocks.white <= 0 ||
state.clocks.black <= 0
) {
return;
}
const key =
state.clocks.turn === "w"
? "white"
: "black";
state.clocks[key] =
Math.max(
0,
state.clocks[key] -
1
);
updateClocks();
},
1000
);
}
function switchMode(
mode
) {
if (
mode === "ai"
) {
state.mode =
"ai";
modeAiButton.classList.add(
"active"
);
modePartyButton.classList.remove(
"active"
);
aiControls.classList.remove(
"hidden"
);
partyControls.classList.add(
"hidden"
);
modeBadge.textContent =
"AI Арена";
connectionBadge.textContent =
"Соло";
connectionBadge.className =
"status-chip connection-solo";
return;
}
state.mode =
"party";
modePartyButton.classList.add(
"active"
);
modeAiButton.classList.remove(
"active"
);
aiControls.classList.add(
"hidden"
);
partyControls.classList.remove(
"hidden"
);
modeBadge.textContent =
"Party";
connectionBadge.textContent =
"Онлайн";
connectionBadge.className =
"status-chip connection-party";
state.gameStarted =
false;
state.engine =
new ChessEngine();
state.selected =
null;
state.legalMoves =
[];
updateGameUI();
}
2026-08-22 17:37:25 +00:00
async function createParty() {
2026-08-23 07:19:12 +00:00
const name =
getPlayerName();
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
try {
2026-08-22 17:37:25 +00:00
2026-08-23 07:19:12 +00:00
const response =
await fetch(
"/api/party/create",
{
method: "POST",
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
headers: {
"Content-Type":
"application/json"
},
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
body: JSON.stringify({
name,
timeControl:
$("partyClockSelect")
.value
})
}
);
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
const data =
await response.json();
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
if (!data.ok) {
throw new Error(
data.error
);
}
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
state.party =
data.party;
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
state.partyClientId =
data.party.you?.id ||
null;
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
state.engine =
createEngineFromSnapshot(
data.party.game
);
2026-08-23 06:42:17 +00:00
2026-08-23 07:19:12 +00:00
state.gameStarted =
true;
state.orientation =
"w";
$("partyCodeInput").value =
data.party.code;
$("leavePartyButton")
.classList.remove(
"hidden"
);
$("copyInviteButton")
.classList.remove(
"hidden"
);
$("partySummary")
.textContent =
`Комната ${data.party.code}. Вы играете белыми.`;
applyPartyState(
data.party
);
connectPartyEvents();
showToast(
`Комната ${data.party.code} создана.`,
"success"
);
} catch (error) {
showToast(
error.message ||
"Не удалось создать комнату.",
"error"
);
2026-08-22 17:37:25 +00:00
}
2026-08-23 07:19:12 +00:00
}
async function joinParty() {
const code =
$("partyCodeInput")
.value
.trim()
.toUpperCase();
if (
!/^[A-Z0-9]{6}$/.test(code)
) {
showToast(
"Введите 6-значный код комнаты.",
"error"
);
2026-08-23 06:42:17 +00:00
return;
2026-08-22 18:59:37 +00:00
}
2026-08-23 07:19:12 +00:00
try {
const response =
await fetch(
"/api/party/join",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
partyCode: code,
name:
getPlayerName(),
clientId:
state.partyClientId
})
}
);
const data =
await response.json();
if (!data.ok) {
throw new Error(
data.error
);
}
state.party =
data.party;
state.partyClientId =
data.party.you?.id ||
null;
state.gameStarted =
true;
applyPartyState(
data.party
);
$("leavePartyButton")
.classList.remove(
"hidden"
);
$("copyInviteButton")
.classList.remove(
"hidden"
);
connectPartyEvents();
showToast(
`Вы вошли в комнату ${code}.`,
"success"
);
} catch (error) {
showToast(
error.message ||
"Не удалось войти в комнату.",
"error"
);
}
2026-08-23 06:42:17 +00:00
}
2026-08-22 18:59:37 +00:00
2026-08-22 17:51:45 +00:00
2026-08-23 07:19:12 +00:00
function createEngineFromSnapshot(
snapshot
) {
const engine =
new ChessEngine(
snapshot.fen
);
engine.state.lastMove =
snapshot.lastMove
? {
...snapshot.lastMove
}
: null;
return engine;
}
function applyPartyState(
party
) {
state.party =
party;
state.engine =
createEngineFromSnapshot(
party.game
);
state.clocks = {
...party.clocks
};
state.selected =
null;
state.legalMoves =
[];
state.gameStarted =
true;
updatePartyPlayers();
updateSpectators();
updateGameUI();
}
function updatePartyPlayers() {
const players =
state.party?.players || {};
whitePlayerLabel.textContent =
players.white?.name ||
"Ожидание игрока";
blackPlayerLabel.textContent =
players.black?.name ||
"Ожидание игрока";
if (
state.party?.you?.role ===
"white"
) {
state.orientation =
"w";
} else if (
state.party?.you?.role ===
"black"
) {
state.orientation =
"b";
}
$("partySummary")
.textContent =
state.party?.code
? `Комната ${state.party.code}`
: "Создайте комнату.";
}
function updateSpectators() {
const spectators =
state.party?.spectators ||
[];
spectatorCount.textContent =
`${spectators.length} / ${
state.party?.maxSpectators || 20
}`;
spectatorList.innerHTML =
"";
if (!spectators.length) {
const li =
document.createElement("li");
li.className =
"empty-state";
li.textContent =
"Пока никого нет";
spectatorList.appendChild(
li
);
return;
}
for (
const spectator
of spectators
) {
const li =
document.createElement("li");
li.textContent =
spectator.name;
spectatorList.appendChild(
li
);
}
}
function connectPartyEvents() {
if (
state.partyEvents
) {
state.partyEvents.close();
}
if (
!state.party?.code ||
!state.partyClientId
) {
return;
}
const params =
new URLSearchParams({
partyCode:
state.party.code,
clientId:
state.partyClientId
});
const source =
new EventSource(
`/api/party/events?${params}`
);
state.partyEvents =
source;
source.addEventListener(
"party",
event => {
try {
const party =
JSON.parse(
event.data
);
applyPartyState(
party
);
} catch {
showToast(
"Ошибка обновления игры.",
"error"
);
}
}
);
source.onerror =
() => {
connectionBadge.textContent =
"Переподключение...";
connectionBadge.className =
"status-chip connection-solo";
};
}
async function performPartyMove(
from,
to,
promotion
) {
if (
!state.party ||
!state.partyClientId
) {
return;
}
try {
const response =
await fetch(
"/api/party/move",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
partyCode:
state.party.code,
clientId:
state.partyClientId,
from,
to,
promotion:
promotion || null
})
}
);
const data =
await response.json();
if (
data.party
) {
applyPartyState(
data.party
);
}
if (!data.ok) {
showToast(
data.error ||
"Ход отклонён.",
"error"
);
}
} catch (error) {
showToast(
error.message ||
"Ошибка соединения.",
"error"
);
}
}
async function leaveParty() {
if (
!state.party ||
!state.partyClientId
) {
return;
}
try {
await fetch(
"/api/party/leave",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
partyCode:
state.party.code,
clientId:
state.partyClientId
})
}
);
} catch {
// ignore
}
if (
state.partyEvents
) {
state.partyEvents.close();
state.partyEvents =
null;
}
state.party =
null;
state.partyClientId =
null;
$("leavePartyButton")
.classList.add(
"hidden"
);
$("copyInviteButton")
.classList.add(
"hidden"
);
$("partySummary")
.textContent =
"Создайте комнату, чтобы играть белыми.";
state.engine =
new ChessEngine();
state.gameStarted =
false;
state.selected =
null;
state.legalMoves =
[];
switchMode("party");
updateGameUI();
}
async function copyInvite() {
if (
!state.party?.code
) {
return;
}
const url =
`${location.origin}${location.pathname}?party=${state.party.code}`;
try {
await navigator.clipboard.writeText(
url
);
showToast(
"Ссылка скопирована.",
"success"
);
} catch {
showToast(
`Код комнаты: ${state.party.code}`
);
}
}
function loadPartyFromUrl() {
const code =
new URLSearchParams(
location.search
).get("party");
if (code) {
switchMode("party");
$("partyCodeInput").value =
code.toUpperCase();
}
}
function flipBoard() {
state.orientation =
state.orientation === "w"
? "b"
: "w";
renderBoard();
}
function bindEvents() {
$("modeAiButton")
.addEventListener(
"click",
() => switchMode("ai")
);
$("modePartyButton")
.addEventListener(
"click",
() => switchMode("party")
);
$("startAiButton")
.addEventListener(
"click",
startNewAiGame
);
$("newGameToolbarButton")
.addEventListener(
"click",
() => {
if (
state.mode === "party"
) {
showToast(
"В Party новую игру создаёт владелец комнаты."
);
return;
}
startNewAiGame();
}
);
$("flipBoardButton")
.addEventListener(
"click",
flipBoard
);
$("createPartyButton")
.addEventListener(
"click",
createParty
);
$("joinPartyButton")
.addEventListener(
"click",
joinParty
);
$("leavePartyButton")
.addEventListener(
"click",
leaveParty
);
$("copyInviteButton")
.addEventListener(
"click",
copyInvite
);
promotionDialog.addEventListener(
"click",
event => {
if (
event.target ===
promotionDialog
) {
promotionDialog.classList.add(
"hidden"
);
}
}
);
}
function initialize() {
bindEvents();
state.engine =
new ChessEngine();
state.orientation =
"w";
state.gameStarted =
false;
resetClockFromSelect(
$("aiClockSelect").value
);
setupAiPlayers();
updateGameUI();
startClockTimer();
loadPartyFromUrl();
}
initialize();