Commit Graph

2 Commits

Author SHA1 Message Date
thejayman77 7382bc8638 Fix the fold experience: honest actions, matched state, action identity
Reported as "folding looks broken". It was five separate defects.

1. Fold silently became Check. sanitise() rewrote FOLD to CHECK whenever
   checking was free, so a UI showing a Fold button folded nothing and the
   player kept being asked to act. Folding is legal at any turn — it simply
   mucks — so FOLD is now honoured literally. The engine must never substitute a
   different action than the caller asked for. The reverse rewrite (an illegal
   CHECK facing a bet becoming FOLD) is legitimate and stays.

   Safe by construction: no bot emits FOLD when it can check, and the 20k-hand
   simulation reproduces byte-identical numbers (289.12 / 113.19 / -195.64).

2. Snapshot and offer could describe different moments. The 32-deep frame
   channel let the engine race far ahead of the animation, so the action on
   offer could belong to a later street, or another hand. The channel is now
   RENDEZVOUS, capping the engine at one frame ahead, and UiState.liveOffer()
   only surfaces an offer whose hand and street match the table on screen.

3. Stale and double taps could act on a later decision. DecisionOffer now
   carries a token; submit() requires it and rejects anything stale, so a second
   tap is dropped rather than applied to whatever comes next.

4. A real fold was invisible. The hero kept normal cards and no folded state, so
   a correctly processed fold looked like a bug. Cards now dim, FOLDED shows in
   red, and the action bar explains the player is sitting out.

5. Non-atomic UiState updates from two coroutines now use update {}.

Also: Fold is hidden when checking is free (folding a free hand is never
correct, and offering it invites an accidental muck), and onCleared no longer
calls human.cancel() — viewModelScope is already cancelled by then so the launch
never ran; scope cancellation already propagates into act()'s finally.

The delivery tests were weak as charged: no slow consumer, and not the app's
capacity. Replaced with a genuinely slow consumer measuring how far the engine
runs ahead — asserting <= 1 on RENDEZVOUS, and > 1 on a 32-deep buffer to
document why the buffer was removed.

Verified on the emulator (physical device untouched): folded facing a bet, hero
showed FOLDED, was never asked again that hand, and play advanced to hand 2.

Tests: 55 -> 62, green on jvmTest and testAndroidHostTest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:04:44 -04:00
thejayman77 950c7ceb57 Playable Android table
The game runs on device: verified on a Pixel 10 Pro emulator (Android 17) by
installing, tapping through a hand, and confirming it advanced pre-flop to flop
with correct pot, folds, and re-offered action.

App:
- :app module on AGP 9.2.1. Note AGP 9 has built-in Kotlin support, so applying
  org.jetbrains.kotlin.android conflicts with it ("extension with name 'kotlin'
  already registered"); only android.application + kotlin.compose are applied,
  matching recipeze.
- PokerViewModel runs a continuous cash game and publishes to Compose.
- Compose table: opponents, board, pot, hero, action bar with a raise slider.

Frames are queued, not conflated. An all-in runout emits flop, turn and river
microseconds apart; pushing those into a StateFlow would collapse them and the
board would jump from empty to complete. The engine's suspending observer sends
into a Channel, a consumer paces each frame, and only then is StateFlow updated
— so backpressure paces the engine rather than the UI dropping frames. Three
tests cover this, including a characterisation test showing a conflating
StateFlow does lose the intermediate frames.

Assets:
- tools/generate_card_assets.sh rasterises the SVGs into four density buckets
  using sips, which renders SVG directly — no librsvg or ImageMagick.
- Resource names are prefixed card_ because Android resource names may not start
  with a digit (10_of_clubs would be rejected).
- CardArt.kt maps deck index to drawable via static R references, so R8 resource
  shrinking cannot strip the artwork the way getIdentifier lookups would risk.

Layout fixes found by actually looking at the running app: five opponents did
not fit a fixed-width scrolling row (Enzo was off-screen), the header collided
with the status bar clock, and the board floated against a large dead space.

Tests: 52 -> 55, green on jvmTest and testAndroidHostTest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 17:23:34 -04:00