診断・統計レポート
このフォルダには、実装の挙動を計測した統計レポートの読み方を格納します。設計そのものを扱う他の フォルダとは性質が異なり、「今の実装をシミュレーションした結果こうなった」という測定値に ついての文書であり、設計判断や仕様を記述するものではありません(次節)。
他のフォルダとの違い
他のフォルダ(../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 と
して解き、抜粋が定義の部分集合であるかを見ます。
- 書いた葉(スカラー)は、数に限らず定義の同じ道の値と一致すること。 省いたキーは見ないので、 要るところだけを抜き出せます。コメントは解いた時点で落ちるので、説明は書けます。
- 並びは、抜粋の要素が定義の要素へ順を保って当たること。 途中の要素は略せます(段の1つだけ、 候補の一部だけ)。並べ替えた抜粋は赤です。
- 複数の定義から集めた抜粋は、出どころごとにフェンスを分けます。
出どころを名乗らない YAML は、例示として読みます(実在しない名前や、文法の形だけを見せるもの)。 名乗り忘れの見張りとして、名乗らない YAML が定義のどこかの部分集合にそのまま当たれば赤にし、 当たった在り処を名乗りの候補として出します。書き写したばかりの抜粋は現物と一致するので、ここで 拾えます。葉が1つだけの断片は、例示でも定義のどこかに当たるので見ません。
例示は、実在の名前を借りて現物と違う値を書きません。 名乗りが無いことは読み手には見えにくく、
実在の型・trait・操作の名前が出ていれば現物の写しとして読まれます。文法の形を見せたいだけなら、実在
しない名前を使います。見張りは同じ試験で、定義の節(object_defs・traits など)の直下と、その中の
どこに置いた interactions の直下にも、実在の名前を書いた項目が同じ名前のどの現物の部分集合にも
当たらなければ赤です。架空の型の中に置いた操作も見ます——在りそうな型の名前は、例示だと名乗りません。
例示の中の数を書き写しとして守りたいとき(文法の形を見せながら、値は現物から借りるもの)は、
フェンスの名乗りではなく数ごとの codex: の印を置きます。
導いた値は、生成器へ移すか、書かない
導いた値を印なしで置かないこと。 生成物を作り直しても、定義を動かしても、その数だけが古いまま 緑で通ります(issue #2163)。
採れる道は次のどれかで、採った道は印の有無でその場で読めます。
- 導出を生成器へ移して、1つのセルにする。 計算が生成器の1箇所に在るので、入力が動けば値も一緒に
動きます。文書はそれを書き写すだけになるので印が置け、数を書かない
docs/diagnostics/**なら列の 名前で指すだけで済みます(terrain.yamlのdaily_budget.day_trip_one_wayがこの形)。 - 数を書かない。 言いたいことを、数ではなく条件や向きで書きます(「窓を逃すと季節2つぶん待つ」)。
定義の 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 に映らない古さを、ここが拾います。
- 生成物を触るPRが2本続いたぶん。 2本目は1本目が入る前の定義から作られているので、gitの上では 衝突しないまま入ります。
- 気候表が指紋の外で古くなったぶん。 シミュレーションのコードを変えても
core.yamlは変わらない ので、指紋は一致したままです。気候表にとっては、ここが丸ごとの突き合わせを持つ唯一の場所です。
収録レポート
- アイテム収支 — 素材から摂取までの総時間(土地ごと・プロパティごと)、
待って得る生産のレートと必要設備数、キャラクタが1日に要る量、1工程あたりの所要時間と産出。
実行結果ではなく、定義(
src/assets/world-codex/*.yaml)だけから計算した値。 読み方はBalanceStats.md。 生成元:tests/diagnostics/balanceStatsReport.test.ts(計算はsrc/analysis/balanceTables.ts。同じ表はコーデックスビューアの#/balanceでも見られる ——絵つきで読めるのはそちら、数値を変えたときの差分を読めるのはこちら)。 - 絵の無い型 — 札に自分の絵を映す型のうち、絵がまだ無いもの。絵の無い型は
種別の代役アイコンで札に出るので、足りていないことはここでしか見えない。揃っていないことの判定は
出さず、数が減ることが進捗になる。
読み方は
CardArtStats.md。 生成元:tests/diagnostics/cardArtStatsReport.test.ts(判定はsrc/analysis/cardArt.ts) - 気候システム統計 — 季節の持続日数・気温・天気ごとの持続時間・
連続降雨/未降雨時間(
ClimateSystem.md参照)と、土地×季節ごとの活動時間。 読み方はClimateSystemStats.md。 生成元:tests/diagnostics/climateStatsReport.test.ts - 発見物の行き渡り — 探索で見つかる型ごと・その型が持つ札ごとに、
それを出す土地が1つも生成されなかった島の割合。発見で腕前が伸びる契機
(
SkillSystem.md3節)を、型を名指しで書くか札で束ねて書くかを 決めるための表で、どちらで書くべきかの判定は出さない。 読み方はDiscoveryCoverageStats.md。 生成元:tests/diagnostics/discoveryCoverageStatsReport.test.ts(計算はsrc/analysis/discoveryCoverage.ts) - 日をまたぐ長さ — 怪我が治るまで・食べ物が腐るまで・季節が変わるまで・
渇きや飢えで倒れるまでといった、1日以上かかる長さを種類を問わず1本の列にしたもの。並びの狂い
(軽い傷のほうが治りが遅い、など)を見つけるための表なので、種類では分けない。あわせて、
使うたびに減る値(道具の耐久)が尽きるまでの回数。
読み方は
DurationStats.md。 生成元:tests/diagnostics/durationStatsReport.test.ts(計算はsrc/analysis/durations.ts) - 島を出るまでの工程数 — 島を出るのに要るもの(
boat・sail・fishing_toolを名乗る型と、そこへ推移的に要求される型)が、島の産物から何工程先にあるか (ContentSkeleton.md3節の系統11参照)。定義だけから計算した値で、 鎖が長すぎるかどうかの判定は出さない。 読み方はEscapeReachStats.md。 生成元:tests/diagnostics/escapeReachStatsReport.test.ts(計算はsrc/analysis/escapeReach.ts) - 島ごとの脱出可否 — 上と同じ鎖を、生成された島が実際に
持つ土地から数えたもの。島を出られない島の割合と、土地の型の取りこぼし・鎖の切れ目。
読み方は
IslandEscapeReachStats.md。 生成元:tests/diagnostics/islandEscapeReachStatsReport.test.ts(計算はsrc/analysis/escapeReach.ts) - 狩りの手数 — 獣と武器の組み合わせごとに、遭遇が決着するまでの手数・
殴る側が負う怪我・結末の割合と、獣の1手の顔ぶれ、現れてから近寄れるまでの手数、一撃入れて
退いた後に獲物が残る手数(
HuntingSystem.md参照)。 定義から解くのではなく、遭遇を実際に通して数えた値で、強すぎる・弱すぎるの判定は出さない。 読み方はHuntStats.md。 生成元:tests/diagnostics/huntStatsReport.test.ts(遭遇の組み方はsrc/analysis/huntEncounter.ts) - 開始地点の立ち上がり — 最初の段を越えるのに要るものが、
各サイトから何歩先にあるか(移動時間・道を見つける探索時間つき)と、島ごとに選抜が選んだ
開始地点の値の分布(
ContentSkeleton.md2.3節参照)。 定義と生成された島だけから計算した値で、しきい値の判定は出さない。 読み方はStartupReachStats.md。 生成元:tests/diagnostics/startupReachStatsReport.test.ts(計算はsrc/analysis/startupReach.ts。 選抜そのものはsrc/domain/generation/StartSiteSelection.ts) - 地形生成統計 — 土地1つあたりの道の本数(連結数)と余分な道の本数の
分布、次数の分布、道の移動時間
(
TerrainGeneration.md3.5節参照)。あわせて局面ごとの1日 ——島を開き切るまでの探索の局面と、開き切った後の定常の局面——を、島の広さと土地ごとの 活動できる時間から数える(ContentSkeleton.md8.2節・8.3節参照)。 読み方はTerrainStats.md。 生成元:tests/diagnostics/terrainStatsReport.test.ts(局面の計算はsrc/analysis/dailyPhases.ts) - 航海 — 海区ごとの見張りの回数・素の横断時間・見張り1回が返すものと、
出航地点ごとの針路(近道と遠回り)の合計と日数、風と季節がその合計へ与える差
(
Voyage.md3節参照)。定義だけから計算した値で、遠回りを選ぶ理由があるか・ 季節の窓が期限として働くかの判定は出さない。 読み方はVoyageStats.md。 生成元:tests/diagnostics/voyageStatsReport.test.ts(計算はsrc/analysis/voyageLegs.ts) - 荒天の押し流しを入れた航海 — 上の針路を、海区と筏を実体化した世界で
実際に渡らせたもの。出航地点×季節ごとに、渡り切るまでの日数と漕ぎ出した回数、押し流された回数と
空振りになった渡り、そして上との差(
Voyage.md3.8節参照)。 押し流しだけは定義から解けないので、ここだけ世界を回す。季節の窓が期限として働くかの判定は出さない。 読み方はVoyageStormStats.md。 生成元:tests/diagnostics/voyageStormStatsReport.test.ts(計算はsrc/analysis/voyageDrift.ts)