「え!? ifもforも使わずにライフゲームの実装を!? 」「できらぁ!!」

[この前Coderetreatというイベントに参加](https://qiita.com/sionn/items/f9ac171ac64d21ef7251)してライフゲームを実装した際、チャレンジ目標に「条件分岐禁止」や「ループ禁止」があった。
イベント中はやれなかったので、今回挑戦してみる。

[ライフゲーム](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)ってのはこんな感じのやつ。
<img width="400" alt="lifegame.gif" src="https://qiita-image-store.s3.amazonaws.com/0/127120/a8bbe887-9395-dc56-f178-1c27dbe0c1c1.gif">

#課題
* [ライフゲーム](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)を実装する<BR>(ルールの詳細はリンク先参照、一部抜粋)

>ライフゲームのルール
ライフゲームでは初期状態のみでその後の状態が決定される。碁盤のような格子があり、一つの格子はセル（細胞）と呼ばれる。各セルには8つの近傍のセルがある (ムーア近傍) 。各セルには「生」と「死」の2つの状態があり、あるセルの次のステップ（世代）の状態は周囲の8つのセルの今の世代における状態により決定される。
セルの生死は次のルールに従う。
誕生　死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存　生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎　生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密　生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

* 今回は、ライフゲームにおける盤上の左右と上下はつながっている仕様とした。
  * つまり、盤上の一番左上のセルの左上のセルは、盤上の一番右下のセルになる。

* Coderetreatだと使用言語は自由

#制約
実装する上での制約事項。Coderetreatのアクティビティより。

* **ループ禁止**:no_good_tone1:
  * for,while等による繰り返し構文を使わない
  * (独自追加)pythonのmapも禁止
* **条件分岐禁止**:japanese_ogre:
  * if,while,switch、三項演算子等による分岐を使わない
  * (独自追加)pythonのfilterも禁止
* **1メソッド４行以下**:smiling_imp:

※他に「不変オブジェクトしか使ってはならない」の制約もあったけど、今回は見送り。

#まずは普通に実装
まずは制約を考えずに普通にライフゲームを実装してみる。
言語はpythonを選択。

ライフゲームクラスは以下のメソッドを持つ。

* `コンストラクタ` - 盤面の縦と横の長さを受け取り盤面を初期生成する。初期状態ではすべてのセルは死亡とする。
* `set_alive(x,y)` - x,yで指定されたセルを生存に設定する
* `board` - 現在の盤面のセルの状態を二次元配列で返す。セルの状態は、生存している場合は1、死亡している場合は0。
* `next_generation()` - 盤面のすべてのセルの状態を変化させ、次の世代に進める。

##テストコード
Coderetreatの趣旨に従って、最初にテストコードを作成。
ライフゲームクラスを生成し、世代を進めたときに、生成された盤面が期待値と一致していることをテストする。

```py
import unittest
from lifegame import LifeGame

class LifeGameTest(unittest.TestCase):
    def test_lifeGame(self):
        #初期状態生成
        lg =LifeGame(4,4)
        for p in [[0,1],[0,3],[1,2],[2,0],[2,1],[3,2],[3,3]]:
            lg.set_alive(p[0],p[1]);
        assert lg.board == [[0,1,0,1],[0,0,1,0],[1,1,0,0],[0,0,1,1]],"初期生成エラー"
        #１世代進めて結果をテスト
        lg.next_generation()
        assert lg.board == [[1,1,0,1],[0,0,1,1],[1,1,0,0],[0,0,0,1]],"世代1エラー"
        #もう１世代進めて結果をテスト
        lg.next_generation()
        assert lg.board == [[0,1,0,0],[0,0,0,0],[1,1,0,0],[0,0,0,1]],"世代2エラー"

if __name__ == "__main__":
    unittest.main()
```

##普通の実装コード:hatching_chick:
制約を考えずに実装した場合のコード。
やはり普通に考えるとif/forは使ってしまう。

```py
ALIVE = 1 #生存
DEAD = 0 #死亡

class LifeGame():
    # セルを初期化
    def __init__(self, x_len,y_len):
        self.x_len = x_len
        self.y_len = y_len
        self.board = [[DEAD for x in range(x_len)] for y in range(y_len)]

    # セルに生存を設定
    def set_alive(self,y,x):
        self.board[y][x] = ALIVE

    # 次の世代に進める
    def next_generation(self):
        new_board = []
        for y in range(len(self.board)):
            row = []
            for x in range(len(self.board[y])):
                row.append(self.judge_status(y,x))
            new_board.append(row)
        self.board = new_board
   
    # セルの次の世代の状態を判定して返す
    def judge_status(self,y,x):
        around_alive = self.count_alive(y,x);
        if self.board[y][x] == ALIVE:
            if 1 < around_alive < 4:
                return 1
            else:
                return 0
        else:
            if around_alive == 3:
                return 1
            else:
                return 0

    # 周囲の生存セルを返す
    def count_alive(self,y,x):
        around = []
        for dy in range(-1,2):
             for dx in range(-1,2):
                if not (dy ==0 and dx == 0):
                     cell = self.board[(y+dy)%self.y_len][(x+dx)%self.x_len]
                     around.append(cell)
        return sum(around)
```

## 出力例
テストケースが無事通ったので、適当なメイン処理を作って標準出力に表示してみる。

```py
import random
import os
import time
import sys
from lifegame import LifeGame

def main():
    # 25×25の盤を生成
    lg =LifeGame(25,25)
    # ランダムに生存セルを設定
    for p in range(80):
        x = int((random.random()*100) %25)
        y = int((random.random()*100) %25)
        lg.set_alive(x,y);

    for cycle in range(100):
        print_board(lg)
        lg.next_generation()
        time.sleep(0.3)

def print_board(lg):
    board_str = "" 
    for y in lg.board:
        for x in y:
           # 生存なら■、死亡なら□で表示
           board_str += "■" if x == 1 else "□"
        board_str +="\n"
    os.system('cls')
    print(board_str)

if __name__ == "__main__":
    main()
```

出力結果
<img width="500" alt="lifegame.gif" src="https://qiita-image-store.s3.amazonaws.com/0/127120/a8bbe887-9395-dc56-f178-1c27dbe0c1c1.gif">

標準出力しているだけなので、コンソールの表示更新のタイミングによってはチラついてしまう。
まぁこれでテストコードと動作確認用のメインが作れた。

# 制約を守ったコードへの修正
ここから本番。
先ほどのコードをif/forを使わない形に修正していく。

## 1.初期化部分
```py
self.board = [[DEAD for x in range(x_len)] for y in range(y_len)]
```
指定されたサイズの二次元配列を生成する処理だが、二重forループを使ってしまっているため、別の方法を考える。
まず、繰り返し処理については**関数の再帰呼び出し**を使えばforを使わずに実現できると気づく。

ただし、普通に再帰呼び出しを行ってしまうと、ifが使えないため無限ループとなってしまう。
そこでifについては、**辞書型とラムダ式の組み合わせ**に置き換えることにする。
例をあげると

```py
if a > 0:
	print("case 1")
else:
	print("case 2")
```
上記は、以下のように変えることでifを使わずに同じことができる。

```py
# 辞書にBooleanキーに対応する関数を登録
dic = {True:lambda:print("case 1"),False:lambda:print("case 2")}
# 条件式の結果(Boolean)で辞書から関数を取得
func = dic[a > 0]
# 関数を呼び出し
func()
```

よって、初期化部分のコードは以下のようにすることで、if/forを使わないで実現が可能。

```py
self.board = self.create_board([])
#再帰でボード初期化
def create_board(self,l):
    l.append([DEAD] * self.x_len)
    return {True:self.create_board,False:lambda x:x}[len(l) < self.y_len](l)
```

* 余談：最初は`[[0]*x_len]*y_len`で簡単じゃーんと思ってたけど[そんなことなかった](https://qiita.com/utgwkk/items/5ad2527f19150ae33322)。
いい勉強になった。

## 2.状態判定部分
```py
def judge_status(self,y,x):
    around_alive = self.count_alive(y,x);
    if self.board[y][x] == ALIVE:
        if 1 < around_alive < 4:
            return 1
        else:
            return 0
    else:
        if around_alive == 3:
            return 1
        else:
            return 0
```
ifを4つも使ってしまっている。
まずは判定は１つにまとまれるのでまとめる。
加えて、`int(<Boolean>)`で0/1に変換できることを利用すると、以下のように変えられる。

```py
def judge_status(self,y,x):
    around_alive = self.count_alive(y,x);
    return int((self.board[y][x] == ALIVE and 1 < around_alive < 4 ) 
               or (self.board[y][x] == DEAD and around_alive == 3))
```
ifを使わない方が、ライン数が少なくて見やすいコードになった:sunny:

## 制約を守った実装コード:rooster:

同じようにして全体を修正して、最終的に制約(条件分岐禁止、ループ禁止、1メソッド4行以下)をすべて守ったコードを作ることができた。

```py
ALIVE = 1 #生存
DEAD = 0 #死滅

class LifeGame():
    # セルの初期化
    def __init__(self, x_len,y_len):
        self.x_len,self.y_len,self.size = x_len,y_len,x_len*y_len
        self.board = self.create_board([])

    #再帰でボード初期化
    def create_board(self,l):
        l.append([DEAD] * self.x_len)
        return {True:self.create_board,False:lambda x:x}[len(l) < self.y_len](l)
	
    # セルに生存を設定
    def set_alive(self,y,x):
        self.board[y][x] = ALIVE

    # 次の世代に進める
    def next_generation(self):
        self.board = self.next(0,self.create_board([]))

    # 再帰で次世代の各セルを更新
    def next(self,i,next_board):
        x,y = i%self.x_len,int(i/self.y_len)
        next_board[y][x] = self.judge_status(y,x)
        return {True:self.next,False:lambda *x:x[1]}[i+1 < self.size](i+1,next_board)

    # セルの次の状態を判定して返す
    def judge_status(self,y,x):
        num = self.count_alive(y,x,0,0) - self.board[y][x];
        return int((self.board[y][x] == ALIVE and 1 < num < 4 ) 
                   or (self.board[y][x] == DEAD and num == 3))

    # 再帰で3×3の範囲の生存セルの数を返す
    def count_alive(self,y,x,i,num):
        ty = (y+int(i/3)-1)%self.y_len
        tx = (x+i%3-1)%self.x_len
        num += self.board[ty][tx]
        return {True:self.count_alive,False:lambda *x:x[3]}[i+1 < 9](y,x,i+1,num)
```


#まとめ
* 感想として、if/forを使わなくても、それほど可読性が落ちないことに驚いた。
 * むしろ使わないほうが、インデントがないので全体がスッキリしている。
 * 全体のコード量も47行から40行に減っている。
 * ただまぁ再帰を多用しているのでパフォーマンス的には悪いかもしれない…
* 制約を課してコーディングすることで自分の引き出しが増えるのを感じた。
 * 普段は脳死でif/forを使ってしまっているため、他の方法がないかを調べることで、言語についての知識が増えた。
 * これでキーボードのfが壊れても実装できる（嬉しいか？）

もし**他にもこんな方法もあるよ**ってのをご存知の方は、ぜひコメントで教えて頂ければ幸いです :eggplant: