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())
|
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
|
var s = from
|
||||||
while (s != Street.RIVER) {
|
while (s != Street.RIVER) {
|
||||||
s = when (s) {
|
s = when (s) {
|
||||||
@@ -314,6 +322,10 @@ class Table(
|
|||||||
Street.TURN -> { dealRiver(); Street.RIVER }
|
Street.TURN -> { dealRiver(); Street.RIVER }
|
||||||
Street.RIVER -> 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")
|
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