fix(web): browser game UX pass — shake feedback, timer, stock count, HUD
- game.js fully rewritten: correct coordinate system (PAD baked into PILE_ORIGIN), undo driven by undo_stack_len, flashIllegal shake with --card-tx CSS variable, game timer, stock count HUD, URL seed persist, foundation suit hints, auto-complete step loop - game.html: adds hud-timer, hud-stock, win-time elements - game.css: @keyframes illegal-shake, .slot-hint, overflow-x on main - solitaire_wasm: adds undo_stack_len to GameSnapshot Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,7 @@ main {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#board {
|
#board {
|
||||||
@@ -237,3 +238,30 @@ main {
|
|||||||
padding: 12px 32px;
|
padding: 12px 32px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Illegal-move shake ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
@keyframes illegal-shake {
|
||||||
|
0% { transform: var(--card-tx) translateX(0); }
|
||||||
|
20% { transform: var(--card-tx) translateX(-6px); }
|
||||||
|
40% { transform: var(--card-tx) translateX(6px); }
|
||||||
|
60% { transform: var(--card-tx) translateX(-4px); }
|
||||||
|
80% { transform: var(--card-tx) translateX(4px); }
|
||||||
|
100% { transform: var(--card-tx) translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.illegal {
|
||||||
|
animation: illegal-shake 320ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Foundation slot suit hints ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.slot-hint {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 26px;
|
||||||
|
color: rgba(255,255,255,0.18);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
<div class="hud-center">
|
<div class="hud-center">
|
||||||
<span id="hud-score">Score: 0</span>
|
<span id="hud-score">Score: 0</span>
|
||||||
<span id="hud-moves">Moves: 0</span>
|
<span id="hud-moves">Moves: 0</span>
|
||||||
|
<span id="hud-timer">0:00</span>
|
||||||
|
<span id="hud-stock">Stock: 24</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hud-right">
|
<div class="hud-right">
|
||||||
<button id="btn-undo" title="Undo (Z)">↩ Undo</button>
|
<button id="btn-undo" title="Undo (Z)">↩ Undo</button>
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
<div class="win-title">You Won!</div>
|
<div class="win-title">You Won!</div>
|
||||||
<div id="win-score" class="win-score"></div>
|
<div id="win-score" class="win-score"></div>
|
||||||
<div id="win-moves" class="win-detail"></div>
|
<div id="win-moves" class="win-detail"></div>
|
||||||
|
<div id="win-time" class="win-detail"></div>
|
||||||
<button id="btn-win-new">Play Again ↺</button>
|
<button id="btn-win-new">Play Again ↺</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+191
-218
@@ -12,22 +12,20 @@
|
|||||||
|
|
||||||
import init, { SolitaireGame } from "/web/pkg/solitaire_wasm.js";
|
import init, { SolitaireGame } from "/web/pkg/solitaire_wasm.js";
|
||||||
|
|
||||||
// ── Layout constants (must match game.css --card-w / --card-h / --gap) ──────
|
// ── Layout constants (must match game.css --card-w / --card-h / --gap / --pad)
|
||||||
const CARD_W = 80;
|
const CARD_W = 80;
|
||||||
const CARD_H = 112;
|
const CARD_H = 112;
|
||||||
const GAP = 12;
|
const GAP = 12;
|
||||||
const PAD = 20; // board padding
|
const PAD = 20; // board inner padding — cards start at (PAD, PAD)
|
||||||
const FAN = 28; // vertical offset per fanned tableau card
|
const FAN = 28; // vertical offset per fanned tableau card
|
||||||
const WASTE_FAN = 18; // horizontal offset for draw-3 waste fan
|
const WASTE_FAN = 18; // horizontal offset for draw-3 waste fan
|
||||||
|
|
||||||
// Top-row Y origin (relative to board interior = after padding).
|
// Pile origins in board-element coordinates (include PAD so (0,0) = board edge).
|
||||||
const TOP_Y = 0;
|
const TOP_Y = PAD;
|
||||||
const BOTTOM_Y = CARD_H + 28; // tableau row
|
const BOTTOM_Y = PAD + CARD_H + 28;
|
||||||
|
|
||||||
const colX = (c) => c * (CARD_W + GAP);
|
const colX = (c) => PAD + c * (CARD_W + GAP);
|
||||||
|
|
||||||
// Absolute position of each pile's origin (top-left of its slot),
|
|
||||||
// relative to the board's padded interior (0, 0).
|
|
||||||
const PILE_ORIGIN = {
|
const PILE_ORIGIN = {
|
||||||
stock: { x: colX(0), y: TOP_Y },
|
stock: { x: colX(0), y: TOP_Y },
|
||||||
waste: { x: colX(1), y: TOP_Y },
|
waste: { x: colX(1), y: TOP_Y },
|
||||||
@@ -44,35 +42,43 @@ const PILE_ORIGIN = {
|
|||||||
"tableau-6": { x: colX(6), y: BOTTOM_Y },
|
"tableau-6": { x: colX(6), y: BOTTOM_Y },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Foundation suit hints shown when the slot is empty.
|
||||||
|
const FOUND_SUIT_HINT = ["♠", "♥", "♦", "♣"];
|
||||||
|
|
||||||
const SUIT_GLYPH = { clubs: "♣", diamonds: "♦", hearts: "♥", spades: "♠" };
|
const SUIT_GLYPH = { clubs: "♣", diamonds: "♦", hearts: "♥", spades: "♠" };
|
||||||
const RANK_LABELS = ["","A","2","3","4","5","6","7","8","9","10","J","Q","K"];
|
const RANK_LABELS = ["","A","2","3","4","5","6","7","8","9","10","J","Q","K"];
|
||||||
const RED_SUITS = new Set(["diamonds", "hearts"]);
|
const RED_SUITS = new Set(["diamonds", "hearts"]);
|
||||||
|
|
||||||
// ── State ────────────────────────────────────────────────────────────────────
|
// ── State ────────────────────────────────────────────────────────────────────
|
||||||
let game = null;
|
let game = null;
|
||||||
let snap = null; // last rendered GameSnapshot
|
let snap = null; // last rendered GameSnapshot
|
||||||
let drawThree = false;
|
let drawThree = false;
|
||||||
|
|
||||||
// Persistent card → DOM element map (keyed by card.id).
|
// Persistent card-id → DOM element map.
|
||||||
const cardEls = new Map();
|
const cardEls = new Map();
|
||||||
|
|
||||||
// Drag state
|
// Drag state
|
||||||
let drag = null;
|
let drag = null;
|
||||||
// drag = {
|
// drag = {
|
||||||
// fromPile: string,
|
// fromPile: string,
|
||||||
// fromIndex: number, // index of bottom card of the dragged run in its pile
|
// fromIndex: number, // index of bottom dragged card in its pile
|
||||||
// cardIds: number[], // ids of cards being dragged (bottom → top)
|
// cardIds: number[], // ids bottom→top
|
||||||
// startX: number, startY: number, // pointer start (board-relative)
|
// startX: number, startY: number, // board-relative pointer start
|
||||||
// offsetX: number, offsetY: number, // cursor offset within the grabbed card
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Auto-complete timer handle
|
// Timer
|
||||||
|
let timerInterval = null;
|
||||||
|
let elapsedSecs = 0;
|
||||||
|
|
||||||
|
// Auto-complete
|
||||||
let acTimer = null;
|
let acTimer = null;
|
||||||
|
|
||||||
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
||||||
const board = document.getElementById("board");
|
const board = document.getElementById("board");
|
||||||
const hudScore = document.getElementById("hud-score");
|
const hudScore = document.getElementById("hud-score");
|
||||||
const hudMoves = document.getElementById("hud-moves");
|
const hudMoves = document.getElementById("hud-moves");
|
||||||
|
const hudTimer = document.getElementById("hud-timer");
|
||||||
|
const hudStock = document.getElementById("hud-stock");
|
||||||
const hudSeed = document.getElementById("hud-seed");
|
const hudSeed = document.getElementById("hud-seed");
|
||||||
const btnUndo = document.getElementById("btn-undo");
|
const btnUndo = document.getElementById("btn-undo");
|
||||||
const btnNew = document.getElementById("btn-new");
|
const btnNew = document.getElementById("btn-new");
|
||||||
@@ -80,16 +86,16 @@ const chkDraw3 = document.getElementById("chk-draw3");
|
|||||||
const winOverlay = document.getElementById("win-overlay");
|
const winOverlay = document.getElementById("win-overlay");
|
||||||
const winScore = document.getElementById("win-score");
|
const winScore = document.getElementById("win-score");
|
||||||
const winMoves = document.getElementById("win-moves");
|
const winMoves = document.getElementById("win-moves");
|
||||||
|
const winTime = document.getElementById("win-time");
|
||||||
const btnWinNew = document.getElementById("btn-win-new");
|
const btnWinNew = document.getElementById("btn-win-new");
|
||||||
|
|
||||||
// ── Bootstrap ────────────────────────────────────────────────────────────────
|
// ── Bootstrap ────────────────────────────────────────────────────────────────
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
// Seed from URL param ?seed=N, otherwise random.
|
const params = new URLSearchParams(window.location.search);
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
const urlSeed = params.has("seed") ? Number(params.get("seed")) : randomSeed();
|
const urlSeed = params.has("seed") ? Number(params.get("seed")) : randomSeed();
|
||||||
drawThree = params.has("draw3");
|
drawThree = params.has("draw3");
|
||||||
chkDraw3.checked = drawThree;
|
chkDraw3.checked = drawThree;
|
||||||
|
|
||||||
buildSlots();
|
buildSlots();
|
||||||
@@ -98,107 +104,135 @@ async function bootstrap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function randomSeed() {
|
function randomSeed() {
|
||||||
// Math.random gives a float in [0,1); multiply to get a large integer.
|
|
||||||
return Math.floor(Math.random() * 9007199254740991);
|
return Math.floor(Math.random() * 9007199254740991);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startGame(seed) {
|
function startGame(seed) {
|
||||||
if (acTimer) { clearInterval(acTimer); acTimer = null; }
|
if (acTimer) { clearInterval(acTimer); acTimer = null; }
|
||||||
|
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
||||||
|
elapsedSecs = 0;
|
||||||
|
updateTimerDisplay();
|
||||||
|
|
||||||
game = new SolitaireGame(seed, drawThree);
|
game = new SolitaireGame(seed, drawThree);
|
||||||
snap = game.state();
|
snap = game.state();
|
||||||
hudSeed.textContent = `seed ${Math.round(game.seed())}`;
|
|
||||||
|
const displaySeed = Math.round(game.seed());
|
||||||
|
hudSeed.textContent = `seed ${displaySeed}`;
|
||||||
winOverlay.classList.add("hidden");
|
winOverlay.classList.add("hidden");
|
||||||
cardEls.clear();
|
cardEls.clear();
|
||||||
board.querySelectorAll(".card").forEach(el => el.remove());
|
board.querySelectorAll(".card, .recycle-label").forEach(el => el.remove());
|
||||||
|
|
||||||
|
// Persist seed in URL so the game can be shared / refreshed.
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set("seed", displaySeed);
|
||||||
|
if (drawThree) url.searchParams.set("draw3", "");
|
||||||
|
else url.searchParams.delete("draw3");
|
||||||
|
history.replaceState(null, "", url);
|
||||||
|
|
||||||
render(snap);
|
render(snap);
|
||||||
|
startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Slot placeholders ────────────────────────────────────────────────────────
|
// ── Timer ────────────────────────────────────────────────────────────────────
|
||||||
|
function startTimer() {
|
||||||
|
timerInterval = setInterval(() => {
|
||||||
|
elapsedSecs++;
|
||||||
|
updateTimerDisplay();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimerDisplay() {
|
||||||
|
const m = Math.floor(elapsedSecs / 60);
|
||||||
|
const s = elapsedSecs % 60;
|
||||||
|
if (hudTimer) hudTimer.textContent = `${m}:${s.toString().padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Slot placeholders ─────────────────────────────────────────────────────────
|
||||||
function buildSlots() {
|
function buildSlots() {
|
||||||
for (const [pile, origin] of Object.entries(PILE_ORIGIN)) {
|
for (const [pile, origin] of Object.entries(PILE_ORIGIN)) {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "slot";
|
el.className = "slot";
|
||||||
el.dataset.pile = pile;
|
el.dataset.pile = pile;
|
||||||
el.style.transform = `translate(${origin.x}px, ${origin.y}px)`;
|
el.style.transform = `translate(${origin.x}px, ${origin.y}px)`;
|
||||||
|
|
||||||
|
if (pile.startsWith("foundation-")) {
|
||||||
|
const slot = parseInt(pile.split("-")[1]);
|
||||||
|
const hint = document.createElement("div");
|
||||||
|
hint.className = "slot-hint";
|
||||||
|
hint.textContent = FOUND_SUIT_HINT[slot];
|
||||||
|
el.appendChild(hint);
|
||||||
|
}
|
||||||
board.appendChild(el);
|
board.appendChild(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Card position math ────────────────────────────────────────────────────────
|
// ── Card position math ────────────────────────────────────────────────────────
|
||||||
function cardPos(pileName, indexInPile, pileLength, pileCards) {
|
function cardPos(pileName, indexInPile, pileLength) {
|
||||||
const origin = PILE_ORIGIN[pileName];
|
const origin = PILE_ORIGIN[pileName];
|
||||||
let x = origin.x;
|
let x = origin.x;
|
||||||
let y = origin.y;
|
let y = origin.y;
|
||||||
|
|
||||||
if (pileName === "waste" && drawThree && pileLength >= 2) {
|
if (pileName === "waste" && drawThree && pileLength >= 2) {
|
||||||
// Show top-3 of waste fanned horizontally.
|
|
||||||
const fanStart = Math.max(0, pileLength - 3);
|
const fanStart = Math.max(0, pileLength - 3);
|
||||||
const fanPos = indexInPile - fanStart;
|
const fanPos = indexInPile - fanStart;
|
||||||
if (fanPos >= 0) {
|
if (fanPos > 0) x += fanPos * WASTE_FAN;
|
||||||
x += fanPos * WASTE_FAN;
|
|
||||||
} else {
|
|
||||||
// Cards below the fan window are stacked at origin.
|
|
||||||
}
|
|
||||||
} else if (pileName.startsWith("tableau-")) {
|
} else if (pileName.startsWith("tableau-")) {
|
||||||
y += indexInPile * FAN;
|
y += indexInPile * FAN;
|
||||||
}
|
}
|
||||||
// Stock, foundations: stack (no offset).
|
|
||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Z-index: higher index in pile = drawn on top.
|
function cardZ(pileName, indexInPile) {
|
||||||
function cardZ(pileName, indexInPile, total) {
|
|
||||||
if (pileName === "stock") return 10 + indexInPile;
|
|
||||||
if (pileName === "waste") return 10 + indexInPile;
|
|
||||||
if (pileName.startsWith("found")) return 10 + indexInPile;
|
|
||||||
return 10 + indexInPile;
|
return 10 + indexInPile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Renderer ─────────────────────────────────────────────────────────────────
|
// ── Renderer ──────────────────────────────────────────────────────────────────
|
||||||
function render(s) {
|
function render(s) {
|
||||||
snap = s;
|
snap = s;
|
||||||
|
|
||||||
// Update HUD
|
|
||||||
hudScore.textContent = `Score: ${s.score}`;
|
hudScore.textContent = `Score: ${s.score}`;
|
||||||
hudMoves.textContent = `Moves: ${s.move_count}`;
|
hudMoves.textContent = `Moves: ${s.move_count}`;
|
||||||
btnUndo.disabled = s.move_count === 0;
|
hudStock.textContent = `Stock: ${s.stock.length}`;
|
||||||
|
btnUndo.disabled = s.undo_stack_len === 0;
|
||||||
|
|
||||||
// Collect all cards visible in this snapshot, keyed by id → {pile, idx}.
|
|
||||||
const visible = new Map();
|
const visible = new Map();
|
||||||
const addPile = (pileName, cards) => {
|
const addPile = (name, cards) =>
|
||||||
cards.forEach((c, i) => visible.set(c.id, { pile: pileName, idx: i, card: c, total: cards.length }));
|
cards.forEach((c, i) => visible.set(c.id, { pile: name, idx: i, card: c, total: cards.length }));
|
||||||
};
|
|
||||||
addPile("stock", s.stock);
|
addPile("stock", s.stock);
|
||||||
addPile("waste", s.waste);
|
addPile("waste", s.waste);
|
||||||
s.foundations.forEach((f, i) => addPile(`foundation-${i}`, f));
|
s.foundations.forEach((f, i) => addPile(`foundation-${i}`, f));
|
||||||
s.tableaus.forEach((t, i) => addPile(`tableau-${i}`, t));
|
s.tableaus.forEach((t, i) => addPile(`tableau-${i}`, t));
|
||||||
|
|
||||||
// Create or update card elements.
|
|
||||||
for (const [id, info] of visible) {
|
for (const [id, info] of visible) {
|
||||||
let el = cardEls.get(id);
|
let el = cardEls.get(id);
|
||||||
if (!el) {
|
if (!el) {
|
||||||
el = createCardEl(info.card);
|
el = document.createElement("div");
|
||||||
|
el.dataset.cardId = id;
|
||||||
cardEls.set(id, el);
|
cardEls.set(id, el);
|
||||||
board.appendChild(el);
|
board.appendChild(el);
|
||||||
}
|
}
|
||||||
|
updateCardEl(el, info.card, info.pile, info.idx, info.total);
|
||||||
updateCardEl(el, info.card, info.pile, info.idx, info.total, s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove cards no longer in the snapshot (shouldn't happen in solitaire
|
|
||||||
// but guards against stale state after undo).
|
|
||||||
for (const [id, el] of cardEls) {
|
for (const [id, el] of cardEls) {
|
||||||
if (!visible.has(id)) {
|
if (!visible.has(id)) { el.remove(); cardEls.delete(id); }
|
||||||
el.remove();
|
|
||||||
cardEls.delete(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update slot drop-active highlights (cleared on every render).
|
// Foundation suit hints: hide when pile has cards.
|
||||||
board.querySelectorAll(".slot.drop-active").forEach(el => el.classList.remove("drop-active"));
|
s.foundations.forEach((f, i) => {
|
||||||
board.querySelectorAll(".card.drop-target").forEach(el => el.classList.remove("drop-target"));
|
const slotEl = board.querySelector(`.slot[data-pile="foundation-${i}"]`);
|
||||||
|
if (slotEl) {
|
||||||
|
const hint = slotEl.querySelector(".slot-hint");
|
||||||
|
if (hint) hint.style.visibility = f.length > 0 ? "hidden" : "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Show recycle indicator on empty stock.
|
// Recycle indicator on empty stock.
|
||||||
let recycleEl = board.querySelector(".recycle-label");
|
let recycleEl = board.querySelector(".recycle-label");
|
||||||
if (s.stock.length === 0 && s.waste.length > 0) {
|
if (s.stock.length === 0 && s.waste.length > 0) {
|
||||||
if (!recycleEl) {
|
if (!recycleEl) {
|
||||||
@@ -208,52 +242,41 @@ function render(s) {
|
|||||||
board.appendChild(recycleEl);
|
board.appendChild(recycleEl);
|
||||||
}
|
}
|
||||||
const o = PILE_ORIGIN.stock;
|
const o = PILE_ORIGIN.stock;
|
||||||
recycleEl.style.left = `${o.x + CARD_W / 2}px`;
|
recycleEl.style.transform = `translate(${o.x + CARD_W / 2}px, ${o.y + CARD_H / 2}px)`;
|
||||||
recycleEl.style.top = `${o.y + CARD_H / 2}px`;
|
|
||||||
} else if (recycleEl) {
|
} else if (recycleEl) {
|
||||||
recycleEl.remove();
|
recycleEl.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger auto-complete if applicable.
|
// Clear drag highlights left from pointer-move.
|
||||||
|
board.querySelectorAll(".slot.drop-active").forEach(e => e.classList.remove("drop-active"));
|
||||||
|
board.querySelectorAll(".card.drop-target").forEach(e => e.classList.remove("drop-target"));
|
||||||
|
|
||||||
if (s.is_auto_completable && !s.is_won && !acTimer) {
|
if (s.is_auto_completable && !s.is_won && !acTimer) {
|
||||||
acTimer = setInterval(doAutoCompleteStep, 400);
|
acTimer = setInterval(doAutoCompleteStep, 380);
|
||||||
}
|
}
|
||||||
if (s.is_won) {
|
if (s.is_won) {
|
||||||
|
stopTimer();
|
||||||
if (acTimer) { clearInterval(acTimer); acTimer = null; }
|
if (acTimer) { clearInterval(acTimer); acTimer = null; }
|
||||||
showWin(s);
|
showWin(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCardEl(card) {
|
function updateCardEl(el, card, pileName, idx, total) {
|
||||||
const el = document.createElement("div");
|
const pos = cardPos(pileName, idx, total);
|
||||||
el.dataset.cardId = card.id;
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCardEl(el, card, pileName, idx, total, s) {
|
|
||||||
const pos = cardPos(pileName, idx, total, null);
|
|
||||||
const z = cardZ(pileName, idx, total);
|
|
||||||
|
|
||||||
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||||
el.style.zIndex = z;
|
el.style.zIndex = cardZ(pileName, idx);
|
||||||
|
|
||||||
const isTop = idx === total - 1;
|
|
||||||
|
|
||||||
if (!card.face_up) {
|
if (!card.face_up) {
|
||||||
el.className = "card face-down";
|
el.className = "card face-down";
|
||||||
if (pileName === "stock") el.classList.add("stock-card");
|
el.innerHTML = "";
|
||||||
el.innerHTML = "";
|
|
||||||
} else {
|
} else {
|
||||||
const isRed = RED_SUITS.has(card.suit);
|
const isRed = RED_SUITS.has(card.suit);
|
||||||
el.className = `card ${isRed ? "red" : "black"}`;
|
el.className = `card ${isRed ? "red" : "black"}`;
|
||||||
if (pileName === "stock") el.classList.add("stock-card");
|
const r = RANK_LABELS[card.rank];
|
||||||
|
const s = SUIT_GLYPH[card.suit];
|
||||||
const rankLabel = RANK_LABELS[card.rank];
|
el.innerHTML = `<div class="corner top">${r}<br>${s}</div>
|
||||||
const suit = SUIT_GLYPH[card.suit];
|
<div class="center">${s}</div>
|
||||||
el.innerHTML = `
|
<div class="corner bottom">${r}<br>${s}</div>`;
|
||||||
<div class="corner top">${rankLabel}<br>${suit}</div>
|
|
||||||
<div class="center">${suit}</div>
|
|
||||||
<div class="corner bottom">${rankLabel}<br>${suit}</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,28 +284,39 @@ function updateCardEl(el, card, pileName, idx, total, s) {
|
|||||||
function showWin(s) {
|
function showWin(s) {
|
||||||
winScore.textContent = `Score: ${s.score}`;
|
winScore.textContent = `Score: ${s.score}`;
|
||||||
winMoves.textContent = `${s.move_count} moves`;
|
winMoves.textContent = `${s.move_count} moves`;
|
||||||
|
const m = Math.floor(elapsedSecs / 60);
|
||||||
|
const sec = elapsedSecs % 60;
|
||||||
|
if (winTime) winTime.textContent = `${m}:${sec.toString().padStart(2, "0")}`;
|
||||||
winOverlay.classList.remove("hidden");
|
winOverlay.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auto-complete ─────────────────────────────────────────────────────────────
|
// ── Auto-complete ─────────────────────────────────────────────────────────────
|
||||||
function doAutoCompleteStep() {
|
function doAutoCompleteStep() {
|
||||||
if (!game || !snap || !snap.is_auto_completable) {
|
if (!game || !snap?.is_auto_completable) {
|
||||||
clearInterval(acTimer);
|
clearInterval(acTimer); acTimer = null; return;
|
||||||
acTimer = null;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const result = game.auto_complete_step();
|
const result = game.auto_complete_step();
|
||||||
if (result && result.ok) {
|
if (result?.ok) render(result.snapshot);
|
||||||
render(result.snapshot);
|
else { clearInterval(acTimer); acTimer = null; }
|
||||||
} else {
|
}
|
||||||
clearInterval(acTimer);
|
|
||||||
acTimer = null;
|
// ── Illegal move flash ────────────────────────────────────────────────────────
|
||||||
|
function flashIllegal(cardIds) {
|
||||||
|
for (const id of cardIds) {
|
||||||
|
const el = cardEls.get(id);
|
||||||
|
if (!el) continue;
|
||||||
|
// Store current translate so the shake keyframe can reference it.
|
||||||
|
el.style.setProperty("--card-tx", el.style.transform || "translate(0,0)");
|
||||||
|
el.classList.add("illegal");
|
||||||
|
el.addEventListener("animationend", () => {
|
||||||
|
el.classList.remove("illegal");
|
||||||
|
el.style.removeProperty("--card-tx");
|
||||||
|
}, { once: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Input handling ────────────────────────────────────────────────────────────
|
// ── Input ─────────────────────────────────────────────────────────────────────
|
||||||
function attachHandlers() {
|
function attachHandlers() {
|
||||||
// Buttons
|
|
||||||
btnUndo.addEventListener("click", () => {
|
btnUndo.addEventListener("click", () => {
|
||||||
const r = game.undo();
|
const r = game.undo();
|
||||||
if (r.ok) render(r.snapshot);
|
if (r.ok) render(r.snapshot);
|
||||||
@@ -294,39 +328,29 @@ function attachHandlers() {
|
|||||||
startGame(randomSeed());
|
startGame(randomSeed());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "z" || e.key === "Z") {
|
if (e.target.tagName === "INPUT") return;
|
||||||
const r = game.undo();
|
if (e.key === "z" || e.key === "Z") { const r = game.undo(); if (r.ok) render(r.snapshot); }
|
||||||
if (r.ok) render(r.snapshot);
|
if (e.key === "n" || e.key === "N") startGame(randomSeed());
|
||||||
}
|
|
||||||
if (e.key === "n" || e.key === "N") {
|
|
||||||
startGame(randomSeed());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Board pointer events (handles both mouse and touch via PointerEvents API)
|
board.addEventListener("pointerdown", onPointerDown);
|
||||||
board.addEventListener("pointerdown", onPointerDown);
|
board.addEventListener("pointermove", onPointerMove);
|
||||||
board.addEventListener("pointermove", onPointerMove);
|
board.addEventListener("pointerup", onPointerUp);
|
||||||
board.addEventListener("pointerup", onPointerUp);
|
|
||||||
board.addEventListener("pointercancel", onPointerCancel);
|
board.addEventListener("pointercancel", onPointerCancel);
|
||||||
board.addEventListener("click", onBoardClick);
|
board.addEventListener("click", onBoardClick);
|
||||||
board.addEventListener("dblclick", onBoardDblClick);
|
board.addEventListener("dblclick", onBoardDblClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Coordinate helpers ────────────────────────────────────────────────────────
|
// ── Coordinate helpers ────────────────────────────────────────────────────────
|
||||||
|
// Returns cursor position in board-element coordinates
|
||||||
|
// (0,0 = board element top-left corner, which is the padding edge).
|
||||||
function boardRelative(clientX, clientY) {
|
function boardRelative(clientX, clientY) {
|
||||||
const rect = board.getBoundingClientRect();
|
const rect = board.getBoundingClientRect();
|
||||||
// Subtract board padding to get interior coordinates.
|
return { x: clientX - rect.left, y: clientY - rect.top };
|
||||||
return {
|
|
||||||
x: clientX - rect.left - PAD,
|
|
||||||
y: clientY - rect.top - PAD,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hitTestCard(bx, by) {
|
function hitTestCard(bx, by) {
|
||||||
// Walk all visible piles, find the topmost card at (bx, by).
|
|
||||||
// Returns { pileName, cardIndex, cardId } or null.
|
|
||||||
const pileOrder = [
|
const pileOrder = [
|
||||||
"waste",
|
"waste",
|
||||||
"foundation-0","foundation-1","foundation-2","foundation-3",
|
"foundation-0","foundation-1","foundation-2","foundation-3",
|
||||||
@@ -334,21 +358,19 @@ function hitTestCard(bx, by) {
|
|||||||
"stock",
|
"stock",
|
||||||
];
|
];
|
||||||
|
|
||||||
let best = null;
|
let best = null, bestZ = -1;
|
||||||
let bestZ = -1;
|
|
||||||
|
|
||||||
for (const pileName of pileOrder) {
|
for (const pileName of pileOrder) {
|
||||||
const cards = getPileCards(pileName);
|
const cards = getPileCards(pileName);
|
||||||
if (!cards) continue;
|
if (!cards) continue;
|
||||||
|
|
||||||
for (let i = 0; i < cards.length; i++) {
|
for (let i = 0; i < cards.length; i++) {
|
||||||
const pos = cardPos(pileName, i, cards.length, cards);
|
const pos = cardPos(pileName, i, cards.length);
|
||||||
if (bx >= pos.x && bx <= pos.x + CARD_W &&
|
if (bx >= pos.x && bx <= pos.x + CARD_W &&
|
||||||
by >= pos.y && by <= pos.y + CARD_H) {
|
by >= pos.y && by <= pos.y + CARD_H) {
|
||||||
const z = cardZ(pileName, i, cards.length);
|
const z = cardZ(pileName, i);
|
||||||
if (z > bestZ) {
|
if (z > bestZ) {
|
||||||
bestZ = z;
|
bestZ = z;
|
||||||
best = { pileName, cardIndex: i, cardId: cards[i].id, card: cards[i] };
|
best = { pileName, cardIndex: i, card: cards[i] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,83 +382,53 @@ function getPileCards(pileName) {
|
|||||||
if (!snap) return null;
|
if (!snap) return null;
|
||||||
if (pileName === "stock") return snap.stock;
|
if (pileName === "stock") return snap.stock;
|
||||||
if (pileName === "waste") return snap.waste;
|
if (pileName === "waste") return snap.waste;
|
||||||
if (pileName.startsWith("foundation-")) {
|
if (pileName.startsWith("foundation-")) return snap.foundations[parseInt(pileName.split("-")[1])];
|
||||||
const slot = parseInt(pileName.split("-")[1]);
|
if (pileName.startsWith("tableau-")) return snap.tableaus [parseInt(pileName.split("-")[1])];
|
||||||
return snap.foundations[slot];
|
|
||||||
}
|
|
||||||
if (pileName.startsWith("tableau-")) {
|
|
||||||
const col = parseInt(pileName.split("-")[1]);
|
|
||||||
return snap.tableaus[col];
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hitTestSlot(bx, by) {
|
// Drop-target: tableau has tall hit areas; foundations use their slot box.
|
||||||
// Returns the pile name whose slot rect contains (bx, by), favouring
|
|
||||||
// tableau column slots over top-row slots when both overlap.
|
|
||||||
for (const [pile, origin] of Object.entries(PILE_ORIGIN)) {
|
|
||||||
if (bx >= origin.x && bx <= origin.x + CARD_W &&
|
|
||||||
by >= origin.y && by <= origin.y + CARD_H) {
|
|
||||||
return pile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For a tableau pile, hit-test for drop: the drop zone extends to the last
|
|
||||||
// card's bottom edge (or the slot if empty).
|
|
||||||
function findDropTarget(bx, by) {
|
function findDropTarget(bx, by) {
|
||||||
// Check tableau columns first (they have tall hit areas).
|
|
||||||
for (let c = 0; c < 7; c++) {
|
for (let c = 0; c < 7; c++) {
|
||||||
const pile = `tableau-${c}`;
|
const pile = `tableau-${c}`;
|
||||||
const cards = snap.tableaus[c];
|
const cards = snap.tableaus[c];
|
||||||
const origin = PILE_ORIGIN[pile];
|
const origin = PILE_ORIGIN[pile];
|
||||||
// Top boundary: origin.y. Bottom boundary: last card bottom or empty slot.
|
|
||||||
const bottomY = cards.length > 0
|
const bottomY = cards.length > 0
|
||||||
? origin.y + (cards.length - 1) * FAN + CARD_H
|
? origin.y + (cards.length - 1) * FAN + CARD_H
|
||||||
: origin.y + CARD_H;
|
: origin.y + CARD_H;
|
||||||
if (bx >= origin.x && bx <= origin.x + CARD_W &&
|
if (bx >= origin.x && bx <= origin.x + CARD_W && by >= origin.y && by <= bottomY)
|
||||||
by >= origin.y && by <= bottomY) {
|
|
||||||
return pile;
|
return pile;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Foundation slots (top row).
|
|
||||||
for (let s = 0; s < 4; s++) {
|
for (let s = 0; s < 4; s++) {
|
||||||
const pile = `foundation-${s}`;
|
const pile = `foundation-${s}`;
|
||||||
const origin = PILE_ORIGIN[pile];
|
const origin = PILE_ORIGIN[pile];
|
||||||
if (bx >= origin.x && bx <= origin.x + CARD_W &&
|
if (bx >= origin.x && bx <= origin.x + CARD_W &&
|
||||||
by >= origin.y && by <= origin.y + CARD_H) {
|
by >= origin.y && by <= origin.y + CARD_H)
|
||||||
return pile;
|
return pile;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Pointer event handlers ────────────────────────────────────────────────────
|
// ── Pointer handlers ──────────────────────────────────────────────────────────
|
||||||
function onPointerDown(e) {
|
function onPointerDown(e) {
|
||||||
if (e.button !== 0 && e.pointerType === "mouse") return;
|
if (e.button !== 0 && e.pointerType === "mouse") return;
|
||||||
if (drag) return; // ignore second finger
|
if (drag) return;
|
||||||
|
|
||||||
const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
|
const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
|
||||||
const hit = hitTestCard(bx, by);
|
const hit = hitTestCard(bx, by);
|
||||||
if (!hit) return;
|
if (!hit || !hit.card.face_up) return;
|
||||||
if (!hit.card.face_up) return; // can't drag face-down cards
|
|
||||||
|
|
||||||
const cards = getPileCards(hit.pileName);
|
const cards = getPileCards(hit.pileName);
|
||||||
if (!cards) return;
|
if (!cards) return;
|
||||||
|
|
||||||
// For tableau, allow dragging a run from any face-up card.
|
|
||||||
// For waste/foundation, only the top card.
|
|
||||||
let fromIndex = hit.cardIndex;
|
let fromIndex = hit.cardIndex;
|
||||||
if (!hit.pileName.startsWith("tableau-")) {
|
if (!hit.pileName.startsWith("tableau-")) {
|
||||||
fromIndex = cards.length - 1; // only top card
|
fromIndex = cards.length - 1;
|
||||||
if (hit.cardIndex !== fromIndex) return;
|
if (hit.cardIndex !== fromIndex) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const draggedCards = cards.slice(fromIndex);
|
const draggedCards = cards.slice(fromIndex);
|
||||||
if (draggedCards.some(c => !c.face_up)) return; // face-down in run — blocked
|
if (draggedCards.some(c => !c.face_up)) return;
|
||||||
|
|
||||||
const cardOriginPos = cardPos(hit.pileName, fromIndex, cards.length, cards);
|
|
||||||
|
|
||||||
drag = {
|
drag = {
|
||||||
fromPile: hit.pileName,
|
fromPile: hit.pileName,
|
||||||
@@ -444,17 +436,11 @@ function onPointerDown(e) {
|
|||||||
cardIds: draggedCards.map(c => c.id),
|
cardIds: draggedCards.map(c => c.id),
|
||||||
startX: bx,
|
startX: bx,
|
||||||
startY: by,
|
startY: by,
|
||||||
offsetX: bx - cardOriginPos.x,
|
|
||||||
offsetY: by - cardOriginPos.y,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lift the dragged cards visually.
|
|
||||||
drag.cardIds.forEach((id, i) => {
|
drag.cardIds.forEach((id, i) => {
|
||||||
const el = cardEls.get(id);
|
const el = cardEls.get(id);
|
||||||
if (el) {
|
if (el) { el.classList.add("selected"); el.style.zIndex = 500 + i; }
|
||||||
el.classList.add("selected");
|
|
||||||
el.style.zIndex = 500 + i;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
board.setPointerCapture(e.pointerId);
|
board.setPointerCapture(e.pointerId);
|
||||||
@@ -471,21 +457,21 @@ function onPointerMove(e) {
|
|||||||
drag.cardIds.forEach((id, i) => {
|
drag.cardIds.forEach((id, i) => {
|
||||||
const el = cardEls.get(id);
|
const el = cardEls.get(id);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const basePos = cardPos(drag.fromPile, drag.fromIndex + i, (cards ? cards.length : 1), null);
|
const base = cardPos(drag.fromPile, drag.fromIndex + i, cards ? cards.length : 1);
|
||||||
el.style.transform = `translate(${basePos.x + dx}px, ${basePos.y + dy}px)`;
|
el.style.transform = `translate(${base.x + dx}px, ${base.y + dy}px)`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Highlight drop target.
|
// Highlight drop target.
|
||||||
board.querySelectorAll(".slot.drop-active").forEach(el => el.classList.remove("drop-active"));
|
board.querySelectorAll(".slot.drop-active").forEach(e => e.classList.remove("drop-active"));
|
||||||
board.querySelectorAll(".card.drop-target").forEach(el => el.classList.remove("drop-target"));
|
board.querySelectorAll(".card.drop-target").forEach(e => e.classList.remove("drop-target"));
|
||||||
const targetPile = findDropTarget(bx, by);
|
const targetPile = findDropTarget(bx, by);
|
||||||
if (targetPile) {
|
if (targetPile) {
|
||||||
const slotEl = board.querySelector(`.slot[data-pile="${targetPile}"]`);
|
const slotEl = board.querySelector(`.slot[data-pile="${targetPile}"]`);
|
||||||
if (slotEl) slotEl.classList.add("drop-active");
|
if (slotEl) slotEl.classList.add("drop-active");
|
||||||
const targetCards = getPileCards(targetPile);
|
const targetCards = getPileCards(targetPile);
|
||||||
if (targetCards && targetCards.length > 0) {
|
if (targetCards?.length > 0) {
|
||||||
const topCard = cardEls.get(targetCards[targetCards.length - 1].id);
|
const topEl = cardEls.get(targetCards[targetCards.length - 1].id);
|
||||||
if (topCard) topCard.classList.add("drop-target");
|
if (topEl) topEl.classList.add("drop-target");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,58 +480,47 @@ function onPointerMove(e) {
|
|||||||
|
|
||||||
function onPointerUp(e) {
|
function onPointerUp(e) {
|
||||||
if (!drag) return;
|
if (!drag) return;
|
||||||
board.querySelectorAll(".slot.drop-active").forEach(el => el.classList.remove("drop-active"));
|
board.querySelectorAll(".slot.drop-active").forEach(e => e.classList.remove("drop-active"));
|
||||||
board.querySelectorAll(".card.drop-target").forEach(el => el.classList.remove("drop-target"));
|
board.querySelectorAll(".card.drop-target").forEach(e => e.classList.remove("drop-target"));
|
||||||
|
|
||||||
const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
|
const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
|
||||||
const targetPile = findDropTarget(bx, by);
|
const targetPile = findDropTarget(bx, by);
|
||||||
|
|
||||||
let moved = false;
|
let moved = false;
|
||||||
if (targetPile && targetPile !== drag.fromPile) {
|
if (targetPile && targetPile !== drag.fromPile) {
|
||||||
const count = drag.cardIds.length;
|
const r = game.move_cards(drag.fromPile, targetPile, drag.cardIds.length);
|
||||||
const r = game.move_cards(drag.fromPile, targetPile, count);
|
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
render(r.snapshot);
|
|
||||||
moved = true;
|
moved = true;
|
||||||
|
drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
|
||||||
|
render(r.snapshot);
|
||||||
|
} else {
|
||||||
|
flashIllegal(drag.cardIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!moved) {
|
if (!moved) {
|
||||||
// Snap cards back to their original positions.
|
drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
|
||||||
drag.cardIds.forEach(id => {
|
render(snap); // snap cards back to their pre-drag positions
|
||||||
const el = cardEls.get(id);
|
|
||||||
if (el) el.classList.remove("selected");
|
|
||||||
});
|
|
||||||
render(snap); // re-render restores transforms
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drag = null;
|
drag = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPointerCancel() {
|
function onPointerCancel() {
|
||||||
if (drag) {
|
if (!drag) return;
|
||||||
drag.cardIds.forEach(id => {
|
drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
|
||||||
const el = cardEls.get(id);
|
render(snap);
|
||||||
if (el) el.classList.remove("selected");
|
drag = null;
|
||||||
});
|
|
||||||
render(snap);
|
|
||||||
drag = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Click handlers ────────────────────────────────────────────────────────────
|
// ── Click / dblclick ──────────────────────────────────────────────────────────
|
||||||
function onBoardClick(e) {
|
function onBoardClick(e) {
|
||||||
if (drag) return; // swallowed by pointer-up
|
if (drag) return;
|
||||||
|
|
||||||
const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
|
const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
|
||||||
|
const stock = PILE_ORIGIN.stock;
|
||||||
// Stock click → draw.
|
if (bx >= stock.x && bx <= stock.x + CARD_W && by >= stock.y && by <= stock.y + CARD_H) {
|
||||||
const stockOrigin = PILE_ORIGIN.stock;
|
|
||||||
if (bx >= stockOrigin.x && bx <= stockOrigin.x + CARD_W &&
|
|
||||||
by >= stockOrigin.y && by <= stockOrigin.y + CARD_H) {
|
|
||||||
const r = game.draw();
|
const r = game.draw();
|
||||||
if (r.ok) render(r.snapshot);
|
if (r.ok) render(r.snapshot);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,11 +529,9 @@ function onBoardDblClick(e) {
|
|||||||
const hit = hitTestCard(bx, by);
|
const hit = hitTestCard(bx, by);
|
||||||
if (!hit || !hit.card.face_up) return;
|
if (!hit || !hit.card.face_up) return;
|
||||||
|
|
||||||
// Only try to move the top card of its pile.
|
|
||||||
const cards = getPileCards(hit.pileName);
|
const cards = getPileCards(hit.pileName);
|
||||||
if (!cards || hit.cardIndex !== cards.length - 1) return;
|
if (!cards || hit.cardIndex !== cards.length - 1) return;
|
||||||
|
|
||||||
// Try each foundation slot.
|
|
||||||
for (let s = 0; s < 4; s++) {
|
for (let s = 0; s < 4; s++) {
|
||||||
const r = game.move_cards(hit.pileName, `foundation-${s}`, 1);
|
const r = game.move_cards(hit.pileName, `foundation-${s}`, 1);
|
||||||
if (r.ok) { render(r.snapshot); return; }
|
if (r.ok) { render(r.snapshot); return; }
|
||||||
|
|||||||
Binary file not shown.
@@ -234,6 +234,8 @@ pub struct GameSnapshot {
|
|||||||
pub is_won: bool,
|
pub is_won: bool,
|
||||||
pub is_auto_completable: bool,
|
pub is_auto_completable: bool,
|
||||||
pub undo_count: u32,
|
pub undo_count: u32,
|
||||||
|
/// Number of snapshots currently on the undo stack; 0 means undo is unavailable.
|
||||||
|
pub undo_stack_len: usize,
|
||||||
pub stock: Vec<CardSnapshot>,
|
pub stock: Vec<CardSnapshot>,
|
||||||
pub waste: Vec<CardSnapshot>,
|
pub waste: Vec<CardSnapshot>,
|
||||||
pub foundations: [Vec<CardSnapshot>; 4],
|
pub foundations: [Vec<CardSnapshot>; 4],
|
||||||
@@ -275,6 +277,7 @@ impl SolitaireGame {
|
|||||||
is_won: self.game.is_won,
|
is_won: self.game.is_won,
|
||||||
is_auto_completable: self.game.is_auto_completable,
|
is_auto_completable: self.game.is_auto_completable,
|
||||||
undo_count: self.game.undo_count,
|
undo_count: self.game.undo_count,
|
||||||
|
undo_stack_len: self.game.undo_stack_len(),
|
||||||
stock: cards(PileType::Stock),
|
stock: cards(PileType::Stock),
|
||||||
waste: cards(PileType::Waste),
|
waste: cards(PileType::Waste),
|
||||||
foundations: [
|
foundations: [
|
||||||
|
|||||||
Reference in New Issue
Block a user