Pythonでブラックジャック作ってみた（その1）

数年前にPythonの独学を始めてから、[コーリー・アルソフ著『独学プログラマー  Python言語の基本から仕事のやり方まで』](https://www.amazon.co.jp/dp/4822292274)という本を、ときどき読み返しています。その中にトランプゲーム「戦争」のコードが載っています。以下のような内容です。

https://github.com/Groves-bot/war

こんなコード、勉強もかねて作ってみたい。けれどもっと遊べるゲームがいい。ブラックジャックなんかどうだろう？　ポーカーやバカラに比べると比較的ルールも簡単そうだし。

そう思って調べていたら、5年ほど前にQiitaでお題が出ていました。

https://qiita.com/hirossyi73/items/cf8648c31898216312e5

出題者の方はC#を想定されていたようですが、Python勢もチャレンジしたようで、いくつか記事を見つけることができました。

https://qiita.com/adoth/items/b3c38d156884565e2c11

https://qiita.com/fantm21/items/de109f2ebf147679c12d

https://qiita.com/p-t-a-p-1/items/e673eba61acc6a7e39ff

他にもたくさんの方が挑戦していて、見比べてみると面白いです。同じルールの同じゲームで、同じように遊べる。けれど、コードはそれぞれかなり違います。
いろいろと見て、勉強させていただいた後で、私もブラックジャック作りに挑戦してみました。ルールは以下です。


>- 初期カードは52枚。引く際にカードの重複は無いようにする
>- プレイヤーとディーラーの2人対戦。プレイヤーは実行者、ディーラーは自動的に実行
>- 実行開始時、プレイヤーとディーラーはそれぞれ、カードを2枚引く。引いたカードは画面に表示する。ただし、ディーラーの2枚目のカードは分からないようにする
>- その後、先にプレイヤーがカードを引く。プレイヤーが21を超えていたらバーストしてプレイヤーの負け、その時点でゲーム終了
>- プレイヤーは、カードを引くたびに、次のカードを引くか選択できる
>- プレイヤーが引き終えたら、その後ディーラーは、自分の手札が17以上になるまで引き続ける
>- プレイヤーとディーラーが引き終えたら勝負。より21に近い方の勝ち
>- JとQとKは10として扱う
>- Aはとりあえず「1」としてだけ扱う。「11」にはしない
>- ダブルダウンなし、スプリットなし、サレンダーなし、その他特殊そうなルールなし

```python:blackjack.py

from random import shuffle
import sys

class Card:

    suits = ['♥','♠','◆','♣']

    ranks = [None, "A","2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]

    def __init__(self, s, r):
        self.suit = s
        self.rank = r

    def __int__(self):
        r = self.ranks[self.rank]
        if r == "J" or r == "Q" or r == "K":
            r = 10
        elif r == "A":
            r = 1
        return int(r)

    def __str__(self):
        v = self.ranks[self.rank] + " of " + self.suits[self.suit]
        return v

class Deck:
    def __init__(self):
        self.cards = []
        for i in range(1, 14):
            for j in range (4):
                self.cards.append(Card(j, i))
        shuffle(self.cards)

    def draw(self):
        if len(self.cards) == 0:
            return
        return self.cards.pop()

class Player():
    def __init__(self, name):
        self.total = 0
        self.name = name
        self.over = False
        
    def match(self):
        if self.total == 21:
            s = '{} の合計スコア {} '
            print(s.format(self.name,self.total))
            w = 'BLACKJACK! {}の勝ち'
            print(w.format(self.name))
            return True
        elif self.total > 21:
            s = '{} の合計スコア {} '
            print(s.format(self.name,self.total))
            l = 'BUST! {}の負け'
            print(l.format(self.name,self.total))
            return True
        else:
            return False
        
        
class Game():
    def __init__(self):
        self.deck = Deck()
        self.p = Player('あなた')
        self.d = Player('ディーラー')
        
    def draw_msg(self,name,card):
        d = '{} は {} を引いた'
        print(d.format(name,card))

    def score_msg(self,name,score):
        s = '{} の合計スコア {} '
        print(s.format(name,score))

    def hit_stand(self):
        while True:
            hs = input("ヒット or スタンド(h/s):")
            if hs == "h":
                return True
            elif hs == "s":
                return False
            else:
                continue
                
    def game_continue(self):
        while True:
            yn = input('続けますか?(y/n):')
            if yn == "y":
                self.p.total = 0
                self.d.total = 0
                self.play_game()
            elif yn == "n":
                print('ゲームを終了します')
                sys.exit(-1)
            else:
                continue

    def play_game(self):
        
        pc1 = self.deck.draw()
        dc1 = self.deck.draw()
        pc2 = self.deck.draw()
        dc2 = self.deck.draw()
        self.p.total += int(pc1) + int(pc2)
        self.d.total += int(dc1) + int(dc2)

        self.draw_msg(self.p.name, str(pc1))
        self.draw_msg(self.p.name, str(pc2))
        self.draw_msg(self.d.name, str(dc1))
        self.draw_msg(self.d.name, "(伏せ札)")
        
        hs = self.hit_stand()

        while hs:
           pc = self.deck.draw()
           self.p.total += int(pc)
           self.draw_msg(self.p.name, str(pc))
           if self.p.match():
               self.game_continue()
           hs = self.hit_stand()
            
        self.score_msg(self.p.name,self.p.total)
        o = '{} の(伏せ札)は {}'
        print(o.format(self.d.name,str(dc2)))
        
        while self.d.total < 17:
            dc = self.deck.draw()
            self.d.total += int(dc)
            self.draw_msg(self.d.name, str(dc))
            if self.d.match():
                self.game_continue()
                    
        self.score_msg(self.d.name,self.d.total)
                
        if self.d.total < self.p.total:
            print('{}の勝ち'.format(self.p.name))
        elif self.d.total > self.p.total:
            print('{}の負け'.format(self.p.name))
        else:
            print('引き分け')
            
        self.game_continue()



if __name__ == '__main__':
    game = Game()
    game.play_game()


```

作成の際のポイントは2つ。

ひとつは、Playerクラスの設定です。

出題者の方も、過去のチャレンジャーの方々も、どちらかというとPlayerのクラスをがっつり作り込んでいる印象でした。私はこのPlayerの比重を抑え、シンプルに作りたかったので、名前と得点のパラメータを保持する程度にしました。それから、ディーラー、プレイヤー共通の要素として、得点が21になったら勝ち(BLACKJACK)、22以上は負け(BUST)になるよう設定しました。後はGameクラスでディーラー、プレイヤーそれぞれの役割を付与するだけです。

もうひとつは、クラスの特殊メソッド。

出題者の方も挙げていたのですが、トランプゲームをコードで表現する際、カードデッキの3通りの「意味」をどう扱うかが問題になります。

>ブラックジャックを実装する場合、カードの「No」には実は、3つの意味があります。
>①トランプの「数値」（1,2,3,4,5,6,7,8,9,10,11,12,13）
>②トランプの「表示」（A,2,3,4,5,6,7,8,9,10,J,Q,K）
>③ブラックジャックの「点」(1,2,3,4,5,6,7,8,9,10,10,10,10)

この問題に対する答えとして、「独学プログラマー」の「戦争」のコードが参考になりました。「戦争」ではCardクラスで特殊メソッド\_\_lt\_\_,\_\_gt\_\_,\_\_repr\_\_などを使っています。これをブラックジャックでは\_\_int\_\_,\_\_str\_\_にしました。特殊メソッドを内蔵したクラスの変数は、例えばstr()で包めば"Q of ♠️"という文字列を、int()で包めば得点"10"を返してきます。

今さらですが、すごいメソッドですね。コレ。私はこの数年間、全部関数で済ませてました。クラス特殊メソッドはおろか、クラスに無縁で生きてきました。独学ってのは、そういうことです。

ブラックジャック作りを通して、クラスの素晴らしさ、面白さに気づけたことで、私もようやく初心者から一歩踏み出そうとしているのでしょうか…そう考えたいものです。そんな私のためにコードレビューしてくださる親切な方がいたら、コメント欄にてよろしくお願いいたします。