[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

[PHP]よくあるエラーメッセージの原因と対処法 2

前回の続きです。

PHP でよくあるエラーメッセージの意味と解決方法を具体例とともに紹介します。


Notice: Undefined offset: (キー)

<?php
$arr = array("test", "sample");
echo $arr[2];

配列変数で、定義されていないキーを指定した時に起こるエラーです。
例ではキー 0 と 1 は値が定義されていますが、2 は未定義です。


Warning: Invalid argument supplied for foreach()

<?php
$arr = "";

foreach($arr as $value){
	echo $value;
}

foreach が扱えるのは配列変数等のオブジェクトに限られます。(厳密にはイテレータを持つオブジェクト)
文字列や null、 false ではエラーが発生します。
例のようにわかりやすければいいのですが、たいていのプログラムは次のようになっていることが多いかと思います。

<?php
$result = get_results();

foreach($result as $value){
  echo $value;
}

仮に何らかの結果を配列で返すユーザー定義関数 get_results があったとして、
結果がない場合に空の配列を返すのであれば問題ないのですが、
もし一つも結果が得られなかったときに false や null を返す仕様になっていた場合、
このエラーが起きる可能性があります。


Warning: Cannot modify header information – headers already sent

<?php
echo "sample";
header("Location: index.php");
exit;

指定したページに移動させるとき、header() で Location を指定するのですが、
ヘッダーを出力するよりも前に何らかの文字等が出力されているとエラーが起こります。
header() より前に echo をしていたり、<?php の前に無駄な文字や改行などが無いか確認して下さい。
setcookie() を使う場合も、使用している行より前に他のヘッダーや文字の出力を行わないように気をつける必要があります。
文字コード UTF-8 の BOM(Byte Order Mark) が原因で起こる可能性もあります。
その場合は BOM なしで保存できるテキストエディタを利用して UTF-8N で保存して下さい。

<?php include "test.php";?>
<?php
header("Location: index.php");
exit;
?>

上の例では一見無駄な文字は無いように見えますが、1行目の末尾で改行を出力しているとみなされ、エラーになる場合があります。


Notice: A session had already been started

<?php
session_start();
session_start();

例はやや極端ですが、既にセッションが開始されている時、session_start() することはできません。
サーバー側の php.ini 等で session.auto_start が有効になっていると、session_start() していなくても
自動的にセッションがスタートしていることがあります。また、include しているファイルの中でも session_start() が使われていないか確認して下さい。

<?php
if( !isset($_SESSION) ) {
  session_start();
}

セッションが開始されていれば重ねてスタートしないようにすれば回避出来ます。


Fatal error: Cannot access private property

<?php
class Test {
	private $sample;
}

$test = new Test();
$test->sample = 10;

private で保護されたプロパティ(クラス内の変数)には直接アクセスできません。
public で宣言すれば可能ですが、クラス内に操作するためののメソッド(セッターメソッド又はゲッターメソッド)がないか確認して下さい。


Fatal error: Maximum execution time of 30 seconds exceeded

具体例は省略しますが、処理に 30 秒以上(環境によります)かかってしまうような重い処理や、
無限ループができてしまっている場合にこのようになる場合があります。
php.ini の max_execution_time を必要な秒数に変更するか、phpファイルに

ini_set("max_execution_time", 120);

あるいは

set_time_limit(120);

のように書いてタイムアウト時間を変更します

共用サーバーなどでタイムアウト時間の変更に制限がある場合があるので気をつけて下さい。
また、処理に長い時間のかかるスクリプトの実行はサーバーに高い負荷をかけるため、
可能な限りスクリプト側の実行時間を短く出来るように改良するのが理想です。
あくまで大型プログラムのインストール時など、一時的に時間が掛かるケースでのみ変更して下さい。


Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 32 bytes)

サイズの大きなファイルを開こうとした場合や画像のリソースなどを開放し忘れている場合など、サーバーが確保した容量が不足している場合に表示されるメッセージです。数値はサーバーの設定により異なります。
画像ファイルのリソースを開放する必要がある場合は imagedestroy() で開放します。
サーバー側の上限を変更するにはサーバーの php.ini にある memory_limit を修正するか、ソースコードに ini_set() を記述して上限を設定します。(例は 512MB の場合)

ini_set('memory_limit', '512M');

あまり大きな数値は設定できない可能性があります。また、極端にメモリを消費する場合はプログラムの書き方を見なおしたほうがいいかもしれません。

[PHP]よくあるエラーメッセージの原因と対処法 1

プログラミングにエラーはつきものですが、エラーメッセージの意味がわかっていれば素早く対処出来ます。

そこで、よくあるエラーメッセージと、それが起こる具体例をいくつかまとめてみました。


Notice: Undefined variable: (変数名)

<?php
echo $a;

「変数が定義されていません」という意味で、例では変数 $a を初期化することなくいきなり
echo しようとしたために起こるエラーです。

こんな単純なミス絶対しないと思うかもしれませんが、次のようになっていたりすると
案外見落としてしまうかもしれません。

<?php
$sample = "red";

if( $sample == "blue" ){
  $a = "OK";
}

echo $a;

条件分岐で真ならば変数 a を定義するようになっているため、条件に合わなければ $a は初期化されません。
あらかじめ初期化するか、else の中で定義することで解決します。


Parse error: syntax error, unexpected end of file, expecting ‘,’ or ‘;’

<?php
echo "hello"

あるべき文字が抜けている場合などに起こる構文エラーです。
この場合は行末のセミコロンがありません。
「unexpected end of file」の部分はエラー箇所の次の処理が入るので、
「unexpected ‘echo’ (T_ECHO)」のようなメッセージになる場合もあります。
セミコロンを入力したつもりがコロン(:)だったということもしばしばです。

<?php
$sample = "ok";

if(isset($sample){
  echo $sample;
}

上の結果は「Parse error: syntax error, unexpected ‘{‘」です。
わかりにくいですが、if の丸括弧を閉じ忘れています。
他にも function() の「{}」の数が合わない場合などが考えられます。
このエラーが起きたときは、セミコロンがきちんと付いているか、括弧は閉じられているかを
よく確認して下さい。


Fatal error: [] operator not supported for strings

<?php
$var = "test";
$var[] = "sample";

変数を文字型として初期化したにもかかわらず、配列として扱おうとしています。
ちなみに数値型などで初期化していた場合は
「Warning: Cannot use a scalar value as an array」
のようになります。


Fatal error: Call to undefined function (関数名)

<?php
my_function();

定義していない関数を呼びだそうとして起こるエラーです。
関数の書かれた別ファイルを include し忘れていることが多いです。
あるいは PEAR や ImageMagick などのライブラリがインストールされていないかもしれません。
PHP のバージョンアップで今まで使えていた関数がなくなっていることもあります。


Warning: Missing argument 2 for (関数名)

<?php
function my_function($a, $b){
	echo $a + $b;
}

my_function( 100 );

関数に必要な引数が省略されている場合に起こりうるエラーです。
例の関数は2つの変数の合計を表示するように作られていますが、
二番目の引数(argument 2)を書いていないので、変数 $b が欠落しています。

もし省略可にするのであれば関数に初期値を設定しておくべきです。

function my_function($a=0, $b=0){}

Notice: Constant (定数名) already defined

<?php
define('TEST', 100);
define('TEST', 200);

同じ名前の定数は一度しか定義できません。
一度 TEST を 100 と決めたら、別の値で上書きすることはできません。
標準関数 defined( 定数名 ) を使って定数が既に定義済みでないか確認して下さい。


Fatal error: Cannot redeclare (関数名)

<?php
function test(){

}

function test(){

}

あるいは

<?php
function trim(){

}

同じ名前の関数が複数存在する場合に起こるエラーです。
上はユーザー定義関数が重複してしまっている場合で、
下はすでに PHP の標準関数として trim() というものがあるにもかかわらず、
同じ名前の関数を定義してしまっているケースです。
クラスにまとめておくのがベストですが、function_exists() を使って
同名の関数が既にないか調べておくのもひとつの手です。


include( ファイル名 ): failed to open stream: No such file or directory

<?php
include "test.php";

ファイルやディレクトリが存在しない場合に起こります。
include 以外にも、file_get_contents などでも同様のエラーになります。
include すると、相対パスのルートは読み込んだ側の場所になるので、
別のディレクトリにあるファイルを include する場合、相対パスの示す位置が変わるので、
dirname(__FILE__) を使って絶対パスに置き換えておくことで解決する場合もあります。


Deprecated: Function 関数名 is deprecated

<?php
$num = 3;
ereg("[0-9]", $num);
Deprecated: Function ereg() is deprecated

厳密にはエラーではないのですが、非推奨の関数を使っていると発生します。
PHP をバージョンアップすると、脆弱性や、新しい関数の登場によって、古い関数を使うべきではなくなることがあります。
例では ereg() は PHP 5.3.0 以降非推奨となった関数で、代わりに preg_match() を使うことが推奨されています。

すべて置き換えるべきですが、対応に時間がかかる場合は、php.ini を

error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED

とするか、php ファイルに

error_reporting(E_ALL ^ E_NOTICE ^ E_DEPRECATED);

を追加することで一時的にエラー表示を抑制します。
非推奨の関数には深刻な脆弱性がある場合がほとんどなので、あくまで対応までの時間稼ぎです。


次回に続きます