「Python でビンゴゲーム の確率シミュレーションしてみた」で doctest を書いてみた

元ネタの元ネタ: [Python でビンゴゲーム の確率シミュレーションしてみた]
(http://qiita.com/elzup/items/d532ffa1d326fbf75d01)
元ネタ: [「Python でビンゴゲーム の確率シミュレーションしてみた」を修正してみた]
(http://qiita.com/t2y/items/8584b77b3fe02dd2ce63)

> > docブロックの書き方がいまいちまだわからないので勉強していこうと思う

> Python の特徴の1つとして [doctest](http://docs.python.jp/3.4/library/doctest.html) がある。その関数の使い方のドキュメントと簡単なテストを兼ねると一石二鳥だったりする。

> 元のコードのロジックをいじらないように手を加えてたら、修正したコードの doctest も良いものじゃないのだけど、doctest 書きながら開発しようとすると、使う立場への視野も広がって自然と使いやすい関数やモジュールを実装できたりする。

doctest 書きながら、インタラクティブに開発するとデバッグしやすかったり、使いやすい API になったり、それがそのままドキュメントとしても活用できると言いたかったのに適切なサンプルコードを書けなかったのでリベンジ。

doctest の用途が分かりやすくなるようにしてみた。

* Card オブジェクトに穴あきの状態を保持する
* Card のコンストラクタにテストデータを渡すことでモックを使わずテストが書ける
* *`__repr__`* を実装すればデバッグ出力関数が不要になる (*`__str__`* を定義していないときは print 関数などで *`__repr__`* が使われるみたい)


余談だけど、カードの数字の生成処理がもっとすっきり書けないかなぁと昨日から思っていて *reduce* を使ったらそれっぽくなってちょっと嬉しかった。

```py3
    def _generate(self):
        def sampling(k=5):
            for i in range(k):
                yield sample(range(15 * i + 1, 15 * (i + 1) + 1), k)

        return reduce(add, sampling())
```

あくまでこんな雰囲気ってのが伝われば嬉しい。

```py3
# -*- coding: utf-8 -*-
from functools import reduce
from operator import add
from random import sample


class Card:
    """ 75-ball Bingo Card

    The middle square is designated a "FREE" space.
    The columns are labeled as follows.
    "B": (numbers 1–15)
    "I": (numbers 16–30)
    "N": (numbers 31–45)
    "G": (numbers 46–60)
    "O": (numbers 61–75)

    http://en.wikipedia.org/wiki/Bingo_card

    >>> data = [  # for testing use
    ... 1, 2, 3, 4, 5,
    ... 6, 7, 8, 9, 10,
    ... 11, 12, 13, 14, 15,
    ... 16, 17, 18, 19, 20,
    ... 21, 22, 23, 24, 25,
    ... ]
    >>> card = Card(data)
    >>> len(card.numbers)
    25
    >>> card.numbers[12]
    True
    >>> card.is_bingo
    False
    >>> print(card)
      1  2  3  4  5
      6  7  8  9 10
     11 12  o 14 15
     16 17 18 19 20
     21 22 23 24 25

    Show 'o' when giving number is found

    >>> card.set_number(14)
    >>> print(card)
      1  2  3  4  5
      6  7  8  9 10
     11 12  o  o 15
     16 17 18 19 20
     21 22 23 24 25

    Check bingo giving number to the card.

    >>> card.set_number(35)  # have no effect if number not in card.numbers
    >>> card.set_number(1)
    >>> card.set_number(7)
    >>> card.set_number(19)
    >>> card.is_bingo
    False
    >>> card.set_number(25)
    >>> card.is_bingo
    True
    >>> print(card)
      o  2  3  4  5
      6  o  8  9 10
     11 12  o  o 15
     16 17 18  o 20
     21 22 23 24  o
    """

    def __init__(self, numbers=None):
        self.numbers = self._generate() if numbers is None else numbers
        self.numbers[12] = True
        self.is_bingo = False

    def __repr__(self):
        ascii_card = ''
        for i in range(int(len(self.numbers) / 5)):
            for num in self.numbers[5 * i:5 * (i + 1)]:
                if num is True:
                    num = 'o'
                elif num is False:
                    num = 'x'
                ascii_card += '{0:>3s}'.format(str(num))
            ascii_card += '\n'
        return ascii_card[:-1]

    def _generate(self):
        def sampling(k=5):
            for i in range(k):
                yield sample(range(15 * i + 1, 15 * (i + 1) + 1), k)

        return reduce(add, sampling())

    def set_number(self, number):
        if number not in self.numbers:
            return
        self.numbers[self.numbers.index(number)] = True
        if not self.is_bingo:
            self.check_bingo()

    def check_bingo(self):
        def check(start, stop, step=1):
            return all(map(lambda x: x is True, self.numbers[start:stop:step]))

        if self.numbers.count(True) < 5:
            return

        for i in range(5):
            if check(i * 5, (i + 1) * 5):  # horizontal
                self.is_bingo = True
                return

        for i in range(5):
            if check(i, i + 21, 5):  # vertical
                self.is_bingo = True
                return

        if check(0, 25, 6) or check(4, 21, 4):  # skew
            self.is_bingo = True
            return
```

こんな感じで doctest を実行する。

```bash
$ python -m doctest Bingo_kai2.py  # エラーが表示されなければ、テストが正しい
$ python -m doctest Bingo_kai2.py -v  # 冗長モード
```