Publish the all-in runout street by street
During an all-in runout, dealRemainingBoard() dealt every remaining card without touching currentStreet, so a pre-flop all-in produced a terminal snapshot labelled PREFLOP carrying a five-card board. Setting currentStreet = RIVER would fix the label but leave a second problem: the board jumped from empty to complete in a single snapshot, so the UI could not animate the runout — the moment a poker table most needs to. Instead dealRemainingBoard() is now suspend and publishes each street as it lands, which keeps currentStreet honest as a consequence rather than as a special case. Each runout street also sweeps its betting into the pot via prepareRound(), matching the normal street transition. Verified by reverting: the board went 0 -> 5 with the terminal snapshot labelled PREFLOP, and three of the four new tests failed. Tests: 48 -> 52, green on jvmTest and testAndroidHostTest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -305,7 +305,15 @@ class Table(
|
||||
board.add(deck.deal())
|
||||
}
|
||||
|
||||
private fun dealRemainingBoard(from: Street) {
|
||||
/**
|
||||
* Runs the board out when nobody can act any more.
|
||||
*
|
||||
* Publishes each street as it lands rather than dealing all five cards
|
||||
* silently: an all-in runout is the moment a poker UI most needs to animate,
|
||||
* and it also keeps [currentStreet] honest — otherwise a pre-flop all-in ends
|
||||
* on a snapshot labelled PREFLOP that carries a five-card board.
|
||||
*/
|
||||
private suspend fun dealRemainingBoard(from: Street) {
|
||||
var s = from
|
||||
while (s != Street.RIVER) {
|
||||
s = when (s) {
|
||||
@@ -314,6 +322,10 @@ class Table(
|
||||
Street.TURN -> { dealRiver(); Street.RIVER }
|
||||
Street.RIVER -> Street.RIVER
|
||||
}
|
||||
currentStreet = s
|
||||
// Sweep the completed betting into the pot before showing the card.
|
||||
prepareRound(s)
|
||||
emit(TableSnapshot.Phase.STREET_COMPLETE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,3 +277,80 @@ class HumanOfferAndStreetStateTest {
|
||||
assertTrue(streetStarts.all { it.pot > 0 }, "the pot carries forward")
|
||||
}
|
||||
}
|
||||
|
||||
/** Shoves everything in at the first opportunity. */
|
||||
private class Shover : PlayerAgent {
|
||||
override suspend fun act(ctx: DecisionContext): Action = when {
|
||||
ctx.canRaise -> Action(ActionType.RAISE, ctx.maxRaiseTo)
|
||||
ctx.toCall > 0 -> Action(ActionType.CALL, ctx.toCall)
|
||||
else -> Action(ActionType.CHECK)
|
||||
}
|
||||
}
|
||||
|
||||
class AllInRunoutTest {
|
||||
|
||||
private suspend fun runPreflopAllIn(): List<TableSnapshot> {
|
||||
val seen = mutableListOf<TableSnapshot>()
|
||||
val seats = listOf(Seat(0, "A", 100, Shover()), Seat(1, "B", 100, Shover()))
|
||||
Table(
|
||||
seats, 5, 10, Random(1),
|
||||
StackedDeck.of(listOf("Ah Ad", "Kh Kd"), "2c 7d 9s Jc 3h"),
|
||||
observer = { seen += it },
|
||||
).playHand()
|
||||
return seen
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a preflop all-in never reports a full board while still labelled preflop`() = runTest {
|
||||
val seen = runPreflopAllIn()
|
||||
for (snap in seen) {
|
||||
if (snap.board.size == 5) {
|
||||
assertEquals(
|
||||
Street.RIVER, snap.street,
|
||||
"a five-card board must be labelled RIVER, was ${snap.street}",
|
||||
)
|
||||
}
|
||||
if (snap.street == Street.PREFLOP) {
|
||||
assertEquals(0, snap.board.size, "PREFLOP must never carry board cards")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the runout is published street by street so the UI can animate it`() = runTest {
|
||||
val seen = runPreflopAllIn()
|
||||
val boardSizes = seen.map { it.board.size }.distinct()
|
||||
assertTrue(
|
||||
boardSizes.containsAll(listOf(0, 3, 4, 5)),
|
||||
"expected the board to appear 0 -> 3 -> 4 -> 5, saw $boardSizes",
|
||||
)
|
||||
|
||||
val streets = seen.filter { it.phase == TableSnapshot.Phase.STREET_COMPLETE }.map { it.street }
|
||||
assertEquals(
|
||||
listOf(Street.FLOP, Street.TURN, Street.RIVER), streets,
|
||||
"each street of the runout should be published in order",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the terminal snapshot of an all-in is a river showdown`() = runTest {
|
||||
val seen = runPreflopAllIn()
|
||||
val last = seen.last()
|
||||
assertEquals(Street.RIVER, last.street)
|
||||
assertEquals(5, last.board.size)
|
||||
assertEquals(TableSnapshot.Phase.SHOWDOWN, last.phase)
|
||||
assertTrue(last.pot > 0, "the terminal snapshot must still show the contested pot")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `runout snapshots do not carry stale betting`() = runTest {
|
||||
val seen = runPreflopAllIn()
|
||||
for (snap in seen.filter { it.phase == TableSnapshot.Phase.STREET_COMPLETE }) {
|
||||
assertEquals(0, snap.currentBet, "no bet is outstanding during a runout")
|
||||
assertTrue(
|
||||
snap.seats.all { it.committedThisRound == 0 },
|
||||
"chips should already be swept into the pot",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user