PHPでライフゲーム

[ライフゲーム(Conway's Game of Life)](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)をPHPで実装してみました。シンプルに、`Universe`というクラスがセルを管理する方式です。

### 実装

```php
<?php

namespace ryo511\LifeGame;

/**
 * Class Universe
 *
 * (1) 生きているセルに隣接する生きたセルが2つより少ない(n < 2)とき、次の世代ではセルは死滅する
 * (2) 生きているセルに隣接する生きたセルが3つより多い(n > 3)とき、次の世代ではセルは死滅する
 * (3) 生きているセルに隣接する生きたセルが2つまたは3つである(2 <= n <= 3)とき、次の世代でもセルは生存する
 * (4) 死んでいるセルに隣接する生きたセルが3つ(n = 3)のとき、次の世代でセルが誕生する
 *
 * @package ryo511\LifeGame
 */
class Universe
{
    /**
     * @var int
     */
    private $width;

    /**
     * @var int
     */
    private $height;

    /**
     * @var array
     */
    private $matrix = [];

    /**
     * @var int
     */
    private $generation = 1;

    /**
     * 1 / 設定値 の割合で初期宇宙に生きたセルが生まれる
     */
    const BIRTH_PROBABILITY = 7;

    /**
     * 生きているセルの表現
     */
    const ALIVE_CELL = '*';

    /**
     * 死んでいるセルの表現
     */
    const DEAD_CELL = ' ';

    /**
     * @param int $width
     * @param int $height
     */
    public function __construct($width, $height)
    {
        $this->width = $width;
        $this->height = $height;
        $this->firstGeneration();
    }

    /**
     * 初期状態を生成する
     */
    private function firstGeneration()
    {
        for ($y = 0; $y < $this->height; $y++) {
            for ($x = 0; $x < $this->width; $x++) {
                if (mt_rand() % self::BIRTH_PROBABILITY === 0) {
                    $this->matrix[$y][$x] = true;
                } else {
                    $this->matrix[$y][$x] = false;
                }
            }
        }
    }

    /**
     * 1世代進化する
     */
    public function evolve()
    {
        $newMatrix = [];

        foreach ($this->matrix as $y => $row) {
            foreach ($row as $x => $cell) {
                if ($cell) {
                    $newMatrix[$y][$x] = $this->alive($x, $y);
                } else {
                    $newMatrix[$y][$x] = $this->birth($x, $y);
                }
            }
        }

        $this->generation++;
        $this->matrix = $newMatrix;
    }

    /**
     * 生きているセルが、次の世代も生き残るか判定する
     *
     * @param int $x
     * @param int $y
     * @return bool
     */
    private function alive($x, $y)
    {
        $n = $this->countAliveNeighbor($x, $y);

        return 2 === $n || 3 === $n;
    }

    /**
     * 死んでいるセルに生きたセルが誕生するか判定する
     *
     * @param int $x
     * @param int $y
     * @return bool
     */
    private function birth($x, $y)
    {
        return 3 === $this->countAliveNeighbor($x, $y);
    }

    /**
     * ムーア近傍（上下左右斜め）において隣接するセルのうち、生きているセルの数を数える
     *
     * @param int $currentX
     * @param int $currentY
     * @return int
     */
    private function countAliveNeighbor($currentX, $currentY)
    {
        $n = 0;

        for ($y = $currentY - 1; $y <= $currentY + 1; $y++) {
            for ($x = $currentX - 1; $x <= $currentX + 1; $x++) {
                if ($y === $currentY && $x === $currentX) { // 自分自身はカウントしない
                    continue;
                } elseif (isset($this->matrix[$y][$x]) && $this->matrix[$y][$x]) {
                    $n++;
                }
            }
        }

        return $n;
    }

    /**
     * 宇宙の状態を行列で設定する
     *
     * @param array $matrix
     */
    public function setMatrix(array $matrix)
    {
        $this->matrix = $matrix;
    }

    /**
     * 現在の宇宙の状態を文字列にして返す
     *
     * @return string
     */
    public function __toString()
    {
        $str = sprintf("[Generation: %d]\n", $this->generation);

        foreach ($this->matrix as $row) {
            foreach ($row as $cell) {
                if ($cell) {
                    $str .= self::ALIVE_CELL;
                } else {
                    $str .= self::DEAD_CELL;
                }
            }
            $str .= PHP_EOL;
        }

        return $str;
    }
}
```

### 使用例

以下のファイルをコマンドラインで実行します。

```php
<?php

namespace ryo511\LifeGame;

require_once 'Universe.php';

$universe = new Universe(50, 40);
display($universe);

while (true) {
    usleep(0.5 * 1000 * 1000);
    $universe->evolve();
    display($universe);
}

/**
 * コマンドラインをclearし、Universeを表示する
 *
 * @param Universe $universe
 */
function display(Universe $universe) {
    if (0 === strpos('WIN', PHP_OS)) {
        system('cls'); // Windows Command Prompt
    } else {
        system('clear');
    }
    echo $universe;
}
```

`Universe#setMatrix()`を使うと、初期状態を固定できるので、レアなパターンを試してみることもできます。以下は「八角形」の例。

```php
<?php

namespace ryo511\LifeGame;

require_once 'Universe.php';

$universe = new Universe(50, 40);
$universe->setMatrix([
    [0, 0, 0, 1, 1, 0, 0, 0],
    [0, 0, 1, 0, 0, 1, 0, 0],
    [0, 1, 0, 0, 0, 0, 1, 0],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [0, 1, 0, 0, 0, 0, 1, 0],
    [0, 0, 1, 0, 0, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0, 0],
]);
display($universe);

while (true) {
    usleep(0.5 * 1000 * 1000);
    $universe->evolve();
    display($universe);
}

/**
 * コマンドラインをclearし、Universeを表示する
 *
 * @param Universe $universe
 */
function display(Universe $universe) {
    if (0 === strpos('WIN', PHP_OS)) {
        system('cls'); // Windows Command Prompt
    } else {
        system('clear');
    }
    echo $universe;
}
```

### その他の実装例

[igorw/conway-php](https://github.com/igorw/conway-php)が、[nikic/iter](https://github.com/nikic/iter)を使って集計処理をやっててオシャレな感じですね。