Polish table and clarify showdown results
This commit is contained in:
@@ -115,15 +115,57 @@ fun interface PlayerAgent {
|
||||
|
||||
data class Pot(val amount: Int, val eligible: List<Int>)
|
||||
|
||||
/** One public hand turned face-up at showdown, with its authoritative engine score. */
|
||||
data class ShowdownHand(
|
||||
val seat: Int,
|
||||
val hole: List<Int>,
|
||||
val score: Int,
|
||||
)
|
||||
|
||||
data class PotPayout(
|
||||
val seat: Int,
|
||||
val amount: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* One settled pot in main-pot then side-pot order.
|
||||
*
|
||||
* A flat winner list cannot explain a side-pot hand: two players may both be
|
||||
* winners without having tied or winning the same chips. Keeping each award
|
||||
* and its exact payouts makes the completed-hand UI able to say exactly who won
|
||||
* what, including the odd chip in a split.
|
||||
*/
|
||||
data class PotAward(
|
||||
val amount: Int,
|
||||
val payouts: List<PotPayout>,
|
||||
) {
|
||||
init {
|
||||
require(amount > 0)
|
||||
require(payouts.isNotEmpty())
|
||||
require(payouts.all { it.amount > 0 })
|
||||
require(payouts.map { it.seat }.distinct().size == payouts.size)
|
||||
require(payouts.sumOf { it.amount } == amount)
|
||||
}
|
||||
|
||||
val winnerSeats: List<Int> get() = payouts.map(PotPayout::seat)
|
||||
}
|
||||
|
||||
data class HandResult(
|
||||
val board: IntArray,
|
||||
/** Net chip change per seat for this hand. */
|
||||
val net: IntArray,
|
||||
val winners: List<Int>,
|
||||
val wentToShowdown: Boolean,
|
||||
val potSize: Int,
|
||||
val events: List<HandEvent>,
|
||||
)
|
||||
/** Empty for an uncontested hand; contains only cards publicly revealed at showdown. */
|
||||
val showdownHands: List<ShowdownHand>,
|
||||
/** Main pot first, then side pots from the shortest contribution upward. */
|
||||
val potAwards: List<PotAward>,
|
||||
) {
|
||||
/** Unique winning seats in main/side-pot award order. */
|
||||
val winners: List<Int>
|
||||
get() = potAwards.flatMap(PotAward::winnerSeats).distinct()
|
||||
}
|
||||
|
||||
/**
|
||||
* A no-limit Texas Hold'em table.
|
||||
@@ -565,7 +607,8 @@ class Table(
|
||||
}
|
||||
|
||||
val contenders = seats.filter { it.contesting }
|
||||
val winners = ArrayList<Int>()
|
||||
val showdownHands = ArrayList<ShowdownHand>()
|
||||
val potAwards = ArrayList<PotAward>()
|
||||
var wentToShowdown = false
|
||||
val potTotal = pot()
|
||||
|
||||
@@ -573,7 +616,7 @@ class Table(
|
||||
val w = contenders.first()
|
||||
w.stack += potTotal
|
||||
for (s in seats) s.committedThisHand = 0
|
||||
winners.add(w.index)
|
||||
potAwards.add(PotAward(potTotal, listOf(PotPayout(w.index, potTotal))))
|
||||
} else {
|
||||
wentToShowdown = true
|
||||
// Cards are face up from here, so snapshots may show them.
|
||||
@@ -583,7 +626,15 @@ class Table(
|
||||
val seven = IntArray(7)
|
||||
seven[0] = c.hole[0]; seven[1] = c.hole[1]
|
||||
for (k in board.indices) seven[2 + k] = board[k]
|
||||
scores[c.index] = HandEvaluator.evaluate(seven, 2 + board.size)
|
||||
val score = HandEvaluator.evaluate(seven, 2 + board.size)
|
||||
scores[c.index] = score
|
||||
showdownHands.add(
|
||||
ShowdownHand(
|
||||
seat = c.index,
|
||||
hole = c.hole.toList(),
|
||||
score = score,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val levels = contenders.map { it.committedThisHand }.distinct().sorted()
|
||||
@@ -610,11 +661,16 @@ class Table(
|
||||
.filter { scores.getValue(it) == best }
|
||||
.sortedBy { (it - button - 1 + seats.size) % seats.size }
|
||||
val share = p.amount / potWinners.size
|
||||
var remainder = p.amount - share * potWinners.size
|
||||
for (w in potWinners) {
|
||||
seats[w].stack += share
|
||||
if (remainder > 0) { seats[w].stack += 1; remainder-- }
|
||||
if (w !in winners) winners.add(w)
|
||||
val remainder = p.amount - share * potWinners.size
|
||||
val payouts = potWinners.mapIndexed { index, winner ->
|
||||
PotPayout(
|
||||
seat = winner,
|
||||
amount = share + if (index < remainder) 1 else 0,
|
||||
)
|
||||
}
|
||||
potAwards.add(PotAward(p.amount, payouts))
|
||||
for (payout in payouts) {
|
||||
seats[payout.seat].stack += payout.amount
|
||||
}
|
||||
}
|
||||
for (s in seats) s.committedThisHand = 0
|
||||
@@ -624,10 +680,11 @@ class Table(
|
||||
return HandResult(
|
||||
board = board.toIntArray(),
|
||||
net = net,
|
||||
winners = winners,
|
||||
wentToShowdown = wentToShowdown,
|
||||
potSize = potTotal,
|
||||
events = ArrayList(events),
|
||||
showdownHands = showdownHands,
|
||||
potAwards = potAwards,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,19 @@ class TableRulesTest {
|
||||
// Kings beat queens for the side pot.
|
||||
assertTrue(result.net[1] > 0, "kings should win the side pot")
|
||||
assertTrue(result.net[2] < 0, "queens should lose")
|
||||
assertEquals(
|
||||
listOf(
|
||||
PotAward(150, listOf(PotPayout(0, 150))),
|
||||
PotAward(300, listOf(PotPayout(1, 300))),
|
||||
),
|
||||
result.potAwards,
|
||||
"presentation must retain who won the main and side pots separately",
|
||||
)
|
||||
assertEquals(
|
||||
listOf(0, 1, 2),
|
||||
result.showdownHands.map { it.seat },
|
||||
"only publicly revealed contenders belong in showdown results",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -239,6 +252,16 @@ class TableRulesTest {
|
||||
assertEquals(3, result.net[2], "seat left of the button gets the odd chip")
|
||||
assertEquals(2, result.net[0], "the other winner gets the smaller share")
|
||||
assertEquals(-5, result.net[1], "the folded small blind loses its post")
|
||||
assertEquals(
|
||||
listOf(PotAward(25, listOf(PotPayout(2, 13), PotPayout(0, 12)))),
|
||||
result.potAwards,
|
||||
"award must preserve the exact odd-chip payout, not only the tied seats",
|
||||
)
|
||||
assertEquals(
|
||||
listOf(0, 2),
|
||||
result.showdownHands.map { it.seat },
|
||||
"a folded player's private cards must not enter the public showdown result",
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- uncalled bets ----------
|
||||
|
||||
Reference in New Issue
Block a user