Say something when the raise isn't a legal raise #56
@@ -340,6 +340,33 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</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">
|
<section class="ruling">
|
||||||
<h3>The odd chip in a split pot</h3>
|
<h3>The odd chip in a split pot</h3>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -2899,3 +2899,43 @@ textarea:focus-visible {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -7px;
|
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
|
// 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.
|
// button, and only reaches for digits when the table does something odd.
|
||||||
renderRaisePad() {
|
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 index = this.state.actingSeatIndex;
|
||||||
const player = this.state.seats[index];
|
const player = this.state.seats[index];
|
||||||
const stack = Betting.stackOf(this.state, player);
|
const stack = Betting.stackOf(this.state, player);
|
||||||
const maxTo = Betting.round(Betting.committed(player) + stack);
|
const maxTo = Betting.round(Betting.committed(player) + stack);
|
||||||
const pot = Betting.potSoFar(this.state);
|
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");
|
const wrap = document.createElement("div");
|
||||||
wrap.className = "raisePad";
|
wrap.className = "raisePad";
|
||||||
@@ -360,6 +374,7 @@ const ActionBar = {
|
|||||||
input.setAttribute("aria-label", "Raise to amount");
|
input.setAttribute("aria-label", "Raise to amount");
|
||||||
input.addEventListener("input", () => {
|
input.addEventListener("input", () => {
|
||||||
this.raiseDraft = input.value.replace(/[^0-9.]/g, "");
|
this.raiseDraft = input.value.replace(/[^0-9.]/g, "");
|
||||||
|
this.updateRaiseNote(wrap, legalMin, maxTo);
|
||||||
});
|
});
|
||||||
entry.appendChild(input);
|
entry.appendChild(input);
|
||||||
|
|
||||||
@@ -376,9 +391,73 @@ const ActionBar = {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
wrap.appendChild(entry);
|
wrap.appendChild(entry);
|
||||||
|
|
||||||
|
const note = document.createElement("p");
|
||||||
|
note.className = "raiseNote";
|
||||||
|
note.hidden = true;
|
||||||
|
wrap.appendChild(note);
|
||||||
|
this.updateRaiseNote(wrap, legalMin, maxTo);
|
||||||
|
|
||||||
return wrap;
|
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() {
|
renderStreetDone() {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "actionButtons";
|
row.className = "actionButtons";
|
||||||
|
|||||||
Reference in New Issue
Block a user