#1 コンパイラが守る、決定論的なコア
· Nick
まだ見せられるものはないので、最初の記事は誰の目にも触れない部分、シミュレーションコアについて書きます。このゲームは、毎秒20ティックで最大1,000人の受刑者と職員を動かし、誰が何をなぜしたのかを説明できることを目指しています。それが成り立つのは、同じ入力から必ず同じ世界ができるときだけです。私はこれを、初日から機械が検証する性質にすることにしました。
現在地
最初のマイルストーン、コアの骨組みの途中です。シミュレーションは Godot のコードを一切含まない素の .NET ライブラリで、Godot 4 のクライアントは今のところ空の 3D シーンを開くだけです。世界のモデルも、タイルも、キャラクターも、セーブもありません。1,000キャラクターという予算は目標であり、その規模では何も計測していません。
ティック
コアの時間は、毎秒20の整数のティック数です。浮動小数点の時間はありません。1ティックは3つのフェーズで進みます。外部からのコマンドを適用し、1か所に書かれた順序でシステムを実行し、保留していた変更を確定してカウンタを進めます。ティックの外までエラーが漏れた場合、世界は異常状態の印が付いて停止します。いまはシステムの一覧が空ですが、最初のシステムの居場所として、ループだけは先に用意してあります。
コマンドとログ
世界が変わるのは、コマンドを通じてのみです。コマンドは、固定の型 ID と手書きのバイナリエンコーディングを持つ小さなレコードです。各コマンドについて、世界はそれをエンコードし、ティックと連番を刻み、(読み取り専用で)検証し、ログに書き込み、そのあとで初めて適用します。拒否されたコマンドもログに残るので、バグ報告の助けになるはずです。シードとログがあれば、実行を正確に再現できる設計です。リプレイのファイル形式は、まだ書いていません。
乱数
乱数生成器は自作(SplitMix64 と xoshiro256**)で、標準ライブラリの Random はコアでは使用禁止にしました。世界には4本の名前付きストリーム(コマンド、欲求、AI、イベント)があり、シードから導出されてワールド状態に保存されるので、セーブされ、ハッシュにも含まれます。順序が結果に影響してはいけない処理には、ドメイン、エンティティ、ティックに基づくステートレスなキー付き生成器があります。シード42の出力は固定してあります。これは事実上リプレイ形式そのものだからで、生成器を変えれば、過去のリプレイはすべて壊れます。
世界のハッシュ
順序に敏感な小さなハッシャーが、シード、ティック、乱数ストリーム、コマンドログのダイジェストを64ビットにまとめます。同一のはずの2回の実行を、数字ひとつで比較できます。いまは世界に他に何もないので、対象はこれらだけです。実行中に N ティックごとにハッシュを比較するのが、次のステップです。
非決定性をビルドエラーにする
ルールを覚えていることに頼りたくありませんでした。コアは、禁止 API を検出するアナライザー付きでビルドされます。実時間、シードなしの乱数、ランダムな GUID、ランダム化された文字列ハッシュ、スレッドと並列ヘルパー、SIMD、カルチャ依存の文字列 API は、すべてビルドが失敗します。さらに別のチェックで可変の静的状態がないことを確かめ、コンパイル済みコードもスキャンして、すり抜けたものがないか調べます。私のマシンはロシア語ロケールで小数点がカンマなので、カルチャ依存 API を禁止する十分な理由になりました。
さらに一歩進めて、標準のハッシュコレクション(Dictionary、HashSet など)もコアでは禁止しました。反復順序は、ずれの典型的な原因だからです。コアで使えるのは配列、リスト、そして順序が文書化された自作の小さなコレクション2つです。完璧ではありません。ジェネリックインターフェース経由でハッシュコレクションを返す API はすり抜けうるし、保護の一部はテストでしか働きません。
作り方
コードの周りのツールも自動化されていて、同じスクリプトが Windows と macOS で動きます。全体の方針は制作の裏側にあります。
次は
このマイルストーンで残っているのは、階層を持つ世界モデル、セーブとリプレイのファイル、決定論テスト、ベンチマークのベースラインを持つヘッドレスのシナリオランナーです。そのあとに、Godot の建設ビューが来ます。ロードマップをご覧ください。