← poker-tutor
import Foundation
public enum Street: Int, Comparable, Sendable, CaseIterable, Codable {
case preflop = 0, flop, turn, river, showdown
public static func < (a: Street, b: Street) -> Bool { a.rawValue < b.rawValue }
public var name: String {
switch self {
case .preflop: return "preflop"
case .flop: return "flop"
case .turn: return "turn"
case .river: return "river"
case .showdown: return "showdown"
}
}
/// Community cards dealt when this street begins.
var cardsDealt: Int {
switch self {
case .preflop, .showdown: return 0
case .flop: return 3
case .turn, .river: return 1
}
}
}
public enum PlayerStatus: Sendable, Equatable, Codable {
case active // still has chips and a decision to make
case folded
case allIn
case sittingOut // busted or not dealt in
public var isInHand: Bool { self == .active || self == .allIn }
}
/// Seat position relative to the button, which is what strategy is actually indexed by.
public enum Position: String, Sendable, Codable, CaseIterable {
case smallBlind = "SB"
case bigBlind = "BB"
case underTheGun = "UTG"
case underTheGunPlus1 = "UTG+1"
case middle = "MP"
case lojack = "LJ"
case hijack = "HJ"
case cutoff = "CO"
case button = "BTN"
public var isBlind: Bool { self == .smallBlind || self == .bigBlind }
public var fullName: String {
switch self {
case .smallBlind: return "small blind"
case .bigBlind: return "big blind"
case .underTheGun: return "under the gun"
case .underTheGunPlus1: return "under the gun + 1"
case .middle: return "middle position"
case .lojack: return "lojack"
case .hijack: return "hijack"
case .cutoff: return "cutoff"
case .button: return "the button"
}
}
/// Where this seat sits in the order of play: the small blind is 1, counting round the table,
/// and the button is last. Nil for a seat that does not exist at a table this size.
///
/// This is display only. The engine and the reference ranges key on the case and never on this
/// number — "UTG+1" is a word you have to be taught, while "seat 4 of 9" is one you can act on
/// the first time you read it, because subtracting tells you how many people still get to
/// speak after you.
///
/// Heads-up the button posts the small blind, so following the blind would make the button
/// seat 1 and stand the whole idea on its head: the number would go *down* as the seat got
/// better. Two-handed is therefore numbered big blind 1, button 2, which keeps both of the
/// things the number is meant to say — a higher number acts later once the cards are out, and
/// the button is always last.
/// Nine is as large as a table can be numbered because there are nine seat names, which is also
/// as large as this app ever deals. Anything else gets no number and falls back to the name,
/// rather than being told it is seat 9 of 10.
public func number(inTableOf count: Int) -> Int? {
guard count >= 2, count <= 9 else { return nil }
if count == 2 {
switch self {
case .bigBlind: return 1
case .button: return 2
default: return nil
}
}
guard let index = Position.names(forPlayerCount: count).firstIndex(of: self) else { return nil }
return index + 1
}
/// "seat 4 of 9" — what leads wherever a seat is shown to the player.
public func seatPhrase(inTableOf count: Int) -> String {
guard let number = number(inTableOf: count) else { return fullName }
return "seat \(number) of \(count)"
}
/// The number with the traditional name kept alongside it, for prose: the number is the part
/// that means something, but the name is what everyone else at a card table will say.
public func described(inTableOf count: Int) -> String {
guard number(inTableOf: count) != nil else { return fullName }
return "\(seatPhrase(inTableOf: count)) (\(fullName))"
}
/// Short enough to print on the felt: "4 UTG+1".
public func badge(inTableOf count: Int) -> String {
guard let number = number(inTableOf: count) else { return rawValue }
return "\(number) \(rawValue)"
}
/// What the number actually means, in the only terms that matter to someone learning: how many
/// people still get to speak after you once the shared cards are down.
public func actOrderNote(inTableOf count: Int) -> String {
guard let number = number(inTableOf: count) else { return "" }
switch count - number {
case 0:
return "Nobody acts after you once the shared cards are out, so you decide knowing what "
+ "everyone else has done. It is the best seat at the table."
case 1:
return "One player acts after you once the shared cards are out, so there is only ever "
+ "one answer you are missing when you decide."
case let after:
let blind = isBlind
? " You also put money in before you had seen your cards, which is why this seat "
+ "loses money over time and is meant to."
: ""
return "\(after) players act after you once the shared cards are out, so you have to "
+ "decide before you know what any of them will do.\(blind)"
}
}
/// Names the seats for a table of `count` players, ordered from the small blind around to the button.
/// Blinds and button always exist; the seats between them are named the way players actually
/// name them as a table shortens — the first seat after the blinds stays "under the gun".
public static func names(forPlayerCount count: Int) -> [Position] {
precondition(count >= 2 && count <= 10, "tables run 2 to 10 handed")
if count == 2 { return [.button, .bigBlind] } // heads-up: the button posts the small blind
let canonical: [Position] = [.underTheGun, .underTheGunPlus1, .middle, .lojack, .hijack, .cutoff]
// Which middle seats survive as the table shrinks, most-kept first.
let keepOrder: [Position] = [.underTheGun, .cutoff, .middle, .hijack, .underTheGunPlus1, .lojack]
let kept = Set(keepOrder.prefix(count - 3))
let middle = canonical.filter { kept.contains($0) }
return [.smallBlind, .bigBlind] + middle + [.button]
}
}
public struct SeatState: Sendable, Equatable {
public let seat: Int
public var playerID: String
public var stack: Int
public var hole: [Card] = []
public var status: PlayerStatus = .active
/// Chips pushed in during the current betting round.
public var committedThisRound: Int = 0
/// Chips pushed in across the whole hand — the basis for side pots.
public var committedThisHand: Int = 0
/// Antes are dead money: they go straight to the main pot and are never matched by anyone,
/// so they must stay out of the side-pot layering.
public var antePaid: Int = 0
var hasActedThisRound = false
/// The bet level when this seat last acted voluntarily; nil means it hasn't acted yet.
/// Used to decide whether an undersized all-in reopens the betting for them.
var lastActedAtBet: Int? = nil
public var isInHand: Bool { status.isInHand }
public var canAct: Bool { status == .active }
public init(seat: Int, playerID: String, stack: Int) {
self.seat = seat
self.playerID = playerID
self.stack = stack
}
}
public enum AnteStyle: Sendable, Equatable, Codable {
case none
/// Every player antes each hand — the old structure.
case perPlayer(Int)
/// The big blind antes for the whole table, which is how modern tournaments run.
case bigBlindAnte(Int)
}
public struct BlindLevel: Sendable, Equatable, Codable {
public let smallBlind: Int
public let bigBlind: Int
public let ante: AnteStyle
public init(smallBlind: Int, bigBlind: Int, ante: AnteStyle = .none) {
self.smallBlind = smallBlind
self.bigBlind = bigBlind
self.ante = ante
}
}
/// How a pot is settled once nobody can bet again.
public enum Settlement: Sendable, Equatable {
/// Deal the rest of the board and pay the winner. This is poker.
case runout
/// Split each pot by the players' chances of winning it, without dealing the cards.
///
/// Not for playing — for measuring. A single all-in swings a hundred big blinds on a river
/// card, which buries any real difference in skill under noise for tens of thousands of hands.
/// Paying out by equity asks "who got their money in better", which is the thing worth
/// measuring, and is the same calculation behind telling a player they played it right and
/// lost anyway.
case allInEquity(trials: Int)
}
public struct HandConfig: Sendable, Equatable {
public let level: BlindLevel
/// Index into the seat array, not a seat number — the engine works with dense seat arrays.
public let buttonSeat: Int
public let seed: UInt64
public let settlement: Settlement
/// No-limit unless a table says otherwise, so every hand ever dealt before the cash apps
/// existed is the hand it always was.
public let structure: BettingStructure
public init(level: BlindLevel, buttonSeat: Int, seed: UInt64, settlement: Settlement = .runout,
structure: BettingStructure = .noLimit) {
self.level = level
self.buttonSeat = buttonSeat
self.seed = seed
self.settlement = settlement
self.structure = structure
}
}
public enum HandError: Error, CustomStringConvertible, Equatable {
case notYourTurn
case handComplete
case illegal(String)
public var description: String {
switch self {
case .notYourTurn: return "it is not that seat's turn to act"
case .handComplete: return "the hand is already over"
case .illegal(let why): return "illegal action: \(why)"
}
}
}