1. 動機
バカラを遊ぶプログラム(Python) を書けたことで,より発展的なコードも書いてみたいと思ったこと.
2. ブラックジャックのルール
プレイヤー(通常1から7人)とディーラーが2枚ずつカードを引き,数の和を得点とする.ただし,絵札は10として扱い,Aは1または11のどちらとして扱ってもよい.全員がカードを引いたら,ディーラーはカードを1枚表にし,プレイヤーにヒット(カードを引く)かスタンド(カードを引かない)かを選択させる.プレイヤーは何度ヒットしてもよいが,バスト(得点が21を超えること)すると失格となる.プレイヤー全員がスタンドするかバストとなったら,ディーラーは伏せていたカードを表にし,得点が17以上になるまでヒットし続ける.ディーラーよりも得点が高いプレイヤーの勝ちとする.
(補足)
得点が21の状態をブラックジャックといい,初めにカードを2枚引いた時点でブラックジャックが成立している状態を特にナチュラルブラックジャックという.また,プレイヤーとディーラーの得点が同じである状態のことをプッシュという.
3. 実装
トランプカードはスートとランクという2つの属性をもつので, suit
および rank
というインスタンス変数を設定したクラスを for
文で呼び出すと山を生成できます.
class Card:
def __init__(self, suit, rank):
self.suit = suit
self.rank = rank
deck = [Card(i, j) for i in ['Clb', 'Dmd', 'Hrt', 'Spd'] for j in range(1, 14)]
deck.pop(0)
で引いたカードはオブジェクトなので, .suit
でスートに, .rank
でランクにアクセスできます.
バカラではプレイヤーおよびバンカーのヒットするカードが1枚までと決まっているため,必要に応じて変数 p3
または b3
を新たに定義するだけで済みました.しかし,ブラックジャックではバストしない限り何度でもヒットできるため,変数の数が動的であり,さらに都度それらの和を求める必要があります.そこで Player
クラスも作成し,手札のカードオブジェクトを格納するリスト self.cards
を定義しておきます.こうすることで,
self.hand = ', '.join(f'{i.suit} {i.rank: >2}' for i in self.cards)
で手札の一覧 self.hand
を表示できます.ただ,このままではA, J, Q, Kがそれぞれ1, 11, 12, 13と表示されてしまい分かりづらいので,
rep_b = ['11', '12', '13', '10', '1', 'X']
rep_a = ['J'.rjust(2), 'Q'.rjust(2), 'K'.rjust(2), 'X', 'A', '10']
for before, after in zip(rep_b, rep_a):
self.hand = self.hand.replace(before, after)
として置換しています.また,Aを含まない手については
sum([i.score for i in self.cards])
で得点を求めることができます.
Aは1として扱っても11として扱ってもよいので,得点を正しく求めるには,カードを引く度に
- プレイヤークラスのリスト
self.scores_list
を,上記の式で求めた値basic_score
(手札のAをすべて1として扱った場合の得点)で初期化する - 手札にAがあり,かつ
basic_score
が11以下である場合,self.scores_list
にbasic_score + 10
を加える
という手順をとります.
basic_score = sum([i.score for i in self.cards])
self.scores_list = [basic_score]
if 1 in [i.score for i in self.cards] and basic_score <= 11:
self.scores_list.append(basic_score + 10)
複数のAを11として扱うとバストとなるため,11として扱われうるAの枚数は必ず1以下です.したがって,手札を for
文で1枚ずつチェックしていく必要はありません.なお,ブラックジャックが成立しているかどうかは
if 21 in self.scores_list:
で,バストしているかどうかは
if self.scores_list[0] > 21:
で判定できます.
プレイヤーの最終得点は,手札にAがある可能性を考慮して max(foo.scores_list)
としています(ブラックジャックおよびバストの場合を除く).これはディーラーについても同様で,例えばディーラーの手札がAと6の場合,得点は17としてヒットしません.
勝敗は,ナチュラルブラックジャック,ブラックジャック,バストの得点をそれぞれ22, 21, 0として扱うことで判定しています.
def ev_score(foo):
if 21 in foo.scores_list:
escore = 22 if len(foo.cards) == 2 else 21
escore = 0 if foo.scores_list[0] > 21 else max(foo.scores_list)
return escore
def comp_scores(foo):
d_escore = ev_score(p[0]); p_escore = ev_score(foo)
if p_escore == 0 or d_escore > p_escore:
judgement = 'LOSE'
else:
judgement = 'WIN' if p_escore > d_escore else 'PUSH'
return f'{judgement: >4}'
4. コード
from random import shuffle
class Card:
def __init__(self, suit, rank):
self.suit = suit
self.rank = rank
self.score = min(self.rank, 10)
class Player:
def __init__(self, name):
self.name = name
self.cards = []
self.scores_list = []
def draw(self):
self.cards.append(deck.pop(0))
self.hand = ', '.join(f'{i.suit} {i.rank: >2}' for i in self.cards)
rep_b = ['11', '12', '13', '10', '1', 'X']
rep_a = ['J'.rjust(2), 'Q'.rjust(2), 'K'.rjust(2), 'X', 'A', '10']
for before, after in zip(rep_b, rep_a):
self.hand = self.hand.replace(before, after)
basic_score = sum([i.score for i in self.cards])
self.scores_list = [basic_score]
if 1 in [i.score for i in self.cards] and basic_score <= 11:
self.scores_list.append(basic_score + 10)
self.scores = ', '.join(str(i) for i in self.scores_list)
red = lambda foo: f'\033[31m{foo}\033[0m'
tsp = 4 #tab_spaces
hyphens = lambda foo: '-' * (foo + 2 * tsp)
deck = [Card(i, j) for i in ['Clb', 'Dmd', 'Hrt', 'Spd'] for j in range(1, 14)]
shuffle(deck)
message = '\nEnter the Number of Players except the Dealer (1-7): '
while True:
try:
players = int(input(message))
if 1 <= players <= 7:
break
except:
pass
message = red('[ERROR] Enter a natural number from 1 to 7: ')
p = [Player('Dealer')]
print('\nEnter each Player\'s Name (up to 8 characters long).\n')
for i in range(1, players + 1):
message = f'Player {i}\'s Name: '
while True:
player_name = input(message)
if 1 <= len(player_name) <= 8:
break
elif len(player_name) > 8:
message = red('[ERROR] Enter shorter name: ')
else:
proceed = input(red('[WARNING] Name not set. Proceed anyway? [Y]es / [N]o: '))
while proceed.upper() not in ['Y', 'N']:
proceed = input(red('[ERROR] Enter either [Y] or [N]: '))
if proceed.upper() == 'Y':
break
p.append(Player(player_name))
for i in range(players + 1):
p[i].draw(); p[i].draw()
NAME_l = 'NAME'.ljust(8); HAND_l = 'HAND'.ljust(16)
print(f'\n{hyphens(32)}\n{NAME_l}\t{HAND_l}\tSCORE(S)\n{hyphens(32)}')
for i in range(1, players + 1):
init_score = 'Nat B.J.' if 21 in p[i].scores_list else f'{p[i].scores: >8}'
print(f'{p[i].name: >8}\t{p[i].hand: <16}\t{init_score}')
print(f'\n{p[0].name: >8}\t{p[0].cards[0].suit} {p[0].cards[0].rank: >2}, ?\n{hyphens(32)}')
for i in range(1, players + 1):
print(f'\n[{p[i].name}\'s TURN]')
while max(p[i].scores_list) < 21:
choice = input(f'{p[i].hand}\t\t\t-> SCORE(S): {p[i].scores}\t\t\t[H]it / [S]tand: ')
while choice.upper() not in ['H', 'S']:
choice = input(red('[ERROR] Enter either [H] or [S]: '))
if choice.upper() == 'S':
break
p[i].draw()
if 21 in p[i].scores_list or p[i].scores_list[0] > 21:
if 21 in p[i].scores_list:
p_result = 'NATURAL BLACK JACK!' if len(p[i].cards) == 2 else 'BLACK JACK!'
else:
p_result = 'BUST!'
print(f'{p[i].hand}\t\t\t-> {p_result}')
print(f'\n[{p[0].name}\'s TURN]')
while max(p[0].scores_list) < 17:
p[0].draw()
if 21 in p[0].scores_list:
d_result = 'NATURAL BLACK JACK!' if len(p[0].cards) == 2 else 'BLACK JACK!'
else:
d_result = 'BUST!' if p[0].scores_list[0] > 21 else f'SCORE: {max(p[0].scores_list)}'
print(f'{p[0].hand}\t\t\t-> {d_result}')
def ev_score(foo):
if 21 in foo.scores_list:
escore = 22 if len(foo.cards) == 2 else 21
escore = 0 if foo.scores_list[0] > 21 else max(foo.scores_list)
return escore
def comp_scores(foo):
d_escore = ev_score(p[0]); p_escore = ev_score(foo)
if p_escore == 0 or d_escore > p_escore:
judgement = 'LOSE'
else:
judgement = 'WIN' if p_escore > d_escore else 'PUSH'
return f'{judgement: >4}'
RESULT_c = 'RESULT'.center(20 + 2 * tsp); SCORE_l = 'SCORE'.ljust(8)
print(f'\n\n\n{RESULT_c}\n{hyphens(20)}\n{NAME_l}\t{SCORE_l}\n{hyphens(20)}')
for i in range(players + 1):
if 21 in p[i].scores_list:
result = 'Nat B.J.' if len(p[i].cards) == 2 else 'B.J.'
else:
result = 'BUST' if p[i].scores_list[0] > 21 else max(p[i].scores_list)
judgement = comp_scores(p[i]) if i != 0 else '\n'
print(f'{p[i].name: >8}\t{result: >8}\t{judgement}')
print(hyphens(20))
5. 実行結果
Enter the Number of Players except the Dealer (1-7): 2
Enter each Player's Name (up to 8 characters long).
Player 1's Name: foo
Player 2's Name: bar
----------------------------------------
NAME HAND SCORE(S)
----------------------------------------
foo Hrt 4, Clb 9 13
bar Spd 8, Dmd K 18
Dealer Clb 3, ?
----------------------------------------
[foo's TURN]
Hrt 4, Clb 9 -> SCORE(S): 13 [H]it / [S]tand: H
Hrt 4, Clb 9, Spd 2 -> SCORE(S): 15 [H]it / [S]tand: H
Hrt 4, Clb 9, Spd 2, Clb 2 -> SCORE(S): 17 [H]it / [S]tand: S
[bar's TURN]
Spd 8, Dmd K -> SCORE(S): 18 [H]it / [S]tand: S
[Dealer's TURN]
Clb 3, Spd A, Clb 7 -> BLACK JACK!
RESULT
----------------------------
NAME SCORE
----------------------------
Dealer B.J.
foo 17 LOSE
bar 18 LOSE
----------------------------
6. 終わりに
今回プログラムを書くにあたり最も苦労したのは,複数人で遊べるようプレイヤーオブジェクトを動的に定義することです.当初は f-string
を用いて f'p{i}'
としていたのですが,これでは f'p{i}'
について何らかの処理を行う際に eval
関数や exec
関数を用いる必要があり,
exec(f'p{i}.draw()') #p1.draw()
print(eval(f'p{i}.hand')) #print(p1.hand)
のようにしなければならないため,プレイヤーオブジェクトを p
というリストに格納し, p[i]
とするようにしました.プレイヤーとディーラーで共通している処理があるためディーラーを p[0]
としてコード量を削減しています.
出力に関する工夫として,タブ幅が4文字でない環境で実行した際のことを考慮し, tsp
というパラメーターを設けています.この値を変更することで,表を綺麗に表示できます.また,各プレイヤーにヒットかスタンドかを訊ねる際,手札を表示した後にタブを挿入する工夫をしました.これにより,ヒットした場合でも得点の表示と新たに引いたカードの表示が重ならないようになり,読みやすくなっています.