← poker-tutor
import Foundation
/// A main pot or side pot: an amount plus the seats allowed to win it.
public struct Pot: Sendable, Equatable {
public var amount: Int
public var eligible: [Int] // seat indexes, ascending
public var isSide: Bool
public init(amount: Int, eligible: [Int], isSide: Bool) {
self.amount = amount
self.eligible = eligible.sorted()
self.isSide = isSide
}
}
public struct PotAward: Sendable, Equatable {
public let potIndex: Int
public let seat: Int
public let amount: Int
/// Nil when the pot was won without a showdown.
public let hand: HandRank?
}
public enum PotBuilder {
/// Splits everything wagered into a main pot and however many side pots the all-ins created.
///
/// Folded players' chips stay in the pot but win nothing. `dead` is ante money, which belongs
/// to the main pot and is contested by everyone still in the hand — it is nobody's wager, so
/// it must not create a layer of its own. Assumes any uncalled bet has already been returned
/// — see `HandEngine.returnUncalledBet`.
public static func build(from seats: [SeatState], dead: Int = 0) -> [Pot] {
let levels = Set(seats.filter { $0.committedThisHand > 0 }.map(\.committedThisHand)).sorted()
var pots: [Pot] = []
var previous = 0
if dead > 0 {
// Everyone still in the hand can win the antes, including a player all in for less
// than the first wager.
pots.append(Pot(amount: dead, eligible: seats.filter(\.isInHand).map(\.seat), isSide: false))
}
guard !levels.isEmpty else { return pots }
for level in levels {
var amount = 0
for seat in seats {
// Each seat contributes whatever part of its wager falls inside this slice.
amount += max(0, min(seat.committedThisHand, level) - previous)
}
let eligible = seats
.filter { $0.isInHand && $0.committedThisHand >= level }
.map(\.seat)
if amount > 0 {
if let last = pots.last, last.eligible == eligible.sorted() {
// Same set of contenders as the previous slice — one pot, not two.
pots[pots.count - 1].amount += amount
} else if eligible.isEmpty {
// Everyone who put in at this level folded; the chips ride along with the pot below.
if pots.isEmpty {
pots.append(Pot(amount: amount, eligible: [], isSide: false))
} else {
pots[pots.count - 1].amount += amount
}
} else {
pots.append(Pot(amount: amount, eligible: eligible, isSide: !pots.isEmpty))
}
}
previous = level
}
return pots
}
/// Awards each pot to the best hand among its eligible seats, splitting ties.
///
/// Odd chips go to the first eligible seat clockwise from the button, which is the standard
/// live rule and keeps chip counts exact.
public static func award(
pots: [Pot],
seats: [SeatState],
board: [Card],
buttonSeat: Int,
seatCount: Int
) -> [PotAward] {
var awards: [PotAward] = []
for (index, pot) in pots.enumerated() {
guard !pot.eligible.isEmpty else { continue }
let contenders = pot.eligible.compactMap { seatIndex in seats.first { $0.seat == seatIndex } }
// Uncontested pots need no cards shown.
if contenders.count == 1 {
awards.append(PotAward(potIndex: index, seat: contenders[0].seat, amount: pot.amount, hand: nil))
continue
}
var ranked: [(seat: Int, rank: HandRank)] = []
for seat in contenders where seat.hole.count == 2 {
ranked.append((seat.seat, HandEvaluator.evaluate(seat.hole + board)))
}
guard let best = ranked.map(\.rank).max() else { continue }
let winners = ranked.filter { $0.rank == best }.map(\.seat).sorted()
let share = pot.amount / winners.count
var odd = pot.amount % winners.count
// Order winners clockwise from the button so the odd chip lands where the rules say:
// the seat immediately left of the button gets it first, the button last.
let ordered = winners.sorted { a, b in
seatsAfterButton(a, button: buttonSeat, count: seatCount) < seatsAfterButton(b, button: buttonSeat, count: seatCount)
}
for seat in ordered {
var amount = share
if odd > 0 { amount += 1; odd -= 1 }
let rank = ranked.first { $0.seat == seat }?.rank
awards.append(PotAward(potIndex: index, seat: seat, amount: amount, hand: rank))
}
}
return awards
}
/// 0 for the seat immediately left of the button, up to count-1 for the button itself.
static func seatsAfterButton(_ seat: Int, button: Int, count: Int) -> Int {
(seat - button - 1 + count) % count
}
}