Help us understand the problem. What is going on with this article?

ポーカー作りで Swift 3 の空気を感じる(後半)

More than 3 years have passed since last update.

ポーカー作りで Swift 3 の空気を感じる(後半)

⛄🎄🎁🎂🎅🎂🎁🎄⛄
めりーさんたさん❗

こんにちは Liaro131e55 です。

この記事は、前半からの続きのもので、Swift 3 でコマンドライン上で動く簡単なポーカーを作る流れを書いたものになります。また、Swift その2 Advent Calendar 2016の25日目の記事となります。トリにしてはなぁ感がある記事ですがその2だしいっかという軽い気持ちで書きます(てへ)

開発環境

  • Swift 3.0.1
  • Xcode 8.1
  • macOS 10.12.1

開発の流れ

以下の流れで作っていきます。項目1から7は前半で終えていますので今回は項目8からとなります。

  1. Xcodeプロジェクト作成 (前半)
  2. カードを管理する構造体 (前半)
  3. カードの表示 (前半)
  4. カードの比較 (前半)
  5. 山札を管理する構造体 (前半)
  6. 手札を管理する構造体 (前半)
  7. 役を管理する列挙型 (前半)
  8. 役判定の実装
  9. ポーカーの実装

では役判定の実装から❗

役判定の実装

役判定をどう実装するかを考えるために、とりあえず各役の関係を整理してみます。

じゃん。

PokerHands.png

図から、ポーカーの役は「同じランクを含む役」と「同じランクを含まない役」のふたつに分類でき、「同じランクを含まない役」はエースハイストレート(A, K, Q, J, 10 によるストレート)、ストレート、フラッシュの3つを判定できればそれらを組み合わせることで4つ全てを判定できることがわかります。対して「同じランクを含む役」は以下の表のように、手札を同じランク別に分けてそれぞれの枚数を見れば判定できることがわかります。

同じランクを含む役 同じランク別の枚数
フォーカード 4, 1 [♣️A ♦️A ♥️A ♠️A] [♥️K]
フルハウス 3, 2 [♣️A ♦️A ♥️A] [♠️2 ♥️2]
スリーカード 3, 1, 1 [♣️A ♦️A ♥️A] [♠️2] [♥️K]
ツーペア 2, 2, 1 [♣️A ♦️A] [♥️2 ♠️2] [♥️K]
ワンペア 2, 1, 1, 1 [♣️A ♦️A] [♥️2] [♠️3] [♥️K]

と、言うわけで、役判定実装の流れは以下の順にしてみます。

  • 同じランク別の枚数の取得
  • 同じランクを含む役の判定
  • エースハイストレートを含むかどうかの判定
  • ストレートを含むかどうかの判定
  • フラッシュを含むかどうかの判定
  • 同じランクを含まない役の判定

役判定に関するコードはちゃんと動くのか怪しいので、テストコードも書いていきます。今までに作成した Suit.swiftRank.swiftCard.swiftPokerHand.swiftPokerCLITests ターゲットから参照できるように以下の図のようにそれぞれチェックをいれます。(図の一番下)

テスト準備.png

同じランク別の枚数の取得

ではまず同じランク別の枚数の取得からです。 PokerHand 列挙型に static なメソッドとして追加します。

PokerHand.swift
enum PokerHand: CustomStringConvertible {

    case highCard
    /* 長いので省略 */
    case royalFlush

    var description: String { /* 長いので省略 */ }

    // ココ❗
    static func countsBySameRank(cards: [Card]) -> [Int] {
        // カードが何枚かあることを保証
        guard !cards.isEmpty else {
            return [0]
        }

        var cards = cards
        var counts: [Int] = []

        // 1枚目から順にそのカードと同じランクのカードを手札から抽出し枚数を数えていく
        // 抽出されたカードは手札から除いていき手札がなくなったら終わり
        while !cards.isEmpty {
            let card = cards.first!
            let sameRankCards = cards.filter({ $0.rank == card.rank })
            cards = cards.filter({ $0.rank != card.rank })
            counts.append(sameRankCards.count)
        }

        // 降順にソート
        counts.sort(by: { $0 > $1 })

        return counts
    }


    init(cards: [Card]) { /* 長いので省略 */ }
}

英語が合ってるか微妙ですが PokerHand.countsBySameRank(cards: [Card]) -> [Int] と定義し、渡された5枚のカードの同じランク別の枚数を配列で返します。

テストコードは簡単に、全ての役の手札を用意しておき、PokerHand.countsBySameRank(cards: [Card]) -> [Int] が期待通りに動くかをテストします。「同じランクを含む役」は同じランクを含む役の各枚数の通りに、「同じランクを含まない役」と🐷は [1, 1, 1, 1, 1] となってほしいです。

PokerCLITests.swift
import XCTest
@testable import PokerCLI

class PokerCLITests: XCTestCase {

    // ココ❗

    // |♣️ A||♣️ 2||♥️ J||♦️ 3||♥️ Q| 🐷手札
    let highCardCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .two), Card(suit: .heart, rank: .jack), Card(suit: .diamond, rank: .three), Card(suit: .heart, rank: .queen)]
    // |♣️ A||♣️ 2||♥️ A||♦️ 3||♥️ Q| ワンペア手札
    let onePairCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .two), Card(suit: .heart, rank: .ace), Card(suit: .diamond, rank: .three), Card(suit: .heart, rank: .queen)]
    // |♣️ A||♣️ 2||♥️ A||♦️ 2||♥️ Q| ツーペア手札
    let twoPairCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .two), Card(suit: .heart, rank: .ace), Card(suit: .diamond, rank: .two), Card(suit: .heart, rank: .queen)]
    // |♣️ A||♣️ 2||♥️ A||♦️ A||♥️ Q| スリーカード手札
    let threeOfAKindCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .two), Card(suit: .heart, rank: .ace), Card(suit: .diamond, rank: .ace), Card(suit: .heart, rank: .queen)]
    // |♣️ A||♣️ 2||♥️ 3||♦️ 4||♥️ 5] ストレート手札
    let straightCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .two), Card(suit: .heart, rank: .three), Card(suit: .diamond, rank: .four), Card(suit: .heart, rank: .five)]
    // |♣️ A||♣️10||♥️ J||♦️ Q||♥️ K| エースハイストレート手札
    let aceHighStraightCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .ten), Card(suit: .heart, rank: .jack), Card(suit: .diamond, rank: .queen), Card(suit: .heart, rank: .king)]
    // |♣️ A||♣️ 3||♣️ 5||♣️ J||♣️ K| フラッシュ手札
    let flushCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .three), Card(suit: .club, rank: .jack), Card(suit: .club, rank: .four), Card(suit: .club, rank: .king)]
    // |♣️ A||♣️ 2||♥️ A||♦️ A||♥️ 2| フルハウス手札
    let fullHouseCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .two), Card(suit: .heart, rank: .ace), Card(suit: .diamond, rank: .ace), Card(suit: .heart, rank: .two)]
    // |♣️ A||♣️ 2||♥️ A||♦️ A||♠️ A| フォーカード手札
    let fourOfAKindCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .two), Card(suit: .heart, rank: .ace), Card(suit: .diamond, rank: .ace), Card(suit: .spade, rank: .ace)]
    // |♣️ A||♣️ 2||♣️ 3||♣️ 4||♣️ 5| ストレートフラッシュ手札
    let straightFlushCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .two), Card(suit: .club, rank: .three), Card(suit: .club, rank: .four), Card(suit: .club, rank: .five)]
    // |♣️ A||♣️10||♣️ J||♣️ Q||♣️ K| ロイヤルストレートフラッシュ手札
    let royalFlushCards = [Card(suit: .club, rank: .ace), Card(suit: .club, rank: .ten), Card(suit: .club, rank: .jack), Card(suit: .club, rank: .queen), Card(suit: .club, rank: .king)]

    override func setUp() { /* 省略 */ }
    override func tearDown() { /* 省略 */ }

    // ココ❗
    func testCountsBySameRank() {
        XCTAssertEqual(PokerHand.countsBySameRank(cards: highCardCards), [1, 1, 1, 1, 1])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: onePairCards), [2, 1, 1, 1])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: twoPairCards), [2, 2, 1])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: threeOfAKindCards), [3, 1, 1])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: straightCards), [1, 1, 1, 1, 1])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: aceHighStraightCards), [1, 1, 1, 1, 1])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: flushCards), [1, 1, 1, 1, 1])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: fullHouseCards), [3, 2])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: fourOfAKindCards), [4, 1])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: straightFlushCards), [1, 1, 1, 1, 1])
        XCTAssertEqual(PokerHand.countsBySameRank(cards: royalFlushCards), [1, 1, 1, 1, 1])
    }
}

ではテストを走らせてみます。

テスト結果.png

左上に  がついているのでテストが通りました。雑なテストケースですが、PokerHand.countsBySameRank(cards: [Card]) -> [Int] はとりあえず期待どおりに動いていそうです。この流れで他のテストコードも書いていきます。長くなるので以降はテスト結果のみ載せます。

同じランクを含む役の判定

同じランク別の枚数の取得ができたら「同じランクを含む役」であるワンペア、ツーペア、スリーカード、フルハウス、フォーカードは全て確定できます。というわけでこれらの役の判定を PokerHand のコンストラクタ init に追記していきましょう。

PokerHand.swift
enum PokerHand: CustomStringConvertible {

    case highCard
    /* 長いので省略 */
    case royalFlush

    var description: String { /* 長いので省略 */ }
    static func countsBySameRank(cards: [Card]) -> [Int] { /* 長いので省略 */ }

    init(cards: [Card]) {
        // カードが5枚であることを保証
        guard cards.count == 5 else {
            self = .highCard
            return
        }

        // ココ❗
        let counts = PokerHand.countsBySameRank(cards: cards)

        if counts == [2, 1, 1, 1] {
            self = .onePair
        }
        else if counts == [2, 2, 1] {
            self = .twoPair
        }
        else if counts == [3, 1, 1] {
            self = .threeOfAKind
        }
        else if counts == [3, 2] {
            self = .fullHouse
        }
        else if counts == [4, 1] {
            self = .fourOfAKind
        }
        else {
            self = .highCard
        }
    }
}

(´-`).。oO( switch で書きたかったのに配列ではできない様子❓誰か教えてください...)

エースハイストレートを含むかどうかの判定

ここからは「同じランクを含まない役」の判定です。

まずはエースハイストレートを含むかどうかを判定します。単純なストレートの判定は手札が連番かどうかを見れば良さそうですが A, K, Q, J, 10 の場合は A がちょっと邪魔なので単純にこの5つ全てを含むかどうかで判定することにします。

PokerHand.swift
enum PokerHand: CustomStringConvertible {

    case highCard
    /* 長いので省略 */
    case royalFlush

    var description: String { /* 長いので省略 */ }
    static func countsBySameRank(cards: [Card]) -> [Int] { /* 長いので省略 */ }

    // ココ❗
    static func containsAceHighStraight(cards: [Card]) -> Bool {
        // カードが5枚であることを保証
        guard cards.count == 5 else {
            return false
        }

        // ランクの降順にソート
        let cards = cards.sorted { $0.rank.rawValue > $1.rank.rawValue }

        // K, Q, J, 10, A ならエースハイストレートを含む。違うなら含まない。
        if cards[0].rank == .king
            && cards[1].rank == .queen
            && cards[2].rank == .jack
            && cards[3].rank == .ten
            && cards[4].rank == .ace {

            return true
        }

        return false
    }

    init(cards: [Card]) { /* 長いので省略 */ }
}

渡されたカード5枚にエースハイストレートの役を含むなら true、含まないなら false を返します。

テストコードは以下です。エースハイストレートを含む役はロイヤルストレートフラッシュとエースハイストレートになるはずです。

AceHighStraightテスト結果

ストレートを含むかどうかの判定

エースハイストレートとならないストレートの判定は単純に連番になるかを見れば良いですね。

PokerHand.swift
enum PokerHand: CustomStringConvertible {

    case highCard
    /* 長いので省略 */
    case royalFlush

    var description: String { /* 長いので省略 */ }
    static func countsBySameRank(cards: [Card]) -> [Int] { /* 長いので省略 */ }
    static func containsAceHighStraight(cards: [Card]) -> Bool { /* 長いので省略 */ }

    // ココ❗
    static func containsStraight(cards: [Card]) -> Bool {
        // カードが5枚であることを保証
        guard cards.count == 5 else {
            return false
        }

        // エースハイストレートだったらストレートを含むとして終了。
        if PokerHand.containsAceHighStraight(cards: cards) {
            return true
        }

        // ランクの降順にソート
        let cards = cards.sorted { $0.rank.rawValue > $1.rank.rawValue }

        // 1枚目から4枚目までそれぞれ注目していく。
        // 全て「注目したカードの数字が次のカードの数字の1大きい数」ならストレートを含む。
        // 違うなら含まない。
        for i in 0..<cards.count - 1 {
            if cards[i].rank.rawValue == cards[i + 1].rank.rawValue + 1 {
                continue
            }

            return false
        }

        return true
    }

    init(cards: [Card]) { /* 長いので省略 */ }
}

もっと上手い方法がありそうですが、連番かどうかを判定しています。エースハイストレートの場合もストレートなので、先に作った PokerHand.containsAceHighStraight(cards: [Card]) -> Bool を使って判定しています。

テストコードは以下です。ストレートを含む役はロイヤルストレートフラッシュ、ストレートフラッシュ、エースハイストレート、ストレートになるはずです。

Straightテスト結果

フラッシュを含むかどうかの判定

含むかどうか判定最後のフラッシュ判定いきます。役判定の中で一番簡単そうです。

PokerHand.swift
enum PokerHand: CustomStringConvertible {

    case highCard
    /* 長いので省略 */
    case royalFlush

    var description: String { /* 長いので省略 */ }
    static func countsBySameRank(cards: [Card]) -> [Int] { /* 長いので省略 */ }
    static func containsAceHighStraight(cards: [Card]) -> Bool { /* 長いので省略 */ }
    static func containsStraight(cards: [Card]) -> Bool { /* 長いので省略 */ }

    // ココ❗
    static func containsFlush(cards: [Card]) -> Bool {
        // カードが5枚であることを保証
        guard cards.count == 5 else {
            return false
        }

        // 1枚目のスートを用意し、手札からそのスートと同じスートを持つカードだけを抽出する
        // 手札の枚数と同じなら全部同じスートなのでフラッシュを含む。違うなら含まない。
        let suit = cards[0].suit
        return cards.filter({ $0.suit == suit }).count == cards.count
    }

    init(cards: [Card]) { /* 長いので省略 */ }
}

1枚目のスートと同じスートを持つカードを抽出して、元の手札の数と同じなら全部同じスートでしょうということですね。

テストコードは以下です。フラッシュを含む役はロイヤルストレートフラッシュ、ストレートフラッシュ、フラッシュになるはずです。

スクリーンショット 2016-12-05 18.06.49.png

同じランクを含まない役の判定

ここまででエースハイストレート、ストレート、フラッシュを含むかどうかを判定できるようになったので、「同じランクを含まない役」であるストレート、フラッシュ、ストレートフラッシュ、ロイヤルストレートフラッシュを全て確定できます。というわけでこれらの役の判定を PokerHand のコンストラクタ init に追記して役判定を完成させましょう。

PokerHand.swift
enum PokerHand: CustomStringConvertible {

    case highCard
    /* 長いので省略 */
    case royalFlush

    var description: String { /* 長いので省略 */ }
    static func countsBySameRank(cards: [Card]) -> [Int] { /* 長いので省略 */ }
    static func containsAceHighStraight(cards: [Card]) -> Bool { /* 長いので省略 */ }
    static func containsStraight(cards: [Card]) -> Bool { /* 長いので省略 */ }
    static func containsFlush(cards: [Card]) -> Bool { /* 長いので省略 */ }

    init(cards: [Card]) {
        // カードが5枚であることを保証
        guard cards.count == 5 else {
            self = .highCard
            return
        }

        let counts = PokerHand.countsBySameRank(cards: cards)

        if counts == [2, 1, 1, 1] {
            self = .onePair
        }
        else if counts == [2, 2, 1] {
            self = .twoPair
        }
        else if counts == [3, 1, 1] {
            self = .threeOfAKind
        }
        else if counts == [3, 2] {
            self = .fullHouse
        }
        else if counts == [4, 1] {
            self = .fourOfAKind
        }
        // ココ❗
        else if PokerHand.containsFlush(cards: cards) {
            if PokerHand.containsAceHighStraight(cards: cards) {
                self = .royalFlush
            }
            else if PokerHand.containsStraight(cards: cards) {
                self = .straightFlush
            }
            else {
                self = .flush
            }
        }
        else if PokerHand.containsStraight(cards: cards) {
            self = .straight
        }
        else {
            self = .highCard
        }
    }
}

まず ココ❗ まで到達するのは同じランク別の枚数が全て1枚、つまり同じランクを含まない手札のときです。ここから、フラッシュとエースハイストレートを含む場合はロイヤルストレートフラッシュ、フラッシュとエースハイでないストレートを含むならストレートフラッシュ、フラッシュだけならフラッシュと確定でき、フラッシュを含まずストレートを含むならストレートと確定、フラッシュもストレートも含まないのなら🐷と確定できます。

これで役判定は完成したはずです。テストしてみましょう。手札に相当する PokerHand の値になっているはずです。(例えばワンペアの手札で PokerHand を生成すると列挙型の値は .onePair)

PokerHandテスト結果.png

ヾ(๑╹◡╹)ノ”いえーい

ポーカーの実装

やっとだー!やっとポーカーを実装できます。main.swift に書いていきますよー。

main.swift
print("ポーカーを始めます❗")

var deck = Deck()
deck.shuffle()

var hand = Hand(cards: deck.draw(count: 5)!)
print()
print("あなたの手札: ", hand)

print()
print("何枚目の手札を交換しますか❓(半角数字、半角スペース区切り)")
print("(交換しない場合は何も入力せずにエンター)")

// コマンドライン上で入力待ちになりエンターを押すまで停止する
print("> ", terminator: "")
let input = readLine(strippingNewline: true)

// 何か文字が入力されていれば交換できるかも
if let input = input, input.characters.count > 0 {

    // 何枚目のカードを捨てるかを配列のインデックスとして保存
    var discardIndices: [Int] = []

    // 入力された文字列を半角スペースで区切った配列としそれぞれ見ていく
    for separatedString in input.components(separatedBy: " ") {

        // Intに変換できて、1以上5以下の数字なら許可
        if let number = Int(separatedString), 1...5 ~= number {

            // 配列のインデックスとして扱うため1減らしたものを追加
            discardIndices.append(number - 1)
        }
    }

    // 重複しているかもしれないので重複を除く
    discardIndices = Array(Set(discardIndices))

    // 空でなければ手札を交換する
    if !discardIndices.isEmpty {
        hand.replace(
            discardIndices: discardIndices,
            newCards: deck.draw(count: discardIndices.count)!
        )

        print()
        print("あなたの手札: ", hand)
    }
}

print()
print(PokerHand(cards: hand.cards), "でした❗")
print()

こんな感じで良いでしょう❗

さっそく何回か実行してみます。

PokerCLI実行1.png

おお

PokerCLI実行2.png

交換しない場合も動いていそう😃

PokerCLI実行3.png

惜しいぶー🐷

PokerCLI実行4.png

Σ(・∀・;)

おわりに

前半、後半(本記事)と長くなってしまいましたが、純粋な Swift 3 を使ってコマンドラインで動くポーカーを作ることができました。ジョーカーを含んでいないのでポーカーのアルゴリズムの参考になるかは怪しいところですが Swift 3 で書くとこんな感じのコードになるのか〜という空気だけでも感じてもらえていたら嬉しいです。

また、今回作成したポーカーの全ソースコードはGitHub 131e55/PokerCLI にて公開していますのでご覧ください😃

これをきっかけに Swift に触れてみようかなという人が増えてくれたらいいなぁヾ(๑╹◡╹)ノ”

おまけ

omake.swift
import Foundation

func omake() {
    print("最初の手札でロイヤルストレートフラッシュを出すぞい")

    var count = 1
    var pokerHand = PokerHand.highCard
    let start = Date().timeIntervalSince1970

    while pokerHand != .royalFlush {

        var deck = Deck()
        deck.shuffle()

        let hand = Hand(cards: deck.draw(count: 5)!)
        pokerHand = PokerHand(cards: hand.cards)

        let elapsed = (Date().timeIntervalSince1970 - start)
        print("\(count)回目❗", hand, pokerHand, "(\(elapsed) 秒経過)")

        count += 1
    }

    print("最初の手札でロイヤルストレートフラッシュを出したぞい❗")
    print()
}

omake1

omake2

131e55
hachidoriinc
「すべての人に、価値ある仕事を」先端技術と上手く共存し、すべての人が、人間にしかできない「価値ある」仕事に集中できる世界を作りたい。そんな思いでhachidoriはサービスを提供しています。
https://hachidoriinc.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした