← poker-tutor
import Foundation
public enum Suit: UInt8, CaseIterable, Sendable, Codable, Hashable {
case clubs = 0, diamonds, hearts, spades
public var symbol: String {
switch self {
case .clubs: return "♣"
case .diamonds: return "♦"
case .hearts: return "♥"
case .spades: return "♠"
}
}
public var letter: Character {
switch self {
case .clubs: return "c"
case .diamonds: return "d"
case .hearts: return "h"
case .spades: return "s"
}
}
}
public enum Rank: UInt8, CaseIterable, Comparable, Sendable, Codable, Hashable {
case two = 2, three, four, five, six, seven, eight, nine, ten, jack, queen, king, ace
public static func < (a: Rank, b: Rank) -> Bool { a.rawValue < b.rawValue }
/// Bit position used in 13-bit rank masks: deuce is bit 0, ace is bit 12.
public var bit: UInt16 { 1 << UInt16(rawValue - 2) }
public var letter: Character {
switch self {
case .two: return "2"
case .three: return "3"
case .four: return "4"
case .five: return "5"
case .six: return "6"
case .seven: return "7"
case .eight: return "8"
case .nine: return "9"
case .ten: return "T"
case .jack: return "J"
case .queen: return "Q"
case .king: return "K"
case .ace: return "A"
}
}
public init?(letter: Character) {
guard let r = Rank.allCases.first(where: { $0.letter == Character(letter.uppercased()) }) else { return nil }
self = r
}
}
/// A playing card, stored as a single index 0..<52 so decks and evaluators can stay cheap.
public struct Card: Hashable, Sendable, Codable, CustomStringConvertible {
public let index: UInt8
public init(rank: Rank, suit: Suit) {
self.index = (rank.rawValue - 2) * 4 + suit.rawValue
}
public init(index: UInt8) {
precondition(index < 52, "card index out of range")
self.index = index
}
/// Parses shorthand like "As", "Td", "7h".
public init?(_ text: String) {
let chars = Array(text)
guard chars.count == 2,
let rank = Rank(letter: chars[0]),
let suit = Suit.allCases.first(where: { $0.letter == Character(String(chars[1]).lowercased()) })
else { return nil }
self.init(rank: rank, suit: suit)
}
public var rank: Rank { Rank(rawValue: index / 4 + 2)! }
public var suit: Suit { Suit(rawValue: index % 4)! }
public var description: String { "\(rank.letter)\(suit.letter)" }
public var pretty: String { "\(rank.letter)\(suit.symbol)" }
public static let all: [Card] = (0..<52).map { Card(index: UInt8($0)) }
}
public extension Array where Element == Card {
/// Parses a space-separated list like "As Kd 7h".
static func parse(_ text: String) -> [Card] {
text.split(whereSeparator: { $0 == " " || $0 == "," }).compactMap { Card(String($0)) }
}
var shorthand: String { map(\.description).joined(separator: " ") }
}