ブラックジャックを遊ぶプログラム（Python）

# 1. 動機

[バカラを遊ぶプログラム（Python）](https://qiita.com/EAGLE_usagi/items/e89f0872d2994e6dab57) を書けたことで，より発展的なコードも書いてみたいと思ったこと．

# 2. ブラックジャックのルール

プレイヤー（通常1から7人）とディーラーが2枚ずつカードを引き，数の和を得点とする．ただし，絵札は10として扱い，Aは1または11のどちらとして扱ってもよい．全員がカードを引いたら，ディーラーはカードを1枚表にし，プレイヤーにヒット（カードを引く）かスタンド（カードを引かない）かを選択させる．プレイヤーは何度ヒットしてもよいが，バスト（得点が21を超えること）すると失格となる．プレイヤー全員がスタンドするかバストとなったら，ディーラーは伏せていたカードを表にし，得点が17以上になるまでヒットし続ける．ディーラーよりも得点が高いプレイヤーの勝ちとする．

（補足）
得点が21の状態をブラックジャックといい，初めにカードを2枚引いた時点でブラックジャックが成立している状態を特にナチュラルブラックジャックという．また，プレイヤーとディーラーの得点が同じである状態のことをプッシュという．

# 3. 実装

トランプカードはスートとランクという2つの属性をもつので， ``` suit ``` および ``` rank ``` というインスタンス変数を設定したクラスを ``` for ``` 文で呼び出すと山を生成できます．

```Python
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 ``` を定義しておきます．こうすることで，

```Python
self.hand = ', '.join(f'{i.suit} {i.rank: >2}' for i in self.cards)
```

で手札の一覧 ``` self.hand ``` を表示できます．ただ，このままではA, J, Q, Kがそれぞれ1, 11, 12, 13と表示されてしまい分かりづらいので，

```Python
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を含まない手については

```Python
sum([i.score for i in self.cards])
```

で得点を求めることができます．

Aは1として扱っても11として扱ってもよいので，得点を正しく求めるには，カードを引く度に

1. プレイヤークラスのリスト ``` self.scores_list ``` を，上記の式で求めた値 ``` basic_score ``` （手札のAをすべて1として扱った場合の得点）で初期化する
2. 手札にAがあり，かつ ``` basic_score ``` が11以下である場合， ``` self.scores_list ``` に ``` basic_score + 10 ``` を加える

という手順をとります．

```Python
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枚ずつチェックしていく必要はありません．なお，ブラックジャックが成立しているかどうかは

```Python
if 21 in self.scores_list:
```

で，バストしているかどうかは

```Python
if self.scores_list[0] > 21:
```

で判定できます．

プレイヤーの最終得点は，手札にAがある可能性を考慮して ``` max(foo.scores_list) ``` としています（ブラックジャックおよびバストの場合を除く）．これはディーラーについても同様で，例えばディーラーの手札がAと6の場合，得点は17としてヒットしません．

勝敗は，ナチュラルブラックジャック，ブラックジャック，バストの得点をそれぞれ22, 21, 0として扱うことで判定しています．

```Python
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. コード

```Python
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 ``` 関数を用いる必要があり，

```Python
exec(f'p{i}.draw()') #p1.draw()
print(eval(f'p{i}.hand')) #print(p1.hand)
```

のようにしなければならないため，プレイヤーオブジェクトを ``` p ``` というリストに格納し， ``` p[i] ``` とするようにしました．プレイヤーとディーラーで共通している処理があるためディーラーを ``` p[0] ``` としてコード量を削減しています．

<br>

出力に関する工夫として，タブ幅が4文字でない環境で実行した際のことを考慮し， ``` tsp ``` というパラメーターを設けています．この値を変更することで，表を綺麗に表示できます．また，各プレイヤーにヒットかスタンドかを訊ねる際，手札を表示した後にタブを挿入する工夫をしました．これにより，ヒットした場合でも得点の表示と新たに引いたカードの表示が重ならないようになり，読みやすくなっています．