fix(engine,wasm,web): detect no-legal-moves correctly and surface banner
Build and Deploy / build-and-push (push) Successful in 4m24s

Engine: replace broken has_legal_moves loop (which checked buried
mid-column cards without sequence validation) with a delegation to
possible_instructions(), mirroring the hint system's logic exactly.

WASM: add has_moves: bool to GameSnapshot, computed in snap() using the
same stock/waste/possible_instructions check so the web client gets the
flag in every state update at no extra round-trip cost.

Web: show a non-blocking no-moves banner (slide-up toast) with Undo and
New Game actions when has_moves is false and the game is not won. Banner
hides automatically once a move restores legal play (e.g. after undo).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-19 16:53:52 -07:00
parent a2dd8d220c
commit da601bebd6
5 changed files with 99 additions and 41 deletions
+61
View File
@@ -355,6 +355,67 @@ main {
animation: illegal-shake 320ms ease;
}
/* ── No-moves banner ─────────────────────────────────────────────────── */
#no-moves-banner {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 900;
animation: slide-up 240ms ease;
}
#no-moves-banner.hidden { display: none; }
.no-moves-card {
background: var(--panel);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 12px;
padding: 20px 32px;
text-align: center;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.7);
min-width: 300px;
}
.no-moves-title {
font-size: 18px;
font-weight: 700;
color: var(--accent);
}
.no-moves-detail {
font-size: 13px;
color: var(--text-muted);
margin: 0;
line-height: 1.5;
}
.no-moves-actions {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 4px;
}
.no-moves-actions button.secondary {
background: transparent;
border: 1px solid rgba(255,255,255,0.2);
color: var(--text-muted);
}
.no-moves-actions button.secondary:hover {
background: rgba(255,255,255,0.05);
}
@keyframes slide-up {
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* ── Foundation slot suit hints ──────────────────────────────────────────── */
.slot-hint {
+11
View File
@@ -77,6 +77,17 @@
</div>
</div>
<div id="no-moves-banner" class="hidden">
<div class="no-moves-card">
<div class="no-moves-title">No Moves Available</div>
<p class="no-moves-detail">No legal moves remain. Undo to go back or start a new game.</p>
<div class="no-moves-actions">
<button id="btn-no-moves-undo">↩ Undo</button>
<button id="btn-no-moves-new" class="secondary">↺ New Game</button>
</div>
</div>
</div>
<script type="module" src="/web/game.js"></script>
</body>
</html>
+11 -5
View File
@@ -136,11 +136,12 @@ const btnBoardUndo = document.getElementById("btn-board-undo");
const btnNew = document.getElementById("btn-new");
const chkDraw3 = document.getElementById("chk-draw3");
const btnTheme = document.getElementById("btn-theme");
const winOverlay = document.getElementById("win-overlay");
const winScore = document.getElementById("win-score");
const winMoves = document.getElementById("win-moves");
const winTime = document.getElementById("win-time");
const btnWinNew = document.getElementById("btn-win-new");
const winOverlay = document.getElementById("win-overlay");
const winScore = document.getElementById("win-score");
const winMoves = document.getElementById("win-moves");
const winTime = document.getElementById("win-time");
const btnWinNew = document.getElementById("btn-win-new");
const noMovesBanner = document.getElementById("no-moves-banner");
// ── Scale to fit ─────────────────────────────────────────────────────────────
// Scales #card-area to fill #board without overflowing either dimension.
@@ -391,9 +392,12 @@ function render(s) {
clearSave();
stopTimer();
if (acTimer) { clearInterval(acTimer); acTimer = null; }
if (noMovesBanner) noMovesBanner.classList.add("hidden");
showWin(s);
} else {
saveState();
const noMoves = !s.has_moves && !s.is_auto_completable;
if (noMovesBanner) noMovesBanner.classList.toggle("hidden", !noMoves);
}
}
@@ -479,6 +483,8 @@ function attachHandlers() {
btnUndo.addEventListener("click", doUndo);
btnBoardUndo.addEventListener("click", doUndo);
btnNew.addEventListener("click", () => startGame(randomSeed()));
document.getElementById("btn-no-moves-undo")?.addEventListener("click", doUndo);
document.getElementById("btn-no-moves-new")?.addEventListener("click", () => startGame(randomSeed()));
btnWinNew.addEventListener("click", () => startGame(randomSeed()));
chkDraw3.addEventListener("change", () => {
drawThree = chkDraw3.checked;