がるの健忘録

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

\Random\Engine クラスの generate() メソッド仕様覚え書き

この記事は「外部から色々たたいてとりあえずこんな挙動だった」くらいの内容で、PHPC言語実装とか追いかけてないので、いったん「今この瞬間、うちの環境だとこう動いた」くらいの内容です。
参考なり参照なりされるさいは、用法用量を守って適切に参照してください。

本題。

いや、元々は以下のようなニーズがありまして。

  • \Random\Randomizer の getInt()を
  • ゲームの文脈で使いたい
  • で、テスト用に「乱数の生成部分」をDI等で差し替えたい

なのでまぁ、コンストラクタ的に「engineをこっちの任意のものに差し替えればよいだろう」くらいの感じでした。
https://www.php.net/manual/ja/random-randomizer.construct.php

なお、その後に使うのは getInt()「だけ」を、いったん、想定しています。
https://www.php.net/manual/ja/random-randomizer.getint.php

んで。
いや「Randomizerを継承したクラスでgetInt()を上書き」できればそれはそれでよかったんですが、いかんせん、final classなんですよね。
なので、差し込むエンジンのほうを触るしかないかなぁ、と。

Random\Engine インターフェイス、generate() メソッドだけなんですよね。
https://www.php.net/manual/ja/class.random-engine.php

ただまぁ、大体必要なことは書いてあります(はじめこのページ見てなくて結構体当たりでかなり馬力な調査をして、この記事書くのに確認したら大体必要なことが書いてあった、的な裏舞台があったことはナイショのお話です)。
https://www.php.net/manual/ja/random-engine.generate.php

結論、必要な箇所はここ。

整数の値をネイティブで操作するアルゴリズムは、 たとえば pack() 関数に P フォーマットコードを指定するなどして、リトルエンディアンで整数値を返すべきです。

ただ、確認すると

  • うちの環境は、いわゆる64bit系の環境なんで、フルビット立っても、8bytes(64bit)
  • 一方で、このメソッド、どうやら16bytes文字列を返すっぽい(コードで確認したらうちの環境だとそうだった)

ってことがわかったので。リトルエンディアンなんで、8~15バイト目に適当なpaddingを突っ込んでおります。

んでまぁ、とっととコードを。

<?php
declare(strict_types=1);

class MockRandomEngine implements \Random\Engine
{
    /** @var list<int> 数値配列 */
    readonly private array $byteValues;

    private int $position = 0;

    /**
     * @param list<int> $byteValues 数値配列(キーは array_values で詰め直す)
     */
    public function __construct(array $byteValues = [])
    {
        $this->byteValues = array_values($byteValues);
        $this->position = 0;
    }

    public function generate(): string
    {
        // byteValuesが空なら0、そうでなければ現在位置の値を使用
        if ($this->byteValues === []) {
            $value = 0;
        } else {
            $count = count($this->byteValues);
            $value = $this->byteValues[$this->position % $count];
            // 次の位置に進める
            $this->position = ($this->position + 1) % $count;
        }

        // リトルエンディアン64bit符号付き整数(8バイト)に変換
        $bytes = '';
        // 符号付き64bit整数をリトルエンディアンでバイト列化
        for ($i = 0; $i < 8; $i++) {
            $bytes .= chr(($value >> ($i * 8)) & 0xFF);
        }

        // 16バイト返す必要があるので、残り8バイトは0で埋める
        return $bytes . str_repeat("\x00", 8);
    }
}

これで「コンストラクタの時に渡した数値配列」を繰り返し出力するようなエンジンを作ることができます。
使用例は、こんな感じ。

$engine = new MockRandomEngine([1, 255, 512, 1024]);
// $engine = new MockRandomEngine();
$randomizer = new \Random\Randomizer($engine);

for ($i = 0; $i < 10; $i++) {
    $value = $randomizer->getInt(0, PHP_INT_MAX);
    var_dump($value);
}

んで、注意点。
getIntの第一引数が「エンジンから帰ってくる値に単純に加算される」。
なので、ゲームを想定しているからなんだけど、例えば以下。

$engine = new MockRandomEngine([1, 2, 3, 4]);
$randomizer = new \Random\Randomizer($engine);

for ($i = 0; $i < 10; $i++) {
    $value = $randomizer->getInt(1, 6);
    var_dump($value);
}

を実行すると、こうなる。

int(2)
int(3)
int(4)
int(5)
int(2)
int(3)
int(4)
int(5)
int(2)
int(3)

あとまぁこっちは当然なんだけど、maxより大きな値をエンジンが返すと、エンジンが生成した整数値を範囲幅で割った剰余を使って指定範囲にマッピングしている感じですね。
なので

$engine = new MockRandomEngine([1, 2, 3, 4]);
$randomizer = new \Random\Randomizer($engine);

for ($i = 0; $i < 10; $i++) {
    $value = $randomizer->getInt(1, 3);
    var_dump($value);
}

だと

int(2)
int(3)
int(1)
int(2)
int(2)
int(3)
int(1)
int(2)
int(2)
int(3)

ってなる。

この辺に留意すれば、一応「エンジンで、乱数のテスト用の差し込み」は作れる感じかなぁ。
(とはいえ面倒なんで、別の方法で作るけど……ってのは次のブログで)。

いろいろ調べたり試行錯誤したりして、知識が霧散するのももったいないんで、備忘録的に。