Source Document: Block52 – Sit In _ Sit Out _ Leave Table State Logic (Cash Games).pdf in repo root
Overview
This ticket defines the complete player seating, sit-in, sit-out, leave-table, and all-in protection logic for Block52 cash games.
The system must:
- Treat seats as placeholders
- Require explicit opt-in to play
- Prevent blind evasion
- Allow graceful or immediate exits
- Protect all-in players from accidental forfeits
- Enforce rules server-side without confusing the player
1) Player States — DONE
SDK Reference: PlayerStatus enum in sdk/src/types/game.ts:51-61
Issue: block52/poker-vm#1831 | PR: block52/poker-vm#1834 (merged)
| PDF Term |
SDK Enum (PlayerStatus) |
Status |
| SEATED |
SEATED |
Aligned |
SIT-IN PENDING |
SITTING_IN |
Renamed — use SITTING_IN |
PLAYING (Active) |
ACTIVE |
Renamed — use ACTIVE |
| SITTING OUT |
SITTING_OUT |
Aligned |
REMOVED |
(not required — player removed from seat array) |
Struck — not a status |
Additional SDK statuses not in this document (used during hand play):
FOLDED — player folded current hand
ALL_IN — player is all-in in current hand
BUSTED — player has 0 chips (cash game: needs top-up, SNG: eliminated)
SHOWING — player is showing cards at showdown
WAITING — Removed in PR block52/poker-vm#1834. Was unused (zero references). Redundant with SITTING_IN.
Sub-issue: block52/poker-vm#1831 — Align player state naming between document and SDK
SEATED (Placeholder / Not Active) → PlayerStatus.SEATED
- Player holds a seat
- Player is not dealt in
- Player must choose how to enter play
SIT-IN PENDING → PlayerStatus.SITTING_IN
- Player has clicked Sit In
- A sit-in method is selected
- Player is waiting for eligibility conditions
- Player is not dealt in
PLAYING (Active) → PlayerStatus.ACTIVE
- Player is dealt cards
- Player can post blinds and act
- Player participates normally
SITTING OUT → PlayerStatus.SITTING_OUT
- Player retains seat
- Player is not dealt in
- Player is skipped for blinds and action
- Removal counter is active (cash games)
REMOVED (not required — player is simply deleted from seat array)
Player is fully removed
Seat is released
2) Initial Join Behavior (Defaults) — DONE
Sub-issue: #50 (closed) | PRs: block52/poker-vm#1836 (merged), block52/poker-vm#1841 (merged)
Current Implementation
When a player joins via NonPlayerActionType.JOIN:
During ANTE (no hand in progress) → player enters PlayerStatus.ACTIVE immediately:
// pvm/ts/src/engine/texasHoldem.ts:516-531
joinAtSeat(player: Player, seat: number): void {
this._playersMap.set(seat, player);
if (this.isHandInProgress()) {
player.updateStatus(PlayerStatus.SITTING_OUT);
} else {
player.updateStatus(PlayerStatus.ACTIVE);
}
}
During a hand (PREFLOP+) → player enters PlayerStatus.SITTING_OUT, becomes ACTIVE on next hand.
Unit tests confirming this behavior:
texasHoldem-join-and-leave.test.ts:63 — "should set player to ACTIVE when joining during ANTE"
texasHoldem-join-and-leave.test.ts:73 — "should set player to SITTING_OUT when joining during PREFLOP"
texasHoldem-join-and-leave.test.ts:100 — "should activate SITTING_OUT players when new hand starts"
texasHoldem-join-and-leave.test.ts:136 — "should not deal cards to SITTING_OUT players"
joinAction.test.ts:46 — "should successfully join a player with valid amount and seat"
What the Document Specifies (Not Yet Implemented)
The document says when a player joins:
- They should enter
PlayerStatus.SEATED (not ACTIVE or SITTING_OUT)
- A Sit In action should be available (
NonPlayerActionType.SIT_IN)
- Sit-in options are shown with defaults:
Sit In options:
Gap Analysis
| Behavior |
Document Says |
Current Code Does |
| Status on join |
SEATED (placeholder) |
ACTIVE (during ANTE) or SITTING_OUT (mid-hand) |
| Sit-in required? |
Yes — explicit opt-in via sit-in options |
No — auto-activates on join |
| Sit-in options |
2 options (Next BB / Post Now) |
Single SIT_IN action, no method selection |
SIT_IN action |
Changes SITTING_OUT → ACTIVE |
Same, but no blind-posting or BB-wait logic |
Current SitInAction (no options yet)
// pvm/ts/src/engine/actions/sitInAction.ts:6-44
class SitInAction extends BaseAction implements IAction {
verify(player: Player): Range {
if (player.status !== PlayerStatus.SITTING_OUT) {
throw new Error("Sit in action is not allowed if player is not sitting out.");
}
return { minAmount: 0n, maxAmount: 0n };
}
execute(player: Player, index: number, _amount: bigint): void {
this.verify(player);
player.updateStatus(PlayerStatus.ACTIVE);
this.game.addAction({ playerId: player.address, action: NonPlayerActionType.SIT_IN, index }, round);
}
}
Note: Current SitInAction only works from SITTING_OUT → ACTIVE. The document requires a new flow: SEATED → SITTING_IN (pending) → ACTIVE (when eligible).
3) Sit In Options — DONE
Sub-issue: #38 (open) | PVM PRs: block52/poker-vm#1836 (merged), block52/poker-vm#1841 (merged) | UI PR: block52/poker-vm#1843
Implementation Summary
PVM side (merged):
SitInAction now accepts method=next-bb or method=post-now via action data
sitInMethod is persisted on the player object
- Chip validation prevents zero-chip sit-in
UI side (PR block52/poker-vm#1843):
PlayerActionButtons rewritten with three states:
- Options panel — two radio buttons ("Next Big Blind" / "Post and Play Now") + CONFIRM button
- Pending state — pulsing waiting indicator + CANCEL button (calls sit-out)
- Sit Out button — unchanged existing behavior
sitIn() hook passes method as 4th data arg to performAction()
handleSitIn accepts method parameter
What Remains for Full Document Compliance
| Feature |
Document Says |
Current Status |
| Sit-in options UI |
2 methods (Next BB / Post Now) |
Done (PR block52/poker-vm#1843) |
| Default |
"Next Big Blind" pre-selected |
Done |
| Blind posting on sit-in |
Post missed blinds |
Not implemented — needs blind tracking |
| Dynamic UI labels |
"Post Big Blind" / "Post Small Blind and Big Blind" |
Not implemented — PVM doesn't expose missed blinds data yet |
| Zero-chip sit-in blocked |
Section 5 |
Done (PR block52/poker-vm#1841 — chip validation) |
4) Entry Restrictions for "Next Available Hand" (Silent Backend Enforcement)
Critical UX Rule
Entry restrictions are enforced silently in the backend only.
The player is never prevented from selecting the option.
Backend Eligibility Checks (Evaluated at Hand Start)
When processing a pending "Next Available Hand..." sit-in, the system must verify:
The player must NOT be brought in if they would be:
- Dealer Button
- Small Blind
Exception: Post-Flop Last-to-Act Protection
Even if the above passes, the player must not be brought in if:
- Due to dead or empty seats behind them,
- Their entry would make them last to act post-flop
This includes scenarios with:
- Dead dealer button
- Multiple seats sitting out or leaving simultaneously
If Entry Is Not Allowed
- Entry is skipped for that hand only
- Player remains in
PlayerStatus.SITTING_IN
- No UI message or error is shown
- Eligibility is re-evaluated automatically on the next hand
If Entry Is Allowed
- Required blind(s) are posted
- Player is dealt cards
- Player transitions to
PlayerStatus.ACTIVE
5) Insufficient Chips to Post Blinds
If a player attempts to sit in but does not have enough chips to post the required blinds:
- Sit-in is blocked
- Show message: "Insufficient chips to post blinds. Please buy in."
- Provide Buy-In / Add Chips action (see block52/poker-vm#774)
- Player remains in their current non-playing state
All-in posting for owed blinds is NOT allowed.
6) Sit Out Options (While Playing ACTIVE)
While PlayerStatus.ACTIVE, the player may click Sit Out (NonPlayerActionType.SIT_OUT), revealing:
- Sit Out Next Hand
- Sit Out Next Big Blind
Sit Out Next Hand
- Clicked during a hand → applies after the hand completes
- Clicked between hands → applies immediately to next deal
Sit Out Next Big Blind
- Player continues playing until they would be BB
- On that hand:
- Player does not post BB
- Player becomes
PlayerStatus.SITTING_OUT
- Player is not dealt in
7) Sitting Out Removal Counter (Cash Games)
Removal Rule
- Player is removed once the dealer button has passed their seat 3 times while sitting out
- This may result in slightly more than 3 full orbits depending on timing
Counter Behavior (Anti-Gaming)
- Counter increments only while in
PlayerStatus.SITTING_OUT
- When player clicks Sit In → counter is paused
- If player cancels sit-in and sits out again → counter resumes from previous value
- When player successfully becomes
PlayerStatus.ACTIVE → counter resets to 0
8) Leave Table & All-In Lock (Critical)
All-In Lock Rule (Non-Negotiable)
If a player is PlayerStatus.ALL_IN in the current hand, they are LOCKED into the hand and cannot forfeit it by clicking "Leave Table".
This prevents accidental forfeits, disputes, and integrity violations.
Leave Table Behavior by State
A) Player clicks Leave Table while NOT in a hand
- Player is removed immediately
- Seat is released
B) Player clicks Leave Table while IN a hand and NOT all-in
- Server immediately processes FOLD if they are required to act and a CHECK is not an option (i.e., if they are facing a bet)
- No client timeout is waited for
- Player is removed from the hand
- Chips already committed remain committed
- Seat is released
- See:
pvm/ts/src/engine/actions/forfeitAndLeaveAction.ts
C) Player clicks Leave Table while IN a hand and ALREADY ALL_IN
ALL-IN LOCK APPLIES
System behavior:
- Player is NOT folded
- Player is NOT removed from the hand
- Hand runs to completion normally
- Player remains eligible for all pots they are entitled to
After the hand completes:
- Player is automatically removed from the table
- Seat is released
Recommended UI behavior:
- Allow click but show tooltip: "You are all-in. You will leave the table after this hand completes."
Disconnect Edge Case
- If an all-in player disconnects or leaves client-side:
- All-in lock still applies
- Server must keep them in the hand until completion
9) Graceful Exit (Sit Out → Leave Table)
- Player may Sit Out first
- Finish obligations cleanly
- Click Leave Table while sitting out
- Exit without disrupting live action
Definition of Done
Pinned Dev Rule
Seat ≠ Playing. All-in ≠ Foldable. Eligibility is enforced silently.
Sub-Issues & PRs
| Section |
Issue |
PR |
Status |
| 1) Player States |
block52/poker-vm#1831 |
block52/poker-vm#1834 |
✅ Merged |
| 2) Initial Join |
#50 |
block52/poker-vm#1836, block52/poker-vm#1841 |
✅ Merged |
| 3) Sit In Options (PVM) |
#38 |
block52/poker-vm#1836, block52/poker-vm#1841 |
✅ Merged |
| 3) Sit In Options (UI) |
#38 |
block52/poker-vm#1843 |
🔄 In Review |
| 4) Entry Restrictions |
TBD |
— |
⬜ Not started |
| 5) Insufficient Chips |
TBD |
— |
⬜ Not started (chip validation done in block52/poker-vm#1841, UI message pending) |
| 6) Sit Out Options |
TBD |
— |
⬜ Not started |
| 7) Removal Counter |
TBD |
— |
⬜ Not started |
| 8) Leave Table & All-In Lock |
TBD |
— |
⬜ Not started |
| 9) Graceful Exit |
TBD |
— |
⬜ Not started |
Related Issues
- block52/poker-vm#1086 - Sit-in options after missing blinds (post now or wait for BB)
- block52/poker-vm#1793 - Dead small blind and dead button rules
- block52/poker-vm#1548 - Sitting-out "Leave table" event corruption bug
- block52/poker-vm#1192 - Auto-sit-out on timeout
- block52/poker-vm#1343 - Player action timer
- block52/poker-vm#774 - BUY CHIPS top-up (ties into insufficient chips flow)
🤖 Generated with Claude Code
Overview
This ticket defines the complete player seating, sit-in, sit-out, leave-table, and all-in protection logic for Block52 cash games.
The system must:
1) Player States — DONE
PlayerStatus)SEATEDSIT-IN PENDINGSITTING_INSITTING_INPLAYING (Active)ACTIVEACTIVESITTING_OUTREMOVEDStruck — not a statusAdditional SDK statuses not in this document (used during hand play):
FOLDED— player folded current handALL_IN— player is all-in in current handBUSTED— player has 0 chips (cash game: needs top-up, SNG: eliminated)SHOWING— player is showing cards at showdown— Removed in PR block52/poker-vm#1834. Was unused (zero references). Redundant withWAITINGSITTING_IN.SEATED (Placeholder / Not Active)→PlayerStatus.SEATEDSIT-IN PENDING→PlayerStatus.SITTING_INPLAYING (Active)→PlayerStatus.ACTIVESITTING OUT →
PlayerStatus.SITTING_OUTREMOVED(not required — player is simply deleted from seat array)Player is fully removedSeat is released2) Initial Join Behavior (Defaults) — DONE
Current Implementation
When a player joins via
NonPlayerActionType.JOIN:During ANTE (no hand in progress) → player enters
PlayerStatus.ACTIVEimmediately:During a hand (PREFLOP+) → player enters
PlayerStatus.SITTING_OUT, becomesACTIVEon next hand.Unit tests confirming this behavior:
texasHoldem-join-and-leave.test.ts:63— "should set player to ACTIVE when joining during ANTE"texasHoldem-join-and-leave.test.ts:73— "should set player to SITTING_OUT when joining during PREFLOP"texasHoldem-join-and-leave.test.ts:100— "should activate SITTING_OUT players when new hand starts"texasHoldem-join-and-leave.test.ts:136— "should not deal cards to SITTING_OUT players"joinAction.test.ts:46— "should successfully join a player with valid amount and seat"What the Document Specifies (Not Yet Implemented)
The document says when a player joins:
PlayerStatus.SEATED(notACTIVEorSITTING_OUT)NonPlayerActionType.SIT_IN)Sit In options:
Gap Analysis
SEATED(placeholder)ACTIVE(during ANTE) orSITTING_OUT(mid-hand)SIT_INaction, no method selectionSIT_INactionSITTING_OUT→ACTIVECurrent
SitInAction(no options yet)Note: Current
SitInActiononly works fromSITTING_OUT→ACTIVE. The document requires a new flow:SEATED→SITTING_IN(pending) →ACTIVE(when eligible).3) Sit In Options — DONE
Implementation Summary
PVM side (merged):
SitInActionnow acceptsmethod=next-bbormethod=post-nowvia action datasitInMethodis persisted on the player objectUI side (PR block52/poker-vm#1843):
PlayerActionButtonsrewritten with three states:sitIn()hook passesmethodas 4thdataarg toperformAction()handleSitInacceptsmethodparameterWhat Remains for Full Document Compliance
4) Entry Restrictions for "Next Available Hand" (Silent Backend Enforcement)
Critical UX Rule
Backend Eligibility Checks (Evaluated at Hand Start)
When processing a pending "Next Available Hand..." sit-in, the system must verify:
The player must NOT be brought in if they would be:
Exception: Post-Flop Last-to-Act Protection
Even if the above passes, the player must not be brought in if:
This includes scenarios with:
If Entry Is Not Allowed
PlayerStatus.SITTING_INIf Entry Is Allowed
PlayerStatus.ACTIVE5) Insufficient Chips to Post Blinds
If a player attempts to sit in but does not have enough chips to post the required blinds:
All-in posting for owed blinds is NOT allowed.
6) Sit Out Options (While
PlayingACTIVE)While
PlayerStatus.ACTIVE, the player may click Sit Out (NonPlayerActionType.SIT_OUT), revealing:Sit Out Next Hand
Sit Out Next Big Blind
PlayerStatus.SITTING_OUT7) Sitting Out Removal Counter (Cash Games)
Removal Rule
Counter Behavior (Anti-Gaming)
PlayerStatus.SITTING_OUTPlayerStatus.ACTIVE→ counter resets to 08) Leave Table & All-In Lock (Critical)
All-In Lock Rule (Non-Negotiable)
This prevents accidental forfeits, disputes, and integrity violations.
Leave Table Behavior by State
A) Player clicks Leave Table while NOT in a hand
B) Player clicks Leave Table while IN a hand and NOT all-in
pvm/ts/src/engine/actions/forfeitAndLeaveAction.tsC) Player clicks Leave Table while IN a hand and ALREADY
ALL_INALL-IN LOCK APPLIES
System behavior:
After the hand completes:
Recommended UI behavior:
Disconnect Edge Case
9) Graceful Exit (Sit Out → Leave Table)
Definition of Done
Pinned Dev Rule
Sub-Issues & PRs
Related Issues
🤖 Generated with Claude Code