496の落書き帳

496がなんか思ったことを書いたり書かなかったりする場所です。

ゲームのバグの話がしたかったんです。(前編)

はじめに

ISer Advent Calendar 2020 - Adventarの11日目の記事です。

20erの496と申します。まぁ名前はどうでもいい。最近はifとelseが逆転したコードを吐くコンパイラを生産していました。

元々コンピュータに興味を持ったのがゲーム好きだったからみたいな節があって、ゲームのバグ分析を読むのは今でも好きなんですが語る機会がねぇ。ということで枠埋め記事にしてしまいましょう。

これがISのAdvent Calendarの記事でいいのかはよくわかりません。誰か教えて。

もくじ。

風来のシレンというゲーム

の話をします。 スーパーファミコンの全盛期(たぶん)に発売された、いわゆる『不思議のダンジョン』の金字塔。入るたびに構造やら落ちているアイテムやらが全部変わる『不思議のダンジョン』を踏破するゲームです。キャッチコピーは『1000回遊べるRPG』、何回遊んでも違う展開が楽しめるぞということですね。

もう20年以上経つのにまだ初代スーパーファミコン版を遊んでいる人がいます。続編リメイクの類はたくさんあり、今度風来のシレン5+なるものが出るそうです。知らんけど。

ゲームシステム的なことを言うと、各種アイテムやら敵の特殊スキルやら罠やらの効果がすごく多くて、

  • アイテムに化けてダンジョンに落ちていて、拾うとそのまま持ち物欄に入って、使おうとすると正体を現し攻撃してくるモンスター
  • モンスターに変身してステータス等が全部変わり、そいつの特殊スキルが使えるようになる肉
  • ダンジョン構造が変わる巻物

こういう感じのシステムの根幹に干渉しそうなものもたくさんあるので、実装もデバッグも困難を極めたであろうなぁという感じです。発売当時から『この効果とアレを衝突させるとどうなるのか』系のバグ研究が流行り、実際トンデモバグがちょいちょい発見されては考察され、謎が謎を呼ぶ展開を繰り返してます。まあ今回語るバグは特殊効果を衝突させる系ではないんですが。

序ー仕様

風来のシレンには、端的に言えばセーブ機能(中断)リプレイ機能(回想)があります。ダンジョンのどこでも中断できて、しかもそのフロアの開始から中断までの展開を再生することができるという優れ物です。不思議のダンジョンは長いので、どこでも中断できるというのは是非ともあって欲しい機能ですが、リプレイ機能があるのは(スーパーファミコンのゲームとしては)中々に豪華な実装ですね。たぶん。

考察ーリプレイ機能の原理

さて、一般にゲームのリプレイ機能ってどのように実装されているかというと、起きたことを逐一全部データとして保存するのは無理です。じゃあどうするのかというと、初期状態と入力だけを覚えておけばその後何が起きたかを(ゲームのルールに沿って)全部シミュレートできるので、そういう実装が賢いです。お、それっぽい話になってきましたね。

伏線ーリプレイにおける乱数の処理

ここでちょっと注意しないといけないのが乱数の存在です。乱数の再現が完璧に行われなければ、元のプレイと同一の展開を生み出すリプレイ機能になってくれません。ゲームを盛り上げるために乱数は必須なわけですが、コンピュータゲームは(少なくともスーパーファミコンは)真の乱数を作り出しているわけではありません。乱数っぽい数列を(決定性計算で!)発生させるための仕掛けとして、

いい感じの漸化式を用意して初項だけ適当に決めると、各項が疑似乱数列として使える

というのが有名(というか実装がほぼソレしかない)ですが、この場合初項(初期seed)だけを覚えておけば以降の乱数列が再現できることになります。

まとめると、リプレイ機能を動かすのに必要なデータは

  • 初期状態
  • 乱数の初期seed
  • 入力の列

ということになります。要は初期seedも初期状態の一成分だと思えばいいんですね。

検証ー中断の真実

結局のところ、リプレイに必要な初期状態とは状況のスナップショット(セーブデータ)に他ならず*1、それに入力を加えたリプレイデータはセーブデータより大きくなり、リプレイ機能はセーブ機能よりずっと重いように見えます。

普通のゲームならば

風来のシレンは、入る度に構造が変わる『不思議のダンジョン』のゲーム。ゆえに、ダンジョンのフロアに突入するたびに

乱数に基づいてフロアマップ(構造、アイテム配置、敵配置)を全部生成する

という処理が走ります。すると、フロア突入時には『マップ全体の状況』という大きなデータが初期seedというたった一つの数値から生成されていることになり、フロア突入直前の初期seedさえ取っておけば、そこからフロアマップ全体の状況が再現可能です。 言い換えると、フロア突入時に限って『マップ全体の状況』という巨大データが一つの数値に圧縮できるわけです。任意の瞬間のスナップショットを取るにはマップ全体の状況を保存する必要がありますが(しかも、むしろ不思議のダンジョンゆえダンジョンマップデータを定数ロードで済ますことができない!)、フロア突入時のスナップショットは主人公のステータスと持ち物といった若干のデータと初期seedだけで済みます。

具体的なデータ量のことを少し考えてみると、不思議のダンジョンのマップ状況は大雑把に以下のようなものを含むそうです。

  • 64×64マスのマップの各マスについて、
    • 壁であるか、通行可能か:1byte
    • 誰がそのマスに立っているか(キャラクターID、あるいは誰もいないことを示す値):1byte
    • 足元には何が落ちているか:1byte
    • その地点が到達済みであるか:1byte
  • マップにいる各敵(最大20個体)のステータス(種族、レベル、HP、状態異常など):各5byte以上?

まだまだあると思いますが、とにかく各マスのデータが重い*2。これだけで16000byte以上になります。

一方、ゲームを動かす入力(主人公の行動)をデータとして記録することを考えると、どの時点でも取れる行動は256種類(=1byteの情報量)程度しかなく、1フロアでの行動を保存するには1000行動分くらい有れば十分だそうです*3

…以上の考察から、

  • フロア途中のスナップショットをセーブデータとして残すより、フロア突入直前のスナップショット(+初期seed)とそこからの行動記録(入力)をデータとして残す方が圧倒的に少ない
  • ゆえに、素直にセーブ機能を実装するよりもフロア開始時からのリプレイ機能を実装する方が(記憶容量において)有効である

ということが分かります。いい話! そういうわけで、風来のシレンではセーブ機能の実装ついでにリプレイ機能が作れてしまった、という想像が付きます。リプレイ機能は豪華な機能でもなんでもなく、自然な実装の副産物だったんですね。

余談.風来のシレンを始めとする不思議のダンジョンシリーズには『同じフロアに居座り続けると突風に飛ばされてゲームオーバーになる』という謎のお約束があります。これはリプレイデータ容量の限界のせいだったりするわけですね。

破ー綻び

さてさて、やっと本題です。

数多のやりこみプレイヤーを抱える風来のシレンですが、実はかなーり前から

中断して再開したら中断時と違う状況になっていることがある

という致命的なバグ(通称:中断バグ)が報告されていました。長年、研究者*4たちが様々な実験を繰り返してもバグ発生の正確な要因は分からずじまいであった一方、その症状は多岐に渡り、

  • 装備が別物になっていた
  • 拾った記憶のないアイテムを持っていた
  • それどころか、全く見たことがない謎のモンスターの肉*5を持っていた
  • (最悪のケース)再開したら何故か死んでいて、いきなりゲームオーバーになった

等々、その光景はもはやシレン界隈屈指の怪談レベル。

解明ー中断バグ

話の流れ的にお気づきでしょうが、これはセーブデータ(リプレイデータ)を取る上で、初期状態や行動の記録がちゃんと取れていなかった、あるいは保存した記録が書き変わってしまった、という類の『よくあるデータ保存のバグ』ではありません。リプレイデータはちゃんと取れていたのに、シミュレートがゲームの実行を完璧に再現できていなかったのです。

しかし、リプレイを行うシミュレータは基本的にゲームを実行するのと同じプログラムを使いまわして出来ているはずです。そう簡単に実機と結果がズレるはずはありません*6。実際のところ、行動の再現処理は何も間違っていませんでした。結論を言ってしまうと、このバグの原因は

鈍足状態*7ダッシュをすると(たまに)乱数関数を1回余計に呼び出してしまう(ゆえに擬似乱数列の消費がズレて以降の再現が変わる)

ことにありました。原因となった行動のシミュレート結果自体は特に問題がなく、食い違いが露呈するのはその次に乱数が絡む処理が行われたときであったために、どの行動が原因であるのかが中々掴めなかったのだと思われます。ついでに言うと、ダッシュという行動には(少なくとも見た目上)全く乱数が関係がないという。うーん、タチが悪いバグだ。

今ここでは悠々と種明かしをしていますが、これらの事実が解明されるまでには非常に長い年月がかかりました。バグの原因行動の特定および再現可能な実験が確立されたのは、風来のシレン発売から実に20年ほど後のこと(2016年)。その研究過程で、先ほど述べた様々な怪現象も説明がつけられるようになったのです。いい話。

補足.この現象はゲーム上かなり簡単に再現実験ができます。シチュエーションとしては十分簡単に起こせてしまうのに開発サイドが何故これを見過ごしてしまったのかという疑問が湧きますが、

  • ゲームの仕様として、鈍足状態でダッシュをしても1マスしか進めないので、普通に歩くのとほとんど何も変わらない
  • ゆえに鈍足状態でわざわざダッシュをするプレイヤーがいない

という事情があり、実は普通にプレイしていると意外に遭遇しない事象だったようです。

崩壊ー回想バグ

ここまではいい話だったわけですが、ゲームのバグを解析している(主にRTA界隈などの)人々はその先に目を向け始めます。研究者たちの目標は、「どうしたらバグらないか」ではなくて、「どうやってバグらせるか、それをどう活用するか」。一つバグが埋まっていて開発者が想定していない動作をすれば、そのバグから派生してさらにとんでもない事態が起きるのはよくあること。それが一番面白いところというわけです。

リプレイがバグって乱数がズレたとして、どれくらいまずいことになるのか?例えば攻撃ダメージの乱数が変わって(なんなら確率で攻撃が外れたり)、元プレイでは倒せた敵が回想では倒せない。あるいはその逆。元プレイ側で敵を倒して行動していたら、回想では倒せていなくて攻撃されまくる。場合によっては死ねます。 さらに進めて、元プレイで通路を塞ぐ敵を倒して進んだとしましょう。回想中では倒せていないとすると、ぶつかって先に進めません。すると位置がズレますね。そうなると、拾った記憶のないアイテムが拾われていてもおかしくはないでしょう。

...その程度のレベルで済めば良かったんですが...済みませんでした。

元プレイで拾った草をすぐにその場で食べたとしましょう。それが回想では拾われていなかったとしたら、どうなるでしょう。シミュレータの処理が「草を食べる」という行動データのところに差し掛かった時、持ち物にその草がなかったらどういう処理が起きるのでしょう。ちょっと嫌な予感がしてきましたね。

そんなところで明日の 後編 に続きたいと思います。
(内容はそんなに多くないと思ってるんだけども、いざ書いてみると結構疲れるよね・・・)

*1:正確にはセーブデータだけを取るなら初期seedがいらないのでリプレイデータの初期状態とセーブデータには僅かに差があります

*2:マップデータが重いのはもう少しなんとかならないのかと思いたくなりますが、壁を掘って通行可能にするアイテムとかあるので結構頑張っているんだと思います

*3:行動数の議論はちょっと適当なのですが、多分後編で詳細に語られます。

*4:ここでいう研究者とは、ゲーム制作サイドのデバッガーではなく、風来のシレンを愛しつづけるプレイヤーたちのこと

*5:このゲームにおける「肉」は凄いアイテムです。詳細は後編で

*6:なにか思い当たる節がある?気のせいでしょう。

*7:鈍足状態というものの説明はあんまり関係ないのでスルーしましたが、これは2ターンに1回しか行動できなくなる状態のことです。めっちゃ不利やん。