← poker-tutor
import Foundation
/// One of the 169 distinct starting hands — "AKs", "77", "T9o".
///
/// Two hole cards can be dealt 1,326 ways, but only 169 of them play differently, because suits
/// have no rank. Every piece of preflop strategy is indexed by this.
public struct HandClass: Hashable, Sendable, Comparable, CustomStringConvertible, Codable {
public let high: Rank
public let low: Rank
public let suited: Bool
public init(high: Rank, low: Rank, suited: Bool) {
precondition(high >= low, "the higher rank comes first")
precondition(!(high == low && suited), "a pair cannot be suited")
self.high = high
self.low = low
self.suited = suited
}
public init(_ cards: [Card]) {
precondition(cards.count == 2, "a starting hand is two cards")
let a = cards[0], b = cards[1]
let high = max(a.rank, b.rank)
let low = min(a.rank, b.rank)
self.init(high: high, low: low, suited: a.suit == b.suit && a.rank != b.rank)
}
/// Parses "AKs", "77", "T9o".
public init?(_ text: String) {
let chars = Array(text)
guard chars.count == 2 || chars.count == 3,
let r1 = Rank(letter: chars[0]),
let r2 = Rank(letter: chars[1])
else { return nil }
let suited = chars.count == 3 && Character(String(chars[2]).lowercased()) == "s"
guard !(r1 == r2 && suited) else { return nil }
self.init(high: max(r1, r2), low: min(r1, r2), suited: suited)
}
public var isPair: Bool { high == low }
public var isSuited: Bool { suited }
/// True for hands like AKo or T9s where the two cards touch.
public var gap: Int { Int(high.rawValue) - Int(low.rawValue) - 1 }
public var isConnected: Bool { !isPair && gap == 0 }
/// How many of the 1,326 possible two-card deals belong to this class.
public var combos: Int { isPair ? 6 : (suited ? 4 : 12) }
public var description: String {
isPair ? "\(high.letter)\(low.letter)" : "\(high.letter)\(low.letter)\(suited ? "s" : "o")"
}
/// Ordering is by rank only, for stable output — not by strength. Use `PreflopTable` for that.
public static func < (a: HandClass, b: HandClass) -> Bool {
if a.high != b.high { return a.high < b.high }
if a.low != b.low { return a.low < b.low }
return !a.suited && b.suited
}
/// All 169 starting hands.
public static let all: [HandClass] = {
var out: [HandClass] = []
let ranks = Rank.allCases.sorted(by: >)
for (i, high) in ranks.enumerated() {
for low in ranks[i...] {
if high == low {
out.append(HandClass(high: high, low: low, suited: false))
} else {
out.append(HandClass(high: high, low: low, suited: true))
out.append(HandClass(high: high, low: low, suited: false))
}
}
}
return out
}()
/// Every two-card combination belonging to this class.
public func combinations() -> [[Card]] {
var out: [[Card]] = []
if isPair {
let cards = Suit.allCases.map { Card(rank: high, suit: $0) }
for i in 0..<cards.count {
for j in (i + 1)..<cards.count { out.append([cards[i], cards[j]]) }
}
} else if suited {
for suit in Suit.allCases { out.append([Card(rank: high, suit: suit), Card(rank: low, suit: suit)]) }
} else {
for a in Suit.allCases {
for b in Suit.allCases where a != b {
out.append([Card(rank: high, suit: a), Card(rank: low, suit: b)])
}
}
}
return out
}
}