Last active
January 14, 2025 19:40
-
-
Save kingreza/f449fe7744741a83d0ca4de5f73eb5b5 to your computer and use it in GitHub Desktop.
A simple poker engine that deals cards and detects various hands.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// main.swift | |
// PokerTest | |
// | |
// Created by Reza Shirazian on 11/8/19. | |
// Copyright © 2019 Reza Shirazian. All rights reserved. | |
// | |
import Foundation | |
enum Suit: CaseIterable { | |
case heart, spade, diamond, club | |
} | |
extension Suit: CustomStringConvertible { | |
var description: String { | |
switch self { | |
case .club: | |
return "♣️" | |
case .diamond: | |
return "♦️" | |
case .heart: | |
return "♥️" | |
case .spade: | |
return "♠️" | |
} | |
} | |
} | |
enum Rank: Int, CaseIterable, Comparable { | |
static func < (lhs: Rank, rhs: Rank) -> Bool { | |
return lhs.rawValue < rhs.rawValue | |
} | |
case two = 2, three, four, five, six, seven, eight, nine, ten, jack, queen, king, ace | |
} | |
struct Card: Hashable { | |
let suit: Suit | |
let rank: Rank | |
} | |
struct Deck { | |
private(set) var cards: [Card] | |
private var cardsTakenOut: [Card] | |
var cardsAvailable: [Card] { | |
self.cards.filter {possibleCard in | |
!cardsTakenOut.contains(possibleCard) | |
} | |
} | |
init() { | |
self.cards = Deck.generateDeck() | |
self.cardsTakenOut = [] | |
assert(self.cards.count == 52) | |
} | |
static func generateDeck() -> [Card] { | |
var cards: [Card] = [] | |
for suit in Suit.allCases { | |
for rank in Rank.allCases { | |
cards.append(Card(suit: suit, rank: rank)) | |
} | |
} | |
return cards | |
} | |
mutating func deal(count: Int) -> [Card] { | |
let cardsAvailable = self.cardsAvailable | |
guard count < cardsAvailable.count else { | |
return [] | |
} | |
// 😱 dealing from the bottom of the deck!! | |
let hand = cardsAvailable.shuffled().suffix(count) | |
self.cardsTakenOut.append(contentsOf: hand) | |
return Array(hand) | |
} | |
mutating func reset() { | |
self.cards = Deck.generateDeck() | |
self.cardsTakenOut = [] | |
} | |
} | |
var deck = Deck() | |
let handOne = deck.deal(count: 5) | |
let handTwo = deck.deal(count: 5) | |
enum PokerHand: Int, Comparable, CaseIterable { | |
static func < (lhs: PokerHand, rhs: PokerHand) -> Bool { | |
return lhs.rawValue < rhs.rawValue | |
} | |
case highCard = 1, pair, twoPair, threeKind, straight, flush, fullHouse, fourOfAKind, straightFlush, royalFlush | |
} | |
extension PokerHand { | |
func isHand(cards: [Card]) -> Bool { | |
switch self { | |
case .highCard: | |
return true | |
case .pair: | |
return PokerHand.isPair(cards: cards) | |
case .twoPair: | |
return PokerHand.isTwoPair(cards: cards) | |
case .threeKind: | |
return PokerHand.isThreeOfAKind(cards: cards) | |
case .straight: | |
return PokerHand.isStraight(cards: cards) | |
case .flush: | |
return PokerHand.isFlush(cards: cards) | |
case .fullHouse: | |
return PokerHand.isFullHouse(cards: cards) | |
case .fourOfAKind: | |
return PokerHand.isFourOfAKind(cards: cards) | |
case .straightFlush: | |
return PokerHand.isStraightFlush(cards: cards) | |
case .royalFlush: | |
return PokerHand.isRoyalFlush(cards: cards) | |
} | |
} | |
private static func isRoyalFlush(cards: [Card]) -> Bool { | |
let hadAce = cards.filter({$0.rank == .ace}).count > 0 | |
return hadAce && PokerHand.isStraightFlush(cards: cards) | |
} | |
private static func isStraightFlush(cards: [Card]) -> Bool { | |
return PokerHand.isStraight(cards: cards) && PokerHand.isFlush(cards: cards) | |
} | |
private static func isFourOfAKind(cards: [Card]) -> Bool { | |
return cards.allRanksWithCount().values.filter({$0 == 4}).count > 0 | |
} | |
private static func isFullHouse(cards: [Card]) -> Bool { | |
return self.isPair(cards: cards) && self.isThreeOfAKind(cards: cards) | |
} | |
private static func isFlush(cards: [Card]) -> Bool { | |
return cards.allUniqueSuits().count == 1 | |
} | |
private static func isStraight(cards: [Card]) -> Bool { | |
return cards.isStraight() | |
} | |
private static func isThreeOfAKind(cards: [Card]) -> Bool { | |
return cards.allRanksWithCount().values.filter({$0 == 3}).count > 0 | |
} | |
private static func isTwoPair(cards: [Card]) -> Bool { | |
var numberOfPairs = 0 | |
for (_, value) in cards.allRanksWithCount() { | |
if value == 2 { | |
numberOfPairs += 1 | |
} | |
} | |
return numberOfPairs == 2 | |
} | |
private static func isPair(cards: [Card]) -> Bool { | |
return cards.allRanksWithCount().values.filter({$0 == 2}).count > 0 | |
} | |
} | |
class PokerEngine { | |
static func detectHand(cards: [Card]) -> PokerHand? { | |
guard Set<Card>(cards).count == 5 else { | |
return nil | |
} | |
for possibleHand in PokerHand.allCases.reversed() { | |
if possibleHand.isHand(cards: cards) { | |
return possibleHand | |
} | |
} | |
return .highCard | |
} | |
} | |
extension Array where Element == Card { | |
func allUniqueRanks() -> [Rank] { | |
Array<Rank>(Set(self.map({$0.rank}))) | |
} | |
func allUniqueSuits() -> [Suit] { | |
Array<Suit>(Set(self.map({$0.suit}))) | |
} | |
func allSuitesWithCount() -> [Suit: Int] { | |
var result: [Suit: Int] = [:] | |
for card in self { | |
result[card.suit] = (result[card.suit] ?? 0) + 1 | |
} | |
return result | |
} | |
func allRanksWithCount() -> [Rank: Int] { | |
var result: [Rank: Int] = [:] | |
for card in self { | |
result[card.rank] = (result[card.rank] ?? 0) + 1 | |
} | |
return result | |
} | |
func isStraight() -> Bool { | |
let sorted = self.sorted { (lhs, rhs) -> Bool in | |
lhs.rank < rhs.rank | |
} | |
guard var current = sorted.first else { | |
return false | |
} | |
for card in sorted.suffix(sorted.count - 1) { | |
guard card.rank.rawValue - current.rank.rawValue == 1 else { | |
return false | |
} | |
current = card | |
} | |
return true | |
} | |
} | |
var cards = [Card(suit: .club, rank: .ace), | |
Card(suit: .club, rank: .king), | |
Card(suit: .club, rank: .queen), | |
Card(suit: .club, rank: .jack), | |
Card(suit: .club, rank: .ten)] | |
var hand = PokerEngine.detectHand(cards: cards) | |
cards = [Card(suit: .club, rank: .ace), | |
Card(suit: .club, rank: .king), | |
Card(suit: .heart, rank: .queen), | |
Card(suit: .club, rank: .jack), | |
Card(suit: .club, rank: .ten)] | |
hand = PokerEngine.detectHand(cards: cards) | |
assert(hand! == .straight) | |
cards = [Card(suit: .club, rank: .queen), | |
Card(suit: .club, rank: .ten), | |
Card(suit: .club, rank: .jack), | |
Card(suit: .club, rank: .nine), | |
Card(suit: .club, rank: .eight)] | |
hand = PokerEngine.detectHand(cards: cards) | |
assert(hand! == .straightFlush) | |
cards = [Card(suit: .club, rank: .jack), | |
Card(suit: .heart, rank: .jack), | |
Card(suit: .diamond, rank: .jack), | |
Card(suit: .spade, rank: .jack), | |
Card(suit: .club, rank: .eight)] | |
hand = PokerEngine.detectHand(cards: cards) | |
assert(hand! == .fourOfAKind) | |
cards = [Card(suit: .club, rank: .jack), | |
Card(suit: .heart, rank: .jack), | |
Card(suit: .diamond, rank: .jack), | |
Card(suit: .spade, rank: .eight), | |
Card(suit: .club, rank: .eight)] | |
hand = PokerEngine.detectHand(cards: cards) | |
assert(hand! == .fullHouse) | |
cards = [Card(suit: .club, rank: .jack), | |
Card(suit: .heart, rank: .jack), | |
Card(suit: .diamond, rank: .jack), | |
Card(suit: .spade, rank: .seven), | |
Card(suit: .club, rank: .eight)] | |
hand = PokerEngine.detectHand(cards: cards) | |
assert(hand! == .threeKind) | |
cards = [Card(suit: .club, rank: .jack), | |
Card(suit: .club, rank: .ace), | |
Card(suit: .club, rank: .two), | |
Card(suit: .club, rank: .seven), | |
Card(suit: .club, rank: .eight)] | |
hand = PokerEngine.detectHand(cards: cards) | |
assert(hand! == .flush) | |
cards = [Card(suit: .club, rank: .seven), | |
Card(suit: .heart, rank: .jack), | |
Card(suit: .diamond, rank: .jack), | |
Card(suit: .spade, rank: .seven), | |
Card(suit: .club, rank: .eight)] | |
hand = PokerEngine.detectHand(cards: cards) | |
assert(hand! == .twoPair) | |
cards = [Card(suit: .club, rank: .seven), | |
Card(suit: .heart, rank: .two), | |
Card(suit: .diamond, rank: .jack), | |
Card(suit: .spade, rank: .seven), | |
Card(suit: .club, rank: .eight)] | |
hand = PokerEngine.detectHand(cards: cards) | |
assert(hand! == .pair) | |
cards = [Card(suit: .club, rank: .seven), | |
Card(suit: .heart, rank: .two), | |
Card(suit: .diamond, rank: .jack), | |
Card(suit: .spade, rank: .six), | |
Card(suit: .club, rank: .eight)] | |
hand = PokerEngine.detectHand(cards: cards) | |
assert(hand! == .highCard) | |
func displayOdds(_ survey: [PokerHand: Int], _ total: Int) { | |
let odds = survey.map { (hand: PokerHand, count: Int) in | |
return (hand, count, Double(count) / Double(total)) | |
} | |
let sortOdds: ((PokerHand, Int, Double), (PokerHand, Int, Double)) -> Bool = { (rhs, lhs) in | |
lhs.0 < rhs.0 | |
} | |
odds.sorted(by: sortOdds).forEach({ (arg0) in | |
let (hand, count, odd) = arg0 | |
print("odds for \(hand) is: \(String(format: "%.8f", odd)) -- \(count) / \(total)") | |
}) | |
} | |
deck = Deck() | |
let total = 10000000 | |
var survey: [PokerHand: Int] = [:] | |
for count in 1..<total { | |
let cards = deck.deal(count: 5) | |
guard let hand = PokerEngine.detectHand(cards: cards) else { | |
fatalError("couldn't detect hand for \(cards)") | |
} | |
survey[hand] = (survey[hand] ?? 0) + 1 | |
deck.reset() | |
if count % (total / 10) == 0 { | |
print("\(count) hands dealt") | |
displayOdds(survey, count) | |
} | |
} | |
print(survey) | |
print("odds are:") | |
displayOdds(survey, total) | |
// odds for royalFlush is: 0.00000150 -- 15 / 10000000 | |
// odds for straightFlush is: 0.00001200 -- 120 / 10000000 | |
// odds for fourOfAKind is: 0.00023650 -- 2365 / 10000000 | |
// odds for fullHouse is: 0.00142530 -- 14253 / 10000000 | |
// odds for flush is: 0.00197490 -- 19749 / 10000000 | |
// odds for straight is: 0.00351380 -- 35138 / 10000000 | |
// odds for threeKind is: 0.02111000 -- 211100 / 10000000 | |
// odds for twoPair is: 0.04752600 -- 475260 / 10000000 | |
// odds for pair is: 0.42236520 -- 4223652 / 10000000 | |
// odds for highCard is: 0.50183470 -- 5018347 / 10000000 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi @kingreza , very interesting. Two questions:
private static func isTwoPair(cards: [Card]) -> Bool
. why not make itreturn cards.allRanksWithCount().values.filter({$0 == 2}).count > 1
?ace, two, three, four, five
. Am I missing something?