Polish table and clarify showdown results

This commit is contained in:
Jay
2026-07-26 21:46:28 -04:00
parent b8087a9872
commit df4d90f40b
9 changed files with 1625 additions and 277 deletions
@@ -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