PHP の GD ライブラリを使って、「たくさんある画像の中で微妙に色違うやつ当てるゲーム」を少しでもチートされないように作りたい

ソースコードは[こちら](https://github.com/noritakaIzumi/identify_colors)

「たくさんある画像の中で微妙に色違うやつ当てるゲーム（名前長い）」がよくあると思います。
これをフロントの世界 (HTML/CSS, JavaScript) だけで実装すると、目で答えがわからなくてもチートされて正解できてしまう懸念があります。
それを少しでもチートしにくくしようというのが今回の企画です。

![top.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/5198a09e-5fc3-489c-3c8b-c2793f828ef8.gif)

## 今回行う対策

### 開発者ツールから色の情報がわからないようにする

CSS で背景色を指定すると、開発者ツールから見えてしまいます。
そのため、バックエンドで画像を動的に生成したのち、フロントではそれをただ読み込むだけにします。

その他、「正誤判定をフロントエンドで実装しない」などの対策も必要ですが、記事では省略します。

## 参考ページ

[PHPで画像を動的に生成する【GD編】 | バシャログ。](http://bashalog.c-brains.jp/17/09/15-150000.php)

## PHP を使います

今回は PHP の GD 拡張モジュールを用いて画像を生成します。
GD 拡張モジュールはよほど古い PHP でなければ同梱されているはずです。
`php.ini` を開き、以下のような行があったら先頭のセミコロンを消去して有効にします。

```ini
;extension=gd2
```

お手持ちの PHP で `phpinfo()` を実行し、 GD に関する情報が見えていることを確認してください。
次のように見えていれば成功です。

![gd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/4457c3a6-495d-e97d-5350-2f88a020f38e.png)

## 実際に作ってみる

### GD ライブラリを使って 100×100 の単色画像（色はランダム）を作成し、出力する

```php
$width = 100;
$height = 100;

// 画像リソースを作成する
$img = imagecreate($width, $height);

// 色を付ける
imagecolorallocate($img, rand(0, 255), rand(0, 255), rand(0, 255));

// 出力する
$filename = 'images/image.png';
imagepng($img, $filename);
```

`imagecreate` 関数で新規画像を作成し、 `imagecolorallocate` 関数で着色、 `imagepng` 関数でファイルに出力します。

### 3 枚の単色画像（1 枚だけ色が異なる）を出力し、正解データを保持する

画像を 1 枚出力するパターンを上でやったので、次は複数枚（今回は 3 枚）出力して、正解を示すものも出力します。
着色パターンは正解・不正解の 2 通り用意すれば OK で、正解データはテキストファイルに数字だけ書いておきます。

メインのロジックです。

```php:create_images.php
<?php

require_once __DIR__ . '/Image/ImageResourceFactory.php';
require_once __DIR__ . '/Image/Image.php';

use Image\Image;

function main($imageCount)
{
    $correctColor = array(
        'red' => getRandomInt(0, 255),
        'green' => getRandomInt(0, 255),
        'blue' => getRandomInt(0, 255),
    );
    $incorrectColor = array(
        'red' => getRandomInt(0, 255),
        'green' => getRandomInt(0, 255),
        'blue' => getRandomInt(0, 255),
    );

    $collectImageIndex = getRandomInt(0, $imageCount - 1);

    $width = 100;
    $height = 100;

    for ($i = 0; $i < $imageCount; ++$i) {
        $filename = __DIR__ . '/../data/image' . $i . '.png';
        if ($i === $collectImageIndex) {
            outputImage($width, $height, $correctColor, $filename);
        } else {
            outputImage($width, $height, $incorrectColor, $filename);
        }
    }

    file_put_contents(__DIR__ . '/../data/answer.txt', $collectImageIndex);
}

function getRandomInt(int $min, int $max = null, int $fixed = null): int
{
    if (isset($fixed)) {
        return $fixed;
    }
    return rand($min, $max);
}

function outputImage(int $width, int $height, array $color, string $filename)
{
    try {
        $image = new Image($width, $height);
        $image->paintIn($color['red'], $color['green'], $color['blue']);
        $image->saveAs($filename);
    } catch (Exception $e) {
    }
}
```

メインのロジックを実行するにあたりクラスとして切り出した、画像の処理類です。

```php:Image/Image.php
<?php

namespace Image;

use Exception;
use Image\ImageResourceFactory;

/**
 * Class Image
 * 画像の作成・編集・保存
 */
class Image
{
    /**
     * @var resource $resource - 画像リソース
     */
    public $resource;

    /**
     * Image constructor.
     * @param int $width - 画像の幅
     * @param int $height - 画像の高さ
     * @throws Exception - 画像作成に失敗した場合、例外を返します。
     */
    public function __construct(int $width, int $height)
    {
        $this->resource = ImageResourceFactory::getImageResource($width, $height);
    }

    public function paintIn(int $red, int $green, int $blue)
    {
        imagecolorallocate($this->resource, $red, $green, $blue);
    }

    public function saveAs(string $filename)
    {
        imagepng($this->resource, $filename);
    }
}
```

```php:Image/ImageResourceFactory.php
<?php

namespace Image;

use Exception;

/**
 * Class ImageResourceFactory
 * 画像リソース作成用
 */
class ImageResourceFactory
{
    /**
     * 画像のリソースを取得します。
     *
     * @param int $width - 画像の幅
     * @param int $height - 画像の高さ
     * @return resource - 画像リソース
     * @throws Exception - 画像作成に失敗した場合、例外を返します。
     */
    public static function getImageResource(int $width, int $height)
    {
        $resource = imagecreate($width, $height);
        if ($resource === false) {
            throw new Exception('Failed to create image resource.');
        }
        return $resource;
    }
}
```

正誤判定についても、フロントの JavaScript ではなく PHP で実装し、画面にリンクを貼ります（ここでは省略します）。

## 感想

この類のゲームは前から作ろうと思っていたのですが、広告系の業務をしていたことから base64 形式を使った方法を探っていました。
base64 では 1 x 1 の黒色・透明画像の作り方ぐらいしか見つからなくて、いろいろ途方に暮れていたのですが、今回 PHP の中に画像を作れるライブラリが同梱されていたことに気づけたのは大きいです。

正直なところ、オンライン対戦やインターネットランキングなどを含まないゲームなら、チートするかどうかはエンドユーザの自由であって、わざわざバックエンドに処理をやらせる必要もないでしょう。
ですが、「フロントはすべて見えていて、いくらでも改造できる」という観点から、チートされたくない部分をバックエンドにより実装できたことは、今後の業務にも活かせそうな部分ではあると思います。

今回の記事がみなさんの何かのきっかけにでもなれば幸いです。