Pythonのジェネレータでじゃんけんプログラム

Pythonのジェネレータを使ってじゃんけんプログラムを作ってみました。
Pythonのジェネレータに慣れていない方のために少しでも役立てばいいかなと思ったので記事化しました。
コードの全体像は[こちら](https://gist.github.com/rhoboro/bcc50fa32ea809081c336b05494e2a3b)にあります。

## グー、チョキ、パーを表現

まずはグー、チョキ、パーを表すEnumを用意します。
ここはジェネレータは関係ありません。

```python
from enum import Enum

class Card(Enum):
    Rock = 0
    Scissors = 1
    Paper = 2
```

Enumはこんな感じで使えます。

```python
>>> card = Card.Rock  # Card(0)でも同じ
>>> card
<Card.Rock: 0>
>>> card.name
'Rock'
>>> card.value
0
```

## プレイヤーをジェネレータで表現

続いてプレイヤーを表現するジェネレータを用意します。

```python
from random import randint

def Player(name):
    yield name
    while True:
        own = Card(randint(0, 2))
        yield own
```

このように`yield`を含む関数をジェネレータ関数と呼び、このジェネレータ関数の戻り値はジェネレータオブジェクトです。
`next()`の引数にジェネレータを渡すと`yield value`の行で`value`を返し、ローカル変数を保持したまま処理が中断されます。
もう一度`next()`に渡すと、中断した箇所から処理を再開します。

このPlayerは最初に名前を返し、以降は`next()`に渡されるたびにグー、チョキ、パーをランダムに返し続けます。

```python
>>> taro = Player('Taro')
>>> taro  # taroはジェネレータオブジェクト
<generator object Player at 0x1042c77c8>
>>> next(taro)  # 初回は名前を返す
'Taro'
>>> next(taro)  # 2回目以降はランダムに手札を返す
<Card.Scissors: 1>
>>> next(taro)
<Card.Rock: 0>
```

## じゃんけんの制御フロー

ジェネレータであるプレイヤーを操作するのがこの関数です。
つまり、`next()`にジェネレータを渡し、戻ってきた値で勝敗を判定したり、それぞれの勝数を残しています。
この関数は、2名のプレイヤーと何勝すればいいかを渡して呼び出すと、即座に動き始めます。

```python
def start(p1, p2, n):
    # 初期化
    count = 0  # 試合数
    count_p1 = 0  # プレイヤー1の勝数
    count_p2 = 0  # プレイヤー2の勝数
    p1_name = next(p1)
    p2_name = next(p2)
    print(f'{p1_name} VS {p2_name}')

    while True:
        # じゃんけん処理
        count += 1
        print(f'#### ROUND {count} ###')
        p1x = next(p1)  # プレイヤー1の手札を取得
        p2x = next(p2)  # プレイヤー2の手札を取得
        result = (p1x.value - p2x.value + 3) % 3  # 勝敗の判定
        if result == 0:
            print(f'{p1x.name}, {p2x.name} => even')
        elif result == 1:
            count_p1 += 1
            print(f'{p1x.name}, {p2x.name} => {p1_name} +1')
        elif result == 2:
            count_p2 += 1
            print(f'{p1x.name}, {p2x.name} => {p2_name} +1')

        # どちらかの勝数がnに届くまで続ける
        if max(count_p1, count_p2) < n:
            print()
            continue
```

### 結果の表示とジェネレータに終了を通知

n勝に届いたプレイヤーが現れたら結果を表示します。
最後に同じプレイヤーで何度も勝負できないようにプレイヤーの`close()`を呼び出しています。

```python
        # start()の続きで結果の表示
        if n <= count_p1:
            print(f'{p1_name}: {count_p1} {p2_name}: {count_p2}')
            print(f'{p1_name} are winner')
            p1.close()  # 以降はnext()に渡されてもStopIterationを送出する
            p2.close()
            break
        elif n <= count_p2:
            print(f'{p1_name}: {count_p1} {p2_name}: {count_p2}')
            print(f'{p1_name} are loser')
            p1.close()
            p2.close()
            break
```  

## プログラムの実行

ここまでのコードは次のコードで実行できます。

```python
def main():
    taro = Player('Taro')
    hanako = Player('Hanako')
    start(taro, hanako, 3)
```  

上記を1つのスクリプトにまとめたら最後にいつものおまじないを書いて実行します。

```python
if __name__ == '__main__':
    main()
```

今回の結果はこのようになりました。

```shell
$ python3 janken.py
Taro VS Hanako
#### ROUND 1 ###
Scissors, Paper => Taro +1

#### ROUND 2 ###
Paper, Scissors => Hanako +1

#### ROUND 3 ###
Rock, Rock => even

#### ROUND 4 ###
Paper, Paper => even

#### ROUND 5 ###
Paper, Scissors => Hanako +1

#### ROUND 6 ###
Paper, Paper => even

#### ROUND 7 ###
Paper, Scissors => Hanako +1
Taro: 1 Hanako: 3
Taro are loser
```