Pythonでポーカーを作る#1

#目標
ポーカーなど数種類のトランプゲームを遊べるコードを書く。

#動機
麻雀を作っていたが、待ちの判定など難しかったため、練習ついでに何となくゲーム性が似ていて比較的簡単に作れそうなポーカーを作ってみたくなった。

#環境
Windows 10
Python 3.8.8

#やったこと
コンピューター同士での5カードドローポーカーの対決を行えるようにした。
カードを配り、手札の状況から捨てるカードを判断し、カードを捨て、カードを引く。最後に役の判定を行い、勝敗を決める。
チップ周りは未実装だが、それ以外は一通り実装できたと思う。
細かい仕様として、プレイヤー数は1~9人を想定、ジョーカーに対応(ワイルドカードとして使用できる)。
ジョーカーは恐らく何枚でも使用できる(100枚まで動作確認)。
また、5カードドローポーカーでは本来ドローは1ターンだけなのだが、一応何ターンでもドローを行えるようにした(999ターンまで動作確認)。

#やってないこと
今後別のルールのポーカーを作ることを考え、手札が5枚でなくても役の判定が行えるようにしたかったがワイルドカードを含むストレートフラッシュの判定が難しく断念した。
一応ワイルドカード無しの場合のコードは書いたがバグが残っているため使用していない。
今後は手札から5枚抜き取りそれを現状のコードで判定するということを考えている。
例としてテキサスホールデムの場合カードが7枚のため5枚の選び方は7C5で21通り。
21回役判定のコードを回して最も強かった場合だけを抜き取ればよいことになる。

また、ポーカーの肝であるチップ周りの処理が完全に未実装なため次回以降なんとかしたい。

#実装
今回は以下の6つのモジュールを作成した。

<details><summary>main.py</summary><div>


```python
import games

game = games.FiveCardDraw()
game.game()
```

</div></details>

<details><summary>games.py</summary><div>

```python
import cards_system
import player
import poker_winner

# 5カードドローポーカーの進行を行うクラス
class FiveCardDraw:
    # number_of_playersは1~9, number_of_cardsは52以上, turnsは1以上を想定
    def __init__(self, number_of_players = 8, number_of_cards = 53, turns = 1):
        self.number_of_players = number_of_players
        self.number_of_cards = number_of_cards
        self.turns = turns

        self.table = cards_system.Table(self.number_of_cards)
        self.winner = poker_winner.PokerWinner()
        self.player = []
        for i in range(self.number_of_players):
            self.player.append(player.Player())
            self.player[i].name = 'player' + str(i + 1)

    # ゲーム開始時の処理
    def deal(self):
        self.table.deck_initialize()
        for i in range(self.number_of_players):
            for j in range(5):
                self.player[i].draw(self.table.deck)
    
    # 1ターンにおける処理
    def one_turn(self):
        for i in range(self.number_of_players):    
            # 山札が10枚未満になったときの処理
            if len(self.table.deck) < 10:
                self.table.discard_return_deck()
                
            print(self.player[i].name)
            self.player[i].number_sort(reverse = True)
            print([self.player[i].hand[j].str for j in range(len(self.player[i].hand))])

            # 捨て札選択、ドローの処理
            throwcard, drawcard = self.player[i].five_card_draw_throw(self.table.deck, self.table.discard)
            print('throw in ', end='')
            print([throwcard[j].str for j in range(len(throwcard))])
            print('draw ', end='')
            print([drawcard[j].str for j in range(len(drawcard))])
    
    # ゲーム全体の進行
    def game(self):
        rank = [''] * self.number_of_players
        cardpower = [0] * self.number_of_players

        self.deal()

        for i in range(self.turns):
            self.one_turn()

            # 最終ターンが終了した際の処理
            if i == self.turns - 1:
                for i in range(self.number_of_players):
                    rank[i], cardpower[i] = self.player[i].poker_rank()
                    print(self.player[i].name + ' ' + rank[i])
                    print([self.player[i].hand[j].str for j in range(len(self.player[i].hand))])
                print('winner ' + self.player[self.winner.poker_winner(cardpower)].name)
```
</div></details>

<details><summary>cards_system.py</summary><div>

```python
import random

# カードの数字, マーク, 表示用の文字列を管理するクラス
# allnumber=0~51が1~13のカード4種類ずつ, 52~はjokerを表す
# 標準で数字は1~13, マークは1~4(順にspade, heart, diamond, clubを想定)
# jokerは数字マークともに0
# ace14, two15をTrueにするとAと2の数字がそれぞれ14, 15扱いとなる
class Card:
    def __init__(self, allnumber, ace14 = False, two15 = False):
        if allnumber >= 52:
            self.number = 0
            self.suit = 0
            self.str = 'joker'
        else:
            self.number = allnumber % 13 + 1
            self.suit =  allnumber // 13 + 1
            if self.suit == 1:
                self.str =  'spade ' + str(self.number)
            elif self.suit == 2:
                self.str = 'heart ' + str(self.number)
            elif self.suit == 3:
                self.str = 'daimond ' + str(self.number)
            elif self.suit == 4:
                self.str = 'club ' + str(self.number)

            if ace14 and self.number == 1:
                self.number = 14
            elif two15 and self.number == 2:
                self.number = 15

# 全カードリスト, 山札, 捨て札の管理
# 山札のシャッフル, 山札の初期化, 捨て札を山札に戻す処理を行うクラス
class Table:
    def __init__(self, number_of_cards, ace14 = False, two15 = False):
        self.card = [Card(i, ace14, two15) for i in range(number_of_cards)]
        self.deck = []
        self.community = []
        self.discard = []

    # 山札のシャッフル
    def shuffle(self):
        self.deck = list(self.deck)
        random.shuffle(self.deck)

    # 山札の初期化
    def deck_initialize(self):
        self.deck = list(self.card)
        random.shuffle(self.deck)

    # 捨て札を山札に戻してシャッフルする
    def discard_return_deck(self):
        self.deck.extend(self.discard)
        self.discard = []
        self.shuffle()
```
</div></details>

<details><summary>player.py</summary><div>

```python
import poker_rank
import random

# 5カードドローポーカーにおける捨て札選択を行うクラス
class FiveCardChoice:
    def throw_card_choice(self, hand):
        prank = poker_rank.PokerRank()
        throw_card = []
        rank_str, cardpower = prank.poker_rank(hand)
        hand = prank.rank_sort(hand, cardpower)
        rank = cardpower[0]

        # ストレート以上確定で何も切らない
        if rank > 3:
            throw_card = []
        # スリーカードのときは他の2枚を切る
        elif rank == 3:
            throw_card = [3, 3]
        
        # ツーペアのときは残り1枚を切る
        elif rank == 2:
            throw_card = [4]
        
        # ワンペアのときは残り3枚を切る
        elif rank == 1:
            throw_card = [2, 2, 2]
        
        # ブタのときはランダムに1~5枚、数字の小さい物から順に捨てる
        else:
            random_number = random.randint(1, 5)
            for i in range(random_number):
                throw_card.append(5 - random_number)
        
        return hand, throw_card

# 手札とプレイヤー名の管理
# 手札のソート, ドロー, 捨て札選択, 手札の役確認の処理を行うクラス
class Player:
    def __init__(self):
        self.hand = []
        self.name = ''

        self.fivedraw = FiveCardChoice()
        self.prank = poker_rank.PokerRank()

    # カードを数字でソートする 標準では小さい順
    def number_sort(self, reverse = False):
        if reverse:
            self.hand.sort(reverse = True, key = lambda h: h.number)
        else:
            self.hand.sort(key = lambda h: h.number)

    # カードをマークでソートする
    def suit_sort(self):
        self.hand.sort(key = lambda h: h.suit)

    # カードをdeckの0から1枚だけドローする
    def draw(self, deck):
        draw_card = deck[0]
        self.hand.append(deck[0])
        del deck[0]
        return draw_card

    # カードを捨てる
    def throw(self, throw_number, discard):
        throw_card = self.hand[throw_number]
        discard.append(self.hand[throw_number])
        del self.hand[throw_number]
        return throw_card

    # 5カードドローポーカーにおける捨て札選択、ドローの一連の処理を行う
    def five_card_draw_throw(self, deck, discard):
        self.hand, throw_num = self.fivedraw.throw_card_choice(self.hand)
        throw_cards = []
        draw_cards = []
        for i in throw_num:
            throw_cards.append(self.throw(i, discard))
            draw_cards.append(self.draw(deck))
        return throw_cards, draw_cards

    # 手札の役を取得する
    def poker_rank(self):
        rank, cardpower = self.prank.poker_rank(self.hand)
        self.hand = self.prank.rank_sort(self.hand, cardpower)
        return rank, cardpower
```
</div></details>


<details><summary>poker_rank.py</summary><div>

```python
# ポーカーの役判定, 役ベースでの手札のソートを行うクラス
class PokerRank:
    # ポーカーの役判定を行う(handが5枚のとき専用)
    def poker_rank(self, hand):
        flush = False
        straight = False

        # rankは上がり役の文字列(表示用)
        # cardpowerは[0]が役の強さ[1]以降は役が同じときの比較用
        rank = ''
        cardpower = []

        numlist = [0 for i in range(15)]
        suitlist = [0 for i in range(5)]

        len_numlist = len(numlist)

        # 4枚以下の時の処理用(未実装)
        len_hand = len(hand)

        for i in range(len_hand):
            numlist[hand[i].number] += 1
            suitlist[hand[i].suit] += 1
        
        # フラッシュの判定
        if max(suitlist) + suitlist[0] == 5:
            flush = True

        straight_numlist = list(numlist)

        # ストレートの判定
        for i in range(2,11):
            if straight_numlist[i] == 1:
                for j in range(4):
                    if straight_numlist[i + j + 1] == 0:
                        if straight_numlist[0] >= 1:
                            straight_numlist[i + j + 1] += 1
                            straight_numlist[0] -= 1
                        else:
                            break
                    if j + 1 == 4:
                        straight_number = i + j + 1
                        straight = True
                break
        
        # ジョーカーを何のカードとして扱うか決定する
        number_of_joker = numlist[0]
        numlist[0] = 0
        if number_of_joker > 0:
            # 前者がスリーカードとフォーカード、後者がワンペアの条件
            if max(numlist) >= 3 or numlist.count(2) == 1:
                numlist[numlist.index(max(numlist))] += number_of_joker
            # ブタとツーペアの時は最も大きい数に重ねる
            else:
                for i in range(len_numlist):
                    if numlist[len_numlist - i - 1] > 0:
                        numlist[len_numlist - i - 1] += number_of_joker
                        break

        # ファイブカードの判定
        if 5 in numlist:
            rank = 'five of a kind'
            cardpower.append(10)
            cardpower.append(numlist.index(5))

        # ストレートフラッシュの判定
        elif flush and straight:
            # ロイヤルストレートフラッシュの判定
            if numlist[14] == 1:
                rank = 'royal flush'
                cardpower.append(9)

            # 普通のストレートフラッシュの判定
            else:
                rank = 'straight flush'
                cardpower.append(8)
                cardpower.append(straight_number)

        # フォーカードの判定
        elif 4 in numlist:
            rank = 'four of a kind'
            cardpower.append(7)
            cardpower.append(numlist.index(4))
            cardpower.append(numlist.index(1))

        # フルハウスの判定
        elif 3 in numlist and 2 in numlist:
            rank = 'full house'
            cardpower.append(6)
            cardpower.append(numlist.index(3))
            cardpower.append(numlist.index(2))

        # フラッシュの判定
        elif flush:
            rank = 'flush'
            cardpower.append(5)
            for i in range(len_numlist):
                # ジョーカー入りのときはジョーカーをAとみなす
                temp = len_numlist - i - 1
                if numlist[temp] > 1:
                    for j in range(numlist[temp] - 1):
                        cardpower.append(14)
                    cardpower.append(temp)
                elif numlist[temp] == 1:
                    cardpower.append(temp)

        # ストレートの判定
        elif straight:
            rank = 'straight'
            cardpower.append(4)
            cardpower.append(straight_number)

        # スリーカードの判定
        elif 3 in numlist:
            rank = 'three of a kind'
            cardpower.append(3)
            cardpower.append(numlist.index(3))
            for i in range(len_numlist):
                temp = len_numlist - i - 1
                if numlist[temp] == 1:
                    cardpower.append(temp)

        # ツーペアの判定
        elif numlist.count(2) == 2:
            rank = 'two pair'
            cardpower.append(2)
            for i in range(len_numlist):
                temp = len_numlist - i - 1
                if numlist[temp] == 2:
                    cardpower.append(temp)
            for i in range(len_numlist):
                temp = len_numlist - i - 1
                if numlist[temp] == 1:
                    cardpower.append(temp)

        # ワンペアの判定
        elif 2 in numlist:
            rank = 'a pair'
            cardpower.append(1)
            cardpower.append(numlist.index(2))
            for i in range(len_numlist):
                temp = len_numlist - i - 1
                if numlist[temp] == 1:
                    cardpower.append(temp)

        # ブタのとき
        else:
            rank = 'high card'
            cardpower.append(0)
            for i in range(len_numlist):
                temp = len_numlist - i - 1
                if numlist[temp] == 1:
                    cardpower.append(temp)

        for i in range(6-len(cardpower)):
            cardpower.append(0)

        return rank, cardpower

    # カードを役ベースでソートする
    def rank_sort(self, hand, cardpower):
        expect_joker_hand = []
        sorted_hand = []
        for card in hand:
            if card.number == 0:
                sorted_hand.append(card)
            else:
                expect_joker_hand.append(card)

        expect_joker_hand.sort(reverse = True, key = lambda h: h.number)

        if cardpower[0] == 0:
            sorted_hand.extend(expect_joker_hand)
        elif cardpower[0] == 1:
            for i in range(4):
                for card in expect_joker_hand:
                    if card.number == cardpower[i + 1]:
                        sorted_hand.append(card)
        elif cardpower[0] == 2 or cardpower[0] == 3:
            for i in range(3):
                for card in expect_joker_hand:
                    if card.number == cardpower[i + 1]:
                        sorted_hand.append(card)
        elif cardpower[0] == 4 or cardpower[0] == 5:
            sorted_hand.extend(expect_joker_hand)
        elif cardpower[0] == 6 or cardpower[0] == 7:
            for i in range(2):
                for card in expect_joker_hand:
                    if card.number == cardpower[i + 1]:
                        sorted_hand.append(card)
        elif cardpower[0] >= 8:
            sorted_hand.extend(expect_joker_hand)

        return sorted_hand
```
</div></details>

<details><summary>poker_winner.py</summary><div>

```python
# 勝者を決定するクラス
class PokerWinner:
    def poker_winner(self, cardpower):
        # 2次元配列の行列の変換
        converted_cardpower = [list(x) for x in zip(*cardpower)]

        max_rank = max(converted_cardpower[0])

        # 役だけで勝敗が確定するときの処理
        if converted_cardpower[0].count(max_rank) == 1:
            winner = converted_cardpower[0].index(max_rank)
            return winner

        # 役で決まらなかった場合は最強役以外のプレイヤーのcardpowerを全て0にする
        else:
            for i in range(len(converted_cardpower[0])):
                if converted_cardpower[0][i] != max_rank:
                    cardpower[i] = [0, 0, 0, 0, 0, 0]

        converted_cardpower = [list(x) for x in zip(*cardpower)]

        # cardpowerを比較し勝敗を決定する
        # 完全に同じ場合はプレイヤー番号が若い方が勝者となる
        for i in range(1, len(converted_cardpower)):
            if converted_cardpower[i].count(max(converted_cardpower[i])) == 1:
                winner = converted_cardpower[i].index(max(converted_cardpower[i]))
                return winner
```
</div></details>

#実行結果
main.pyを実行した際の実行結果を以下に示す。

<details><summary>実行結果</summary><div>

```
player1
['daimond 13', 'spade 12', 'heart 11', 'club 8', 'spade 5']
throw in ['heart 11', 'club 8', 'spade 5']
draw ['daimond 12', 'spade 10', 'heart 9']
player2
['club 1', 'daimond 8', 'heart 6', 'daimond 6', 'club 4']
throw in ['club 1', 'daimond 8', 'club 4']
draw ['heart 10', 'joker', 'club 2']
player3
['club 12', 'club 11', 'spade 7', 'daimond 7', 'spade 3']
throw in ['club 12', 'club 11', 'spade 3']
draw ['spade 1', 'daimond 5', 'heart 11']
player4
['daimond 1', 'club 10', 'club 9', 'heart 7', 'daimond 4']
throw in ['daimond 1', 'club 10', 'club 9', 'heart 7', 'daimond 4']
draw ['heart 13', 'spade 2', 'club 4', 'club 8', 'daimond 2']
player5
['daimond 10', 'spade 8', 'spade 6', 'spade 4', 'heart 3']
throw in ['spade 4', 'heart 3']
draw ['heart 7', 'daimond 4']
player6
['heart 1', 'heart 8', 'club 6', 'heart 4', 'heart 2']
throw in ['club 6', 'heart 4', 'heart 2']
draw ['daimond 8', 'club 10', 'club 1']
player7
['heart 12', 'club 7', 'heart 5', 'club 5', 'daimond 3']
throw in ['heart 12', 'club 7', 'daimond 3']
draw ['spade 13', 'spade 5', 'club 6']
player8
['club 13', 'spade 11', 'daimond 11', 'spade 9', 'club 3']
throw in ['club 13', 'spade 9', 'club 3']
draw ['heart 2', 'daimond 1', 'heart 3']
player1 a pair
['spade 12', 'daimond 12', 'daimond 13', 'spade 10', 'heart 9']
player2 three of a kind
['joker', 'heart 6', 'daimond 6', 'heart 10', 'club 2']
player3 a pair
['spade 7', 'daimond 7', 'spade 1', 'heart 11', 'daimond 5']
player4 a pair
['spade 2', 'daimond 2', 'heart 13', 'club 8', 'club 4']
player5 high card
['daimond 10', 'spade 8', 'heart 7', 'spade 6', 'daimond 4']
player6 two pair
['heart 1', 'club 1', 'heart 8', 'daimond 8', 'club 10']
player7 three of a kind
['heart 5', 'club 5', 'spade 5', 'spade 13', 'club 6']
player8 a pair
['spade 11', 'daimond 11', 'daimond 1', 'heart 3', 'heart 2']
winner player2
```
</div></details>

ジョーカーがワイルドカードとして機能しており、役が判定できていることが分かる。
また、最高役が同じ際にカードの強さで勝敗を判定できていることが分かる。