Files
ad_fun/chessstars/internal/web/templates/play.html
T
2026-07-15 13:29:08 +03:00

355 lines
13 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<title>Play - ChessStars</title>
{{template "head" .}}
<script src="/static/board.js"></script>
</head>
<body>
{{template "nav" .}}
<main>
<div id="lobby-view">
<div class="panel">
<h2>New game</h2>
<div class="tabs-row">
<button class="opp-tab active" data-mode="bot">vs Bot</button>
<button class="opp-tab" data-mode="challenge">Challenge a player</button>
<button class="opp-tab" data-mode="open">Open seat</button>
</div>
<div class="field" id="challenge-field" style="display:none">
<label>Opponent's username</label>
<input type="text" id="opponent-username" placeholder="username">
</div>
<div class="field" id="bot-depth-field">
<label>Bot strength (search depth 1-4)</label>
<input type="range" id="bot-depth" min="1" max="4" value="3">
</div>
<div class="field">
<label>Variant</label>
<select id="variant">
<option value="0">Standard</option>
<option value="1">Relaxed (no check restriction)</option>
</select>
</div>
<div class="field">
<label><input type="checkbox" id="rule-no-castling" style="width:auto"> disable castling</label>
<label><input type="checkbox" id="rule-no-ep" style="width:auto"> disable en passant</label>
</div>
<div class="field">
<label>Custom start FEN (optional)</label>
<input type="text" id="start-fen" placeholder="leave blank for standard start">
</div>
<button id="new-game-btn">Create game</button>
<p class="auth-error" id="new-game-msg"></p>
</div>
<div class="panel">
<h2>Open games waiting for an opponent</h2>
<div id="open-list" class="muted">none right now</div>
</div>
<div class="panel">
<h2>My games</h2>
<div id="my-list" class="muted">log in to see your games</div>
</div>
<div class="panel">
<h2>Browse games</h2>
<p class="muted">Every game on the server, most recent first -- click to spectate.</p>
<div id="all-list" class="muted">loading...</div>
</div>
</div>
<div id="game-view" style="display:none">
<p><a href="/play">&larr; back to lobby</a></p>
<div id="status-banner" class="status-banner ongoing"></div>
<div class="board-wrap">
<div id="board"></div>
<div class="side-panel panel">
<p><b id="white-name"></b> (white) vs <b id="black-name"></b> (black)</p>
<h3>Moves</h3>
<div id="move-list" class="move-list"></div>
</div>
</div>
</div>
</main>
<script>
const params = new URLSearchParams(location.search);
let gameID = params.get("id");
let selected = null;
let legalTargets = [];
let lastBoard = new Array(64).fill(0);
let lastMove = null;
let pollTimer = null;
let myUserID = null;
let currentState = null;
// --- lobby view ---
document.querySelectorAll(".opp-tab").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".opp-tab").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
const mode = btn.dataset.mode;
document.getElementById("challenge-field").style.display = mode === "challenge" ? "block" : "none";
document.getElementById("bot-depth-field").style.display = mode === "bot" ? "block" : "none";
});
});
function currentOpponentMode() {
const active = document.querySelector(".opp-tab.active");
return active ? active.dataset.mode : "bot";
}
document.getElementById("new-game-btn").addEventListener("click", async () => {
let ruleFlags = 0;
if (document.getElementById("rule-no-castling").checked) ruleFlags |= 0x01;
if (document.getElementById("rule-no-ep").checked) ruleFlags |= 0x02;
const mode = currentOpponentMode();
let opponent = "";
if (mode === "bot") opponent = "bot";
else if (mode === "challenge") opponent = document.getElementById("opponent-username").value.trim();
const res = await api.post("/api/games", {
opponent,
variant: parseInt(document.getElementById("variant").value, 10),
start_fen: document.getElementById("start-fen").value,
rule_flags: ruleFlags,
bot_depth: parseInt(document.getElementById("bot-depth").value, 10),
});
if (res.ok) {
location.search = "?id=" + res.id;
} else {
document.getElementById("new-game-msg").textContent = res.error || "could not create game";
}
});
async function loadOpen() {
const res = await api.get("/api/games/open");
const el = document.getElementById("open-list");
const games = (res.games || []);
if (games.length === 0) {
el.className = "muted";
el.textContent = "none right now";
return;
}
el.className = "";
el.innerHTML = "";
games.forEach((g) => {
const div = document.createElement("div");
div.className = "game-card";
div.innerHTML = "<div><b>" + g.white + "</b><div class='meta'>waiting for an opponent</div></div>";
const btn = document.createElement("button");
btn.textContent = "Join";
btn.addEventListener("click", async () => {
const r = await api.post("/api/games/" + g.id + "/join");
if (r.id) location.href = "/play?id=" + r.id;
else alert(r.error || "could not join");
});
div.appendChild(btn);
el.appendChild(div);
});
}
async function loadMine() {
const el = document.getElementById("my-list");
const res = await api.get("/api/me/games");
if (!res.ok) {
el.className = "muted";
el.textContent = "log in to see your games";
return;
}
const games = res.games || [];
if (games.length === 0) {
el.className = "muted";
el.textContent = "no games yet -- create one above";
return;
}
el.className = "";
el.innerHTML = "";
games.forEach((g) => {
const div = document.createElement("div");
div.className = "game-card";
const opponent = g.vs_bot ? "bot" : (g.black || "(open seat)");
const statusText = g.status === 0 ? (g.black ? "in progress" : "waiting for opponent") : STATUS_LABELS[g.status];
div.innerHTML = "<div><b>" + g.white + "</b> vs <b>" + opponent + "</b>" +
"<div class='meta'>" + statusText + "</div></div>";
const a = document.createElement("a");
a.className = "btn secondary";
a.href = "/play?id=" + g.id;
a.textContent = "Open";
div.appendChild(a);
el.appendChild(div);
});
}
async function loadAll() {
const el = document.getElementById("all-list");
const res = await api.get("/api/games/all");
const games = res.games || [];
if (games.length === 0) {
el.className = "muted";
el.textContent = "no games yet";
return;
}
el.className = "";
el.innerHTML = "";
games.forEach((g) => {
const div = document.createElement("div");
div.className = "game-card";
const opponent = g.vs_bot ? "bot" : (g.black || "(open seat)");
const statusText = g.status === 0 ? (g.black || g.vs_bot ? "in progress" : "waiting for opponent") : STATUS_LABELS[g.status];
div.innerHTML = "<div><b>" + g.white + "</b> vs <b>" + opponent + "</b>" +
"<div class='meta'>" + statusText + "</div></div>";
const a = document.createElement("a");
a.className = "btn secondary";
a.href = "/play?id=" + g.id;
a.textContent = "Watch";
div.appendChild(a);
el.appendChild(div);
});
}
// --- game view ---
function renderMoveList(sanMoves) {
const el = document.getElementById("move-list");
el.innerHTML = "";
for (let i = 0; i < sanMoves.length; i += 2) {
const num = document.createElement("div");
num.className = "mv-num";
num.textContent = (i / 2 + 1) + ".";
el.appendChild(num);
const whiteEl = document.createElement("div");
whiteEl.textContent = sanMoves[i] || "";
el.appendChild(whiteEl);
const blackEl = document.createElement("div");
blackEl.textContent = sanMoves[i + 1] || "";
el.appendChild(blackEl);
}
el.scrollTop = el.scrollHeight;
}
async function loadMoveList() {
const res = await api.get("/api/games/" + gameID + "/moves");
renderMoveList(res.moves || []);
}
function updateStatusBanner(state) {
const banner = document.getElementById("status-banner");
if (state.status !== 0) {
banner.className = "status-banner over";
banner.textContent = "Game over: " + STATUS_LABELS[state.status];
} else if (state.in_check) {
banner.className = "status-banner check";
banner.textContent = (state.turn === 0 ? "White" : "Black") + " is in check";
} else {
banner.className = "status-banner ongoing";
banner.textContent = (state.turn === 0 ? "White" : "Black") + " to move";
}
}
async function refresh() {
const state = await api.get("/api/games/" + gameID);
if (!state || !state.id) return;
currentState = state;
lastBoard = state.board;
document.getElementById("white-name").textContent = state.white || "?";
document.getElementById("black-name").textContent = state.vs_bot ? "bot" : (state.black || "(waiting)");
updateStatusBanner(state);
if (state.history_count > 0) {
const mv = await api.get("/api/games/" + gameID + "/history/" + (state.history_count - 1));
if (mv && mv.from !== undefined) lastMove = { from: mv.from, to: mv.to };
}
let checkSquare = null;
if (state.in_check) {
const kingCode = state.turn === 0 ? 6 : 14;
const idx = state.board.findIndex((p) => p === kingCode);
if (idx >= 0) checkSquare = idx;
}
renderBoard(document.getElementById("board"), state.board, {
selected, targets: legalTargets, lastMove, checkSquare, onClick: onSquareClick, flipped: isFlipped(),
});
loadMoveList();
if (state.status !== 0 && pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function isMyTurn() {
if (!currentState || myUserID === null) return true;
const turnName = currentState.turn === 0 ? currentState.white : currentState.black;
return turnName === myUserID;
}
// Each player sees their own pieces at the bottom: flip the board for
// whoever is playing black. Spectators (and white) see it unflipped.
function isFlipped() {
return !!(currentState && myUserID !== null && currentState.black === myUserID);
}
async function onSquareClick(sq) {
if (currentState && currentState.status !== 0) return;
if (!isMyTurn()) return;
if (selected === null) {
if (lastBoard[sq] === 0) return;
selected = sq;
const res = await api.get("/api/games/" + gameID + "/legal-moves?from=" + sq);
legalTargets = res.targets || [];
renderBoard(document.getElementById("board"), lastBoard, { selected, targets: legalTargets, lastMove, onClick: onSquareClick, flipped: isFlipped() });
return;
}
const from = selected;
const to = sq;
selected = null;
const wasTarget = legalTargets.includes(to);
legalTargets = [];
if (!wasTarget) {
// clicking a different own piece re-selects instead of attempting an illegal move
if (lastBoard[sq] !== 0) {
return onSquareClick(sq);
}
renderBoard(document.getElementById("board"), lastBoard, { onClick: onSquareClick, flipped: isFlipped() });
return;
}
let promo = 5;
if (isPromotionMove(lastBoard, from, to)) {
const color = lastBoard[from] === 1 ? 0 : 1;
promo = await askPromotion(color);
}
await api.post("/api/games/" + gameID + "/move", { from, to, promo });
refresh();
}
if (gameID) {
document.getElementById("lobby-view").style.display = "none";
document.getElementById("game-view").style.display = "block";
api.get("/api/me").then((me) => {
if (me && me.username) {
// white_id/black_id come back with the game state itself; just
// compare usernames instead of round-tripping for an id.
myUserID = me.username;
}
refresh();
});
pollTimer = setInterval(refresh, 2000);
} else {
loadOpen();
loadMine();
loadAll();
}
</script>
</body>
</html>