games: the clock beats the walk button, and the rack isn't betting

The trivia ladder handled a walk before it looked at the clock, so the
timeout only ever bit if the browser volunteered it. Sit on a question,
look it up, answer if you find it and walk if you don't, and you never
lose a ladder. The clock is now the first thing that happens to a move.

The house's chip rack was wired up as bet buttons on blackjack and
hangman: it's four spans with data-chip on them and nothing said the
handler only wanted the real ones. Clicking the house's money raised
your bet.

Hangman had two definitions of "a letter you'd guess" — unicode in the
engine, ASCII in the renderer — and a phrase with an accent in it would
have had no tile to fill and no key to fill it with. One definition now.

Plus: trivia's countdown no longer freezes at zero when the server turns
down a timeout report it was early for, questions whose wrong answer
decodes into the right one are dropped at the door, and hangman bets on
PeteFX's spot like every other table instead of its own copy of it.
This commit is contained in:
prosolis
2026-07-14 06:28:38 -07:00
parent 2d653bf439
commit 3e9b93af55
8 changed files with 120 additions and 87 deletions

View File

@@ -339,7 +339,9 @@
if (dealBtn) dealBtn.disabled = bet <= 0 || !money || money.chips < bet;
}
root.querySelectorAll("[data-chip]").forEach(function (btn) {
// Scoped to buttons: the bare [data-chip] spans in the corner are the house's
// rack, and the house is not betting.
root.querySelectorAll("button[data-chip]").forEach(function (btn) {
btn.addEventListener("click", function () {
if (busy) return;
var d = parseInt(btn.dataset.chip, 10);

View File

@@ -42,12 +42,17 @@
// The three places a chip can be, exactly as at the other table.
var purseEl = document.querySelector("[data-chips]");
var spotEl = root.querySelector("[data-spot]");
var stackEl = root.querySelector("[data-stack]");
var spotTotalEl = root.querySelector("[data-spot-total]");
var houseEl = root.querySelector("[data-house]");
// The bet spot, and the rule that comes with it: the number under the pile is a
// readout of the pile, never the other way round.
var spot = FX.spot({
spot: spotEl,
stack: root.querySelector("[data-stack]"),
total: root.querySelector("[data-spot-total]"),
});
var bet = 0; // what you're building between games
var staked = 0; // what is actually on the spot
var busy = false;
var game = null; // the board as the server last described it
var tier = "medium";
@@ -244,41 +249,12 @@
}
// ---- the money on the felt -------------------------------------------------
// Lifted wholesale from the blackjack table, and deliberately identical: a chip
// has to behave the same way in both rooms or it isn't a chip, it's a widget.
function renderStack(amount) {
staked = amount || 0;
stackEl.innerHTML = "";
spotEl.dataset.live = staked > 0 ? "1" : "0";
if (!staked) { spotTotalEl.classList.add("hidden"); return; }
FX.chipsFor(staked).forEach(function (d, i) {
var c = FX.disc(d);
c.style.setProperty("--i", i);
c.style.setProperty("--spin", FX.jitter(i, 12).toFixed(1) + "deg");
c.style.animationDelay = pace(i * 40) + "ms";
stackEl.appendChild(c);
});
spotTotalEl.textContent = staked.toLocaleString();
spotTotalEl.classList.remove("hidden");
}
function pour(from, to, amount, opts) {
if (amount <= 0) return Promise.resolve();
var base = staked;
var chips = FX.chipsFor(amount, 8);
var run = 0;
return FX.flyMany(from, to, chips, Object.assign({
onLand: function (d, i) {
run += d;
renderStack(base + (i === chips.length - 1 ? amount : run));
},
}, opts || {}));
}
// The spot is PeteFX's, the same one every other table in the room bets onto: a
// chip has to behave the same way in both rooms or it isn't a chip, it's a
// widget.
function stake(amount, from) {
return pour(from || purseEl, spotEl, amount);
return spot.pour(from || purseEl, amount);
}
function settleChips(final) {
@@ -286,19 +262,14 @@
var back = payout - final.bet;
if (payout <= 0) {
var lost = FX.chipsFor(final.bet, 8);
var chain = FX.flyMany(spotEl, houseEl, lost, { gap: 45, lift: 0.6, fade: true });
renderStack(0);
return chain;
// The house takes it. The stack goes to the rack and doesn't come back.
return spot.sweep(houseEl, final.bet, { gap: 45, lift: 0.6, fade: true });
}
var pay = pour(houseEl, spotEl, back, { gap: 60 });
return pay
// Paid into the spot beside your stake, then the whole lot swept home.
return spot
.pour(houseEl, back, { gap: 60 })
.then(function () { return wait(back > 0 ? 380 : 200); })
.then(function () {
var home = FX.flyMany(spotEl, purseEl, FX.chipsFor(payout, 8), { gap: 40, lift: 0.8 });
renderStack(0);
return home;
});
.then(function () { return spot.sweep(purseEl, payout, { gap: 40, lift: 0.8 }); });
}
// ---- phases ----------------------------------------------------------------
@@ -341,7 +312,7 @@
drawGallows(0, false);
renderWrong(null);
renderMeter(null);
renderStack(0);
spot.render(0);
setPhase(null);
return;
}
@@ -349,7 +320,7 @@
drawGallows((v.wrong || []).length, false);
renderWrong(v);
renderMeter(v);
renderStack(v.phase === "done" ? 0 : v.bet);
spot.render(v.phase === "done" ? 0 : v.bet);
setPhase(v);
}
@@ -367,8 +338,8 @@
if (!settles) money();
if (final && final.bet > staked) {
var extra = final.bet - staked;
if (final && final.bet > spot.amount) {
var extra = final.bet - spot.amount;
chain = chain.then(function () { return stake(extra); });
}
@@ -466,7 +437,7 @@
.catch(function (err) {
say(err.message, "bad", where);
return window.PeteGames.refresh().then(function (v) {
if (v && !v.hangman) renderStack(0);
if (v && !v.hangman) spot.render(0);
});
})
.then(function () { busy = false; });
@@ -500,7 +471,9 @@
});
});
root.querySelectorAll("[data-chip]").forEach(function (btn) {
// Scoped to buttons: the bare [data-chip] spans in the corner are the house's
// rack, and the house is not betting.
root.querySelectorAll("button[data-chip]").forEach(function (btn) {
btn.addEventListener("click", function () {
if (busy) return;
var d = parseInt(btn.dataset.chip, 10);
@@ -513,9 +486,9 @@
showBet();
var target = bet;
staked = bet;
spot.amount = bet;
FX.fly(btn, spotEl, { denom: d }).then(function () {
if (bet >= target) renderStack(target);
if (bet >= target) spot.render(target); // unless Clear got there first
});
});
});
@@ -523,10 +496,9 @@
var clearBtn = root.querySelector("[data-bet-clear]");
if (clearBtn) {
clearBtn.addEventListener("click", function () {
if (busy || !staked) { bet = 0; showBet(); return; }
FX.flyMany(spotEl, purseEl, FX.chipsFor(staked, 8), { gap: 40, lift: 0.7 });
if (busy || !spot.amount) { bet = 0; showBet(); return; }
spot.sweep(purseEl, null, { gap: 40, lift: 0.7 });
bet = 0;
renderStack(0);
showBet();
});
}

View File

@@ -127,7 +127,12 @@
// and the server (which has been holding the real clock all along) agreeing.
function timeUp() {
stopClock();
if (timedOut || busy || !game || game.phase !== "playing") return;
if (timedOut || !game || game.phase !== "playing") return;
// A move is already in flight. Come back rather than give up: this fires when
// the server has rejected our last timeout report (its clock hadn't run out
// yet) and the refresh has re-armed a countdown that is already at zero. Give
// up here and the clock sits frozen at 0.0s and the question never resolves.
if (busy) { setTimeout(timeUp, 250); return; }
timedOut = true;
lockAnswers();
setTimeout(function () {