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, val handDescription: String?, ) data class PotAwardSummary( val label: String, val amount: Int, val winners: List, ) /** * 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, /** Display labels in the engine's winner order; the human seat is always "You". */ val winnerLabels: List, /** 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, ) { 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, 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, ) }