[PHP]ライフゲームを作る

lifegame

ライフゲームは、格子状に並んだセル(マス)が、周囲 8 マスの状態に応じて世代ごとに生きたり死んだりするシミュレーションです。ルールは次の 3 つだけです。

  • 誕生:死んでいるセルの周囲に、生きているセルがちょうど 3 つあると、次の世代で生まれます。
  • 生存:生きているセルの周囲に、生きているセルが 2 つか 3 つあると、次の世代でも生き残ります。
  • 死滅:それ以外の場合は、周囲が 1 つ以下なら過疎、4 つ以上なら過密で死滅します。
<?php
declare(strict_types=1);

session_start();

const WIDTH = 20;
const HEIGHT = 20;

/**
 * 盤面をランダムに初期化する(1 = 生, 0 = 死)
 */
function randomCells(int $width, int $height): array
{
    $cells = [];
    for ($y = 0; $y < $height; $y++) {
        for ($x = 0; $x < $width; $x++) {
            $cells[$y][$x] = random_int(0, 1);
        }
    }
    return $cells;
}

/**
 * 周囲 8 マスの生きているセルを数える(盤面の外は死んでいるものとして扱う)
 */
function countNeighbors(array $cells, int $x, int $y): int
{
    $count = 0;
    for ($dy = -1; $dy <= 1; $dy++) {
        for ($dx = -1; $dx <= 1; $dx++) {
            if ($dx === 0 && $dy === 0) {
                continue;
            }
            $count += $cells[$y + $dy][$x + $dx] ?? 0;
        }
    }
    return $count;
}

/**
 * ルールに従って次の世代の盤面を作る
 */
function nextGeneration(array $cells): array
{
    $next = [];
    foreach ($cells as $y => $row) {
        foreach ($row as $x => $alive) {
            $neighbors = countNeighbors($cells, $x, $y);
            // 周囲が 3 なら誕生(または生存)、生きていて周囲が 2 なら生存、それ以外は死滅
            $next[$y][$x] = ($neighbors === 3 || ($alive === 1 && $neighbors === 2)) ? 1 : 0;
        }
    }
    return $next;
}

if (isset($_GET['reset']) || !isset($_SESSION['cells'])) {
    $_SESSION['cells'] = randomCells(WIDTH, HEIGHT);
    $_SESSION['generation'] = 1;
} elseif (isset($_GET['next'])) {
    $_SESSION['cells'] = nextGeneration($_SESSION['cells']);
    $_SESSION['generation']++;
}

$cells = $_SESSION['cells'];
$generation = $_SESSION['generation'];
$auto = isset($_GET['auto']);

header('Content-Type: text/html; charset=UTF-8');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
<?php if ($auto): ?>
    <meta http-equiv="refresh" content="1;url=?next&amp;auto">
<?php endif; ?>
    <title>ライフゲーム(第 <?= $generation ?> 世代)</title>
    <style>
        .board {
            display: grid;
            grid-template-columns: repeat(<?= WIDTH ?>, 16px);
            gap: 1px;
            width: max-content;
            background: #ccc;
            border: 1px solid #ccc;
        }
        .cell { width: 16px; height: 16px; background: #fff; }
        .cell.alive { background: #333; }
    </style>
</head>
<body>
    <p>第 <?= $generation ?> 世代</p>

    <div class="board">
<?php foreach ($cells as $row): ?>
<?php foreach ($row as $alive): ?>
        <div class="cell<?= $alive ? ' alive' : '' ?>"></div>
<?php endforeach; ?>
<?php endforeach; ?>
    </div>

    <p>
        <a href="?next">次の世代へ</a> |
<?php if ($auto): ?>
        <a href="?">止める</a> |
<?php else: ?>
        <a href="?next&amp;auto">自動で進める</a> |
<?php endif; ?>
        <a href="?reset">リセット</a>
    </p>
</body>
</html>

仕組み

countNeighbors() で周囲 8 マスの生きているセルを数え、nextGeneration() でルールに従って次の世代の盤面を作ります。
盤面の外にあたるマスは存在しないので、null 合体演算子(??)で 0、つまり死んでいるセルとして扱っています。

盤面と世代数はセッションに保存しているので、ブラウザを閉じるとリセットされます。
「次の世代へ」を押すごとに世代が進み、「自動で進める」を選ぶと 1 秒ごとにページを読み込み直して進み続けます。

試すときはコードを index.php として保存し、同じフォルダで php -S localhost:8000 を実行してから、ブラウザで http://localhost:8000/ を開いてください。
自動で進めるとアクセスが 1 秒ごとに発生するので、共用サーバーではなくローカル環境で動かすことをおすすめします。

リアルタイムに動かすなら JavaScript で組むのが一般的ですが、PHP で組むと、訪問者が来るたびに世代を進めたり、アクセスごとに餌のようなものを加えてみたりといった処理ができて面白いかもしれません。

JavaScript と Canvas の組み合わせでリアルタイムに動くタイプはこちらの記事で解説しています。

密集の度合いに応じて色を付けてみるのもなかなか綺麗です。

life