Advertisement

← poker-tutor

Sources/PokerCoach/ReferenceRanges.swift

9.4 KB Download file
import Foundation
import PokerCore
import PokerAI

/// What a competent player does before the flop.
///
/// This is the coach's yardstick, and it is deliberately not a solver output. It is a sound,
/// conventional set of ranges of the kind a winning amateur plays — which is the standard worth
/// holding someone to when the goal is to stop losing money, not to beat professionals. Fractions
/// are shares of all starting hands, so 0.15 means the strongest fifteen percent.
public enum ReferenceRanges {

    /// Hands worth opening with a raise when nobody has entered the pot.
    public static func open(position: Position, players: Int, format: CoachFormat = .tournament) -> Double {
        if format == .limit { return Limit.open(position: position, players: players) }
        let base: Double
        switch position {
        case .underTheGun: base = 0.12
        case .underTheGunPlus1: base = 0.14
        case .middle: base = 0.16
        case .lojack: base = 0.19
        case .hijack: base = 0.22
        case .cutoff: return 0.28 * PositionAdjustment.tableSizeMultiplier(playerCount: players)
        case .button: return 0.42 * PositionAdjustment.tableSizeMultiplier(playerCount: players)
        case .smallBlind: return 0.38 * PositionAdjustment.tableSizeMultiplier(playerCount: players)
        case .bigBlind: return 0.0                    // nobody to open against
        }
        return base * PositionAdjustment.tableSizeMultiplier(playerCount: players)
    }

    /// Hands worth re-raising with against a single opener.
    public static func threeBet(from position: Position, versus opener: Position, players: Int,
                                format: CoachFormat = .tournament) -> Double {
        if format == .limit { return Limit.threeBet(from: position, versus: opener, players: players) }
        // Wider against a late opener, whose range is far weaker.
        let openerWidth = open(position: opener, players: players)
        let base = 0.045 + 0.10 * max(0, openerWidth - 0.15)
        let positional = position == .button || position == .cutoff ? 1.35 : 1.0
        let blind = position.isBlind ? 0.85 : 1.0     // out of position for the rest of the hand
        return (base * positional * blind).clamped(to: 0.02...0.28)
    }

    /// Hands worth calling an open with, over and above the ones worth re-raising.
    public static func callVersusOpen(from position: Position, versus opener: Position, players: Int,
                                      format: CoachFormat = .tournament) -> Double {
        if position == .bigBlind { return defendBigBlind(versus: opener, players: players, format: format) }
        if format == .limit { return Limit.callVersusOpen(from: position, versus: opener, players: players) }
        let openerWidth = open(position: opener, players: players)
        let base = 0.06 + 0.22 * max(0, openerWidth - 0.15)
        let inPosition = PositionAdjustment.isInPosition(position, versus: opener) ? 1.5 : 1.0
        return (base * inPosition).clamped(to: 0.03...0.35)
    }

    /// The big blind is already half-invested and getting a price, so it continues far wider than
    /// anywhere else. Folding it too often is the most common and most expensive amateur habit.
    public static func defendBigBlind(versus opener: Position, players: Int,
                                      format: CoachFormat = .tournament) -> Double {
        if format == .limit { return Limit.defendBigBlind(versus: opener, players: players) }
        let openerWidth = open(position: opener, players: players)
        switch opener {
        case .button, .smallBlind: return 0.55
        case .cutoff: return 0.45
        default: return (0.28 + openerWidth).clamped(to: 0.28...0.42)
        }
    }

    public static func fourBet(from position: Position, players: Int, format: CoachFormat = .tournament) -> Double {
        if format == .limit { return Limit.fourBet(from: position) }
        return position.isBlind ? 0.022 : 0.030
    }

    public static func callVersusThreeBet(from position: Position, players: Int,
                                          format: CoachFormat = .tournament) -> Double {
        if format == .limit { return Limit.callVersusThreeBet(from: position) }
        return position.isBlind ? 0.045 : 0.070
    }

    /// What a sound, conventional fixed-limit player does before the flop.
    ///
    /// Everything is wider than at no-limit, and for one reason: a raise is one extra small bet,
    /// so nobody can be priced out of a pot and nobody can be made to risk a stack for a mistake.
    /// The money is made by having the better hand more often over thousands of small pots, so a
    /// hand that plays well multiway — a pair, a suited ace, two big cards — is worth a call
    /// behind limpers where it would be a fold at no-limit; and a hand worth playing is worth
    /// raising with, because a raise for value collects from everyone still to act. The one thing
    /// the limit player does less is cold-call a raise: two bets cold with a hand that is not
    /// good enough to re-raise is the classic way to be second best for a whole pot.
    public enum Limit {
        public static func open(position: Position, players: Int) -> Double {
            let table = PositionAdjustment.tableSizeMultiplier(playerCount: players)
            let base: Double
            switch position {
            case .underTheGun: base = 0.16
            case .underTheGunPlus1: base = 0.18
            case .middle: base = 0.20
            case .lojack: base = 0.24
            case .hijack: base = 0.28
            case .cutoff: base = 0.36
            case .button: base = 0.50
            case .smallBlind: base = 0.42
            case .bigBlind: return 0.0
            }
            return (base * table).clamped(to: 0.05...0.95)
        }

        /// Re-raising is for value: the top of the deck against a wide opener, less against a
        /// tight one. Isolating a raiser with a marginal hand wins nothing at limit, because the
        /// raise cannot make anyone fold.
        public static func threeBet(from position: Position, versus opener: Position, players: Int) -> Double {
            let openerWidth = open(position: opener, players: players)
            let base = 0.07 + 0.12 * max(0, openerWidth - 0.15)
            return base.clamped(to: 0.04...0.30)
        }

        /// Cold-calling — two bets with nobody else in — wants a hand that plays: pairs, big
        /// suited cards, a suited ace. Behind callers the pot is bigger and the price better.
        public static func callVersusOpen(from position: Position, versus opener: Position, players: Int) -> Double {
            let openerWidth = open(position: opener, players: players)
            let base = 0.06 + 0.12 * max(0, openerWidth - 0.15)
            let inPosition = PositionAdjustment.isInPosition(position, versus: opener) ? 1.4 : 1.0
            return (base * inPosition).clamped(to: 0.04...0.25)
        }

        /// One more small bet to see a flop with money already in: three and a half to one or
        /// better, so the big blind carries on with most of what it is dealt.
        public static func defendBigBlind(versus opener: Position, players: Int) -> Double {
            let openerWidth = open(position: opener, players: players)
            switch opener {
            case .button, .smallBlind: return 0.70
            case .cutoff: return 0.60
            default: return (0.45 + openerWidth).clamped(to: 0.45...0.60)
            }
        }

        /// The third and fourth bets are for the best hands only.
        public static func fourBet(from position: Position) -> Double { position.isBlind ? 0.03 : 0.04 }

        /// Calling a re-raise is one more small bet in a pot that already holds five or six;
        /// far cheaper than at no-limit, so far wider.
        public static func callVersusThreeBet(from position: Position) -> Double { position.isBlind ? 0.09 : 0.12 }
    }

    /// Below this many big blinds, raising to anything less than all in only invites trouble:
    /// the stack is short enough that any raise commits it anyway.
    public static let pushFoldDepth: Double = 12

    /// Hands worth moving all in with, by stack depth. Shallower stacks shove wider because the
    /// blinds and antes are worth more relative to the risk.
    public static func shove(depth: Double, position: Position, players: Int, facingRaise: Bool) -> Double {
        let base: Double
        switch depth {
        case ..<3: base = 0.62
        case ..<5: base = 0.42
        case ..<8: base = 0.28
        case ..<10: base = 0.20
        default: base = 0.14
        }
        if facingRaise {
            // Calling off or re-shoving into a raise needs a real hand.
            return (base * 0.30).clamped(to: 0.02...0.35)
        }
        let positional = PositionAdjustment.openMultiplier(position)
        return (base * positional * PositionAdjustment.tableSizeMultiplier(playerCount: players)).clamped(to: 0.04...0.92)
    }

    /// The opening raise a competent player makes, in chips.
    public static func openSize(bigBlind: Int, limpers: Int, players: Int) -> Int {
        let multiple = (players <= 3 ? 2.5 : 2.3) + 1.0 * Double(limpers)
        return Int((Double(bigBlind) * multiple).rounded())
    }
}