ペアプロでTDDやってみよう(ライフゲーム) その2

さて、前回は誕生のテストを書いたところまででした。ここからは、誕生の処理を実装します。

テストの書き方によっては、中央の1セルだけ考えれば済んでいたかもしれないのですが、3x3のグリッド全体をちゃんと対応しなくてはいけなくなってしまいました。マジメにやろうと思うと、まずは文字列で持っているパターンを内部処理の都合がいい形にしてやるほうがよさそうです。

では、どんなデータ構造が「内部処理に都合がいい」でしょうか? ここはコードに語ってもらいましょう。データ構造をあまり想定せず、誕生のロジックを先に書くことにします。そこから、どういうふうにデータがアクセスできると便利か導き出しましょう。ことによるとテストを追加することになるかもしれません。

誕生のルールは「周囲にちょうど3つ生きたセルがいる」です。このロジックは簡単です。

life.py:

    def will_born(self, neighbours):
        alive_cells = 0
        for cell in neighbours:
            if cell.is_alive():
                alive_cells += 1
        return alive_cells == 3

neighboursは周囲8つのセルです。forで生きているセルの数を数えていますが、セルはどうもオブジェクトで、is_alive()というメソッドを持っているらしいです。

このメソッドのテストも書きたいところですが、いまは書きません。ここで書いたコードは実は、プロダクトコードではありません。実装方針を検討するために書いている一種の使い捨てコードです。言い換えると、いまはスパイク(技術的調査)のための簡単なコーディングをしているので、TDDから離れているのです。方針が決まったらTDDに戻りますが、それまでは、メソッド自体が姿を変えたり、なくなってしまうかもしれません。

will_born()は1セルについて、誕生の判定をしているだけです。グリッド上のすべてのセルについてこの処理を呼び出した上で、次の世代を組み立ててやる処理が必要です。書きたいように書いてみます。

life.py:

    def prepare_next_generation(self, cells):
        for cell in cells:
            if self.will_born(cell.neighbours):
                cell.born()

なんだか思ったよりシンプルになりました。周囲のセルは、セル自身が知っているようです(cell.neighbours)。誕生も、cell.born()というメソッドだけで済んでいます。このメソッドでは、次の世代では生きた状態になると記録しているはずです。その発想に従って、メソッド名も「次の世代を準備する」という意味にしました。

ここまで書いてきて、セルを表すクラスの存在が鮮明になってきました。Cellクラスは、以下のような責務を持っているようです。

  • 自分自身の状態(生きているか死んでいるか)を知っている
  • 周囲のセルを知っている
  • 次の世代の状態を持っていて、次の世代に変化できる

そろそろTDDの出番です。CellクラスをTDDで実装していきましょう。まずは、自分自身の状態を知っていることについて。

life_test.py:

class CellTest(unittest.TestCase):
    def test_self_state_initial(self):
        cell = Cell()
        self.assertFalse(cell.is_alive())

    def test_self_state_alive(self):
        cell = Cell()
        cell.live()
        self.assertTrue(cell.is_alive())

対応するCellの実装です。これは、最初にRedを確認したら、一気に書けます。

life.py

class Cell(object):
    ALIVE = True
    DEAD = False
   
    def __init__(self):
        self.state = Cell.DEAD

    def is_alive(self):
        return self.state == Cell.ALIVE

    def live(self):
        self.state = Cell.ALIVE

「周囲のセルを知っている」も追加します。neighboursは単純なリストで、外側から直接アクセスできるようにします。これはPythonの流儀です。

life_test.py:

    def test_knows_neighbours(self):
        cell = Cell()
        another_cell = Cell()
        cell.neighbours.append(another_cell)

        self.assertTrue(another_cell in cell.neighbours)

実装はこんな感じ。コンストラクタだけいじっています。

life.py

class Cell(object):
    ...

    def __init__(self):
        self.state = Cell.DEAD
        self.neighbours = []

最後、次の世代に関する内容です。今のところborn()というメソッドが見えているので、これを使ってテストを書きます。

life_test.py:

    def test_next_generation_when_born(self):
        cell = Cell()
        cell.born()

        self.assertFalse(cell.is_alive())
        cell.tick()
        self.assertTrue(cell.is_alive())

born()を呼んだ直後は生死は変わらず、tick()を呼ぶと反映されるという内容になっていますが、ちょっとわかりにくい気もします。テストの見直しをしたい気がしますが、少し後にしましょうか。TODOリストに追加しておきましょう

  1. 初期パターンを渡せる。
  2. 死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
  3. 生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
  4. 生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
  5. 生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
  6. テストをわかりやすく修正する。 <- 追加

続けて実装もいきます。これも、単純ですね。

life.py:

    def born(self):
        self.next_state = Cell.ALIVE

    def tick(self):
        self.state = self.next_state

tick()の実装は、すでにあるlive()を使う方がよさそうに見えますが、まだ確信がないのでこのままにしておきます。

さて、今回はここまで。次回は、Cellクラスを使って今度こそ誕生の実装を完了します。コードはこちら https://github.com/yattom/tdd-life です。