Advertisement

← poker-tutor

Sources/PokerCore/Pots.swift

5.3 KB Download file
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
    }
}