Blackjack has a split. It was the last rule missing from a game that has been live for a week, and it is the only move in blackjack that takes chips out of your stack *after* the cards are out — which is most of what there is to get wrong about it. So the state stops pretending. State.Player is gone; there is a slice of Hands, each with its own cards, its own bet, its own outcome and its own payout, and an Active index the player works left to right. Settle runs per hand and rakes per hand: netting them against each other first would mean a player who won one and lost one paid no rake at all, which is not a rake, it's a discount for splitting. The web layer takes the second bet before the move and hands it straight back if the engine refuses — the same shape double already used, except double was staking st.Bet, the whole table's stake, which was the same number as the hand's until today and is now emphatically not. DoubleCost/SplitCost are the active hand's, and the felt would have found this by charging you 300 to double the third hand of a split. The rules that cost money if you guess them: split aces get one card each and no say (a pair of aces is otherwise the best hand in the game, forever), 21 on a split hand is twenty-one and not a natural (it does not pay 3:2 — the test that pins this is the most expensive one in the file), same rank rather than same value (a king and a queen are not a pair), four hands maximum, double after split allowed, and if every hand busts the dealer does not turn over. A live hand outlives a deploy, so State.UnmarshalJSON still reads the old blobs: "player" with no "hands" becomes one hand holding the whole stake. Without it, a player mid-hand at restart is a player whose cards vanished — which is not a decode error, and would not have looked like one. On the felt a hand is now a box with its own spot, and a split is a card lifting out of one hand into a new one with a second stack of chips flying after it from your pile. Verified in a browser against a real pair: chips 4738 -> 4638 on the split, two hands played out, one push and one loss, "Down on the deal. -100", 4738 back. Three hands stack without collision at 390px. Settled hands come back to full brightness — dimming means "not your turn", and when the deal is over they are the thing you are reading. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
586 lines
23 KiB
JavaScript
586 lines
23 KiB
JavaScript
// The blackjack table.
|
|
//
|
|
// The browser holds no game. It sends intents — deal, hit, stand, double, split
|
|
// — and the server answers with the cards you're allowed to see plus the *script*
|
|
// of how they got there: one event per card off the shoe, in the order the shoe
|
|
// gave them up. This file's job is to play that script back at a human speed
|
|
// rather than snapping the finished hand into place.
|
|
//
|
|
// Which is also why the hole card works the way it does: the server sends a
|
|
// "dealer_hole" event with no card attached, because while you are still acting
|
|
// it hasn't told anyone what that card is. It arrives with the reveal.
|
|
//
|
|
// The money is animated on the same principle. Every number the server sends is
|
|
// also a movement: a stake is chips leaving your pile and landing on the spot, a
|
|
// payout is chips coming out of the house's rack, a loss is your stack being
|
|
// taken away. Nothing about the money changes on this table without something
|
|
// crossing the felt to make it change — including the count in the chip bar,
|
|
// which is deliberately not updated until the chips that justify it have landed.
|
|
//
|
|
// Since split, "the spot" is not one place. A hand has a spot, and a split makes
|
|
// a second hand with a second bet on a second spot — two bets that win and lose
|
|
// separately, which is the whole point of splitting and has to look like it.
|
|
(function () {
|
|
"use strict";
|
|
|
|
var root = document.querySelector("[data-blackjack]");
|
|
if (!root) return;
|
|
|
|
var FX = window.PeteFX;
|
|
|
|
var dealerEl = root.querySelector("[data-dealer]");
|
|
var dTotalEl = root.querySelector("[data-dealer-total]");
|
|
var dLabelEl = root.querySelector("[data-dealer-label]");
|
|
var verdictEl = root.querySelector("[data-verdict]");
|
|
var betting = root.querySelector("[data-betting]");
|
|
var actions = root.querySelector("[data-actions]");
|
|
var betAmount = root.querySelector("[data-bet-amount]");
|
|
var dealBtn = root.querySelector("[data-deal]");
|
|
var msgEl = root.querySelector("[data-table-msg]");
|
|
|
|
// Where the chips live: your pile (in the bar above), a spot in front of each
|
|
// hand, and the house's rack on the felt.
|
|
var purseEl = document.querySelector("[data-chips]");
|
|
var houseEl = root.querySelector("[data-house]");
|
|
var handsEl = root.querySelector("[data-hands]");
|
|
var handTpl = root.querySelector("[data-hand-template]");
|
|
|
|
var bet = 0; // what you're building between hands
|
|
var base = 0; // what one hand of the last deal cost, for standing it back up
|
|
var busy = false; // a request is in flight, or cards are still landing
|
|
var hand = null; // the deal as the server last described it
|
|
|
|
var DEAL_MS = 380; // one card's flight, and the gap before the next
|
|
var FLIP_MS = 450;
|
|
var BEAT_MS = 600; // the dealer thinking before they draw out
|
|
var SPLIT_MS = 340; // the card lifting out of the hand it's leaving
|
|
|
|
var reduced = FX.reduced;
|
|
function pace(ms) { return reduced ? 0 : ms; }
|
|
function wait(ms) { return new Promise(function (r) { setTimeout(r, pace(ms)); }); }
|
|
|
|
function say(text, tone) {
|
|
if (!text) { msgEl.classList.add("hidden"); return; }
|
|
msgEl.textContent = text;
|
|
msgEl.classList.remove("hidden");
|
|
msgEl.style.color = tone === "bad" ? "#cc3d4a" : "";
|
|
}
|
|
|
|
// ---- drawing --------------------------------------------------------------
|
|
//
|
|
// The deck itself — the faces, the pips, the flip — is PeteCards, shared with
|
|
// every other table in the room. Here a card is always dealt out of the shoe
|
|
// and always lands with a degree or two of tilt on it, which are this table's
|
|
// two opinions about a card and the only ones it has.
|
|
|
|
var CARDS = window.PeteCards;
|
|
|
|
function cardEl(face) { return CARDS.el(face); }
|
|
var turnOver = CARDS.turnOver;
|
|
|
|
// ---- the hands -------------------------------------------------------------
|
|
//
|
|
// A box per hand: its chips, its cards, its total, its own verdict. There is
|
|
// always at least one, and before a deal that one is where you build your bet —
|
|
// so the chips you stack are already sitting on the hand they're about to buy.
|
|
|
|
var hands = [];
|
|
|
|
function makeHand(at) {
|
|
var el = handTpl.content.firstElementChild.cloneNode(true);
|
|
var spotEl = el.querySelector("[data-spot]");
|
|
var box = {
|
|
el: el,
|
|
spotEl: spotEl, // where a chip flies to, as opposed to what counts it
|
|
cards: el.querySelector("[data-cards]"),
|
|
total: el.querySelector("[data-total]"),
|
|
verdict: el.querySelector("[data-hand-outcome]"),
|
|
ranks: [], // what's in it, for a running total while the cards are landing
|
|
spot: FX.spot({
|
|
spot: spotEl,
|
|
stack: el.querySelector("[data-stack]"),
|
|
total: el.querySelector("[data-spot-total]"),
|
|
}),
|
|
};
|
|
if (at === undefined || at >= hands.length) {
|
|
handsEl.appendChild(el);
|
|
hands.push(box);
|
|
} else {
|
|
handsEl.insertBefore(el, hands[at].el);
|
|
hands.splice(at, 0, box);
|
|
}
|
|
handsEl.dataset.count = hands.length;
|
|
return box;
|
|
}
|
|
|
|
// reset tears the row down to n empty hands. It does not preserve chips: a
|
|
// caller that has some to keep (a deal, whose stake is already on the spot)
|
|
// reads the amount first and puts it back.
|
|
function reset(n) {
|
|
handsEl.innerHTML = "";
|
|
hands = [];
|
|
for (var i = 0; i < (n || 1); i++) makeHand();
|
|
}
|
|
|
|
function ensure(n) { while (hands.length < n) makeHand(); }
|
|
|
|
// The bet you build between hands is hand zero's.
|
|
function betSpot() { return hands[0].spot; }
|
|
|
|
// live marks the hand you're being asked about, and dims the ones you aren't.
|
|
//
|
|
// -1 means nobody is acting — the dealer is drawing, or the deal is over — and
|
|
// that is *not* the same as every hand being inactive: a settled hand is the
|
|
// thing you are reading, so it goes back to full brightness rather than sitting
|
|
// there greyed out. Hence the empty string: it matches neither CSS rule.
|
|
function live(i) {
|
|
hands.forEach(function (h, k) {
|
|
h.el.dataset.live = i < 0 ? "" : k === i ? "1" : "0";
|
|
});
|
|
}
|
|
|
|
// The running total under a hand, worked out from the cards you can see. The
|
|
// server sends totals too, but only with the finished state — and a hand you're
|
|
// playing needs to know what it's holding *now*, not once it's over. It's a
|
|
// readout, never a decision: every ruling is the server's.
|
|
var TEN = { K: 10, Q: 10, J: 10 };
|
|
function totalOf(ranks) {
|
|
var total = 0, aces = 0;
|
|
ranks.forEach(function (r) {
|
|
if (r === "A") { aces++; total += 11; }
|
|
else total += TEN[r] || parseInt(r, 10) || 0;
|
|
});
|
|
while (total > 21 && aces > 0) { total -= 10; aces--; }
|
|
return { total: total, soft: aces > 0 };
|
|
}
|
|
|
|
function showTotal(box) {
|
|
if (!box.ranks.length) { box.total.classList.add("hidden"); return; }
|
|
var v = totalOf(box.ranks);
|
|
box.total.textContent = v.total + (v.soft ? " (soft)" : "");
|
|
box.total.classList.remove("hidden");
|
|
}
|
|
|
|
var SHORT = {
|
|
blackjack: "Blackjack", win: "Won", dealer_bust: "Won",
|
|
lose: "Lost", bust: "Bust", push: "Push",
|
|
};
|
|
|
|
// A hand's own result, on the hand. With one hand this says the same thing as
|
|
// the pill above it and is a bit redundant; with four it is the only way to
|
|
// read what happened, because "you win" is not true of all of them.
|
|
function showHandVerdict(box, h) {
|
|
var text = h && SHORT[h.outcome];
|
|
if (!text) { box.verdict.classList.add("hidden"); return; }
|
|
box.verdict.textContent = text;
|
|
box.verdict.classList.remove("hidden");
|
|
}
|
|
|
|
// ---- the money on the felt -------------------------------------------------
|
|
|
|
// settleChips is what the felt does about the outcome, after the cards have
|
|
// finished telling you what it is. Each hand is paid on its own, in order, left
|
|
// to right — because each hand *is* its own bet, and a split that lost one and
|
|
// won the other has to be watchable as exactly that.
|
|
function settleChips(final) {
|
|
var chain = Promise.resolve();
|
|
(final.hands || []).forEach(function (h, i) {
|
|
chain = chain.then(function () {
|
|
var box = hands[i];
|
|
if (!box) return;
|
|
var payout = h.payout || 0;
|
|
|
|
if (payout <= 0) {
|
|
// The house takes it. The stack goes to the rack and doesn't come back.
|
|
return box.spot.sweep(houseEl, h.bet, { gap: 45, lift: 0.6, fade: true });
|
|
}
|
|
// The house pays first, into the spot beside the stake, so you watch the
|
|
// winnings arrive on top of the bet that earned them.
|
|
var back = payout - h.bet;
|
|
return box.spot
|
|
.pour(houseEl, back, { gap: 60 })
|
|
.then(function () { return wait(back > 0 ? 340 : 160); })
|
|
.then(function () { return box.spot.sweep(purseEl, payout, { gap: 40, lift: 0.8 }); });
|
|
});
|
|
});
|
|
return chain;
|
|
}
|
|
|
|
// While the hole card is down, the dealer's total is only what's showing — so
|
|
// say so, rather than printing a number that quietly means something else.
|
|
function dealerTotal(v) {
|
|
if (v.dealer && v.dealer.length) {
|
|
dTotalEl.textContent = v.hole ? v.dealer_total + " showing" : String(v.dealer_total);
|
|
dTotalEl.classList.remove("hidden");
|
|
} else {
|
|
dTotalEl.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
// paint puts a deal on the felt with no animation. This is the resume path: you
|
|
// reloaded, or Pete restarted, and your cards are simply there — including the
|
|
// stakes, which are still on their spots because the server still has them.
|
|
function paint(v) {
|
|
dealerEl.innerHTML = "";
|
|
if (!v) { reset(1); setPhase(null); return; }
|
|
|
|
reset(v.hands.length || 1);
|
|
v.hands.forEach(function (h, i) {
|
|
var box = hands[i];
|
|
(h.cards || []).forEach(function (c) {
|
|
box.cards.appendChild(cardEl(c));
|
|
box.ranks.push(c.rank);
|
|
});
|
|
box.spot.render(v.phase === "done" ? 0 : h.bet);
|
|
showTotal(box);
|
|
showHandVerdict(box, v.phase === "done" ? h : null);
|
|
});
|
|
|
|
v.dealer.forEach(function (c) { dealerEl.appendChild(cardEl(c)); });
|
|
if (v.hole) dealerEl.appendChild(cardEl(null));
|
|
dealerTotal(v);
|
|
setPhase(v);
|
|
}
|
|
|
|
var VERDICTS = {
|
|
blackjack: "Blackjack! 🎉",
|
|
win: "You win!",
|
|
dealer_bust: "Dealer busts. You win!",
|
|
lose: "Dealer takes it.",
|
|
bust: "Bust.",
|
|
push: "Push — your bet comes back.",
|
|
};
|
|
|
|
function verdict(v) {
|
|
var text = VERDICTS[v.outcome] || "";
|
|
// Across several hands "you win" is a claim about the money, not about the
|
|
// cards: you can win one, lose one, and still be up. The pill reports the
|
|
// deal; the badge on each hand reports the hand.
|
|
if ((v.hands || []).length > 1) {
|
|
text = v.net > 0 ? "You're up on the deal." : v.net < 0 ? "Down on the deal." : "All square.";
|
|
}
|
|
if (!text) { verdictEl.classList.add("hidden"); return; }
|
|
if (v.net > 0) text += " +" + v.net.toLocaleString();
|
|
else if (v.net < 0) text += " " + v.net.toLocaleString();
|
|
verdictEl.textContent = text;
|
|
verdictEl.classList.remove("hidden");
|
|
|
|
// The one thing in this room that gets confetti. A natural is rare, it pays
|
|
// 3:2, and if everything celebrated then nothing would.
|
|
//
|
|
// The *sound* is not so precious: a win is a win and you should hear it. So
|
|
// the fanfare rides on the money, not on the confetti.
|
|
if (v.outcome === "blackjack") FX.burst(verdictEl, { count: 34 });
|
|
else if (v.net > 0) FX.sfx("win");
|
|
else if (v.net < 0) FX.sfx("lose");
|
|
else FX.sfx("push");
|
|
}
|
|
|
|
// setPhase swaps the controls: bet between hands, act during one.
|
|
function setPhase(v) {
|
|
hand = v;
|
|
var acting = !!v && v.phase === "player";
|
|
betting.classList.toggle("hidden", acting);
|
|
actions.classList.toggle("hidden", !acting);
|
|
|
|
if (acting) {
|
|
var dbl = actions.querySelector('[data-move="double"]');
|
|
var spl = actions.querySelector('[data-move="split"]');
|
|
if (dbl) dbl.disabled = !v.can_double;
|
|
if (spl) spl.disabled = !v.can_split;
|
|
live(v.active || 0);
|
|
} else {
|
|
live(-1);
|
|
}
|
|
if (!acting) verdictEl.classList.toggle("hidden", !(v && v.outcome));
|
|
}
|
|
|
|
// ---- the script -----------------------------------------------------------
|
|
|
|
// play walks the server's events, one card at a time. It is deliberately the
|
|
// only thing that renders during a hand: the final state is applied at the end,
|
|
// so what you watch and what the server says can't disagree halfway through.
|
|
//
|
|
// `money` is the one exception, and it's a deliberate one. On a hand that is
|
|
// still running, the chip bar is right immediately — your stake left your pile
|
|
// when you pressed Deal, and it's sitting on the spot where you can see it. On
|
|
// a hand that *settles*, the bar is left alone until the chips have physically
|
|
// come home, because a counter that pays you before the dealer has turned over
|
|
// is a counter that has told you the ending.
|
|
function play(view, money) {
|
|
var events = view.events || [];
|
|
var final = view.hand;
|
|
var settles = !!final && final.phase === "done";
|
|
var hole = null; // the face-down card element, once one has been dealt
|
|
var chain = Promise.resolve();
|
|
var drew = false; // has the dealer drawn since the reveal?
|
|
|
|
if (!settles) money();
|
|
|
|
// A deal whose bet was typed rather than stacked (you kept last hand's number
|
|
// and just pressed Deal) has chips to put down before the first card does.
|
|
// Everything else that costs money — a double, a split — announces itself as
|
|
// an event, and pays for itself there.
|
|
if (final && final.hands && final.hands.length && events.length && events[0].kind === "deal") {
|
|
var want = final.hands[0].bet;
|
|
var have = betSpot().amount;
|
|
if (want > have) {
|
|
chain = chain.then(function () { return betSpot().pour(purseEl, want - have); });
|
|
}
|
|
}
|
|
|
|
events.forEach(function (e) {
|
|
chain = chain.then(function () {
|
|
var box;
|
|
switch (e.kind) {
|
|
case "deal":
|
|
// Clear the felt, but not the stake: those chips are yours, they are
|
|
// already on the spot, and they are what this deal is riding on.
|
|
var staked = betSpot().amount;
|
|
dealerEl.innerHTML = "";
|
|
reset(1);
|
|
betSpot().render(staked);
|
|
verdictEl.classList.add("hidden");
|
|
FX.sfx("shuffle");
|
|
return;
|
|
|
|
case "player_card":
|
|
ensure(e.hand + 1);
|
|
box = hands[e.hand];
|
|
box.cards.appendChild(cardEl(e.card));
|
|
box.ranks.push(e.card.rank);
|
|
showTotal(box);
|
|
live(e.hand);
|
|
FX.sfx("deal");
|
|
return wait(DEAL_MS);
|
|
|
|
case "split":
|
|
// The second card lifts out of the hand it was in and becomes a hand
|
|
// of its own, and the bet that follows it is a second bet: the same
|
|
// chips again, out of your pile, onto a spot that didn't exist a
|
|
// moment ago.
|
|
var src = hands[e.hand];
|
|
var moved = src.cards.lastElementChild;
|
|
var stake = src.spot.amount;
|
|
var fresh = makeHand(e.hand + 1);
|
|
if (moved) moved.classList.add("bj-splitting");
|
|
FX.sfx("deal", { v: 1 });
|
|
return wait(SPLIT_MS).then(function () {
|
|
if (moved) {
|
|
moved.classList.remove("bj-splitting");
|
|
fresh.cards.appendChild(moved);
|
|
}
|
|
fresh.ranks.push(src.ranks.pop());
|
|
showTotal(src);
|
|
showTotal(fresh);
|
|
return fresh.spot.pour(purseEl, stake);
|
|
});
|
|
|
|
case "double":
|
|
// The stake goes down again on that hand, and only that hand.
|
|
box = hands[e.hand];
|
|
return box.spot.pour(purseEl, box.spot.amount);
|
|
|
|
case "dealer_card":
|
|
// The dealer takes a moment before the first card they draw out.
|
|
// Card, card, card with no breath in between is a machine dealing;
|
|
// the pause is the only thing on this table that plays as suspense.
|
|
var beat = drew ? Promise.resolve() : think();
|
|
drew = true;
|
|
live(-1);
|
|
return beat.then(function () {
|
|
dealerEl.appendChild(cardEl(e.card));
|
|
FX.sfx("deal", { v: 1 });
|
|
return wait(DEAL_MS);
|
|
});
|
|
|
|
case "dealer_hole":
|
|
hole = cardEl(null);
|
|
dealerEl.appendChild(hole);
|
|
FX.sfx("deal", { v: 2 });
|
|
return wait(DEAL_MS);
|
|
|
|
case "reveal":
|
|
// The hole card turns over. Its face is in the final hand — this is
|
|
// the first moment the server has been willing to say what it was.
|
|
live(-1);
|
|
if (!hole) hole = dealerEl.querySelector('.pete-card[data-face="down"]');
|
|
if (hole && final && final.dealer && final.dealer[1]) {
|
|
turnOver(hole, final.dealer[1]);
|
|
}
|
|
return wait(FLIP_MS);
|
|
|
|
case "settle":
|
|
return;
|
|
}
|
|
});
|
|
});
|
|
|
|
return chain.then(function () {
|
|
if (!final) { paint(null); money(); return; }
|
|
|
|
dealerTotal(final);
|
|
if (!settles) { setPhase(final); return; }
|
|
|
|
// The deal is over: nothing is on offer while the money is moving. The
|
|
// actions go now, and Deal comes back at the far end.
|
|
actions.classList.add("hidden");
|
|
live(-1);
|
|
(final.hands || []).forEach(function (h, i) {
|
|
if (hands[i]) showHandVerdict(hands[i], h);
|
|
});
|
|
verdict(final);
|
|
// The chips move, and the bar catches up with them when they arrive. The
|
|
// betting controls come back last, once the felt is clear: offering Deal
|
|
// over a table that is still being paid out invites a click the table then
|
|
// has to refuse.
|
|
return settleChips(final)
|
|
.then(money)
|
|
.then(function () { return standing(base || (final.hands[0] && final.hands[0].bet)); })
|
|
.then(function () { setPhase(final); });
|
|
});
|
|
}
|
|
|
|
// standing leaves your bet up for the next hand, the way you would at a table:
|
|
// one hand's stake goes straight back on the spot. It costs nothing — chips on
|
|
// the spot are a proposal until you press Deal — and it's what keeps the number
|
|
// in the panel honest, because otherwise a settled hand leaves "your bet: 300"
|
|
// printed over an empty spot.
|
|
//
|
|
// It's one hand's worth, not the whole deal's: a split cost you four hundred,
|
|
// and standing four hundred back up on one spot would be betting a stranger's
|
|
// money on your behalf.
|
|
function standing(amount) {
|
|
var money = window.PeteGames.view();
|
|
if (!amount || !money || money.chips < amount) {
|
|
bet = 0;
|
|
showBet();
|
|
return;
|
|
}
|
|
bet = amount;
|
|
showBet();
|
|
return betSpot().pour(purseEl, amount);
|
|
}
|
|
|
|
// think is the dealer's beat: a pause with something to look at, so it reads as
|
|
// deliberation rather than as the page having hung.
|
|
function think() {
|
|
if (reduced || !dLabelEl) return wait(0);
|
|
dLabelEl.classList.add("pete-dealer-think");
|
|
return wait(BEAT_MS).then(function () {
|
|
dLabelEl.classList.remove("pete-dealer-think");
|
|
});
|
|
}
|
|
|
|
// ---- talking to the table -------------------------------------------------
|
|
|
|
function send(path, body) {
|
|
if (busy) return;
|
|
busy = true;
|
|
say("");
|
|
return window.PeteGames.post(path, body)
|
|
.then(function (view) {
|
|
// play() decides *when* the money lands; see the note on it.
|
|
return play(view, function () { window.PeteGames.apply(view); });
|
|
})
|
|
.catch(function (err) {
|
|
say(err.message, "bad");
|
|
// Whatever we thought was on the felt, the server is the authority on it.
|
|
return window.PeteGames.refresh().then(function (v) {
|
|
if (v && !v.hand) { reset(1); setPhase(null); }
|
|
else if (v && v.hand) paint(v.hand);
|
|
});
|
|
})
|
|
.then(function () { busy = false; });
|
|
}
|
|
|
|
// ---- betting --------------------------------------------------------------
|
|
//
|
|
// A bet is built by putting chips on the spot, one at a time, and it is those
|
|
// chips the deal then rides on — the number under the pile is a readout of the
|
|
// pile, not the other way round.
|
|
|
|
function showBet() {
|
|
betAmount.textContent = bet.toLocaleString();
|
|
var money = window.PeteGames.view();
|
|
if (dealBtn) dealBtn.disabled = bet <= 0 || !money || money.chips < bet;
|
|
}
|
|
|
|
// 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);
|
|
var money = window.PeteGames.view();
|
|
if (money && bet + d > money.chips) {
|
|
say("You haven't got that many chips.", "bad");
|
|
return;
|
|
}
|
|
bet += d;
|
|
showBet();
|
|
|
|
// The chip you clicked is the chip that flies: same colour, same size, off
|
|
// the button and onto the felt. The pile only grows once it gets there —
|
|
// but the spot's total moves now, so a Deal pressed mid-flight still knows
|
|
// the chip is on its way and doesn't put a second one down.
|
|
var target = bet;
|
|
var spot = betSpot();
|
|
spot.amount = bet;
|
|
FX.fly(btn, hands[0].spotEl, { denom: d }).then(function () {
|
|
if (bet >= target) spot.render(target); // unless Clear got there first
|
|
});
|
|
});
|
|
});
|
|
|
|
var clearBtn = root.querySelector("[data-bet-clear]");
|
|
if (clearBtn) {
|
|
clearBtn.addEventListener("click", function () {
|
|
if (busy || !betSpot().amount) { bet = 0; showBet(); return; }
|
|
betSpot().sweep(purseEl, null, { gap: 40, lift: 0.7 });
|
|
bet = 0;
|
|
showBet();
|
|
});
|
|
}
|
|
|
|
if (dealBtn) {
|
|
dealBtn.addEventListener("click", function () {
|
|
if (bet <= 0) { say("Put something on it first.", "bad"); return; }
|
|
base = bet; // one hand's worth, which is what a split doubles and a stand puts back
|
|
send("/api/games/blackjack/deal", { bet: bet });
|
|
});
|
|
}
|
|
|
|
root.querySelectorAll("[data-move]").forEach(function (btn) {
|
|
btn.addEventListener("click", function () {
|
|
send("/api/games/blackjack/move", { move: btn.dataset.move });
|
|
});
|
|
});
|
|
|
|
document.addEventListener("keydown", function (e) {
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
if (/^(input|textarea|select)$/i.test((e.target.tagName || ""))) return;
|
|
if (!hand || hand.phase !== "player" || busy) return;
|
|
|
|
var move = { h: "hit", s: "stand", d: "double", p: "split" }[e.key.toLowerCase()];
|
|
if (!move) return;
|
|
if (move === "double" && !hand.can_double) return;
|
|
if (move === "split" && !hand.can_split) return;
|
|
e.preventDefault();
|
|
send("/api/games/blackjack/move", { move: move });
|
|
});
|
|
|
|
// The money bar owns the first fetch; the table picks up whatever it found,
|
|
// including a deal left sitting on the felt by a reload or a redeploy.
|
|
reset(1);
|
|
var resumed = false;
|
|
window.PeteGames.onUpdate(function (v) {
|
|
if (!resumed) {
|
|
resumed = true;
|
|
if (v.hand) paint(v.hand);
|
|
if (v.hand && v.hand.phase === "done") verdict(v.hand);
|
|
}
|
|
showBet();
|
|
});
|
|
})();
|