Moonchess: vanishing tic-tac-toe, online and practice modes
Online play over WebSockets with any-string game codes, seats and spectators, and a 1-second heartbeat. Practice mode against Columbina, backed by a full solver, with an accuracy slider, spoilers, undo and piece freshness. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
c52f30dc59
18 files changed
+1743
No files matched your search
@@ -0,0 +1,58 @@
|
||||
"use strict";
|
||||
|
||||
// Board rendering shared by the online game (app.js) and practice mode (practice.js).
|
||||
|
||||
const MARKS = ["X", "O"];
|
||||
|
||||
const PIECE_SVG = [
|
||||
'<svg viewBox="0 0 100 100" class="x-mark"><path d="M15 15 L85 85 M85 15 L15 85"/></svg>',
|
||||
'<svg viewBox="0 0 100 100" class="o-mark"><circle cx="50" cy="50" r="36"/></svg>',
|
||||
];
|
||||
|
||||
// Fills `container` with nine cells. `onPlay(cell)` fires when a playable cell is clicked.
|
||||
function createBoard(container, onPlay) {
|
||||
const cells = [];
|
||||
let shown = null; // board as currently drawn; null means draw the next state without animation
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const cell = document.createElement("button");
|
||||
cell.className = "cell";
|
||||
cell.addEventListener("click", () => {
|
||||
if (cell.classList.contains("playable")) onPlay(i);
|
||||
});
|
||||
container.appendChild(cell);
|
||||
cells.push(cell);
|
||||
}
|
||||
|
||||
return {
|
||||
// `s` is a game state from the server; `canPlay` makes empty cells clickable.
|
||||
render(s, canPlay) {
|
||||
// Age of each piece within its owner's pieces: 0 = newest. Styled only when the
|
||||
// board has the "freshness" class.
|
||||
const ages = Array(9).fill(null);
|
||||
for (const hist of s.history || []) {
|
||||
hist.forEach((cell, i) => { ages[cell] = hist.length - 1 - i; });
|
||||
}
|
||||
s.board.forEach((v, i) => {
|
||||
const cell = cells[i];
|
||||
if (!shown || v !== shown[i]) {
|
||||
cell.innerHTML = v === null ? "" : PIECE_SVG[v];
|
||||
cell.classList.toggle("placed", v !== null && shown !== null);
|
||||
}
|
||||
cell.classList.toggle("playable", canPlay && v === null);
|
||||
cell.classList.toggle("doomed", s.doomed === i);
|
||||
if (ages[i] === null) delete cell.dataset.age;
|
||||
else cell.dataset.age = ages[i];
|
||||
cell.classList.toggle("win", !!(s.winLine && s.winLine.includes(i)));
|
||||
cell.setAttribute("aria-label",
|
||||
`Square ${i + 1}: ${v === null ? "empty" : MARKS[v]}${s.doomed === i ? " (vanishes next)" : ""}`);
|
||||
});
|
||||
shown = s.board.slice();
|
||||
},
|
||||
|
||||
// Next render draws without the placement animation (e.g. after joining a game).
|
||||
reset() {
|
||||
shown = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user