Say something when the raise isn't a legal raise #56
@@ -340,6 +340,33 @@
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling">
|
||||
<h3>One big chip, nothing said</h3>
|
||||
<p>
|
||||
The most common argument at a home table. A player facing a bet who
|
||||
pushes out a single oversized chip without saying anything has
|
||||
<em>called</em> — not raised, however big the chip is. To raise with one
|
||||
chip you have to say "raise" before it lands; say it without an amount
|
||||
and the raise is the most that chip allows.
|
||||
</p>
|
||||
<p>
|
||||
Facing no bet, the same silent chip is a bet of the whole chip. And a
|
||||
clear verbal declaration in turn is binding either way — if words and
|
||||
chips disagree, whichever came first is the action.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling">
|
||||
<h3>A raise has to be a full raise</h3>
|
||||
<p>
|
||||
At least as much again as the last raise, or the big blind if there
|
||||
hasn't been one. Anything between a call and that minimum isn't a raise
|
||||
a player can make — it's a call, unless they're putting their last chips
|
||||
in. The raise pad says so if you type one, and won't stop you: your
|
||||
ruling beats the arithmetic.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling">
|
||||
<h3>The odd chip in a split pot</h3>
|
||||
<p>
|
||||
|
||||
@@ -2899,3 +2899,43 @@ textarea:focus-visible {
|
||||
position: absolute;
|
||||
inset: -7px;
|
||||
}
|
||||
|
||||
/* A note under the raise entry when the amount typed isn't a legal raise. It informs
|
||||
and never blocks: this tool records what the humans at the table actually did, and
|
||||
a dealer overruling the arithmetic is right more often than the arithmetic is.
|
||||
Under-minimum raises are the single most common thing that stops a home game, and
|
||||
the bar used to accept one without a word. */
|
||||
.raiseNote {
|
||||
margin: 0.45rem 0 0;
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.raiseNote.raiseNoteWarn {
|
||||
color: #f0b46a;
|
||||
}
|
||||
|
||||
.raiseNote.raiseNoteInfo {
|
||||
color: #9fc7e8;
|
||||
}
|
||||
|
||||
/* On a short phone the raise pad already makes the bar tall enough to cover the
|
||||
lowest seats, and the note must not make that materially worse. Measured at
|
||||
360x640: bar 122px closed, 178px with the pad open, 217px with a note. Tightened
|
||||
here so the note costs a line rather than a paragraph — the bar is a transient
|
||||
input mode with Cancel and Escape, but it shouldn't grow for the sake of it. */
|
||||
@media (max-height: 700px) {
|
||||
.raiseNote {
|
||||
margin-top: 0.3rem;
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* The pot breakdown is reference material — which side pots exist and who can win
|
||||
them. While an amount is being typed it isn't what the dealer is reading, and
|
||||
it's the tallest thing competing for the strip. */
|
||||
body.hasActionBar #actionBar:has(.raisePad) .potBreakdown {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,12 +319,26 @@ const ActionBar = {
|
||||
// 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);
|
||||
const minTo = Math.min(Betting.minRaiseTo(this.state), maxTo);
|
||||
// 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";
|
||||
@@ -360,6 +374,7 @@ const ActionBar = {
|
||||
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);
|
||||
|
||||
@@ -376,9 +391,73 @@ const ActionBar = {
|
||||
}));
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
renderStreetDone() {
|
||||
const row = document.createElement("div");
|
||||
row.className = "actionButtons";
|
||||
|
||||
Reference in New Issue
Block a user