Three front-end changes to the casino games:
- uno: a draw-card stack now names the seat it's actually pointed at
("+N on Beep") instead of always reading "+N on you". The bill reads
v.turn, which the engine advances to the target when a draw card lands.
- uno: the No Mercy rule buries a seat with some ceremony now. bury()
rolls one of four send-offs over the seat (or your hand, if it's you) —
a rockslide that piles up, a tombstone, a coffin, or the Mega Man death
burst — each with its own synth sound, and a static headstone under
reduced motion. The "Buried on N" badge still rides alongside.
- all games: winning makes it rain. FX.moneyRain drops a curtain of bills
and coins with a jackpot coin-cascade sound, gated by significance so it
reads as a win and not as noise: hold'em keeps its light per-pot confetti
and only rains on a real haul (>= 20bb), while the confetti stays the
rare cherry (natural 21, full board clear, phrase solved outright).
593 lines
23 KiB
JavaScript
593 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");
|
|
|
|
// Every win gets it to rain, because a win should feel like one. A natural is
|
|
// rarer and pays 3:2, so it keeps the confetti on top of the money as well.
|
|
if (v.outcome === "blackjack") { FX.burst(verdictEl, { count: 34 }); FX.moneyRain({ count: 30 }); }
|
|
else if (v.net > 0) FX.moneyRain();
|
|
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 || unitBet(final.hands[0])); })
|
|
.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.
|
|
// unitBet is what one hand of a deal actually cost, read back off a settled
|
|
// hand. It exists for the reload: `base` only knows what you pressed Deal with,
|
|
// and if you reloaded mid-hand nobody pressed Deal. A doubled hand carries twice
|
|
// the stake it was dealt with, so standing its bet back up would quietly put you
|
|
// on double the number you thought you were playing.
|
|
function unitBet(h) {
|
|
if (!h) return 0;
|
|
return h.doubled ? h.bet / 2 : h.bet;
|
|
}
|
|
|
|
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();
|
|
});
|
|
})();
|