Handle the messy table: joining, leaving, and arguing about blinds #45

Merged
jheaps merged 1 commits from feature/casino-holdem-messy-tables into master 2026-09-01 09:29:59 -06:00
Owner

Home games don't run like a cardroom. I probed each messy case rather than assuming, and found four problems — one of which loses chips. Plus the mobile occlusion sweep. Stacked on #44.

A player taken out mid-hand took their chips with them

computeContribution opened with if (!player.inHand) return 0, so the moment a dealer switched a seat off, everything that seat had already put in vanished from the pot.

Measured: four players in for $2 each → pot $8. Take one out → pot $6. Their $2 was still sitting on the seat record, it just stopped counting. A dealer following the tool pays out $6 of an $8 pot.

Chips already in the middle are dead money: they stay in the pot and go to whoever wins, and they are not the departing player's to take back. The inHand check now guards only the table-wide base-pot default — which does have to stay tied to being dealt in, or every empty chair would be treated as having anted. The pot wizard counts a departed seat as folded, so its chips are in the pot but it can never be picked as a winner.

Passing the button before the cards come out

Moving the button left the posted blinds where they were while everything positional started reading off the new spot — money and blind positions silently disagreeing. While nothing but the blinds are in, moving the button now refunds and re-posts them. Pass it round six times, chip count unchanged. Once anyone has acted, the blinds are settled and it stops offering.

Naming the blinds by hand

There was no way to overrule the positional blinds. The seat menu now offers Make small blind / Make big blind while only the blinds are in, and a ringed badge marks a blind that was named rather than dealt. Naming one pins the other. Refused if it would put both blinds on one player or post to someone not dealt in. Lasts one hand — carrying it forward would freeze the blinds on two people all night.

The first hand, and a table that empties out

A fresh table puts the button on seat 1 whether or not anyone is sitting there, and a table can empty out around it. The button is moved onto a real player before a hand is dealt.

Joining mid-hand

A newcomer went straight into the live seats and would be asked to act in a hand they hadn't paid into. They now sit the hand out and are a full player from the next one.


Nothing is covered on a phone any more

Swept 13 viewport sizes with elementFromPoint, five points per control, across the table and every overlay.

Before:

Problem Where
Action bar covered the bottom seats 62px of felt at 360x640
Toast covered up to 6 seats at once — Undo button included, so a tap meant for a player hit "Undo" short screens
Table 765px tall in a 390px window → 6 of 9 seats off-screen all landscape

Fixes:

  • the action bar's real height is published as --actionBarHeight and drives both the page's bottom padding and the toast — replacing a hardcoded 9rem that was wrong the moment the raise pad opened
  • landscape sizes the felt from the height that's left. The first attempt did nothing: padding-bottom is a percentage of the containing block's width, so narrowing the wrap left it full height — and a max-width: 900px rule later in the file was overriding it anyway. Set outright now, and placed at the end of the sheet.
  • on short screens the toast floats over the action bar's status readout rather than over the seats. Nowhere is clear of both; covering something you re-read beats covering something you tap.
  • headers go compact on short screens, which is where the vertical room came from

Result: 13/13 clean, 360x640 through 844x390, portrait and landscape, measured with the toast up (worst case).

Also

A marker that flickered: classList.toggle(cls, undefined) toggles rather than clearing, and manual is undefined for positional blinds — so the hand-picked ring appeared on alternate renders. Caught by validation, fixed with !!.

Validation

18 new tests (76 total, all passing), plus Playwright: the 13-size occlusion sweep, overlay controls at 6 sizes, and a UI walkthrough driving the real long-press menu — naming a blind, re-rendering to confirm the marker is stable, and confirming it clears next hand. Chips conserved throughout. No console errors.

One thing I did not change

Pressing Next hand with an unresolved pot still discards those chips from the accounting (they leave with amountOverride being cleared). That's arguably intended — it's the abandon-the-hand path, and the undo toast covers a misclick — but it is the same shape as the bug fixed above. Say the word and I'll guard it.

Home games don't run like a cardroom. I probed each messy case rather than assuming, and found four problems — one of which **loses chips**. Plus the mobile occlusion sweep. Stacked on #44. ## A player taken out mid-hand took their chips with them `computeContribution` opened with `if (!player.inHand) return 0`, so the moment a dealer switched a seat off, everything that seat had already put in vanished from the pot. **Measured:** four players in for $2 each → pot $8. Take one out → **pot $6**. Their $2 was still sitting on the seat record, it just stopped counting. A dealer following the tool pays out $6 of an $8 pot. Chips already in the middle are dead money: they stay in the pot and go to whoever wins, and they are not the departing player's to take back. The `inHand` check now guards only the table-wide base-pot default — which does have to stay tied to being dealt in, or every empty chair would be treated as having anted. The pot wizard counts a departed seat as folded, so its chips are in the pot but it can never be picked as a winner. ## Passing the button before the cards come out Moving the button left the posted blinds where they were while everything positional started reading off the new spot — money and blind positions silently disagreeing. While nothing but the blinds are in, moving the button now refunds and re-posts them. Pass it round six times, chip count unchanged. Once anyone has acted, the blinds are settled and it stops offering. ## Naming the blinds by hand There was no way to overrule the positional blinds. The seat menu now offers **Make small blind** / **Make big blind** while only the blinds are in, and a ringed badge marks a blind that was named rather than dealt. Naming one pins the other. Refused if it would put both blinds on one player or post to someone not dealt in. Lasts one hand — carrying it forward would freeze the blinds on two people all night. ## The first hand, and a table that empties out A fresh table puts the button on seat 1 whether or not anyone is sitting there, and a table can empty out around it. The button is moved onto a real player before a hand is dealt. ## Joining mid-hand A newcomer went straight into the live seats and would be asked to act in a hand they hadn't paid into. They now sit the hand out and are a full player from the next one. --- # Nothing is covered on a phone any more Swept **13 viewport sizes** with `elementFromPoint`, five points per control, across the table and every overlay. **Before:** | Problem | Where | | --- | --- | | Action bar covered the bottom seats | 62px of felt at 360x640 | | Toast covered up to **6 seats at once** — Undo button included, so a tap meant for a player hit "Undo" | short screens | | Table 765px tall in a 390px window → **6 of 9 seats off-screen** | all landscape | **Fixes:** - the action bar's real height is published as `--actionBarHeight` and drives both the page's bottom padding and the toast — replacing a hardcoded `9rem` that was wrong the moment the raise pad opened - landscape sizes the felt from the height that's left. *The first attempt did nothing:* `padding-bottom` is a percentage of the **containing block's width**, so narrowing the wrap left it full height — and a `max-width: 900px` rule later in the file was overriding it anyway. Set outright now, and placed at the end of the sheet. - on short screens the toast floats over the action bar's **status readout** rather than over the seats. Nowhere is clear of both; covering something you re-read beats covering something you tap. - headers go compact on short screens, which is where the vertical room came from **Result: 13/13 clean**, 360x640 through 844x390, portrait and landscape, measured with the toast up (worst case). ## Also A marker that flickered: `classList.toggle(cls, undefined)` *toggles* rather than clearing, and `manual` is undefined for positional blinds — so the hand-picked ring appeared on alternate renders. Caught by validation, fixed with `!!`. ## Validation 18 new tests (**76 total, all passing**), plus Playwright: the 13-size occlusion sweep, overlay controls at 6 sizes, and a UI walkthrough driving the real long-press menu — naming a blind, re-rendering to confirm the marker is stable, and confirming it clears next hand. Chips conserved throughout. No console errors. ## One thing I did not change Pressing **Next hand** with an unresolved pot still discards those chips from the accounting (they leave with `amountOverride` being cleared). That's arguably intended — it's the abandon-the-hand path, and the undo toast covers a misclick — but it is the same shape as the bug fixed above. Say the word and I'll guard it.
jheaps changed target branch from feature/casino-holdem-touch-mobile to master 2026-09-01 09:29:56 -06:00
jheaps added 1 commit 2026-09-01 09:29:56 -06:00
Home games don't run like a cardroom. People walk up mid-hand, storm off
mid-hand, and pass the button round three times before anyone deals. Probing
each of those found four problems, one of which loses chips.

A player taken out mid-hand took their chips with them

`computeContribution` opened with `if (!player.inHand) return 0`, so the moment
a dealer switched a seat off, everything that seat had already put in vanished
from the pot. Measured: four players in for $2 each, one taken out, pot drops
from $8 to $6. Their $2 was still sitting on the seat record — it just stopped
counting. A dealer following the tool would have paid out $6 of an $8 pot.

Chips already pushed into the middle are dead money. They stay in the pot and go
to whoever wins, whether or not the player is still in the hand, and it is not
theirs to take back. The `inHand` check now guards only the table-wide base-pot
default, which does have to stay tied to being dealt in or every empty chair
would be treated as having anted. The pot wizard counts a departed seat as
folded, so its chips are in the pot but it can never be picked as a winner.

Passing the button before the cards come out

Moving the button left the posted blinds where they were while everything
positional started reading off the new spot, so the money and the blind
positions silently disagreed. While nothing but the blinds are in, moving the
button now refunds them and re-posts from where it lands — pass it round six
times and the chip count is unchanged. Once anybody has acted the blinds are
settled and it stops offering.

Naming the blinds by hand

Blinds were purely positional with no way to overrule them. The seat menu now
offers "Make small blind" / "Make big blind" while only the blinds are in, and a
badge with a ring marks a blind that was named rather than dealt. Naming one
pins the other where it is. It's refused if it would put both blinds on one
player or post to somebody who isn't dealt in, and it lasts one hand — carrying
it forward would freeze the blinds on two people for the night.

The first hand, and a table that empties out

A fresh table puts the button on seat 1 whether or not anyone is sitting there,
and a table can empty out around it. The button is moved onto a real player
before a hand is dealt.

Joining mid-hand

A newcomer was added straight into the live seats and would be asked to act in a
hand they hadn't been paying into. They now sit the hand out and are a full
player from the next one.

Nothing is covered on a phone any more

Swept 13 viewport sizes with elementFromPoint, five points per control, on the
table and every overlay. Before: the action bar covered the bottom seats on
short screens (62px of felt at 360x640), the toast covered up to six seats at
once — Undo button included, so a tap meant for a player hit "Undo" — and in
landscape the table came out 765px tall in a 390px window, putting six of nine
seats off-screen entirely.

- the action bar's real height is published as `--actionBarHeight` and drives
  both the page's bottom padding and the toast, replacing a hardcoded 9rem that
  was wrong the moment the raise pad opened
- landscape sizes the felt from the height that's left, not from width. The
  first attempt did nothing: `padding-bottom` is a percentage of the containing
  block's *width*, so narrowing the wrap left it full height, and a
  `max-width: 900px` rule later in the file was overriding it anyway. It's set
  outright now and lives at the end of the sheet.
- on short screens the toast floats over the action bar's status readout rather
  than over the seats. Nowhere is clear of both; covering something you re-read
  beats covering something you tap.
- headers go compact on short screens, which is where the vertical room came from

13/13 sizes clean, 360x640 through 844x390, portrait and landscape.

Also fixes a marker that flickered: `classList.toggle(cls, undefined)` toggles
rather than clearing, and `manual` is undefined for positional blinds, so the
hand-picked ring appeared on alternate renders.

18 new tests (76 total, all passing).

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
jheaps merged commit f5108de288 into master 2026-09-01 09:29:59 -06:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jheaps/JoshHeaps.Net#45