Your bug report was about a category, not one button, so I went looking for the
rest of the guessing rather than picking a new topic. Two left, both about the
button -- and one of them was in the fix I'd just shipped.
The deal preview lied
The prompt added last time promises who will post. With the button parked on an
empty chair it promised the wrong pair: it read the button where it currently sat,
while dealing relocates the button onto a player first and only then works out the
blinds.
three players in seats 2-4, button parked on empty seat 1
preview said: s2 posts $1, s3 posts $2
dealing gave: s3 posts $1, s4 posts $2
Exactly the fault you reported -- the display and the thing it describes computed
from different moments -- inside the change meant to remove it. buttonSeatForDeal
answers where the button will be without moving it, so the preview asks the same
question the deal will answer. And the move is now stated before it happens:
"Button moves to Ben - seat 1 is empty."
Moving the button after the deal moved the blinds silently
Once the blinds are posted, moving the button takes them back and re-posts them,
because otherwise the badges and the money disagree. That's the right behaviour
and it was completely silent. The toast says so now: "Button on Dee - blinds taken
back and re-posted."
It also only does it when there is actually something posted to move. Before the
deal there is nothing committed, so passing the button is just moving a marker --
which is the whole point of the setup step.
Checked and deliberately left alone
Streets still advance one press at a time during an all-in run-out, which is right
-- the dealer is physically dealing the board between them. A player who joins
mid-hand is still marked folded for that hand and plays the next one. The
base-pot-amount shorthand still stands in for "everyone antes the same". Nothing
mutates state during a render.
4 tests (152 total): the preview matches what dealing produces with the button on
an empty chair, asking where the button will be doesn't move it, a button already
on a player is left alone, and an empty table doesn't break the question.
Driven through the UI: the prompt warns about the move and then the deal matches
it exactly; moving the button after the deal announces the re-post; and the blinds
survive being passed around three seats with the chip count fixed at 400. Layout
and contrast audits clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
554 lines
23 KiB
JavaScript
554 lines
23 KiB
JavaScript
// The bar a dealer's thumb lives on while a hand is running: who's to act, what it
|
|
// costs them, and one tap per decision. Sits at the bottom of the screen on a phone
|
|
// so it's reachable without looking, and re-renders after every action because the
|
|
// next player's numbers are different.
|
|
const ActionBar = {
|
|
state: null,
|
|
hooks: null,
|
|
raiseOpen: false,
|
|
raiseDraft: null,
|
|
|
|
mount(state, hooks) {
|
|
this.state = state;
|
|
this.hooks = hooks;
|
|
this.watchHeight();
|
|
this.render();
|
|
},
|
|
|
|
// The bar is fixed to the bottom of the screen, so everything else has to know how
|
|
// tall it is or the table simply runs underneath it — on a 640px-tall phone the
|
|
// bottom seats of the oval were sitting behind the Fold button. Its height isn't a
|
|
// constant either: opening the raise pad grows it. Publishing the measured height
|
|
// as a custom property lets the layout and the toast both stay clear of it without
|
|
// anyone guessing a number.
|
|
watchHeight() {
|
|
const host = document.getElementById("actionBar");
|
|
if (!host || this.heightWatcher) return;
|
|
|
|
const publish = () => {
|
|
const visible = !host.hidden;
|
|
const height = visible ? host.getBoundingClientRect().height : 0;
|
|
document.documentElement.style.setProperty("--actionBarHeight", `${Math.round(height)}px`);
|
|
};
|
|
|
|
if (typeof ResizeObserver === "function") {
|
|
this.heightWatcher = new ResizeObserver(publish);
|
|
this.heightWatcher.observe(host);
|
|
}
|
|
window.addEventListener("resize", publish);
|
|
window.addEventListener("orientationchange", publish);
|
|
publish();
|
|
},
|
|
|
|
// Anything to act on? A hand with nobody dealt in doesn't get a bar — and that
|
|
// still holds during setup, because a seated player is a live seat whether or not
|
|
// the betting has been opened. An empty table stays uncluttered.
|
|
isRunning() {
|
|
return Betting.liveSeats(this.state).length > 0;
|
|
},
|
|
|
|
render() {
|
|
const host = document.getElementById("actionBar");
|
|
if (!host) return;
|
|
host.innerHTML = "";
|
|
|
|
if (!this.isRunning()) {
|
|
host.hidden = true;
|
|
document.body.classList.remove("hasActionBar");
|
|
document.documentElement.style.setProperty("--actionBarHeight", "0px");
|
|
this.stopTankTimer();
|
|
this.tankSeatIndex = null;
|
|
return;
|
|
}
|
|
host.hidden = false;
|
|
document.body.classList.add("hasActionBar");
|
|
|
|
host.appendChild(this.renderStatus());
|
|
|
|
const breakdown = this.renderPotBreakdown();
|
|
if (breakdown) host.appendChild(breakdown);
|
|
|
|
if (!this.state.bettingOpen) {
|
|
host.appendChild(this.renderDealPrompt());
|
|
} else if (Betting.isHandOver(this.state)) {
|
|
host.appendChild(this.renderHandOver());
|
|
} else if (this.state.actingSeatIndex === null || this.state.actingSeatIndex === undefined) {
|
|
host.appendChild(this.renderStreetDone());
|
|
} else if (this.raiseOpen) {
|
|
host.appendChild(this.renderRaisePad());
|
|
} else {
|
|
host.appendChild(this.renderActions());
|
|
}
|
|
|
|
this.syncTankTimer();
|
|
},
|
|
|
|
renderStatus() {
|
|
const row = document.createElement("div");
|
|
row.className = "actionStatus";
|
|
|
|
const street = document.createElement("span");
|
|
street.className = "actionStreet";
|
|
street.textContent = Betting.streetLabel(this.state.street);
|
|
row.appendChild(street);
|
|
|
|
const pot = document.createElement("span");
|
|
pot.className = "actionPot";
|
|
pot.textContent = `Pot ${Money.format(Betting.potSoFar(this.state))}`;
|
|
row.appendChild(pot);
|
|
|
|
// How many are still in the hand — part of the standard per-street
|
|
// announcement, and the number a dealer is asked for most often.
|
|
const remaining = Betting.liveSeats(this.state).length;
|
|
const players = document.createElement("span");
|
|
players.className = "actionPlayers";
|
|
players.textContent = `${remaining} ${remaining === 1 ? "player" : "players"}`;
|
|
row.appendChild(players);
|
|
|
|
// Nobody is being asked to act because nobody can — say so, or the bar just
|
|
// looks like it's waiting for something.
|
|
if (Betting.isRunningOut(this.state) && !Betting.isHandOver(this.state)) {
|
|
const runout = document.createElement("span");
|
|
runout.className = "actionRunout";
|
|
const stillIn = Betting.liveSeats(this.state).length;
|
|
runout.textContent = `All-in — running out the board (${stillIn} players)`;
|
|
row.appendChild(runout);
|
|
}
|
|
|
|
const index = this.state.actingSeatIndex;
|
|
if (index !== null && index !== undefined && !Betting.isHandOver(this.state)) {
|
|
const player = this.state.seats[index];
|
|
const who = document.createElement("span");
|
|
who.className = "actionWho";
|
|
who.textContent = `${HoldemState.displayName(player, index)} · ${Money.format(Betting.stackOf(this.state, player))} behind`;
|
|
row.appendChild(who);
|
|
|
|
const tank = document.createElement("span");
|
|
tank.className = "actionTank";
|
|
tank.id = "actionTank";
|
|
tank.title = "How long this player has been thinking";
|
|
tank.textContent = "0:00";
|
|
row.appendChild(tank);
|
|
}
|
|
|
|
// A standing control rather than a toast per action: a dealer taps this bar
|
|
// constantly, and a toast after every fold would be noise they'd learn to
|
|
// ignore — which is exactly when they'd miss the one that mattered.
|
|
const undoLabel = this.hooks.lastActionLabel();
|
|
if (undoLabel) {
|
|
const undo = document.createElement("button");
|
|
undo.type = "button";
|
|
undo.className = "actionUndo";
|
|
undo.title = `Undo ${undoLabel}`;
|
|
undo.textContent = `↶ ${undoLabel}`;
|
|
undo.addEventListener("click", () => this.hooks.undoLast());
|
|
row.appendChild(undo);
|
|
}
|
|
|
|
return row;
|
|
},
|
|
|
|
// ---- Think timer ----
|
|
//
|
|
// A player who hasn't noticed the action is on them costs more time than anything
|
|
// else a dealer does, so the bar shows how long the current player has been in the
|
|
// tank. It's a stopwatch, not a shot clock: nothing happens when it grows, it just
|
|
// tells the dealer when it's worth a nudge.
|
|
NUDGE_AFTER_S: 30,
|
|
SLOW_AFTER_S: 60,
|
|
|
|
tankSeatIndex: null,
|
|
tankStartedAt: null,
|
|
tankInterval: null,
|
|
|
|
// Restarts only when the action actually moves — a re-render for any other reason
|
|
// (a chip edit, a name change) must not put a thinking player back to zero.
|
|
syncTankTimer() {
|
|
const index = this.state.actingSeatIndex;
|
|
const running = index !== null && index !== undefined && !Betting.isHandOver(this.state);
|
|
|
|
if (!running) {
|
|
this.stopTankTimer();
|
|
this.tankSeatIndex = null;
|
|
return;
|
|
}
|
|
|
|
if (this.tankSeatIndex !== index) {
|
|
this.tankSeatIndex = index;
|
|
this.tankStartedAt = Date.now();
|
|
}
|
|
|
|
if (!this.tankInterval) {
|
|
this.tankInterval = setInterval(() => this.paintTank(), 1000);
|
|
}
|
|
this.paintTank();
|
|
},
|
|
|
|
stopTankTimer() {
|
|
if (this.tankInterval) {
|
|
clearInterval(this.tankInterval);
|
|
this.tankInterval = null;
|
|
}
|
|
},
|
|
|
|
// Updates only the timer's own text, so the seconds ticking over never rebuilds the
|
|
// buttons under the dealer's thumb.
|
|
paintTank() {
|
|
const el = document.getElementById("actionTank");
|
|
if (!el || this.tankStartedAt === null) return;
|
|
|
|
const seconds = Math.floor((Date.now() - this.tankStartedAt) / 1000);
|
|
const mins = Math.floor(seconds / 60);
|
|
el.textContent = `${mins}:${String(seconds % 60).padStart(2, "0")}`;
|
|
el.classList.toggle("tankNudge", seconds >= this.NUDGE_AFTER_S && seconds < this.SLOW_AFTER_S);
|
|
el.classList.toggle("tankSlow", seconds >= this.SLOW_AFTER_S);
|
|
},
|
|
|
|
// Shown only once someone is all-in and the money has actually split — a single
|
|
// undivided pot needs no announcement, and a line that's always there stops being
|
|
// read.
|
|
renderPotBreakdown() {
|
|
// Only worth showing once someone is actually all-in. Mid-street the amounts in
|
|
// front of players are unequal all the time — the blinds alone are unequal —
|
|
// and layering that would announce a side pot on every hand before anyone had
|
|
// even acted.
|
|
const someoneAllIn = this.state.seats.some(
|
|
player => player.inHand && Betting.handTotal(player) > 0 && Betting.stackOf(this.state, player) <= 0
|
|
);
|
|
if (!someoneAllIn) return null;
|
|
|
|
const contributions = this.state.seats.map((player, index) => ({
|
|
id: player.id,
|
|
index,
|
|
contribution: Betting.handTotal(player),
|
|
folded: player.folded,
|
|
}));
|
|
|
|
const nameById = new Map(
|
|
this.state.seats.map((player, index) => [player.id, HoldemState.displayName(player, index)])
|
|
);
|
|
|
|
return Render.buildPotBreakdown(
|
|
PotCalculator.layers(contributions),
|
|
PotCalculator.uncalledReturns(contributions),
|
|
id => nameById.get(id)
|
|
);
|
|
},
|
|
|
|
// Which key presses each button. Shown on the button itself and hidden on touch
|
|
// devices — a shortcut nobody can discover may as well not exist, and a key badge
|
|
// on a phone is just clutter.
|
|
KEY_HINTS: {
|
|
actionFold: "F",
|
|
actionCheck: "C",
|
|
actionCall: "C",
|
|
actionRaise: "R",
|
|
actionAdvance: "↵",
|
|
raiseConfirm: "↵",
|
|
raiseCancel: "esc",
|
|
},
|
|
|
|
button(label, className, onClick, sublabel) {
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = `actionBtn ${className}`;
|
|
|
|
const main = document.createElement("span");
|
|
main.className = "actionBtnLabel";
|
|
main.textContent = label;
|
|
btn.appendChild(main);
|
|
|
|
const hint = this.KEY_HINTS[className];
|
|
if (hint) {
|
|
const kbd = document.createElement("kbd");
|
|
kbd.className = "actionKey";
|
|
kbd.textContent = hint;
|
|
btn.appendChild(kbd);
|
|
}
|
|
|
|
if (sublabel) {
|
|
const sub = document.createElement("span");
|
|
sub.className = "actionBtnSub";
|
|
sub.textContent = sublabel;
|
|
btn.appendChild(sub);
|
|
}
|
|
|
|
btn.addEventListener("click", onClick);
|
|
return btn;
|
|
},
|
|
|
|
// Every action taken from the bar goes through here, which is also where it gets
|
|
// recorded so it can be taken back. A misclick on Fold is the one mistake in this
|
|
// tool a dealer can't talk their way out of.
|
|
commit(label, mutate) {
|
|
this.hooks.beginAction(label);
|
|
mutate();
|
|
this.hooks.save(this.state);
|
|
this.hooks.rerender();
|
|
},
|
|
|
|
renderActions() {
|
|
const index = this.state.actingSeatIndex;
|
|
const player = this.state.seats[index];
|
|
const owed = Betting.toCall(this.state, player);
|
|
const stack = Betting.stackOf(this.state, player);
|
|
const who = HoldemState.displayName(player, index);
|
|
|
|
const row = document.createElement("div");
|
|
row.className = "actionButtons";
|
|
|
|
row.appendChild(this.button("Fold", "actionFold", () => this.commit(`${who}'s fold`, () => Betting.fold(this.state, index))));
|
|
|
|
if (owed <= 0) {
|
|
row.appendChild(this.button("Check", "actionCheck", () => this.commit(`${who}'s check`, () => Betting.check(this.state, index))));
|
|
} else if (owed >= stack) {
|
|
// Calling would put them all-in, so say that rather than quoting a number
|
|
// they can't cover.
|
|
row.appendChild(this.button("Call all-in", "actionCall", () => this.commit(`${who}'s all-in`, () => Betting.allIn(this.state, index)), Money.format(stack)));
|
|
} else {
|
|
row.appendChild(this.button("Call", "actionCall", () => this.commit(`${who}'s call`, () => Betting.call(this.state, index)), Money.format(owed)));
|
|
}
|
|
|
|
if (stack > owed) {
|
|
row.appendChild(this.button(owed > 0 ? "Raise" : "Bet", "actionRaise", () => {
|
|
this.raiseOpen = true;
|
|
this.raiseDraft = Math.min(Betting.minRaiseTo(this.state), Betting.committed(player) + stack);
|
|
this.render();
|
|
}));
|
|
}
|
|
|
|
return row;
|
|
},
|
|
|
|
// Quick amounts first, keypad second — a dealer calling "raise to sixty" wants a
|
|
// button, and only reaches for digits when the table does something odd.
|
|
renderRaisePad() {
|
|
// A toast is transient news about the action just taken; the raise pad is the
|
|
// dealer deciding the next one. On a short screen they compete for the same
|
|
// strip: the pad makes the bar 217px tall on a 640px phone, and the toast —
|
|
// which floats just above the bar — gets pushed up over the table, covering
|
|
// seats outright. Rather than squeeze both in, the older one gets out of the
|
|
// way. Nothing is lost: the toast's Undo is also the bar's own undo control.
|
|
if (typeof Toast !== "undefined" && Toast.dismiss) Toast.dismiss();
|
|
|
|
const index = this.state.actingSeatIndex;
|
|
const player = this.state.seats[index];
|
|
const stack = Betting.stackOf(this.state, player);
|
|
const maxTo = Betting.round(Betting.committed(player) + stack);
|
|
const pot = Betting.potSoFar(this.state);
|
|
// Two different minimums, and conflating them was a bug. `minTo` is clamped to
|
|
// what this player can actually put in, which is what the Min button should
|
|
// offer. `legalMin` is what the rules require — for a short stack the two are
|
|
// different, and it's the rules one the warnings have to be measured against,
|
|
// or a player whose whole stack is under a full raise never gets told that.
|
|
const legalMin = Betting.minRaiseTo(this.state);
|
|
const minTo = Math.min(legalMin, maxTo);
|
|
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "raisePad";
|
|
|
|
const quick = document.createElement("div");
|
|
quick.className = "raiseQuick";
|
|
|
|
const options = [
|
|
{ label: "Min", value: minTo },
|
|
{ label: "½ pot", value: Betting.round(this.state.currentBet + pot / 2) },
|
|
{ label: "Pot", value: Betting.round(this.state.currentBet + pot) },
|
|
{ label: "All-in", value: maxTo },
|
|
];
|
|
options.forEach(option => {
|
|
const value = Math.max(minTo, Math.min(option.value, maxTo));
|
|
const btn = this.button(option.label, "raiseQuickBtn", () => {
|
|
this.raiseDraft = value;
|
|
this.render();
|
|
}, Money.format(value));
|
|
if (Betting.round(this.raiseDraft) === value) btn.classList.add("selected");
|
|
quick.appendChild(btn);
|
|
});
|
|
wrap.appendChild(quick);
|
|
|
|
const entry = document.createElement("div");
|
|
entry.className = "raiseEntry";
|
|
|
|
const input = document.createElement("input");
|
|
input.type = "text";
|
|
input.inputMode = "decimal";
|
|
input.className = "raiseInput";
|
|
input.value = this.raiseDraft ?? minTo;
|
|
input.setAttribute("aria-label", "Raise to amount");
|
|
input.addEventListener("input", () => {
|
|
this.raiseDraft = input.value.replace(/[^0-9.]/g, "");
|
|
this.updateRaiseNote(wrap, legalMin, maxTo);
|
|
});
|
|
entry.appendChild(input);
|
|
|
|
entry.appendChild(this.button("Cancel", "raiseCancel", () => {
|
|
this.raiseOpen = false;
|
|
this.render();
|
|
}));
|
|
|
|
entry.appendChild(this.button("Raise to", "raiseConfirm", () => {
|
|
const target = Math.max(0, Math.min(Number(this.raiseDraft) || 0, maxTo));
|
|
this.raiseOpen = false;
|
|
const who = HoldemState.displayName(player, index);
|
|
this.commit(`${who}'s raise to ${Money.format(target)}`, () => Betting.raiseTo(this.state, index, target));
|
|
}));
|
|
|
|
wrap.appendChild(entry);
|
|
|
|
const note = document.createElement("p");
|
|
note.className = "raiseNote";
|
|
note.hidden = true;
|
|
wrap.appendChild(note);
|
|
this.updateRaiseNote(wrap, legalMin, maxTo);
|
|
|
|
return wrap;
|
|
},
|
|
|
|
// Says something when the amount typed isn't a legal raise. It never blocks —
|
|
// a dealer overruling the arithmetic is right more often than the arithmetic is,
|
|
// and the whole tool is built on tapping what the humans actually did.
|
|
//
|
|
// But an under-minimum raise from a player with chips behind them is the single
|
|
// most common thing that stops a home game dead, and until now the bar took it
|
|
// without a word: type 13 when the minimum is 18 and it was simply accepted. The
|
|
// engine then treats it as a short raise and correctly doesn't reopen the betting,
|
|
// which is right for an all-in and wrong for somebody who just misheard the bet.
|
|
updateRaiseNote(wrap, minTo, maxTo) {
|
|
const note = wrap.querySelector(".raiseNote");
|
|
if (!note) return;
|
|
|
|
const typed = Number(this.raiseDraft);
|
|
const current = this.state.currentBet || 0;
|
|
|
|
if (!Number.isFinite(typed) || typed <= 0) { note.hidden = true; return; }
|
|
|
|
// Going all-in for less than a full raise is perfectly legal, and worth saying
|
|
// out loud because of what follows from it.
|
|
const isAllIn = Math.abs(typed - maxTo) < Betting.EPSILON_CHIPS;
|
|
|
|
if (typed > current && typed < minTo - Betting.EPSILON_CHIPS && !isAllIn) {
|
|
note.textContent =
|
|
`${Money.format(typed)} is under the ${Money.format(minTo)} minimum. A legal raise is ${Money.format(minTo)} or more — otherwise it's a call, or all-in.`;
|
|
note.className = "raiseNote raiseNoteWarn";
|
|
note.hidden = false;
|
|
return;
|
|
}
|
|
|
|
if (isAllIn && typed > current && typed < minTo - Betting.EPSILON_CHIPS) {
|
|
note.textContent =
|
|
`All-in for less than a full raise. Legal, but it doesn't reopen the betting — anyone who has already acted owes the difference or folds.`;
|
|
note.className = "raiseNote raiseNoteInfo";
|
|
note.hidden = false;
|
|
return;
|
|
}
|
|
|
|
if (typed > current && typed < current + Betting.EPSILON_CHIPS) { note.hidden = true; return; }
|
|
|
|
if (typed > maxTo + Betting.EPSILON_CHIPS) {
|
|
note.textContent = `More than they have. This goes in as all-in for ${Money.format(maxTo)}.`;
|
|
note.className = "raiseNote raiseNoteInfo";
|
|
note.hidden = false;
|
|
return;
|
|
}
|
|
|
|
if (typed <= current) {
|
|
note.textContent = `${Money.format(typed)} is not above the ${Money.format(current)} already bet — that's a call, not a raise.`;
|
|
note.className = "raiseNote raiseNoteWarn";
|
|
note.hidden = false;
|
|
return;
|
|
}
|
|
|
|
note.hidden = true;
|
|
},
|
|
|
|
// The table is set but the cards aren't out. This is the step that used to happen
|
|
// by itself the instant the previous hand ended, which meant blinds were committed
|
|
// while people were still sitting down — and the blind badges would drift away from
|
|
// the money already posted as seats changed. Now the dealer says when.
|
|
renderDealPrompt() {
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "dealPrompt";
|
|
|
|
const preview = Betting.blindPreview(this.state);
|
|
const seated = Betting.liveSeats(this.state).length;
|
|
|
|
const line = document.createElement("p");
|
|
line.className = "dealPromptLine";
|
|
|
|
if (!preview) {
|
|
// Two players is the least you can deal. Say what's missing rather than
|
|
// offering a button that would quietly do nothing.
|
|
line.textContent = seated === 0
|
|
? "Nobody is dealt in yet. Tap a seat to add a player."
|
|
: "Only one player is dealt in — a hand needs two.";
|
|
line.classList.add("dealPromptBlocked");
|
|
wrap.appendChild(line);
|
|
return wrap;
|
|
}
|
|
|
|
const name = i => HoldemState.displayName(this.state.seats[i], i);
|
|
line.textContent =
|
|
`${name(preview.sbIndex)} posts ${Money.format(preview.smallBlind)}, ` +
|
|
`${name(preview.bbIndex)} posts ${Money.format(preview.bigBlind)}` +
|
|
`${preview.headsUp ? " · heads-up" : ""}${preview.manual ? " · blinds set by hand" : ""}`;
|
|
wrap.appendChild(line);
|
|
|
|
// Dealing relocates a button sitting on an empty chair. That has to be said
|
|
// before it happens rather than discovered afterwards — a fresh table starts
|
|
// the button on seat 1 whether or not anyone is sitting there.
|
|
if (preview.buttonMoves) {
|
|
const moved = document.createElement("p");
|
|
moved.className = "dealPromptLine dealPromptAside";
|
|
moved.textContent =
|
|
`Button moves to ${name(preview.buttonIndex)} — seat ${this.state.dealerSeatIndex + 1} is empty.`;
|
|
wrap.appendChild(moved);
|
|
}
|
|
|
|
const row = document.createElement("div");
|
|
row.className = "actionButtons";
|
|
row.appendChild(this.button("Post blinds & deal", "actionDeal", () => {
|
|
this.commit("dealing the hand", () => Betting.beginHand(this.state));
|
|
}));
|
|
wrap.appendChild(row);
|
|
|
|
return wrap;
|
|
},
|
|
|
|
renderStreetDone() {
|
|
const row = document.createElement("div");
|
|
row.className = "actionButtons";
|
|
|
|
const isRiver = this.state.street === "river";
|
|
const label = isRiver ? "Showdown — resolve pot" : `Deal ${Betting.streetLabel(Betting.STREETS[Betting.STREETS.indexOf(this.state.street) + 1])}`;
|
|
|
|
row.appendChild(this.button(label, "actionAdvance", () => {
|
|
if (isRiver) {
|
|
this.hooks.resolvePot();
|
|
return;
|
|
}
|
|
this.commit(`dealing the ${Betting.streetLabel(Betting.STREETS[Betting.STREETS.indexOf(this.state.street) + 1]).toLowerCase()}`, () => Betting.closeStreet(this.state));
|
|
}));
|
|
|
|
return row;
|
|
},
|
|
|
|
renderHandOver() {
|
|
const row = document.createElement("div");
|
|
row.className = "actionButtons";
|
|
const live = Betting.liveSeats(this.state);
|
|
const winner = live.length === 1 ? HoldemState.displayName(live[0].player, live[0].index) : null;
|
|
|
|
row.appendChild(this.button(
|
|
winner ? `Award pot to ${winner}` : "Resolve pot",
|
|
"actionAdvance",
|
|
() => this.hooks.resolvePot()
|
|
));
|
|
return row;
|
|
},
|
|
};
|
|
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = ActionBar;
|
|
}
|