diff --git a/chess/public/js/app.js b/chess/public/js/app.js
index 218276c..3eece9e 100644
--- a/chess/public/js/app.js
+++ b/chess/public/js/app.js
@@ -43,142 +43,80 @@ const STORAGE_KEYS = {
};
+/* =========================================================
+ TIME CONTROLS
+========================================================= */
+
+const TIME_CONTROLS = {
+ none: 0,
+
+ "1+0": 60,
+ "3+0": 180,
+ "3+2": 180,
+ "5+0": 300,
+ "5+3": 300,
+ "10+0": 600,
+ "15+10": 900,
+ "30+0": 1800,
+ "30+20": 1800,
+};
+
+
/* =========================================================
ELEMENTS
========================================================= */
const elements = {
- modeBadge:
- document.querySelector("#modeBadge"),
+ modeBadge: document.querySelector("#modeBadge"),
+ connectionBadge: document.querySelector("#connectionBadge"),
- connectionBadge:
- document.querySelector("#connectionBadge"),
+ board: document.querySelector("#board"),
- board:
- document.querySelector("#board"),
+ statusMessage: document.querySelector("#statusMessage"),
+ turnBadge: document.querySelector("#turnBadge"),
- statusMessage:
- document.querySelector("#statusMessage"),
+ modeAiButton: document.querySelector("#modeAiButton"),
+ modePartyButton: document.querySelector("#modePartyButton"),
- turnBadge:
- document.querySelector("#turnBadge"),
+ playerNameInput: document.querySelector("#playerNameInput"),
- modeAiButton:
- document.querySelector("#modeAiButton"),
+ aiControls: document.querySelector("#aiControls"),
+ partyControls: document.querySelector("#partyControls"),
- modePartyButton:
- document.querySelector("#modePartyButton"),
+ aiLevelSelect: document.querySelector("#aiLevelSelect"),
+ playerColorSelect: document.querySelector("#playerColorSelect"),
- playerNameInput:
- document.querySelector("#playerNameInput"),
+ aiTimeControl: document.querySelector("#aiClockSelect"),
+ partyTimeControl: document.querySelector("#partyClockSelect"),
- aiControls:
- document.querySelector("#aiControls"),
+ startAiButton: document.querySelector("#startAiButton"),
- partyControls:
- document.querySelector("#partyControls"),
+ createPartyButton: document.querySelector("#createPartyButton"),
+ partyCodeInput: document.querySelector("#partyCodeInput"),
+ joinPartyButton: document.querySelector("#joinPartyButton"),
+ leavePartyButton: document.querySelector("#leavePartyButton"),
- aiLevelSelect:
- document.querySelector("#aiLevelSelect"),
+ partySummary: document.querySelector("#partySummary"),
+ copyInviteButton: document.querySelector("#copyInviteButton"),
- playerColorSelect:
- document.querySelector("#playerColorSelect"),
+ flipBoardButton: document.querySelector("#flipBoardButton"),
- aiTimeControl:
- document.querySelector("#aiClockSelect"),
+ whitePlayerLabel: document.querySelector("#whitePlayerLabel"),
+ blackPlayerLabel: document.querySelector("#blackPlayerLabel"),
- partyTimeControl:
- document.querySelector("#partyClockSelect"),
+ rosterWhite: document.querySelector("#rosterWhite"),
+ rosterBlack: document.querySelector("#rosterBlack"),
- startAiButton:
- document.querySelector("#startAiButton"),
+ spectatorList: document.querySelector("#spectatorList"),
+ spectatorCount: document.querySelector("#spectatorCount"),
- createPartyButton:
- document.querySelector("#createPartyButton"),
+ whiteClock: document.querySelector("#whiteClock"),
+ blackClock: document.querySelector("#blackClock"),
- partyCodeInput:
- document.querySelector("#partyCodeInput"),
+ promotionDialog: document.querySelector("#promotionDialog"),
+ promotionOptions: document.querySelector("#promotionOptions"),
- joinPartyButton:
- document.querySelector("#joinPartyButton"),
-
- leavePartyButton:
- document.querySelector("#leavePartyButton"),
-
- partySummary:
- document.querySelector("#partySummary"),
-
- copyInviteButton:
- document.querySelector("#copyInviteButton"),
-
- flipBoardButton:
- document.querySelector("#flipBoardButton"),
-
- whitePlayerLabel:
- document.querySelector("#whitePlayerLabel"),
-
- blackPlayerLabel:
- document.querySelector("#blackPlayerLabel"),
-
- rosterWhite:
- document.querySelector("#rosterWhite"),
-
- rosterBlack:
- document.querySelector("#rosterBlack"),
-
- spectatorList:
- document.querySelector("#spectatorList"),
-
- spectatorCount:
- document.querySelector("#spectatorCount"),
-
- whiteClock:
- document.querySelector("#whiteClock"),
-
- blackClock:
- document.querySelector("#blackClock"),
-
- resignButton:
- document.querySelector("#resignButton"),
-
- drawButton:
- document.querySelector("#drawButton"),
-
- rematchButton:
- document.querySelector("#rematchButton"),
-
- drawRequest:
- document.querySelector("#drawRequest"),
-
- drawRequestName:
- document.querySelector("#drawRequestName"),
-
- acceptDrawButton:
- document.querySelector("#acceptDrawButton"),
-
- declineDrawButton:
- document.querySelector("#declineDrawButton"),
-
- chatPanel:
- document.querySelector("#chatPanel"),
-
- chatMessages:
- document.querySelector("#chatMessages"),
-
- chatForm:
- document.querySelector("#chatForm"),
-
- chatInput:
- document.querySelector("#chatInput"),
-
- promotionDialog:
- document.querySelector("#promotionDialog"),
-
- promotionOptions:
- document.querySelector("#promotionOptions"),
-
- toastHost:
- document.querySelector("#toastHost"),
+ toastHost: document.querySelector("#toastHost"),
};
@@ -189,81 +127,37 @@ const elements = {
const appState = {
mode: "ai",
- engine:
- new ChessEngine(),
+ engine: new ChessEngine(),
orientation: "w",
+ selectedSquare: null,
- selectedSquare:
- null,
+ aiLevel: "intermediate",
+ playerColor: "w",
+ aiTimeControl: "10+0",
- aiLevel:
- "intermediate",
+ isAiThinking: false,
+ aiMoveTimer: null,
+ aiClockTimer: null,
- playerColor:
- "w",
+ pendingPromotionMoves: null,
- aiTimeControl:
- "10+0",
+ playerName: "Guest",
- isAiThinking:
- false,
+ party: null,
+ partySnapshot: null,
+ partyStream: null,
- pendingPromotionMoves:
- null,
-
- party:
- null,
-
- partySnapshot:
- null,
-
- partyStream:
- null,
-
- playerName:
- "Guest",
-
- gameOver:
- false,
-
- gameOverMessage:
- null,
-
- drawOfferPending:
- false,
-
- drawOfferFrom:
- null,
+ gameOver: false,
+ gameOverMessage: null,
localClocks: {
- white: 600,
- black: 600,
+ white: 0,
+ black: 0,
running: false,
- lastTick: Date.now(),
turn: "w",
+ lastTick: 0,
},
-
- aiClockTimer:
- null,
-};
-
-
-/* =========================================================
- TIME CONTROLS
-========================================================= */
-
-const TIME_CONTROLS = {
- none: 0,
- "1+0": 60,
- "3+0": 180,
- "3+2": 180,
- "5+0": 300,
- "5+3": 300,
- "10+0": 600,
- "15+10": 900,
- "30+0": 1800,
- "30+20": 1800,
};
@@ -290,15 +184,11 @@ function currentHumanColor() {
return null;
}
- if (
- appState.party.role === "white"
- ) {
+ if (appState.party.role === "white") {
return "w";
}
- if (
- appState.party.role === "black"
- ) {
+ if (appState.party.role === "black") {
return "b";
}
@@ -307,11 +197,8 @@ function currentHumanColor() {
function isHumanTurn() {
- const color =
- currentHumanColor();
-
- const status =
- currentStatus();
+ const color = currentHumanColor();
+ const status = currentStatus();
return Boolean(
color &&
@@ -323,6 +210,17 @@ function isHumanTurn() {
}
+function isFinished() {
+ const status = currentStatus();
+
+ return Boolean(
+ appState.gameOver ||
+ status.phase === "checkmate" ||
+ status.phase === "draw"
+ );
+}
+
+
function getLegalMovesForSelected() {
if (!appState.selectedSquare) {
return [];
@@ -334,6 +232,12 @@ function getLegalMovesForSelected() {
}
+function clearSelection() {
+ appState.selectedSquare = null;
+ appState.pendingPromotionMoves = null;
+}
+
+
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
@@ -345,27 +249,17 @@ function escapeHtml(value) {
function formatClock(seconds) {
- const safe =
- Math.max(
- 0,
- Number(seconds) || 0
- );
+ const total = Math.max(
+ 0,
+ Math.ceil(Number(seconds) || 0)
+ );
- const total =
- Math.ceil(safe);
-
- const minutes =
- Math.floor(total / 60);
-
- const secs =
- total % 60;
+ const minutes = Math.floor(total / 60);
+ const secs = total % 60;
if (minutes >= 60) {
- const hours =
- Math.floor(minutes / 60);
-
- const mins =
- minutes % 60;
+ const hours = Math.floor(minutes / 60);
+ const mins = minutes % 60;
return (
`${String(hours).padStart(2, "0")}:` +
@@ -381,31 +275,33 @@ function formatClock(seconds) {
}
+function getIncrementForTimeControl(value) {
+ const parts = String(value || "").split("+");
+ return Number(parts[1] || 0);
+}
+
+
+function getTimeControlSeconds(value) {
+ return TIME_CONTROLS[value] || 0;
+}
+
+
/* =========================================================
TOAST
========================================================= */
-function showToast(
- message,
- type = "info"
-) {
+function showToast(message, type = "info") {
if (!elements.toastHost) {
- alert(message);
+ console.log(message);
return;
}
- const toast =
- document.createElement("div");
+ const toast = document.createElement("div");
- toast.className =
- `toast ${type}`;
+ toast.className = `toast ${type}`;
+ toast.textContent = message;
- toast.textContent =
- message;
-
- elements.toastHost.appendChild(
- toast
- );
+ elements.toastHost.appendChild(toast);
window.setTimeout(
() => toast.remove(),
@@ -421,44 +317,35 @@ function showToast(
function setMode(mode) {
appState.mode = mode;
- elements.modeAiButton
- ?.classList.toggle(
- "active",
- mode === "ai"
- );
+ const isAi = mode === "ai";
+ const isParty = mode === "party";
- elements.modePartyButton
- ?.classList.toggle(
- "active",
- mode === "party"
- );
+ elements.modeAiButton?.classList.toggle(
+ "active",
+ isAi
+ );
- elements.aiControls
- ?.classList.toggle(
- "hidden",
- mode !== "ai"
- );
+ elements.modePartyButton?.classList.toggle(
+ "active",
+ isParty
+ );
- elements.partyControls
- ?.classList.toggle(
- "hidden",
- mode !== "party"
- );
+ elements.aiControls?.classList.toggle(
+ "hidden",
+ !isAi
+ );
- elements.chatPanel
- ?.classList.toggle(
- "hidden",
- mode !== "party"
- );
+ elements.partyControls?.classList.toggle(
+ "hidden",
+ !isParty
+ );
if (elements.modeBadge) {
elements.modeBadge.textContent =
- mode === "ai"
+ isAi
? "AI Arena"
: "Party Lounge";
}
-
- render();
}
@@ -467,20 +354,12 @@ function setMode(mode) {
========================================================= */
function updatePartyUrl(code = null) {
- const url =
- new URL(
- window.location.href
- );
+ const url = new URL(window.location.href);
if (code) {
- url.searchParams.set(
- "party",
- code
- );
+ url.searchParams.set("party", code);
} else {
- url.searchParams.delete(
- "party"
- );
+ url.searchParams.delete("party");
}
window.history.replaceState(
@@ -505,15 +384,12 @@ function renderBoard() {
const selectedMoves =
getLegalMovesForSelected();
- const moveTargets =
- new Map(
- selectedMoves.map(
- move => [
- move.to,
- move,
- ]
- )
- );
+ const moveTargets = new Map(
+ selectedMoves.map(move => [
+ move.to,
+ move,
+ ])
+ );
const orientation =
appState.orientation;
@@ -528,8 +404,7 @@ function renderBoard() {
? [7, 6, 5, 4, 3, 2, 1, 0]
: [0, 1, 2, 3, 4, 5, 6, 7];
- const status =
- currentStatus();
+ const status = currentStatus();
const checkColor =
status.check
@@ -539,28 +414,18 @@ function renderBoard() {
const lastMove =
appState.engine.state.lastMove;
- for (
- const y of yOrder
- ) {
- for (
- const x of xOrder
- ) {
+ for (const y of yOrder) {
+ for (const x of xOrder) {
const squareName =
- ChessRules.toSquare(
- x,
- y
- );
+ ChessRules.toSquare(x, y);
const piece =
appState.engine.state.board[y][x];
const square =
- document.createElement(
- "button"
- );
+ document.createElement("button");
square.type = "button";
-
square.className =
`square ${
(x + y) % 2 === 0
@@ -581,9 +446,7 @@ function renderBoard() {
}
const move =
- moveTargets.get(
- squareName
- );
+ moveTargets.get(squareName);
if (move) {
square.classList.add(
@@ -617,9 +480,7 @@ function renderBoard() {
if (piece) {
const pieceNode =
- document.createElement(
- "span"
- );
+ document.createElement("span");
pieceNode.className =
`piece ${
@@ -633,29 +494,22 @@ function renderBoard() {
piece.color
][piece.type];
- square.appendChild(
- pieceNode
- );
+ square.appendChild(pieceNode);
}
square.addEventListener(
"click",
- () =>
- handleSquareClick(
- squareName
- )
+ () => handleSquareClick(squareName)
);
- elements.board.appendChild(
- square
- );
+ elements.board.appendChild(square);
}
}
}
/* =========================================================
- STATUS
+ GAME STATUS
========================================================= */
function formatStatus() {
@@ -663,45 +517,33 @@ function formatStatus() {
return getGameOverMessage();
}
- const status =
- currentStatus();
+ const status = currentStatus();
- if (
- status.phase ===
- "checkmate"
- ) {
+ if (status.phase === "checkmate") {
return "Checkmate";
}
- if (
- status.phase ===
- "draw"
- ) {
+ if (status.phase === "draw") {
return "Draw";
}
if (status.check) {
- return (
- appState.engine.state.turn === "w"
- ? "White is in check"
- : "Black is in check"
- );
+ return appState.engine.state.turn === "w"
+ ? "White is in check"
+ : "Black is in check";
}
- return (
- appState.engine.state.turn === "w"
- ? "White to move"
- : "Black to move"
- );
+ return appState.engine.state.turn === "w"
+ ? "White to move"
+ : "Black to move";
}
-/* =========================================================
- GAME OVER
-========================================================= */
-
function getGameOverMessage() {
- if (!appState.partySnapshot) {
+ const snapshot =
+ appState.partySnapshot;
+
+ if (!snapshot) {
return (
appState.gameOverMessage ||
"Game over."
@@ -709,49 +551,31 @@ function getGameOverMessage() {
}
const reason =
- appState.partySnapshot
- .gameOverReason;
+ snapshot.gameOverReason;
const winner =
- appState.partySnapshot.winner;
+ snapshot.winner;
- if (
- reason === "agreement"
- ) {
- return "Draw agreed.";
+ switch (reason) {
+ case "timeout":
+ return winner === "w"
+ ? "White wins on time."
+ : "Black wins on time.";
+
+ case "checkmate":
+ return winner === "w"
+ ? "White wins by checkmate."
+ : "Black wins by checkmate.";
+
+ case "draw":
+ return "Draw.";
+
+ default:
+ return (
+ appState.gameOverMessage ||
+ "Game over."
+ );
}
-
- if (
- reason === "timeout"
- ) {
- return winner === "w"
- ? "White wins on time."
- : "Black wins on time.";
- }
-
- if (
- reason === "resignation"
- ) {
- return winner === "w"
- ? "White wins — Black resigned."
- : "Black wins — White resigned.";
- }
-
- if (
- reason === "checkmate"
- ) {
- return winner === "w"
- ? "White wins by checkmate."
- : "Black wins by checkmate.";
- }
-
- if (
- reason === "draw"
- ) {
- return "Draw.";
- }
-
- return "Game over.";
}
@@ -762,8 +586,7 @@ function getGameOverMessage() {
function renderRoster() {
if (appState.mode === "ai") {
const human =
- appState.playerName ||
- "You";
+ appState.playerName || "You";
const whiteName =
appState.playerColor === "w"
@@ -775,44 +598,32 @@ function renderRoster() {
? human
: `Computer (${appState.aiLevel})`;
- if (
- elements.whitePlayerLabel
- ) {
+ if (elements.whitePlayerLabel) {
elements.whitePlayerLabel.textContent =
whiteName;
}
- if (
- elements.blackPlayerLabel
- ) {
+ if (elements.blackPlayerLabel) {
elements.blackPlayerLabel.textContent =
blackName;
}
- if (
- elements.rosterWhite
- ) {
+ if (elements.rosterWhite) {
elements.rosterWhite.textContent =
whiteName;
}
- if (
- elements.rosterBlack
- ) {
+ if (elements.rosterBlack) {
elements.rosterBlack.textContent =
blackName;
}
- if (
- elements.spectatorList
- ) {
+ if (elements.spectatorList) {
elements.spectatorList.innerHTML =
"
None in AI mode";
}
- if (
- elements.spectatorCount
- ) {
+ if (elements.spectatorCount) {
elements.spectatorCount.textContent =
"0 / 20";
}
@@ -821,12 +632,10 @@ function renderRoster() {
}
const white =
- appState.partySnapshot
- ?.players?.white;
+ appState.partySnapshot?.players?.white;
const black =
- appState.partySnapshot
- ?.players?.black;
+ appState.partySnapshot?.players?.black;
const whiteName =
white
@@ -846,48 +655,31 @@ function renderRoster() {
}`
: "Open seat";
- if (
- elements.whitePlayerLabel
- ) {
- elements.whitePlayerLabel.textContent =
- whiteName;
- }
+ elements.whitePlayerLabel &&
+ (elements.whitePlayerLabel.textContent =
+ whiteName);
- if (
- elements.blackPlayerLabel
- ) {
- elements.blackPlayerLabel.textContent =
- blackName;
- }
+ elements.blackPlayerLabel &&
+ (elements.blackPlayerLabel.textContent =
+ blackName);
- if (
- elements.rosterWhite
- ) {
- elements.rosterWhite.textContent =
- whiteName;
- }
+ elements.rosterWhite &&
+ (elements.rosterWhite.textContent =
+ whiteName);
- if (
- elements.rosterBlack
- ) {
- elements.rosterBlack.textContent =
- blackName;
- }
+ elements.rosterBlack &&
+ (elements.rosterBlack.textContent =
+ blackName);
const spectators =
- appState.partySnapshot
- ?.spectators || [];
+ appState.partySnapshot?.spectators || [];
- if (
- elements.spectatorCount
- ) {
+ if (elements.spectatorCount) {
elements.spectatorCount.textContent =
`${spectators.length} / 20`;
}
- if (
- !elements.spectatorList
- ) {
+ if (!elements.spectatorList) {
return;
}
@@ -930,10 +722,13 @@ function renderPartySummary() {
return;
}
+ const role =
+ appState.party.role;
+
const roleText =
- appState.party.role === "white"
+ role === "white"
? "playing as White"
- : appState.party.role === "black"
+ : role === "black"
? "playing as Black"
: "watching as a spectator";
@@ -953,30 +748,23 @@ function renderPartySummary() {
function renderClocks() {
let clocks;
-
let timeControl;
- if (
- appState.mode === "party"
- ) {
+ if (appState.mode === "party") {
clocks =
- appState.partySnapshot
- ?.clocks;
+ appState.partySnapshot?.clocks;
timeControl =
- appState.partySnapshot
- ?.timeControl;
+ appState.partySnapshot?.timeControl;
} else {
clocks =
appState.localClocks;
- const seconds =
- TIME_CONTROLS[
- appState.aiTimeControl
- ];
-
timeControl = {
- initial: seconds,
+ initial:
+ getTimeControlSeconds(
+ appState.aiTimeControl
+ ),
label:
elements.aiTimeControl
@@ -986,21 +774,26 @@ function renderClocks() {
};
}
- const hasClock =
+ const enabled =
Boolean(
- timeControl &&
- timeControl.initial > 0
+ timeControl?.initial > 0
);
- if (!hasClock) {
+ if (!enabled) {
if (elements.whiteClock) {
- elements.whiteClock.textContent =
- "∞";
+ elements.whiteClock.textContent = "∞";
+ elements.whiteClock.classList.remove(
+ "active",
+ "low"
+ );
}
if (elements.blackClock) {
- elements.blackClock.textContent =
- "∞";
+ elements.blackClock.textContent = "∞";
+ elements.blackClock.classList.remove(
+ "active",
+ "low"
+ );
}
return;
@@ -1047,70 +840,39 @@ function renderClocks() {
/* =========================================================
- BUTTONS
+ CONNECTION BADGE
========================================================= */
-function updateGameButtons() {
- const status =
- currentStatus();
-
- const finished =
- appState.gameOver ||
- status.phase === "checkmate" ||
- status.phase === "draw";
-
- const human =
- Boolean(
- currentHumanColor()
- );
-
- if (
- elements.resignButton
- ) {
- elements.resignButton.disabled =
- finished ||
- !human;
+function renderConnectionBadge() {
+ if (!elements.connectionBadge) {
+ return;
}
- if (
- elements.drawButton
- ) {
- const show =
- appState.mode === "party" &&
- human;
+ if (appState.mode === "ai") {
+ elements.connectionBadge.textContent =
+ appState.isAiThinking
+ ? "Computer thinking"
+ : `AI: ${appState.aiLevel}`;
- elements.drawButton.classList.toggle(
- "hidden",
- !show
- );
-
- elements.drawButton.disabled =
- finished ||
- !human ||
- appState.drawOfferPending;
-
- elements.drawButton.innerHTML =
- appState.drawOfferPending
- ? "🤝 Ничья предложена"
- : "🤝 Предложить ничью";
+ return;
}
- if (
- elements.rematchButton
- ) {
- const show =
- appState.mode === "party" &&
- human;
+ if (!appState.party) {
+ elements.connectionBadge.textContent =
+ "Party idle";
- elements.rematchButton.classList.toggle(
- "hidden",
- !show
- );
-
- elements.rematchButton.disabled =
- !show ||
- !finished;
+ return;
}
+
+ const role =
+ appState.party.role === "white"
+ ? "White"
+ : appState.party.role === "black"
+ ? "Black"
+ : "Spectator";
+
+ elements.connectionBadge.textContent =
+ `Party ${appState.party.code} · ${role}`;
}
@@ -1120,117 +882,32 @@ function updateGameButtons() {
function render() {
renderBoard();
-
renderRoster();
-
renderPartySummary();
-
renderClocks();
+ renderConnectionBadge();
- updateGameButtons();
+ const status = formatStatus();
- if (
- elements.turnBadge
- ) {
+ if (elements.turnBadge) {
elements.turnBadge.textContent =
- formatStatus();
+ status;
}
- if (
- elements.statusMessage
- ) {
+ if (elements.statusMessage) {
elements.statusMessage.textContent =
- formatStatus();
+ status;
}
- if (
- elements.connectionBadge
- ) {
- if (appState.mode === "ai") {
- elements.connectionBadge.textContent =
- appState.isAiThinking
- ? "Computer thinking"
- : `AI: ${appState.aiLevel}`;
- } else if (appState.party) {
- const role =
- appState.party.role === "white"
- ? "White"
- : appState.party.role === "black"
- ? "Black"
- : "Spectator";
-
- elements.connectionBadge.textContent =
- `Party ${appState.party.code} · ${role}`;
- } else {
- elements.connectionBadge.textContent =
- "Party idle";
- }
- }
-
- elements.copyInviteButton
- ?.classList.toggle(
- "hidden",
- !appState.party
- );
-
- elements.leavePartyButton
- ?.classList.toggle(
- "hidden",
- !appState.party
- );
-
- renderDrawRequest();
-}
-
-
-/* =========================================================
- DRAW REQUEST
-========================================================= */
-
-function renderDrawRequest() {
- if (
- !elements.drawRequest
- ) {
- return;
- }
-
- const offer =
- appState.partySnapshot
- ?.drawOffer;
-
- const isIncoming =
- Boolean(
- offer &&
- offer.by !==
- appState.party?.clientId &&
- !appState.gameOver
- );
-
- elements.drawRequest.classList.toggle(
+ elements.copyInviteButton?.classList.toggle(
"hidden",
- !isIncoming
+ !appState.party
);
- if (
- isIncoming &&
- elements.drawRequestName
- ) {
- elements.drawRequestName.textContent =
- `${offer.name} предлагает ничью`;
- }
-}
-
-
-/* =========================================================
- SELECTION
-========================================================= */
-
-function clearSelection() {
- appState.selectedSquare =
- null;
-
- appState.pendingPromotionMoves =
- null;
+ elements.leavePartyButton?.classList.toggle(
+ "hidden",
+ !appState.party
+ );
}
@@ -1238,46 +915,37 @@ function clearSelection() {
PROMOTION
========================================================= */
-function openPromotionDialog(
- moves
-) {
+function openPromotionDialog(moves) {
appState.pendingPromotionMoves =
moves;
- if (
- !elements.promotionOptions
- ) {
+ if (!elements.promotionOptions) {
return;
}
- elements.promotionOptions.innerHTML =
- "";
+ elements.promotionOptions.innerHTML = "";
- for (
- const move of moves
- ) {
+ for (const move of moves) {
const button =
- document.createElement(
- "button"
- );
+ document.createElement("button");
button.type = "button";
-
button.className =
"promotion-button";
+ const promotion =
+ String(move.promotion || "Q")
+ .toUpperCase();
+
button.textContent =
PIECE_GLYPHS[
move.color
- ][
- move.promotion.toUpperCase()
- ];
+ ][promotion];
button.addEventListener(
"click",
() => {
closePromotionDialog();
-
submitMove(move);
}
);
@@ -1287,21 +955,18 @@ function openPromotionDialog(
);
}
- elements.promotionDialog
- ?.classList.remove(
- "hidden"
- );
+ elements.promotionDialog?.classList.remove(
+ "hidden"
+ );
}
function closePromotionDialog() {
- appState.pendingPromotionMoves =
- null;
+ appState.pendingPromotionMoves = null;
- elements.promotionDialog
- ?.classList.add(
- "hidden"
- );
+ elements.promotionDialog?.classList.add(
+ "hidden"
+ );
}
@@ -1309,12 +974,8 @@ function closePromotionDialog() {
MOVE
========================================================= */
-async function submitMove(
- move
-) {
- if (
- appState.gameOver
- ) {
+async function submitMove(move) {
+ if (appState.gameOver) {
return;
}
@@ -1334,15 +995,11 @@ async function submitMove(
clientId:
appState.party.clientId,
- from:
- move.from,
-
- to:
- move.to,
+ from: move.from,
+ to: move.to,
promotion:
- move.promotion ||
- null,
+ move.promotion || null,
}
);
@@ -1373,15 +1030,10 @@ async function submitMove(
const result =
appState.engine.makeMove({
- from:
- move.from,
-
- to:
- move.to,
-
+ from: move.from,
+ to: move.to,
promotion:
- move.promotion ||
- null,
+ move.promotion || null,
});
if (!result.ok) {
@@ -1403,12 +1055,10 @@ async function submitMove(
/* =========================================================
- BOARD CLICK
+ BOARD INPUT
========================================================= */
-function handleSquareClick(
- square
-) {
+function handleSquareClick(square) {
if (
appState.pendingPromotionMoves ||
appState.gameOver
@@ -1416,53 +1066,38 @@ function handleSquareClick(
return;
}
+ if (!isHumanTurn()) {
+ clearSelection();
+ renderBoard();
+ return;
+ }
+
const piece =
- appState.engine.getPiece(
- square
- );
+ appState.engine.getPiece(square);
- const humanColor =
- currentHumanColor();
-
- if (
- appState.selectedSquare
- ) {
+ if (appState.selectedSquare) {
const matchingMoves =
getLegalMovesForSelected()
.filter(
move =>
- move.to ===
- square
+ move.to === square
);
- if (
- matchingMoves.length === 1
- ) {
- submitMove(
- matchingMoves[0]
- );
-
+ if (matchingMoves.length === 1) {
+ submitMove(matchingMoves[0]);
return;
}
- if (
- matchingMoves.length > 1
- ) {
+ if (matchingMoves.length > 1) {
openPromotionDialog(
matchingMoves
);
-
return;
}
}
- if (!isHumanTurn()) {
- clearSelection();
-
- renderBoard();
-
- return;
- }
+ const humanColor =
+ currentHumanColor();
if (
piece &&
@@ -1471,18 +1106,15 @@ function handleSquareClick(
appState.engine.state.turn
) {
appState.selectedSquare =
- appState.selectedSquare ===
- square
+ appState.selectedSquare === square
? null
: square;
renderBoard();
-
return;
}
clearSelection();
-
renderBoard();
}
@@ -1492,25 +1124,21 @@ function handleSquareClick(
========================================================= */
function scheduleAiTurn() {
- if (
- appState.mode !== "ai"
- ) {
+ stopAiMoveTimer();
+
+ if (appState.mode !== "ai") {
return;
}
- const status =
- currentStatus();
+ const status = currentStatus();
if (
!status ||
status.phase !== "playing" ||
appState.gameOver
) {
- appState.isAiThinking =
- false;
-
+ appState.isAiThinking = false;
render();
-
return;
}
@@ -1518,49 +1146,58 @@ function scheduleAiTurn() {
appState.engine.state.turn ===
appState.playerColor
) {
- appState.isAiThinking =
- false;
-
+ appState.isAiThinking = false;
render();
-
return;
}
- appState.isAiThinking =
- true;
-
+ appState.isAiThinking = true;
render();
- window.setTimeout(
- () => {
- if (
- appState.mode !== "ai" ||
- appState.gameOver
- ) {
- appState.isAiThinking =
- false;
+ appState.aiMoveTimer =
+ window.setTimeout(
+ () => {
+ appState.aiMoveTimer = null;
- return;
- }
+ if (
+ appState.mode !== "ai" ||
+ appState.gameOver
+ ) {
+ appState.isAiThinking = false;
+ render();
+ return;
+ }
- const move =
- chooseComputerMove(
- appState.engine,
- appState.aiLevel
- );
+ const move =
+ chooseComputerMove(
+ appState.engine,
+ appState.aiLevel
+ );
- appState.isAiThinking =
- false;
+ appState.isAiThinking = false;
- if (!move) {
- render();
- return;
- }
+ if (!move) {
+ render();
+ return;
+ }
- submitMove(move);
- },
- 400
- );
+ submitMove(move);
+ },
+ 400
+ );
+}
+
+
+function stopAiMoveTimer() {
+ if (appState.aiMoveTimer) {
+ window.clearTimeout(
+ appState.aiMoveTimer
+ );
+
+ appState.aiMoveTimer = null;
+ }
+
+ appState.isAiThinking = false;
}
@@ -1572,17 +1209,21 @@ function startAiClock() {
stopAiClock();
const seconds =
- TIME_CONTROLS[
+ getTimeControlSeconds(
appState.aiTimeControl
- ] || 0;
+ );
appState.localClocks = {
white: seconds,
black: seconds,
+
running: seconds > 0,
- lastTick: Date.now(),
+
turn:
appState.engine.state.turn,
+
+ lastTick:
+ Date.now(),
};
render();
@@ -1595,14 +1236,7 @@ function startAiClock() {
window.setInterval(
() => {
updateAiClock();
-
renderClocks();
-
- if (
- appState.gameOver
- ) {
- stopAiClock();
- }
},
250
);
@@ -1610,22 +1244,20 @@ function startAiClock() {
function stopAiClock() {
- if (
- appState.aiClockTimer
- ) {
+ if (appState.aiClockTimer) {
window.clearInterval(
appState.aiClockTimer
);
- appState.aiClockTimer =
- null;
+ appState.aiClockTimer = null;
}
}
function updateAiClock() {
if (
- appState.mode !== "ai"
+ appState.mode !== "ai" ||
+ appState.gameOver
) {
return;
}
@@ -1633,55 +1265,50 @@ function updateAiClock() {
const clocks =
appState.localClocks;
- if (
- !clocks.running ||
- !TIME_CONTROLS[
- appState.aiTimeControl
- ]
- ) {
+ if (!clocks.running) {
return;
}
- const now =
- Date.now();
+ const now = Date.now();
const elapsed =
- (now - clocks.lastTick) /
- 1000;
+ Math.max(
+ 0,
+ (now - clocks.lastTick) / 1000
+ );
- if (elapsed <= 0) {
+ if (!elapsed) {
return;
}
- const role =
- clocks.turn === "w"
+ clocks.lastTick = now;
+
+ const color =
+ clocks.turn;
+
+ const key =
+ color === "w"
? "white"
: "black";
- clocks[role] =
+ clocks[key] =
Math.max(
0,
- clocks[role] -
- elapsed
+ clocks[key] - elapsed
);
- clocks.lastTick =
- now;
+ if (clocks[key] <= 0) {
+ clocks.running = false;
- if (
- clocks[role] <= 0
- ) {
- clocks.running =
- false;
-
- appState.gameOver =
- true;
+ appState.gameOver = true;
appState.gameOverMessage =
- clocks.turn === "w"
+ color === "w"
? "Black wins on time."
: "White wins on time.";
+ stopAiClock();
+
showToast(
appState.gameOverMessage,
"error"
@@ -1696,32 +1323,28 @@ function updateAiClockAfterMove() {
const clocks =
appState.localClocks;
- const increment =
- getIncrementForTimeControl(
- appState.aiTimeControl
- );
-
- if (
- !clocks.running ||
- !TIME_CONTROLS[
- appState.aiTimeControl
- ]
- ) {
+ if (!clocks.running) {
return;
}
updateAiClock();
+ if (appState.gameOver) {
+ return;
+ }
+
const previousTurn =
clocks.turn;
- const role =
+ const key =
previousTurn === "w"
? "white"
: "black";
- clocks[role] +=
- increment;
+ clocks[key] +=
+ getIncrementForTimeControl(
+ appState.aiTimeControl
+ );
clocks.turn =
appState.engine.state.turn;
@@ -1731,57 +1354,28 @@ function updateAiClockAfterMove() {
}
-function getIncrementForTimeControl(
- value
-) {
- const parts =
- String(value).split("+");
-
- return Number(
- parts[1] || 0
- );
-}
-
-
/* =========================================================
- LOCAL RESET
+ LOCAL GAME
========================================================= */
function resetLocalGame() {
+ stopAiMoveTimer();
stopAiClock();
closePromotionDialog();
- clearSelection();
-
appState.engine =
new ChessEngine();
- appState.partySnapshot =
- null;
+ appState.partySnapshot = null;
appState.orientation =
appState.playerColor;
- appState.isAiThinking =
- false;
-
- appState.gameOver =
- false;
-
- appState.gameOverMessage =
- null;
-
- appState.drawOfferPending =
- false;
-
- appState.drawOfferFrom =
- null;
+ appState.gameOver = false;
+ appState.gameOverMessage = null;
startAiClock();
-
- render();
-
scheduleAiTurn();
}
@@ -1790,10 +1384,7 @@ function resetLocalGame() {
API
========================================================= */
-async function postJson(
- url,
- body
-) {
+async function postJson(url, body) {
try {
const response =
await fetch(
@@ -1823,6 +1414,7 @@ async function postJson(
) {
return {
ok: false,
+
error:
`Server returned ${response.status} instead of JSON.`,
};
@@ -1832,6 +1424,7 @@ async function postJson(
} catch (error) {
return {
ok: false,
+
error:
error instanceof Error
? error.message
@@ -1853,13 +1446,19 @@ function hydratePartyState(
return;
}
+ const previousCode =
+ appState.party?.code;
+
+ const previousClientId =
+ appState.party?.clientId;
+
appState.party = {
code:
partyPayload.code,
clientId:
partyPayload.you?.id ||
- appState.party?.clientId ||
+ previousClientId ||
null,
role:
@@ -1886,30 +1485,14 @@ function hydratePartyState(
? getGameOverMessage()
: null;
- appState.drawOfferPending =
- Boolean(
- partyPayload.drawOffer &&
- partyPayload.drawOffer.by ===
- appState.party.clientId
- );
-
- appState.drawOfferFrom =
- partyPayload.drawOffer
- ? partyPayload.drawOffer.name
- : null;
-
if (
appState.party.role === "white"
) {
- appState.orientation =
- "w";
- }
-
- if (
+ appState.orientation = "w";
+ } else if (
appState.party.role === "black"
) {
- appState.orientation =
- "b";
+ appState.orientation = "b";
}
setMode("party");
@@ -1920,14 +1503,6 @@ function hydratePartyState(
render();
- if (
- partyPayload.chat
- ) {
- renderChat(
- partyPayload.chat
- );
- }
-
if (
announce &&
partyPayload.gameOverReason
@@ -1937,11 +1512,23 @@ function hydratePartyState(
"success"
);
}
+
+ if (
+ previousCode &&
+ previousCode !== partyPayload.code
+ ) {
+ console.info(
+ "Party changed:",
+ previousCode,
+ "→",
+ partyPayload.code
+ );
+ }
}
/* =========================================================
- SSE
+ PARTY SSE
========================================================= */
function connectPartyStream() {
@@ -1949,21 +1536,18 @@ function connectPartyStream() {
return;
}
- if (
- appState.partyStream
- ) {
- appState.partyStream.close();
- }
+ closePartyStream();
+
+ const {
+ code,
+ clientId,
+ } = appState.party;
const url =
`/api/party/events?partyCode=${
- encodeURIComponent(
- appState.party.code
- )
+ encodeURIComponent(code)
}&clientId=${
- encodeURIComponent(
- appState.party.clientId
- )
+ encodeURIComponent(clientId)
}`;
const stream =
@@ -1984,23 +1568,7 @@ function connectPartyStream() {
);
} catch (error) {
console.error(
- error
- );
- }
- }
- );
-
- stream.addEventListener(
- "chat",
- event => {
- try {
- appendChatMessage(
- JSON.parse(
- event.data
- )
- );
- } catch (error) {
- console.error(
+ "Party SSE error:",
error
);
}
@@ -2013,12 +1581,19 @@ function connectPartyStream() {
elements.connectionBadge
) {
elements.connectionBadge.textContent =
- `Party ${appState.party.code} · reconnecting`;
+ `Party ${code} · reconnecting`;
}
};
- appState.partyStream =
- stream;
+ appState.partyStream = stream;
+}
+
+
+function closePartyStream() {
+ if (appState.partyStream) {
+ appState.partyStream.close();
+ appState.partyStream = null;
+ }
}
@@ -2032,8 +1607,8 @@ async function createParty() {
}
const timeControl =
- elements.partyTimeControl
- ?.value || "10+0";
+ elements.partyTimeControl?.value ||
+ "10+0";
const response =
await postJson(
@@ -2079,9 +1654,7 @@ async function createParty() {
JOIN PARTY
========================================================= */
-async function joinParty(
- code
-) {
+async function joinParty(code) {
const normalizedCode =
String(code || "")
.trim()
@@ -2114,11 +1687,6 @@ async function joinParty(
const storedIds =
getStoredPartyIds();
- const storedId =
- storedIds[
- normalizedCode
- ] || null;
-
const response =
await postJson(
"/api/party/join",
@@ -2130,7 +1698,8 @@ async function joinParty(
normalizedCode,
clientId:
- storedId,
+ storedIds[normalizedCode] ||
+ null,
}
);
@@ -2164,7 +1733,7 @@ async function joinParty(
/* =========================================================
- LEAVE
+ LEAVE PARTY
========================================================= */
async function leaveParty(
@@ -2174,43 +1743,31 @@ async function leaveParty(
return;
}
- const oldParty =
+ const party =
appState.party;
+ closePartyStream();
+
await postJson(
"/api/party/leave",
{
partyCode:
- oldParty.code,
+ party.code,
clientId:
- oldParty.clientId,
+ party.clientId,
}
);
- if (
- appState.partyStream
- ) {
- appState.partyStream.close();
-
- appState.partyStream =
- null;
- }
-
removeStoredPartyId(
- oldParty.code
+ party.code
);
- appState.party =
- null;
-
- appState.partySnapshot =
- null;
+ appState.party = null;
+ appState.partySnapshot = null;
updatePartyUrl(null);
- clearChat();
-
setMode("ai");
resetLocalGame();
@@ -2223,306 +1780,6 @@ async function leaveParty(
}
-/* =========================================================
- NEW PARTY GAME
-========================================================= */
-
-async function startNewPartyGame() {
- if (!appState.party) {
- return;
- }
-
- const response =
- await postJson(
- "/api/party/new-game",
- {
- partyCode:
- appState.party.code,
-
- clientId:
- appState.party.clientId,
- }
- );
-
- if (!response.ok) {
- showToast(
- response.error ||
- "Unable to start a new game.",
- "error"
- );
-
- return;
- }
-
- hydratePartyState(
- response.party,
- false
- );
-
- showToast(
- "New game started.",
- "success"
- );
-}
-
-
-/* =========================================================
- RESIGN
-========================================================= */
-
-async function resignGame() {
- if (appState.gameOver) {
- return;
- }
-
- const color =
- currentHumanColor();
-
- if (!color) {
- return;
- }
-
- if (
- !window.confirm(
- "Вы действительно хотите сдаться?"
- )
- ) {
- return;
- }
-
- if (
- appState.mode === "party" &&
- appState.party
- ) {
- const response =
- await postJson(
- "/api/party/resign",
- {
- partyCode:
- appState.party.code,
-
- clientId:
- appState.party.clientId,
- }
- );
-
- if (!response.ok) {
- showToast(
- response.error ||
- "Unable to resign.",
- "error"
- );
-
- return;
- }
-
- hydratePartyState(
- response.party,
- false
- );
-
- showToast(
- "Вы сдались.",
- "info"
- );
-
- return;
- }
-
- appState.gameOver =
- true;
-
- appState.gameOverMessage =
- color === "w"
- ? "Black wins — White resigned."
- : "White wins — Black resigned.";
-
- stopAiClock();
-
- showToast(
- appState.gameOverMessage,
- "success"
- );
-
- render();
-}
-
-
-/* =========================================================
- DRAW
-========================================================= */
-
-async function offerDraw() {
- if (
- appState.mode !== "party" ||
- appState.gameOver ||
- !appState.party
- ) {
- return;
- }
-
- if (
- appState.drawOfferPending
- ) {
- showToast(
- "Вы уже предложили ничью."
- );
-
- return;
- }
-
- const response =
- await postJson(
- "/api/party/draw-offer",
- {
- partyCode:
- appState.party.code,
-
- clientId:
- appState.party.clientId,
- }
- );
-
- if (!response.ok) {
- showToast(
- response.error ||
- "Unable to offer draw.",
- "error"
- );
-
- return;
- }
-
- hydratePartyState(
- response.party,
- false
- );
-
- showToast(
- "Предложение ничьей отправлено.",
- "success"
- );
-}
-
-
-/* =========================================================
- DRAW RESPONSE
-========================================================= */
-
-async function respondToDraw(
- accept
-) {
- if (
- !appState.party
- ) {
- return;
- }
-
- const response =
- await postJson(
- "/api/party/draw-response",
- {
- partyCode:
- appState.party.code,
-
- clientId:
- appState.party.clientId,
-
- accept:
- Boolean(accept),
- }
- );
-
- if (!response.ok) {
- showToast(
- response.error ||
- "Unable to respond.",
- "error"
- );
-
- return;
- }
-
- hydratePartyState(
- response.party,
- false
- );
-
- showToast(
- accept
- ? "Ничья согласована."
- : "Предложение отклонено.",
- accept
- ? "success"
- : "info"
- );
-}
-
-
-/* =========================================================
- REMATCH
-========================================================= */
-
-async function requestRematch() {
- if (
- appState.mode !== "party" ||
- !appState.party
- ) {
- return;
- }
-
- if (!appState.gameOver) {
- showToast(
- "Реванш доступен после окончания партии."
- );
-
- return;
- }
-
- const response =
- await postJson(
- "/api/party/rematch",
- {
- partyCode:
- appState.party.code,
-
- clientId:
- appState.party.clientId,
- }
- );
-
- if (!response.ok) {
- showToast(
- response.error ||
- "Unable to request rematch.",
- "error"
- );
-
- return;
- }
-
- hydratePartyState(
- response.party,
- false
- );
-
- if (
- response.party.rematch.white &&
- response.party.rematch.black
- ) {
- showToast(
- "Реванш начинается!",
- "success"
- );
- } else {
- showToast(
- "Вы выбрали реванш. Ждём второго игрока.",
- "success"
- );
- }
-}
-
-
/* =========================================================
INVITE
========================================================= */
@@ -2556,221 +1813,6 @@ async function copyInviteLink() {
}
-/* =========================================================
- CHAT
-========================================================= */
-
-function clearChat() {
- if (
- elements.chatMessages
- ) {
- elements.chatMessages.innerHTML =
- `
- Chat becomes available in Party mode.
-
`;
- }
-}
-
-
-function renderChat(
- messages
-) {
- clearChat();
-
- if (
- !messages ||
- !messages.length
- ) {
- showChatEmpty();
-
- return;
- }
-
- elements.chatMessages.innerHTML =
- "";
-
- for (
- const message of messages
- ) {
- appendChatMessage(
- message,
- false
- );
- }
-
- scrollChatToBottom();
-}
-
-
-function showChatEmpty() {
- if (
- elements.chatMessages
- ) {
- elements.chatMessages.innerHTML =
- `
- Пока сообщений нет.
- Начните разговор.
-
`;
- }
-}
-
-
-function appendChatMessage(
- message,
- scroll = true
-) {
- if (
- !elements.chatMessages
- ) {
- return;
- }
-
- const empty =
- elements.chatMessages
- .querySelector(
- ".chat-empty"
- );
-
- empty?.remove();
-
- const wrapper =
- document.createElement(
- "div"
- );
-
- wrapper.className =
- "chat-message";
-
- const meta =
- document.createElement(
- "div"
- );
-
- meta.className =
- "chat-meta";
-
- const name =
- document.createElement(
- "span"
- );
-
- name.className =
- "chat-name";
-
- name.textContent =
- message.name ||
- "Guest";
-
- const time =
- document.createElement(
- "span"
- );
-
- time.textContent =
- new Date(
- message.time ||
- Date.now()
- ).toLocaleTimeString(
- [],
- {
- hour: "2-digit",
- minute: "2-digit",
- }
- );
-
- meta.append(
- name,
- time
- );
-
- const text =
- document.createElement(
- "div"
- );
-
- text.className =
- "chat-text";
-
- text.textContent =
- message.text ||
- "";
-
- wrapper.append(
- meta,
- text
- );
-
- elements.chatMessages.appendChild(
- wrapper
- );
-
- if (scroll) {
- scrollChatToBottom();
- }
-}
-
-
-function scrollChatToBottom() {
- if (
- elements.chatMessages
- ) {
- elements.chatMessages.scrollTop =
- elements.chatMessages.scrollHeight;
- }
-}
-
-
-async function sendChatMessage(
- event
-) {
- event.preventDefault();
-
- if (
- !appState.party
- ) {
- return;
- }
-
- const message =
- elements.chatInput?.value.trim();
-
- if (!message) {
- return;
- }
-
- const response =
- await postJson(
- "/api/party/chat",
- {
- partyCode:
- appState.party.code,
-
- clientId:
- appState.party.clientId,
-
- message,
- }
- );
-
- if (!response.ok) {
- showToast(
- response.error ||
- "Unable to send message.",
- "error"
- );
-
- return;
- }
-
- if (
- elements.chatInput
- ) {
- elements.chatInput.value =
- "";
- }
-}
-
-
/* =========================================================
STORAGE
========================================================= */
@@ -2792,11 +1834,14 @@ function setStoredPartyId(
code,
clientId
) {
+ if (!code || !clientId) {
+ return;
+ }
+
const ids =
getStoredPartyIds();
- ids[code] =
- clientId;
+ ids[code] = clientId;
localStorage.setItem(
STORAGE_KEYS.partyIds,
@@ -2805,9 +1850,11 @@ function setStoredPartyId(
}
-function removeStoredPartyId(
- code
-) {
+function removeStoredPartyId(code) {
+ if (!code) {
+ return;
+ }
+
const ids =
getStoredPartyIds();
@@ -2825,250 +1872,192 @@ function removeStoredPartyId(
========================================================= */
function bindEvents() {
- elements.playerNameInput
- ?.addEventListener(
- "input",
- event => {
- appState.playerName =
- event.target.value.trim() ||
- "Guest";
+ elements.playerNameInput?.addEventListener(
+ "input",
+ event => {
+ appState.playerName =
+ event.target.value.trim() ||
+ "Guest";
- localStorage.setItem(
- STORAGE_KEYS.name,
- appState.playerName
- );
+ localStorage.setItem(
+ STORAGE_KEYS.name,
+ appState.playerName
+ );
- renderRoster();
- }
- );
+ renderRoster();
+ }
+ );
- elements.aiLevelSelect
- ?.addEventListener(
- "change",
- event => {
- appState.aiLevel =
- event.target.value;
+ elements.aiLevelSelect?.addEventListener(
+ "change",
+ event => {
+ appState.aiLevel =
+ event.target.value;
- render();
- }
- );
+ render();
+ }
+ );
- elements.playerColorSelect
- ?.addEventListener(
- "change",
- event => {
- appState.playerColor =
- event.target.value;
-
- if (
- appState.mode ===
- "ai"
- ) {
- resetLocalGame();
- }
- }
- );
-
-
- elements.aiTimeControl
- ?.addEventListener(
- "change",
- event => {
- appState.aiTimeControl =
- event.target.value;
-
- if (
- appState.mode ===
- "ai"
- ) {
- resetLocalGame();
- }
- }
- );
-
-
- elements.modeAiButton
- ?.addEventListener(
- "click",
- async () => {
- if (appState.party) {
- await leaveParty(true);
- } else {
- setMode("ai");
- resetLocalGame();
- }
- }
- );
-
-
- elements.modePartyButton
- ?.addEventListener(
- "click",
- () => {
- setMode("party");
- }
- );
-
-
- elements.startAiButton
- ?.addEventListener(
- "click",
- async () => {
- if (appState.party) {
- await leaveParty(true);
- }
-
- setMode("ai");
+ elements.playerColorSelect?.addEventListener(
+ "change",
+ event => {
+ appState.playerColor =
+ event.target.value;
+ if (appState.mode === "ai") {
resetLocalGame();
-
- showToast(
- "New AI game ready.",
- "success"
- );
}
- );
+ }
+ );
- elements.createPartyButton
- ?.addEventListener(
- "click",
- createParty
- );
+ elements.aiTimeControl?.addEventListener(
+ "change",
+ event => {
+ appState.aiTimeControl =
+ event.target.value;
-
- elements.joinPartyButton
- ?.addEventListener(
- "click",
- () =>
- joinParty(
- elements.partyCodeInput
- ?.value
- )
- );
-
-
- elements.leavePartyButton
- ?.addEventListener(
- "click",
- () =>
- leaveParty(false)
- );
-
-
- elements.copyInviteButton
- ?.addEventListener(
- "click",
- copyInviteLink
- );
-
-
- elements.flipBoardButton
- ?.addEventListener(
- "click",
- () => {
- appState.orientation =
- appState.orientation ===
- "w"
- ? "b"
- : "w";
-
- renderBoard();
+ if (appState.mode === "ai") {
+ resetLocalGame();
}
- );
+ }
+ );
- elements.resignButton
- ?.addEventListener(
- "click",
- resignGame
- );
-
-
- elements.drawButton
- ?.addEventListener(
- "click",
- offerDraw
- );
-
-
- elements.rematchButton
- ?.addEventListener(
- "click",
- requestRematch
- );
-
-
- elements.acceptDrawButton
- ?.addEventListener(
- "click",
- () =>
- respondToDraw(true)
- );
-
-
- elements.declineDrawButton
- ?.addEventListener(
- "click",
- () =>
- respondToDraw(false)
- );
-
-
- elements.chatForm
- ?.addEventListener(
- "submit",
- sendChatMessage
- );
-
-
- elements.partyCodeInput
- ?.addEventListener(
- "input",
- event => {
- event.target.value =
- event.target.value
- .toUpperCase()
- .replace(
- /[^A-Z0-9]/g,
- ""
- );
+ elements.modeAiButton?.addEventListener(
+ "click",
+ async () => {
+ if (appState.party) {
+ await leaveParty(true);
+ return;
}
- );
+
+ setMode("ai");
+ resetLocalGame();
+ }
+ );
- elements.partyCodeInput
- ?.addEventListener(
- "keydown",
- event => {
- if (
- event.key ===
- "Enter"
- ) {
- event.preventDefault();
+ elements.modePartyButton?.addEventListener(
+ "click",
+ () => {
+ setMode("party");
+ render();
+ }
+ );
- joinParty(
- elements.partyCodeInput.value
+
+ elements.startAiButton?.addEventListener(
+ "click",
+ async () => {
+ if (appState.party) {
+ await leaveParty(true);
+ }
+
+ setMode("ai");
+ resetLocalGame();
+
+ showToast(
+ "New AI game ready.",
+ "success"
+ );
+ }
+ );
+
+
+ elements.createPartyButton?.addEventListener(
+ "click",
+ createParty
+ );
+
+
+ elements.joinPartyButton?.addEventListener(
+ "click",
+ () =>
+ joinParty(
+ elements.partyCodeInput?.value
+ )
+ );
+
+
+ elements.leavePartyButton?.addEventListener(
+ "click",
+ () => leaveParty(false)
+ );
+
+
+ elements.copyInviteButton?.addEventListener(
+ "click",
+ copyInviteLink
+ );
+
+
+ elements.flipBoardButton?.addEventListener(
+ "click",
+ () => {
+ appState.orientation =
+ appState.orientation === "w"
+ ? "b"
+ : "w";
+
+ renderBoard();
+ }
+ );
+
+
+ elements.partyCodeInput?.addEventListener(
+ "input",
+ event => {
+ event.target.value =
+ event.target.value
+ .toUpperCase()
+ .replace(
+ /[^A-Z0-9]/g,
+ ""
);
- }
- }
- );
+ }
+ );
- elements.promotionDialog
- ?.addEventListener(
- "click",
- event => {
- if (
- event.target ===
- elements.promotionDialog
- ) {
- closePromotionDialog();
- }
+ elements.partyCodeInput?.addEventListener(
+ "keydown",
+ event => {
+ if (event.key !== "Enter") {
+ return;
}
- );
+
+ event.preventDefault();
+
+ joinParty(
+ elements.partyCodeInput.value
+ );
+ }
+ );
+
+
+ elements.promotionDialog?.addEventListener(
+ "click",
+ event => {
+ if (
+ event.target ===
+ elements.promotionDialog
+ ) {
+ closePromotionDialog();
+ }
+ }
+ );
+
+
+ window.addEventListener(
+ "beforeunload",
+ () => {
+ stopAiMoveTimer();
+ stopAiClock();
+ closePartyStream();
+ }
+ );
}
@@ -3088,9 +2077,7 @@ async function init() {
appState.playerName =
savedName;
- if (
- elements.playerNameInput
- ) {
+ if (elements.playerNameInput) {
elements.playerNameInput.value =
savedName;
}
@@ -3107,23 +2094,23 @@ async function init() {
if (partyCode) {
setMode("party");
- if (
- elements.partyCodeInput
- ) {
+ if (elements.partyCodeInput) {
elements.partyCodeInput.value =
partyCode.toUpperCase();
}
- await joinParty(
- partyCode
- );
-
+ await joinParty(partyCode);
return;
}
setMode("ai");
-
resetLocalGame();
}
+
+/* =========================================================
+ START
+========================================================= */
+
init();
+