診断・統計レポート

このフォルダには、実装の挙動を計測した統計レポートの読み方を格納します。設計そのものを扱う他の フォルダとは性質が異なり、「今の実装をシミュレーションした結果こうなった」という測定値に ついての文書であり、設計判断や仕様を記述するものではありません(次節)。

他のフォルダとの違い

他のフォルダ(../README.md 参照)が「こう設計する」という判断を記すのに対し、 diagnostics/ が扱うのは特定時点の実装に対する測定結果です。実装(レート・閾値等の数値)を 変えれば結果も変わるため、内容を手で書き換えるのではなく、対応するテストを再実行して再生成します。 バランス調整のたびに上書きしてよく、Git履歴が変化の記録になります。

数値の置き場は stats/

数値の生成物は、リポジトリ直下の stats/ に YAML で置きます。 主要な読み手はもう人間ではなく エージェントなので、生成するのは機械可読な数値だけにし、docs/ には人が書いたものだけを置きます。 1レコード1行のフロー形式で書くのは、git diff に値だけが並んでどのレコードの値かが分からなく なるのを避けるためです。

どのレポートもこの形です。 docs/diagnostics/*.md が持つのは読み方——何を測ったか・どこに線を 引いたか・何を数えていないか——だけで、数値は1行も書きません(書けば、再生成したYAMLとずれます)。 どちらが古いかを運用で守らずに済むよう、手書きの文書の「YAMLの節」の表とYAMLの節は、両方向で 突き合わせています(片方にしか無い節があると npm test が赤くなる)。

文書へ書き写した数値には、出どころの印を置く

docs/ の本文が stats/*.yaml の数値を書き写すときは、書いた数値の直後に出どころの印を置きます。 tests/docs/docStatsCitations.test.ts が印の指すセルと 突き合わせます——再生成すると文書だけが古い値を持ったまま緑になるからです (issue #860)。

**片道の平均は91.61分**<!-- stats: terrain.yaml base_one_way base=shortest_mean mean -->

形は <!-- stats: <ファイル> <節> [<列>=<値> …] <読む列> [<畳み方>] [±<粗さ>] --> です。 <列>=<値> はレコードを選ぶ条件で、畳み方を書かなければ1件に絞れないと赤になります(節に別の base が入った、など)。畳み方を書いたときはその逆で、2件以上に当たらないと赤です(下の 「節から出る幅は、畳み方を印に書く」)。印が掛かるのは 直前の1つの数だけです。数と印のあいだに置けるのは、単位や強調ぶんの8文字までで、数字と表の 区切り(|)は挟めません——印は数値と同じセルに置くことになります。その数に接した -(U+002D)・ −(U+2212)は符号として読むので、負のセルはどちらの字でも書き写せます。離れていれば符号では ありません(箇条書きの - など)。

印は、コードとして囲んだ中——フェンスの中でもバッククォートの中でも——同じに突き合わせます。 印はHTMLのコメントなので、描画した文書で読めるのは囲みの中に置いたときだけです。この節に置いた 例も実在するセルの書き写しで、再生成でずれれば赤くなります。読み飛ばすと、印の付いた数値に 誰も突き合わせていないものが混ざり、読み手には見分けが付きません。囲みは数の読み方にも 効きません——約`4,000`分 のように囲んだ中に書いた数も、印の直前の数として読みます。

出どころを持たない書式そのものを見せるときは、上の形のようにプレースホルダで書きます (<ファイル> は > を含むので、印としては読まれません)。

区切りは空白なので、空白を含む値は二重引用符で囲みます(chain_routes・daily_minimum_menu 節の route は、工程を → でつないだ値です)。囲んだ中の = は値の一部で、列と値の切れ目は最初の1つだけ です。

**青いヤシの実から水だけを取るなら1日152分**<!-- stats: balance.yaml chain_routes place=島全体 property=hydration route="palm_tree.pick_green_coconut → green_coconut.bore" daily_minutes -->

印を付けるのは、書き写しと、節を畳んだ読み方だけ

見るのは YAMLの1つのセルを、丸めだけを挟んで書き写した数値と、同じ列を条件で絞って畳んだ数値 (下の「節から出る幅は、畳み方を印に書く」)です。線は「畳むか、計算するか」に引いてあります ——違う列どうし・生成物の外の数と組み合わせた数値には付けません(掛ける・裏返す・別の列を引く)。 そちらは書き写しではなく文書の主張で、式を印に書けるようにすると同じ計算が文書と生成器の2箇所に 立ちます。

定義から書き写した数値には、codex: の印を置く

人が書く定義(src/assets/world-codex/**.yaml)は再生成では動きませんが、人が定義を動かせば、 そこから書き写した文書は静かに嘘になります(issue #2357)。 そこで定義の値を書き写すときも、直後に印を置きます。突き合わせるのは同じ tests/docs/docStatsCitations.test.ts です。

| なめし革の衣類 | −4℃<!-- codex: clothing.yaml object_defs.tanned_leather_clothing.passives.0.modify.parent.chill_point --> |

形は <!-- codex: <ファイル> <道> [±<粗さ>] --> です。<ファイル> は src/assets/world-codex/ からの 相対パス(characters/player_character.yaml のように入れ子も書けます)、<道> はトップレベルからの キーをドットでつないだもので、リストの中は0始まりの添字で指します。定義は入れ子のマップなので、 生成物の印のようにレコードを条件で選ぶのではなく、道そのものを書きます。道の先が数でなければ赤です ——添字で指した要素の並びが変われば、道が途切れるか数でないものに当たって赤くなります。

数の読み方・粗さ・コードとして囲んだ中の扱いは、生成物の印と同じです。畳み方は持ちません(定義の 中の1つの値を指すだけなので)。定義を足し算・掛け算した数は、生成物の場合と同じく書き写しでは ありません——次節のとおり、生成器へ移すか(balance.yaml の object_costs がレシピを素材まで 遡った手間を持っています)、書かないかのどちらかです。

定義を YAML のまま抜粋するなら、フェンスに出どころを名乗る

定義をブロックごと文書へ抜き出すときは、数ごとに印を置くのではなく、開きのフェンスの情報文字列に 出どころを書きます。

```yaml codex: fire.yaml traits.ignitable.props.moisture

形は codex: <ファイル> [<道>] で、ファイルと道の書き方は上の印と同じです。道は抜粋のトップレベルが 定義のどこに当たるかを指し、書かなければ定義ファイルのトップレベルです。 tests/docs/docStatsCitations.test.ts が抜粋を YAML と して解き、抜粋が定義の部分集合であるかを見ます。

出どころを名乗らない YAML は、例示として読みます(実在しない名前や、文法の形だけを見せるもの)。 名乗り忘れの見張りとして、名乗らない YAML が定義のどこかの部分集合にそのまま当たれば赤にし、 当たった在り処を名乗りの候補として出します。書き写したばかりの抜粋は現物と一致するので、ここで 拾えます。葉が1つだけの断片は、例示でも定義のどこかに当たるので見ません。

例示は、実在の名前を借りて現物と違う値を書きません。 名乗りが無いことは読み手には見えにくく、 実在の型・trait・操作の名前が出ていれば現物の写しとして読まれます。文法の形を見せたいだけなら、実在 しない名前を使います。見張りは同じ試験で、定義の節(object_defs・traits など)の直下と、その中の どこに置いた interactions の直下にも、実在の名前を書いた項目が同じ名前のどの現物の部分集合にも 当たらなければ赤です。架空の型の中に置いた操作も見ます——在りそうな型の名前は、例示だと名乗りません。

例示の中の数を書き写しとして守りたいとき(文法の形を見せながら、値は現物から借りるもの)は、 フェンスの名乗りではなく数ごとの codex: の印を置きます。

導いた値は、生成器へ移すか、書かない

導いた値を印なしで置かないこと。 生成物を作り直しても、定義を動かしても、その数だけが古いまま 緑で通ります(issue #2163)。

採れる道は次のどれかで、採った道は印の有無でその場で読めます。

定義の YAML と検査のコメントにも同じことが掛かります。 そこには印を置けないので、採れるのは 「数を書かない」か、同じ値を検査が直接見ていることを書き添える形(「材料の目方の和 (tests/world-codex/loadEffects.test.ts が見る)」)だけです。定義のコメントに数どうしの式 (1,000×6・8000+車輪2枚4000)を書けば tests/docs/codexCommentArithmetic.test.ts が赤くします ——文章で説いた導出(「10日で朽ちる」)までは拾えません。

下位桁が主張に効いていないなら、粗さを書く

± を書かなければ、書いた桁へ丸めた厳密一致です。170.45 を「170分」とは書けますが、 4,156.0 を「約4,000分」とは書けません。この既定のままだと、定義を触るPRが結論に効かない下位桁 まで書き直す義務を負い、同じ行を触るPRどうしがぶつかります。

± を足すと、書いた数を中心とした幅の中にセルがあれば緑になります。幅の書き方は次のとおりで、 どちらも「どれだけ粗く書いたか」の1点だけを言います。

粗さ 見方 「4,000」と書いたとき緑になる範囲
(書かない) 書いた桁へ丸めて一致 3,999.5以上 4,000.5未満
±100 出どころと同じ単位で±100 3,900〜4,100
±5% 書いた数の±5% 3,800〜4,200

既定の行だけ上端が入らないのは、それが幅ではなく丸めの規則そのものだからです(4,000.5 は 4,001 へ丸まります)。

4桁の労働がかかるのは筏(約4,000分<!-- stats: balance.yaml object_costs object=raft total_minutes ±5% -->)

幅は、その一文の主張が壊れない広さにします。 上の主張は「4桁の労働がかかる」なので、3桁や5桁へ 落ちる幅は取りません。この幅に収まっている限り、定義が動いても本文は動かさずに済みます。

セルの単位が既に%のときも、±5% は書いた数の5%です(24.75% なら±1.24ポイント)。ポイントで 幅を取りたいなら ±1 と書きます。

節から出る幅は、畳み方を印に書く

「この節から出る幅」——条件で絞ったレコードを同じ列の中で見た最小・最大・その差——は、導出では なく生成物の読み方なので印に書けます。読む列の次、粗さの手前に畳み方を置きます。幅 が引き算でも 禁じた導出に当たらないのは、引く相手が同じ列の中に在るからです。

畳み方 読む値
最小 当たったレコードの、その列の最小
最大 同じく最大
幅 最大と最小の差
**差は砂浜で 0.3 日**<!-- stats: voyage.yaml course_season coast=sandy_beach course=shortest days 幅 ±0.1 -->

同じ数へ印を重ねれば、どれも掛かります——最小 と 最大 を重ねると「当たったレコードがどれも この値」まで言えます(1つの数が複数のセルを代表している表でも同じで、代表する全部を並べます)。

畳み方を書いた印は、条件が2件以上に当たらないと赤です——1件しか当たらないならそれは書き写しで、 畳み方は要りません。逆に、畳み方を書かない印が2件以上に当たるのも赤のままです。畳み方の名前を 日本語で持つのは、生成物の列名と紛れないため——同じ名前の列が生えれば印が黙ってそちらを畳み方 として読むので、レポートの全部の列を見て突き合わせています(tests/docs/docStatsCitations.test.ts)。

生成方法

各レポートは、tests/diagnostics/ に置かれた対応するテスト(専用の環境変数が立っているときだけ実行され、 通常の npm test では実行されない)を実行して生成します。生成元テストと出力先はレポート冒頭にコメントで 記載しています。

コマンドは package.json の stats:* のうち、tests/diagnostics/*StatsReport.test.ts を走らせるものです(npm run stats:balance など)。レポートを足すたびにここを書き換えずに済むよう、 一覧は写しません。

再生成し忘れると赤くなる

再生成は運用で守るものでしたが、1度すり抜けました(issue #775) ——古いレポートと、そこから手で書き写した定数が互いにだけ一致したまま、どちらも今の定義を映していない 状態がしばらく続きました。運用で守るものは、守れなかったときに誰も気づけません。

そこで、入力を変えたまま再生成しないと赤くなるようにしてあります。見張りは2本立てで、 PRの段では npm test が、main へ入った後は regenerate-stats.yml が見ます。前者には原理的に 届かない古さがあり、そこを後者が持ちます。

npm test ——PRの段

手立てはレポートの作り直しにかかる時間で分けています。

レポート 見張り方
アイテム収支 丸ごと作り直して比べる(1秒で済むので取りこぼしが無い)
絵の無い型 丸ごと作り直して比べる(定義を読むだけなので一瞬)
気候システム統計 シミュレーションの入力(core.yaml)の指紋をレポートへ書き込み、突き合わせる。定義から静的に解ける activity_hours・excluded_locations の節だけは作り直して比べる
発見物の行き渡り 丸ごと作り直して比べる(2秒)
日をまたぐ長さ 丸ごと作り直して比べる(定義から解くだけなので一瞬)
島を出るまでの工程数 丸ごと作り直して比べる(定義から解くだけなので一瞬)
狩りの手数 丸ごと作り直して比べる(遭遇を実際に通すので6秒)
島ごとの脱出可否 丸ごと作り直して比べる(2秒)
開始地点の立ち上がり 丸ごと作り直して比べる(2秒)
地形生成統計 丸ごと作り直して比べる(1秒)
航海 丸ごと作り直して比べる(2秒)
荒天の押し流しを入れた航海 シミュレーションの入力(core.yaml・voyage.yaml)の指紋をレポートへ書き込み、突き合わせる。押し流しを数えない側の baseline 節だけは作り直して比べる

いずれも、節が消えていないことはキーが在って中身が空でないことで見ます。

指紋を使っているのは、再生成に1分以上かかるものだけです(気候はクラウドのセッションで67秒、 押し流しは75秒。npm test 全体が25秒なので、丸ごと比べると3倍以上になります)。作り直しが数秒で 済むなら丸ごと比べます——指紋は「入力だと決めたファイル」の外を見ないので、そこから外れた入力 (解析側が持つ定数など)を取りこぼします。

どこまでを指紋に含めるかは、レポートごとに読み方の文書が書いています (ClimateSystemStats.md・VoyageStormStats.md)。 土地や食べ物の変更まで含めると、YAMLを1行直すたびに1分以上の再生成を要求することになるので、どちらも 世界を回す側が読むファイルだけに絞り、そこから外れた入力は再計算する節で突き合わせます。

regenerate-stats.yml ——main へ入った後

main への push のたびに全部を丸ごと作り直し、差分が出たら main へ push し直します。 npm test に映らない古さを、ここが拾います。

収録レポート