Files
Pete/internal/web/static/js/games.js
prosolis 6961f90634 games: the money moves
The table dealt cards but settled money by editing a number. So the felt got
the two things it was missing: a bet spot in front of you, and the house's rack
beside the shoe. Every chip is now always travelling between one of those and
the other.

You build a bet by throwing chips onto the spot — the chip you clicked is the
chip that flies. The stake sits there through the hand. The house pays out of
its rack into the spot, and the pile is then swept back to your stack. A loss
goes to the rack and does not come back.

Two rules hold it together. The number under the pile is a readout of the pile,
never the other way round: the bet starts at nothing rather than at a default
nobody put down, and a settled hand leaves your stake back up on the spot,
because otherwise the panel prints "your bet: 300" over an empty circle. And
the chip bar does not move until the chips that justify it have landed — a
counter that pays you before the dealer turns over is a counter that has told
you the ending.

casino-fx.js is the engine underneath: chips fly on an arc, out of a fixed
overlay so no container clips one crossing from a button to the felt. It knows
nothing about blackjack.

Also: cards land with weight and a degree or two of tilt, so a hand looks dealt
rather than typeset; the dealer takes a beat before drawing out; and a natural
gets confetti, which is the only thing in the room that does.

Driven in a real browser, which is the only way to review an animation — and
which is what caught the verdict pill rendering white on white in a dark room,
a chip rack sitting on top of the dealer, and Hit being offered over a table
that was still being paid out. devcasino_test.go is that harness, kept.
2026-07-14 00:33:49 -07:00

169 lines
5.7 KiB
JavaScript

// The money bar: chips, wallet, buying in, cashing out.
//
// Buying chips is not instant and cannot be. gogobee owns the euros and has no
// inbound API, so it polls Pete for the crossing, moves the money on its side,
// and pushes the verdict back — a few seconds, end to end. So the button does
// not lie about it: the chips show as "buying" until they are real, and this
// file polls until they are. Nothing spendable appears until gogobee has said
// it took the euros.
//
// Exposed as window.PeteGames so the table (blackjack.js) shares one view of
// the money rather than keeping a second copy that drifts from this one.
(function () {
"use strict";
var bar = document.querySelector("[data-chipbar]");
if (!bar) return;
var chipsEl = bar.querySelector("[data-chips]");
var pendingEl = bar.querySelector("[data-pending]");
var eurosEl = bar.querySelector("[data-euros]");
var amountEl = bar.querySelector("[data-buyin-amount]");
var buyBtn = bar.querySelector("[data-buyin]");
var cashBtn = bar.querySelector("[data-cashout]");
var msgEl = bar.querySelector("[data-chipbar-msg]");
var listeners = [];
var view = null;
var pollTimer = null;
var pollUntil = 0;
function money(n) {
return (n || 0).toLocaleString();
}
function say(text, tone) {
if (!msgEl) return;
if (!text) { msgEl.classList.add("hidden"); return; }
msgEl.textContent = text;
msgEl.classList.remove("hidden");
msgEl.style.color = tone === "bad" ? "#cc3d4a" : "";
}
function paint(v) {
var first = view === null;
view = v;
// The chip count rolls to its new value rather than jumping to it — the table
// times this to land with the chips that caused it, so a payout reads as the
// number catching up with the felt. On the first paint there's nothing to
// catch up with, so it just sets.
if (chipsEl) {
if (first || !window.PeteFX) chipsEl.textContent = money(v.chips);
else window.PeteFX.count(chipsEl, v.chips);
}
if (eurosEl) eurosEl.textContent = (v.euros || 0).toFixed(2);
if (pendingEl) {
if (v.pending > 0) {
pendingEl.textContent = "+" + money(v.pending) + " buying…";
pendingEl.classList.remove("hidden");
} else {
pendingEl.classList.add("hidden");
}
}
// You cannot cash out mid-hand: the stake is already on the table.
if (cashBtn) cashBtn.disabled = v.chips <= 0 || !!v.hand;
if (buyBtn) buyBtn.disabled = v.chips + v.pending >= v.cap;
listeners.forEach(function (fn) { fn(v); });
}
// pollPending keeps asking while a buy-in is in flight, and stops the moment
// it lands — or is refused, which looks the same from here (pending drops to
// zero) and is told apart by whether the chips arrived.
function pollPending() {
clearTimeout(pollTimer);
if (Date.now() > pollUntil) {
say("gogobee hasn't answered yet. Your euros are safe — give it a moment and reload.");
return;
}
pollTimer = setTimeout(function () {
get().then(function (v) {
if (!v) return;
if (v.pending > 0) { pollPending(); return; }
if (v.chips > (pollPending.was || 0)) {
say("Chips are yours. Good luck!");
} else {
say("gogobee wouldn't cover that — not enough euros in your wallet.", "bad");
}
});
}, 1200);
}
function request(path, body) {
return fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body || {}),
}).then(function (res) {
return res.json().catch(function () { return {}; }).then(function (data) {
if (!res.ok) throw new Error(data.error || "that didn't work");
return data;
});
});
}
function get() {
return fetch("/api/games/table", { headers: { "Accept": "application/json" } })
.then(function (res) { return res.ok ? res.json() : null; })
.then(function (v) { if (v) paint(v); return v; })
.catch(function () { return null; });
}
if (buyBtn) {
buyBtn.addEventListener("click", function () {
var amount = parseInt(amountEl && amountEl.value, 10);
if (!(amount > 0)) { say("How many chips?", "bad"); return; }
buyBtn.disabled = true;
say("Asking gogobee for " + money(amount) + " euros…");
pollPending.was = view ? view.chips : 0;
request("/api/games/buyin", { amount: amount })
.then(function (v) {
paint(v);
pollUntil = Date.now() + 60000;
pollPending();
})
.catch(function (err) {
say(err.message, "bad");
buyBtn.disabled = false;
});
});
}
if (cashBtn) {
cashBtn.addEventListener("click", function () {
cashBtn.disabled = true;
say("Cashing you out…");
request("/api/games/cashout", { amount: 0 })
.then(function (v) {
paint(v);
say("Chips are on their way back to euros. They'll show in your wallet shortly.");
// The euro balance Pete shows is whatever gogobee last told it, so it
// only moves once the credit has actually gone through over there.
setTimeout(get, 4000);
})
.catch(function (err) {
say(err.message, "bad");
cashBtn.disabled = false;
});
});
}
window.PeteGames = {
// onUpdate registers a listener called on every fresh view of the money.
onUpdate: function (fn) { listeners.push(fn); if (view) fn(view); },
// apply pushes a view the table already fetched (a deal answers with one),
// so playing a hand doesn't need a second round-trip to refresh the chips.
apply: paint,
refresh: get,
post: request,
say: say,
view: function () { return view; },
};
get();
})();