Polish table and clarify showdown results
This commit is contained in:
@@ -22,6 +22,11 @@ data class ActionButtons(
|
||||
val fixedRaiseTo: Int,
|
||||
)
|
||||
|
||||
data class RaisePreset(
|
||||
val label: String,
|
||||
val amount: Int,
|
||||
)
|
||||
|
||||
fun buttonsFor(offer: DecisionOffer): ActionButtons {
|
||||
// When a stack cannot cover a full min-raise it may still shove; the engine
|
||||
// clamps such a raise to maxRaiseTo. Requiring maxRaiseTo > minRaiseTo to show
|
||||
@@ -55,3 +60,35 @@ fun buttonsFor(offer: DecisionOffer): ActionButtons {
|
||||
/** Label for the raise button at [amount]. */
|
||||
fun raiseLabel(buttons: ActionButtons, amount: Int): String =
|
||||
if (amount >= buttons.sliderMax) "All in" else "Raise $amount"
|
||||
|
||||
/**
|
||||
* Honest, legal raise-to shortcuts for the always-visible sizing rail.
|
||||
*
|
||||
* Facing a bet, a fraction describes the extra raise after calling: the pot
|
||||
* after a call is [DecisionOffer.pot] plus the affordable call, and the final
|
||||
* round commitment is the current bet plus that fraction. Every result is
|
||||
* clamped to the exact engine-provided range; duplicates disappear rather than
|
||||
* showing several labels that submit the same action.
|
||||
*/
|
||||
fun raisePresets(
|
||||
offer: DecisionOffer,
|
||||
buttons: ActionButtons = buttonsFor(offer),
|
||||
): List<RaisePreset> {
|
||||
if (!buttons.showRaise) return emptyList()
|
||||
val low = buttons.sliderMin
|
||||
val high = buttons.sliderMax
|
||||
if (low >= high) return listOf(RaisePreset("All in", high))
|
||||
|
||||
val potAfterCall = offer.pot + offer.callAmount
|
||||
val halfPot = (offer.currentBet + potAfterCall / 2).coerceIn(low, high)
|
||||
val fullPot = (offer.currentBet + potAfterCall).coerceIn(low, high)
|
||||
|
||||
return buildList {
|
||||
add(RaisePreset("Min", low))
|
||||
if (halfPot in (low + 1) until high) add(RaisePreset("½ pot", halfPot))
|
||||
if (fullPot in (low + 1) until high && fullPot != halfPot) {
|
||||
add(RaisePreset("Pot", fullPot))
|
||||
}
|
||||
add(RaisePreset("All in", high))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
package com.jsjdesigns.poker
|
||||
|
||||
import com.jsjdesigns.poker.core.HandEvaluator
|
||||
import com.jsjdesigns.poker.game.HandResult
|
||||
|
||||
data class AwardWinner(
|
||||
val seat: Int,
|
||||
val label: String,
|
||||
val amountWon: Int,
|
||||
val hole: List<Int>,
|
||||
val handDescription: String?,
|
||||
)
|
||||
|
||||
data class PotAwardSummary(
|
||||
val label: String,
|
||||
val amount: Int,
|
||||
val winners: List<AwardWinner>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Immutable, presentation-ready outcome for one completed hand.
|
||||
*
|
||||
@@ -18,6 +33,8 @@ data class HandSummary(
|
||||
val potSize: Int,
|
||||
val heroNet: Int,
|
||||
val wentToShowdown: Boolean,
|
||||
/** Main/side-pot breakdown with public winning cards and evaluated hand names. */
|
||||
val awards: List<PotAwardSummary>,
|
||||
) {
|
||||
val title: String
|
||||
get() = when {
|
||||
@@ -47,6 +64,40 @@ fun handSummaryFor(
|
||||
require(heroSeat in result.net.indices) { "hero seat is outside the result" }
|
||||
require(result.winners.isNotEmpty()) { "a completed hand must have a winner" }
|
||||
require(result.winners.all { it in seatNames.indices }) { "winner seat is outside the roster" }
|
||||
require(result.potAwards.isNotEmpty()) { "a completed hand must award at least one pot" }
|
||||
require(result.potAwards.sumOf { it.amount } == result.potSize) {
|
||||
"pot awards must account for the contested pot"
|
||||
}
|
||||
val showdownBySeat = result.showdownHands.associateBy { it.seat }
|
||||
if (result.wentToShowdown) {
|
||||
require(result.potAwards.flatMap { it.payouts }.all { it.seat in showdownBySeat }) {
|
||||
"every showdown winner must include its public hand"
|
||||
}
|
||||
}
|
||||
val multiplePots = result.potAwards.size > 1
|
||||
val awards = result.potAwards.mapIndexed { index, award ->
|
||||
require(award.payouts.all { it.seat in seatNames.indices }) {
|
||||
"pot winner seat is outside the roster"
|
||||
}
|
||||
PotAwardSummary(
|
||||
label = when {
|
||||
!multiplePots -> "Pot"
|
||||
index == 0 -> "Main pot"
|
||||
else -> "Side pot $index"
|
||||
},
|
||||
amount = award.amount,
|
||||
winners = award.payouts.map { payout ->
|
||||
val showdown = showdownBySeat[payout.seat]
|
||||
AwardWinner(
|
||||
seat = payout.seat,
|
||||
label = if (payout.seat == heroSeat) "You" else seatNames[payout.seat],
|
||||
amountWon = payout.amount,
|
||||
hole = showdown?.hole.orEmpty(),
|
||||
handDescription = showdown?.let { HandEvaluator.describe(it.score) },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return HandSummary(
|
||||
handNumber = handNumber,
|
||||
@@ -57,5 +108,6 @@ fun handSummaryFor(
|
||||
potSize = result.potSize,
|
||||
heroNet = result.net[heroSeat],
|
||||
wentToShowdown = result.wentToShowdown,
|
||||
awards = awards,
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,47 @@ class ActionButtonsTest {
|
||||
assertFalse(buttonsFor(offer(canRaise = false)).showRaise)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `raise presets use the pot after calling and legal raise-to amounts`() {
|
||||
val decision = offer(
|
||||
toCall = 10,
|
||||
minRaiseTo = 20,
|
||||
maxRaiseTo = 500,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
RaisePreset("Min", 20),
|
||||
RaisePreset("½ pot", 35),
|
||||
RaisePreset("Pot", 60),
|
||||
RaisePreset("All in", 500),
|
||||
),
|
||||
raisePresets(decision),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `raise presets drop clamped duplicates instead of mislabelling them`() {
|
||||
val decision = offer(
|
||||
toCall = 10,
|
||||
minRaiseTo = 45,
|
||||
maxRaiseTo = 50,
|
||||
stack = 50,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(RaisePreset("Min", 45), RaisePreset("All in", 50)),
|
||||
raisePresets(decision),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a single legal shove has one all-in preset`() {
|
||||
val decision = offer(minRaiseTo = 200, maxRaiseTo = 120, stack = 120)
|
||||
|
||||
assertEquals(listOf(RaisePreset("All in", 120)), raisePresets(decision))
|
||||
}
|
||||
|
||||
// ---------- fold and call ----------
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.jsjdesigns.poker
|
||||
|
||||
import com.jsjdesigns.poker.core.HandEvaluator
|
||||
import com.jsjdesigns.poker.core.cardsOf
|
||||
import com.jsjdesigns.poker.game.HandResult
|
||||
import com.jsjdesigns.poker.game.PotAward
|
||||
import com.jsjdesigns.poker.game.PotPayout
|
||||
import com.jsjdesigns.poker.game.ShowdownHand
|
||||
import com.jsjdesigns.poker.game.Street
|
||||
import com.jsjdesigns.poker.game.TableSnapshot
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -20,14 +25,41 @@ class HandSummaryTest {
|
||||
winners: List<Int> = listOf(1),
|
||||
showdown: Boolean = true,
|
||||
pot: Int = 16,
|
||||
) = HandResult(
|
||||
board = intArrayOf(0, 1, 2, 3, 4),
|
||||
net = net,
|
||||
winners = winners,
|
||||
wentToShowdown = showdown,
|
||||
potSize = pot,
|
||||
events = emptyList(),
|
||||
)
|
||||
awards: List<PotAward>? = null,
|
||||
): HandResult {
|
||||
val board = cardsOf("2c 7d 9s Jc 3h")
|
||||
val holes = listOf(cardsOf("Ah Ad"), cardsOf("Kh Kd"), cardsOf("Qh Qd"))
|
||||
return HandResult(
|
||||
board = board,
|
||||
net = net,
|
||||
wentToShowdown = showdown,
|
||||
potSize = pot,
|
||||
events = emptyList(),
|
||||
showdownHands = if (showdown) {
|
||||
winners.map { winner ->
|
||||
val hole = holes[winner]
|
||||
ShowdownHand(
|
||||
seat = winner,
|
||||
hole = hole.toList(),
|
||||
score = HandEvaluator.evaluate(hole + board),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
potAwards = awards ?: listOf(
|
||||
PotAward(
|
||||
amount = pot,
|
||||
payouts = winners.mapIndexed { index, winner ->
|
||||
PotPayout(
|
||||
winner,
|
||||
pot / winners.size + if (index < pot % winners.size) 1 else 0,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `summary names the winner and reports the hero's actual net`() {
|
||||
@@ -76,6 +108,43 @@ class HandSummaryTest {
|
||||
assertTrue(summary.heroWon)
|
||||
assertTrue(summary.detail.endsWith("You broke even"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `side pots name each award and the winning made hand`() {
|
||||
val board = cardsOf("2c 7d 9s Jc 3h")
|
||||
val aces = cardsOf("Ah Ad")
|
||||
val kings = cardsOf("Kh Kd")
|
||||
val result = HandResult(
|
||||
board = board,
|
||||
net = intArrayOf(100, 100, -200),
|
||||
wentToShowdown = true,
|
||||
potSize = 450,
|
||||
events = emptyList(),
|
||||
showdownHands = listOf(
|
||||
ShowdownHand(0, aces.toList(), HandEvaluator.evaluate(aces + board)),
|
||||
ShowdownHand(1, kings.toList(), HandEvaluator.evaluate(kings + board)),
|
||||
),
|
||||
potAwards = listOf(
|
||||
PotAward(150, listOf(PotPayout(0, 150))),
|
||||
PotAward(300, listOf(PotPayout(1, 300))),
|
||||
),
|
||||
)
|
||||
|
||||
val summary = handSummaryFor(
|
||||
handNumber = 11,
|
||||
result = result,
|
||||
seatNames = listOf("Jay", "Ada", "Bruno"),
|
||||
)
|
||||
|
||||
assertEquals("Main pot", summary.awards[0].label)
|
||||
assertEquals(150, summary.awards[0].amount)
|
||||
assertEquals("You", summary.awards[0].winners.single().label)
|
||||
assertEquals(150, summary.awards[0].winners.single().amountWon)
|
||||
assertEquals("Pair", summary.awards[0].winners.single().handDescription)
|
||||
assertEquals("Side pot 1", summary.awards[1].label)
|
||||
assertEquals("Ada", summary.awards[1].winners.single().label)
|
||||
assertEquals(kings.toList(), summary.awards[1].winners.single().hole)
|
||||
}
|
||||
}
|
||||
|
||||
class VisibleHandSummaryTest {
|
||||
@@ -87,6 +156,7 @@ class VisibleHandSummaryTest {
|
||||
potSize = 20,
|
||||
heroNet = -4,
|
||||
wentToShowdown = true,
|
||||
awards = emptyList(),
|
||||
)
|
||||
|
||||
private fun snapshot(
|
||||
|
||||
Reference in New Issue
Block a user