diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml index ee8482b..751f20b 100644 --- a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml +++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml @@ -549,8 +549,12 @@
- If the fourth board card gets turned before the betting round is finished, - that card is out of play for the round. Finish the betting, then burn and - turn what would have been the fifth card in the fourth card's place. -
-- A one-off is usually just backed up if no one has acted behind them. - Doing it repeatedly is what earns a penalty. -
-+ If the fourth board card gets turned before the betting round is finished, + that card is out of play for the round. Finish the betting, then burn and + turn what would have been the fifth card in the fourth card's place. +
+- Any player may ask for a clock once a decision is taking unreasonably - long; it is the dealer, not the table, who grants it. The player then - gets a set time and a countdown out loud — thirty seconds and ten is the - usual form. -
-- If the count reaches zero with no action, the hand is dead if - they were facing a bet, and checked if they were not: nobody is - folded for slowness when checking was free. The action bar runs the - count once you tap Call the clock, and offers the ruling as one - button when it ends. -
-+ A one-off is usually just backed up if no one has acted behind them. + Doing it repeatedly is what earns a penalty. +
+- The player left of the big blind may put out twice it before the cards, - without looking. It is a blind bet, not a raise made in turn, so it does - three things at once: everyone else acts first, the minimum raise becomes - twice the straddle, and the straddler still has the option when the - action comes back — they may raise even if nobody raised into them. -
-- Re-straddles carry on to the left, each doubling again, and stop before - the small blind. Tap Straddle on the deal prompt and the tool - posts it, puts the right player on the clock and prices the raise; - the seat wears an STR badge for the hand. -
-+ Any player may ask for a clock once a decision is taking unreasonably + long; it is the dealer, not the table, who grants it. The player then + gets a set time and a countdown out loud — thirty seconds and ten is the + usual form. +
++ If the count reaches zero with no action, the hand is dead if + they were facing a bet, and checked if they were not: nobody is + folded for slowness when checking was free. The action bar runs the + count once you tap Call the clock, and offers the ruling as one + button when it ends. +
+- Somebody shoving in less than a full raise — say $3 more when the last - raise was $8 — does not give the players behind them another go. Anyone - who has already acted owes the difference or folds; they don't get to - raise again because a short stack happened to land between them. -
-- A player who hadn't acted yet keeps full rights and may still raise, and - the minimum raise stays measured off the last full raise, not - off the short one. The action bar follows this for you. -
-+ The player left of the big blind may put out twice it before the cards, + without looking. It is a blind bet, not a raise made in turn, so it does + three things at once: everyone else acts first, the minimum raise becomes + twice the straddle, and the straddler still has the option when the + action comes back — they may raise even if nobody raised into them. +
++ Re-straddles carry on to the left, each doubling again, and stop before + the small blind. Tap Straddle on the deal prompt and the tool + posts it, puts the right player on the clock and prices the raise; + the seat wears an STR badge for the hand. +
+- Bet more than anyone was able to call and the excess is returned to you - before the showdown — it was never contested, so it isn't part of the - pot. The pot breakdown shows it as "uncalled" rather than pretending it's - a side pot you could win. -
-+ Somebody shoving in less than a full raise — say $3 more when the last + raise was $8 — does not give the players behind them another go. Anyone + who has already acted owes the difference or folds; they don't get to + raise again because a short stack happened to land between them. +
++ A player who hadn't acted yet keeps full rights and may still raise, and + the minimum raise stays measured off the last full raise, not + off the short one. The action bar follows this for you. +
+- The most common argument at a home table. A player facing a bet who - pushes out a single oversized chip without saying anything has - called — 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. -
-- 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. -
-+ Bet more than anyone was able to call and the excess is returned to you + before the showdown — it was never contested, so it isn't part of the + pot. The pot breakdown shows it as "uncalled" rather than pretending it's + a side pot you could win. +
+- 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. -
-+ The most common argument at a home table. A player facing a bet who + pushes out a single oversized chip without saying anything has + called — 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. +
++ 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. +
+- If there was a bet on the river, the last player to bet or raise shows - first. If the river was checked all the way round, it's the first live - player left of the button. Nobody has to be asked twice — a player may - always turn their hand over early if they want to. -
-- When everyone is all-in before the river there was no final round to - lead, so it falls to whoever last put money in, on whichever street that - was. The action bar names the seat and the reason at every showdown, so - the call is already made before anyone asks. -
-+ 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. +
+- When a chopped pot won't divide evenly, the odd chip goes to the first - player left of the button, not to whoever the dealer happened to tick - first. Pot payouts here order the winners that way, so the leftover lands - on the right seat every time. -
-+ If there was a bet on the river, the last player to bet or raise shows + first. If the river was checked all the way round, it's the first live + player left of the button. Nobody has to be asked twice — a player may + always turn their hand over early if they want to. +
++ When everyone is all-in before the river there was no final round to + lead, so it falls to whoever last put money in, on whichever street that + was. The action bar names the seat and the reason at every showdown, so + the call is already made before anyone asks. +
+- A bet is whatever goes in on the first trip to the stack, unless the - amount was said out loud first. Reaching back for more chips without - having said "raise" is a string bet, and only the first motion stands. - Saying the number is always enough on its own: "raise to sixty" binds - whatever order the chips arrive in. -
-- It's the rule home games invoke most and enforce least, and both are - fine — the point is that a player can't watch the table react and then - decide how much to bet. Say it, or push it all at once. -
-+ When a chopped pot won't divide evenly, the odd chip goes to the first + player left of the button, not to whoever the dealer happened to tick + first. Pot payouts here order the winners that way, so the leftover lands + on the right seat every time. +
++ A bet is whatever goes in on the first trip to the stack, unless the + amount was said out loud first. Reaching back for more chips without + having said "raise" is a string bet, and only the first motion stands. + Saying the number is always enough on its own: "raise to sixty" binds + whatever order the chips arrive in. +
++ It's the rule home games invoke most and enforce least, and both are + fine — the point is that a player can't watch the table react and then + decide how much to bet. Say it, or push it all at once. +
+
Follows the Tournament Directors Association rules, which most rooms and
diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css
index 860a271..84e953a 100644
--- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css
+++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css
@@ -2179,29 +2179,60 @@ input[type="checkbox"] {
max-width: 460px;
max-height: 86vh;
overflow-y: auto;
+ overscroll-behavior: contain;
background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
border: 1px solid var(--line);
border-bottom: none;
border-radius: var(--r-lg) var(--r-lg) 0 0;
- padding: var(--sp-1) var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom, 0px));
+ /* No padding at the top: the header owns it, because the header is sticky and a
+ gap above a sticky thing is a gap the list scrolls through. The seat menu's
+ sheet is built the same way, one section down. */
+ padding: 0 var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
}
+/* Stuck to the top of the sheet's own scrollport, for the reason the seat menu's header
+ is: the way out should not scroll away. This sheet is the longer of the two and never
+ had it. Measured before: on a 360x640 phone the twelve shut rulings are 791px of a
+ 549px sheet, so at the bottom of that scroll Done sits 221px above the top edge — and
+ on a 390x844 phone with one ruling opened, which is the only reason anybody is in
+ here, 262px. A dealer who has read the answer has to scroll back past it to leave. */
.rulingsHeader {
- display: flex;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
- gap: var(--sp-2);
- margin-bottom: var(--sp-2);
+ column-gap: var(--sp-2);
+ /* Out to the sheet's edges and back again, so nothing shows through either side of
+ a header sitting over a scrolling list. */
+ margin: 0 calc(var(--sp-4) * -1) var(--sp-2);
+ padding: var(--sp-1) var(--sp-4) var(--sp-2);
+ background: var(--surface-2);
+ border-bottom: 1px solid var(--line);
+}
+
+/* The grip is inside the header now rather than above it — above a sticky element is
+ the one place on this sheet that scrolls out of sight. */
+.rulingsHeader .seatSheetGrip {
+ grid-column: 1 / -1;
}
.rulingsHeader h2 {
- flex: 1;
margin: 0;
font-size: var(--fs-lg);
font-weight: 500;
color: var(--text-bright);
}
+/* Two boxes of six that are not boxes at all: `display: contents` hands the twelve
+ rulings straight to the list, so every layout but landscape sees the single column it
+ always had. Landscape turns them back into boxes and stands them side by side. */
+.rulingsColumn {
+ display: contents;
+}
+
.rulingsIntro {
margin: 0 0 var(--sp-4);
color: var(--text-muted);
@@ -5853,6 +5884,116 @@ body.hasActionBar #toastHost {
inset: max(24px, 12%) 44px min(26px, 12%);
}
+ /* ---------- Common rulings: the sheet becomes the screen ----------
+
+ Everywhere else this is a bottom sheet a dealer scrolls, and that works because
+ upright there is height to scroll through. Sideways there is not. Measured at
+ 844x390 before this: a 460px-wide sheet standing in the middle of an 844px window
+ with 384px of that window unused either side of it, 754px of rulings in 334px of
+ scrollport, 111px of it spent on a title and a caveat that stand still — four of
+ the twelve names on screen, and the dealer is looking one up with an argument
+ going on at the table.
+
+ So the sheet takes the window, and the width pays for the height. Material's rule
+ for a bottom sheet on a landscape screen is to stop being one — a side sheet on a
+ wide screen, a full-screen dialog when the content needs real scrolling — and this
+ is the tool's own move as well: the action bar becomes a right-hand rail under
+ exactly this query, for exactly this reason.
+
+ The twelve go into two columns of six. Measured after: all twelve names on screen
+ at 844x390 with nothing to scroll at all, and the columns are 400px wide, which is
+ a better measure for the prose inside an open ruling than the 460 it had.
+
+ Capped rather than unbounded, because this query also matches a short desktop
+ window, and two columns of a 1400px window would be 680px lines of body text. */
+ #rulingsOverlay {
+ align-items: stretch;
+ }
+
+ #rulingsSheet {
+ max-width: 62rem;
+ max-height: none;
+ height: 100%;
+ border: none;
+ border-radius: 0;
+ /* The list inside does the scrolling now, so the sheet itself does none. */
+ overflow: hidden;
+ padding:
+ var(--sp-2)
+ calc(var(--sp-4) + env(safe-area-inset-right, 0px))
+ calc(var(--sp-2) + env(safe-area-inset-bottom, 0px))
+ calc(var(--sp-4) + env(safe-area-inset-left, 0px));
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ grid-template-rows: auto minmax(0, 1fr) auto;
+ grid-template-areas:
+ "title intro done"
+ "list list list"
+ "src src src";
+ column-gap: var(--sp-3);
+ row-gap: var(--sp-2);
+ }
+
+ /* The title, the caveat and the way out share one line, which is the whole of the
+ 111px this screen was spending on them. `display: contents` because the three sit
+ in two different boxes in the markup and the sheet's own grid is where they need
+ to be. The sticky positioning goes with the box it is dropping, and is not wanted
+ here: nothing above the list scrolls, so nothing has to stick. */
+ .rulingsHeader {
+ display: contents;
+ }
+
+ /* A grab handle for a sheet that is not a sheet any more. */
+ .rulingsHeader .seatSheetGrip {
+ display: none;
+ }
+
+ .rulingsHeader h2 {
+ grid-area: title;
+ align-self: center;
+ }
+
+ .rulingsIntro {
+ grid-area: intro;
+ align-self: center;
+ margin: 0;
+ font-size: var(--fs-sm);
+ }
+
+ #closeRulingsBtn {
+ grid-area: done;
+ align-self: center;
+ }
+
+ /* Two tracks holding two independent flows, not one flow wrapped into two. That is
+ what the pair of wrappers in the markup buys: a grid with twelve children of its
+ own would pair them into six shared rows, and opening the third ruling on the left
+ would push the ninth down the right-hand side to keep the row square. Here a
+ column only ever moves itself.
+
+ `scrollbar-gutter` because the gutter arrives the moment a ruling is opened, and
+ without the reservation it takes 8px off the tracks and slides the right-hand
+ column sideways while the dealer is reading it. */
+ .rulingsList {
+ grid-area: list;
+ min-height: 0;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ scrollbar-gutter: stable;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ column-gap: var(--sp-5);
+ align-items: start;
+ }
+
+ .rulingsColumn {
+ display: block;
+ }
+
+ .rulingsSource {
+ grid-area: src;
+ padding-top: var(--sp-1);
+ }
}
/* ---------- Short phones, upright ----------
diff --git a/tests/holdem/theRulings.test.js b/tests/holdem/theRulings.test.js
index 6d61211..e0c808a 100644
--- a/tests/holdem/theRulings.test.js
+++ b/tests/holdem/theRulings.test.js
@@ -40,6 +40,35 @@ const ruleFor = selector => {
return CSS.slice(at, CSS.indexOf("}", at));
};
+// Every @media block written at exactly this condition, concatenated — the same helper
+// theHeader.test.js uses, and for the same reason: this condition is opened more than
+// once in the file, and a helper that stops at the first finds a block with none of
+// these rules in it and reports them missing.
+const mediaBlock = condition => {
+ const found = [];
+ const needle = "@media ";
+ for (let at = CSS.indexOf(needle); at >= 0; at = CSS.indexOf(needle, at + 1)) {
+ const open = CSS.indexOf("{", at);
+ if (CSS.slice(at + needle.length, open).trim() !== condition) continue;
+ let depth = 0;
+ for (let i = open; i < CSS.length; i++) {
+ if (CSS[i] === "{") depth++;
+ else if (CSS[i] === "}" && --depth === 0) { found.push(CSS.slice(open + 1, i)); break; }
+ }
+ }
+ assert.ok(found.length > 0, `no @media block written as "${condition}"`);
+ return found.join("\n");
+};
+
+const ruleIn = (block, selector) => {
+ const re = new RegExp(`(^|[},])\\s*${selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\{([^}]*)\\}`, "m");
+ const found = block.match(re);
+ assert.ok(found, `there is no \`${selector}\` rule in that block`);
+ return found[2];
+};
+
+const SIDEWAYS = () => mediaBlock("(orientation: landscape) and (max-height: 560px)");
+
// Each ruling as its own slice of markup.
const rulings = () => {
const parts = sheet().split('