この記事は、ゲームの『乱数調整』という現象を通して、コンピュータの擬似乱数の仕組みをプログラミング視点で分かりやすく解説するものです。
1. 未来は「計算」できるか?
「乱数調整」という言葉を聞いたことがあるでしょうか?
ゲームの RTA(Real Time Attack) や TAS(Tool-Assisted Speedrun) などの攻略動画を見ていると、突如謎の儀式のような行動を取り始めたり、何度もリセットしたりしているアレです。
「乱数調整」を行なった結果、本来ランダムであるべきはずの結果が、魔法のように操作されているのを見たことある人は多いのでないでしょうか。
この「乱数調整」は、チートコードを使ってゲームのメモリを直接書き換える「改造(ハッキング)」とは全く異なります。
ゲームのプログラムが仕様通りに100%正しく動いていることを逆手に取り、人間の力で入力を完璧にコントロールすることで、望む結果を意図的に引き当てる「究極の仕様ハック」なのです。
本記事では、当時のゲーマーたちがどのようにして「ゲーム内の未来」を計算し、コントロールしていたのか。その裏側にある「擬似乱数」の仕組みを、プログラミングの視点から紐解いていきます。
2. ランダムとは?
コンピュータは計算や決められた処理を正確にこなすのは得意ですが、「サイコロを振る」ような予測不可能な結果(ランダム)を生み出すのは非常に苦手です。
そのため、コンピュータの世界の「乱数」は大きく以下の2つに分けられます。
-
真の乱数(True Random Number):
熱雑音や放射性崩壊など、物理的なノイズをセンサーで読み取って生成する乱数。
完全に予測不可能ですが、生成コストが高くゲームには不向きです。 -
擬似乱数(Pseudo-Random Number):
あらかじめ決められた「計算式(アルゴリズム)」を使って、人間にはランダムに見える数字の列を作り出す乱数。
ゲームの処理速度を保つため、昔のゲーム機では「擬似乱数(以下、PRNG)」が使われていました。
そして、この擬似乱数にはある致命的特徴があります。
それは「最初のスタート地点の数字(シード値)さえ分かれば、次に出る数字は計算で100%予測できる」ということです。
3. ゲームはどうやって「シード」を決めていたか?
先ほどの説明で、「シード値(スタート地点)が同じなら、全く同じ未来(乱数)が生成される」ことが分かりました。
では、ゲーム機はどうやって毎回違うシード値を用意していたのでしょうか?
答えは「ゲームを起動した瞬間の、本体の内蔵時計」です。
当時の多くのゲームでは、「年・月・日・時・分・秒」、そして「フレーム(1/60秒単位の経過時間)」を計算式に当てはめ、初期シードを決定していました。
言葉で説明するより、JavaScriptのコードを見たほうが直感的に理解できるはずです。「ゲームの起動時間によって、その後のサイコロの目が完全に固定される仕組み」をJSで再現してみましょう。
// ゲーム内の擬似乱数生成器(線形合同法を簡略化したもの)
class GameRNG {
constructor(seed) {
this.state = seed;
}
// サイコロを振る(乱数を1つ進める)
rollDice() {
this.state = (1103515245 * this.state + 12345) % 2147483648;
return this.state;
}
// 1〜6のサイコロの目を出す
getDiceValue() {
return (this.rollDice() % 6) + 1;
}
}
// 「起動時間」から初期シードを計算する関数
function getSeedFromTime(timeString) {
const startTime = new Date(timeString);
// 分、秒、ミリ秒(フレームの代わり)を組み合わせてシードを作る
const seed = startTime.getMinutes() * 60000
+ startTime.getSeconds() * 1000
+ startTime.getMilliseconds();
return seed;
}
// --- 実験開始 ---
// パターンA: 2023年10月25日 14時30分00秒000ミリ秒 に起動
const seedA = getSeedFromTime("2023-10-25T14:30:00.000");
const rngA = new GameRNG(seedA);
console.log("14:30:00.000に起動: ", [rngA.getDiceValue(), rngA.getDiceValue(), rngA.getDiceValue()]);
// パターンB: 全く同じ時刻「14:30:00.000」に起動(再現性テスト)
const seedB = getSeedFromTime("2023-10-25T14:30:00.000");
const rngB = new GameRNG(seedB);
console.log("全く同じ時刻に再起動: ", [rngB.getDiceValue(), rngB.getDiceValue(), rngB.getDiceValue()]);
// パターンC: 1ミリ秒だけ遅れて「14:30:00.001」に起動
const seedC = getSeedFromTime("2023-10-25T14:30:00.001");
const rngC = new GameRNG(seedC);
console.log("1ミリ秒遅れた場合: ", [rngC.getDiceValue(), rngC.getDiceValue(), rngC.getDiceValue()]);
実行結果:
14:30:00.000に起動: [ 3, 1, 5 ]
全く同じ時刻に再起動: [ 3, 1, 5 ]
1ミリ秒遅れた場合: [ 1, 6, 2 ]
この結果から、恐ろしい事実が浮かび上がります。
「もし1ミリ秒(実際のゲーム機では1/60秒=1フレーム)の狂いもなく、同じ日時にゲームを起動することができれば、未来に起こる出来事を100%再現できる」のです。
4. 未来を手繰り寄せる「乱数消費」
特定の時間に起動してシードを固定できても、まだ終わりではありません。
なぜなら、欲しいステータスのキャラクター(=狙ったサイコロの目)が出るのが、起動した時点から「100回サイコロを振った後」かもしれないからです。
ゲーム内のサイコロ(乱数)は、戦闘に入った時だけでなく、以下のような些細な行動でも「振られて」しまいます。これを乱数消費と呼びます。
- 主人公が1歩歩く
- フィールド上のNPC(村人など)がランダムに振り向く
- 特定のステータス画面を開く
- 天候(雨や雪)のエフェクトが描画される
NPCが勝手に動き回る町の中などでは、自分の意図しないタイミングで乱数が進んでしまい、計算が狂います。
そのため、乱数調整を行うプレイヤーは以下のような厳密な手順を踏んでいました。
- 逆算: 「欲しい結果が出るのは、いつ起動して、何回乱数を消費した時か」を割り出す。
- 起動: 指定された日時に1フレーム(1/60秒)のズレもなくゲームを起動する。
- 消費: 「ステータス画面を43回開いて閉じる」などの、決まった数だけ正確に乱数を消費する行動を繰り返す。
- 遭遇: 乱数が目標の位置に到達した瞬間に、行動する。
端から見れば「特定の時刻にゲームを立ち上げ、無意味にメニュー画面を何度も開閉してから行動する」という奇行にしか見えません。
しかし彼らの頭の中では、プログラムのメモリ上で「乱数のリスト」が1つずつ正確に進み、ゴールへと向かっていたのです。
5. おわりに
乱数調整は「ゲームのバグ」ではありません。
開発者が実装した「時計の時刻からシードを生成する処理」も、「メニューを開くたびに乱数を使って描画する処理」も、すべてプログラムとしては100%仕様通りに正しく動作しています。
ただ、その仕様が完全に解明された結果、「人間側が入力を極限まで制御すれば、出力(未来)を完全にコントロールできる」というハックが誕生してしまったのです。
現代のゲームでも「未来の計算」は可能なのか?
「じゃあ、今のSwitchやスマホのゲームでも同じことができるの?」と思うかもしれません。
結論から言うと、現代のゲーム機で昔のような乱数調整を行うことは極めて困難(実質不可能)です。
その理由は、技術の進歩によって「アルゴリズム」と「シード値の決め方」が劇的に進化したからです。
-
アルゴリズムの進化:
昔のゲームで使われていたLCG(線形合同法)は計算が軽い反面、予測が容易でした。現代では「メルセンヌ・ツイスタ(Mersenne Twister)」や「PCG」「Xoshiro」といった、内部状態が天文学的に大きく、数回結果を見ただけでは逆算が不可能なアルゴリズムが標準的に使われています。 -
シード値(エントロピー)の複雑化:
単なる「年月日時分秒+フレーム」ではなく、ナノ秒単位の時刻、CPUの温度、メモリの空き状態、ユーザーのコントローラー入力の微妙なアナログ的揺らぎなど、予測不可能なノイズ(エントロピー)をかき集めてシード値を生成するようになりました。 -
サーバーサイドでの生成:
ソーシャルゲームのガチャなど、絶対に操作されてはいけない乱数は、そもそもユーザーの端末ではなく、手出しできないサーバーの奥深くで「暗号論的擬似乱数(CSPRNG)」を用いて生成されています。
まとめ
ブラックボックス化されたコンパイル済みのゲームから、乱数生成のアルゴリズムをリバースエンジニアリングで特定し、逆算ツールを作り上げた解析勢。
そして、1/60秒の目押しという身体操作でその理論を実証したプレイヤーたち。
昔のゲームの「乱数調整」は、当時のハードウェアの制約と、それをハックしようとした人間たちの執念が生んだ、エンジニアリングの歴史に残る美しい芸術作品と言えるかもしれません。
次にプログラムで Math.random() や乱数ライブラリを呼び出すときは、ぜひ「このランダムな数字の裏側では、一体どんな計算が行われているのだろう」と思いを馳せてみてください。
最後に
「裏側でどんな計算が行われているのだろう?」
そんな純粋な好奇心こそが、エンジニアリングの第一歩です。
株式会社Hello worldでは、そんな「技術の裏側を知りたい」「仕組みから理解してモノづくりをしたい」という知的好奇心を持った仲間を募集しています!