117 lines
4.1 KiB
Kotlin
117 lines
4.1 KiB
Kotlin
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.
|
|
*
|
|
* This is built from [HandResult], not inferred from the final table snapshot:
|
|
* side pots can produce several winners and a player's net can differ from the
|
|
* amount of any pot they won.
|
|
*/
|
|
data class HandSummary(
|
|
val handNumber: Int,
|
|
/** Public community cards retained so the modal can explain the winning hand. */
|
|
val board: List<Int>,
|
|
/** Display labels in the engine's winner order; the human seat is always "You". */
|
|
val winnerLabels: List<String>,
|
|
/** Derived from winner seat indices, never from a player-controlled name. */
|
|
val heroWon: Boolean,
|
|
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 {
|
|
winnerLabels.size == 1 && heroWon -> "You win"
|
|
winnerLabels.size == 1 -> "${winnerLabels.single()} wins"
|
|
else -> "${winnerLabels.joinToString(" & ")} win"
|
|
}
|
|
|
|
val detail: String
|
|
get() {
|
|
val ending = when {
|
|
heroNet > 0 -> "You won $heroNet"
|
|
heroNet < 0 -> "You lost ${-heroNet}"
|
|
else -> "You broke even"
|
|
}
|
|
val finish = if (wentToShowdown) "Showdown" else "Uncontested"
|
|
return "$finish • Pot $potSize • $ending"
|
|
}
|
|
}
|
|
|
|
fun handSummaryFor(
|
|
handNumber: Int,
|
|
result: HandResult,
|
|
seatNames: List<String>,
|
|
heroSeat: Int = HERO_SEAT,
|
|
): HandSummary {
|
|
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,
|
|
board = result.board.toList(),
|
|
winnerLabels = result.winners.map { winner ->
|
|
if (winner == heroSeat) "You" else seatNames[winner]
|
|
},
|
|
heroWon = heroSeat in result.winners,
|
|
potSize = result.potSize,
|
|
heroNet = result.net[heroSeat],
|
|
wentToShowdown = result.wentToShowdown,
|
|
awards = awards,
|
|
)
|
|
}
|