数年前にPythonの独学を始めてから、コーリー・アルソフ著『独学プログラマー Python言語の基本から仕事のやり方まで』という本を、ときどき読み返しています。その中にトランプゲーム「戦争」のコードが載っています。以下のような内容です。
こんなコード、勉強もかねて作ってみたい。けれどもっと遊べるゲームがいい。ブラックジャックなんかどうだろう? ポーカーやバカラに比べると比較的ルールも簡単そうだし。
そう思って調べていたら、5年ほど前にQiitaでお題が出ていました。
出題者の方はC#を想定されていたようですが、Python勢もチャレンジしたようで、いくつか記事を見つけることができました。
他にもたくさんの方が挑戦していて、見比べてみると面白いです。同じルールの同じゲームで、同じように遊べる。けれど、コードはそれぞれかなり違います。
いろいろと見て、勉強させていただいた後で、私もブラックジャック作りに挑戦してみました。ルールは以下です。
- 初期カードは52枚。引く際にカードの重複は無いようにする
- プレイヤーとディーラーの2人対戦。プレイヤーは実行者、ディーラーは自動的に実行
- 実行開始時、プレイヤーとディーラーはそれぞれ、カードを2枚引く。引いたカードは画面に表示する。ただし、ディーラーの2枚目のカードは分からないようにする
- その後、先にプレイヤーがカードを引く。プレイヤーが21を超えていたらバーストしてプレイヤーの負け、その時点でゲーム終了
- プレイヤーは、カードを引くたびに、次のカードを引くか選択できる
- プレイヤーが引き終えたら、その後ディーラーは、自分の手札が17以上になるまで引き続ける
- プレイヤーとディーラーが引き終えたら勝負。より21に近い方の勝ち
- JとQとKは10として扱う
- Aはとりあえず「1」としてだけ扱う。「11」にはしない
- ダブルダウンなし、スプリットなし、サレンダーなし、その他特殊そうなルールなし
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"を返してきます。
今さらですが、すごいメソッドですね。コレ。私はこの数年間、全部関数で済ませてました。クラス特殊メソッドはおろか、クラスに無縁で生きてきました。独学ってのは、そういうことです。
ブラックジャック作りを通して、クラスの素晴らしさ、面白さに気づけたことで、私もようやく初心者から一歩踏み出そうとしているのでしょうか…そう考えたいものです。そんな私のためにコードレビューしてくださる親切な方がいたら、コメント欄にてよろしくお願いいたします。