6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-01-05

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

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

Python の特徴の1つとして doctest がある。その関数の使い方のドキュメントと簡単なテストを兼ねると一石二鳥だったりする。

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

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

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

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

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

    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())

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

# -*- 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 を実行する。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?