Say something when the raise isn't a legal raise

Web search turned up the oversized-chip rule described as the single most common
cause of floor disputes, and specifically a home-game problem. Chasing that led to
checking what the bar does with an illegal raise, and the answer was: nothing.

With the minimum at $18 and a player holding $200, typing 13 was accepted without
a word. The engine then treated it as a short raise and correctly declined to
reopen the betting -- right for an all-in, wrong for somebody who just misheard
the bet, and either way the dealer was never told.

The raise entry now says what's wrong and still lets it through. 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, so Confirm stays enabled in every case:

  $13 is under the $18 minimum. A legal raise is $18 or more -- otherwise
    it's a call, or all-in.
  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.
  More than they have. This goes in as all-in for $200.
  $10 is not above the $10 already bet -- that's a call, not a raise.

The second one surfaces the rule fixed two rounds ago at the moment it decides
something, rather than only in the rulings sheet.

Two minimums, which is where the first attempt went wrong

The pad already had a minimum, clamped to what the player can actually put in --
correct for the Min button, wrong for the warnings. For a short stack the clamped
minimum equals their all-in, so neither warning could ever fire for exactly the
player the rule exists for. Kept separate now: one for the button, the rules one
for the warnings.

A regression the audit caught, in the same round that introduced it

The note makes the raise pad taller, --actionBarHeight grows, and the toast --
which floats just above the bar since the occlusion round -- got pushed up over
the table. Measured at 360x640: bar 122px closed, 178px with the pad, 217px with a
note, and the toast climbing from y506 to y411, covering seats outright.

A toast is transient news about the action just taken; the pad is the dealer
deciding the next one. The older one gets out of the way when the pad opens --
nothing is lost, its Undo is also the bar's own undo control. The note is tighter
on short screens, and the pot breakdown hides during entry, since which side pots
exist isn't what's being read while typing an amount. Net: 210px rather than 217px,
and no seat covered that the pad wasn't already covering at 178px.

Two rulings entries, from the same search

The oversized chip -- a single big chip pushed out silently is a call, not a raise,
however big it is; to raise you say so before it lands. And what a full raise is,
which the pad now enforces in words.

137 tests passing. Raise warnings verified across four amounts on a deep stack and
three on a short one, at eight viewports: note shows, pad stays reachable, contrast
clean, Confirm never disabled. Toast still behaves normally when the pad is closed.

Sources: cardplayer.com/.../contracts-and-poker-the-oversized-chip-rule,
pokernews.com/strategy/be-careful-when-betting-essential-wsop-rules-to-remember

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
2026-09-01 09:12:17 -06:00
co-authored by Claude Opus 5
parent d66abda05a
commit e0eb0d2079
3 changed files with 147 additions and 1 deletions
@@ -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";