地形生成 実装ガイド

概要

本ドキュメントは、src/domain/generation/(定義と実行)・ src/loader/parseGeneration.ts(ロード)にまたがる地形生成の実装を、実際の クラス名・関数名を使ってトップダウンに説明するものです。

読む順番の目安: 全体の呼び出し関係は 1 節、個々のクラスの詳細は 2 節以降、ファイルを直接開く前の インデックスは 8 節(ファイル一覧)。

1. 全体の呼び出し関係

地形生成は大きく分けて次の入口を持ちます。ロード(YAML → WorldCodex.generation、ゲーム起動時に1回)と、 生成・実体化(WorldCodex.generation → 実際の WorldObject の島、ゲーム開始時に1回)です。

[ロード]
WorldCodexYamlLoader.load(label, yamlText)               src/loader/WorldCodexYamlLoader.ts
  └─ loadGenerationSections(loader, label, root)          src/loader/parseGeneration.ts(地形生成のルートキーを読む)
       ├─ parseAxis / parseGeneratorLayer                  → loader.generationAxes へ蓄積
       ├─ parseLocationType                                 → loader.generationLocationTypes へ蓄積
       └─ parseGenerationScope                              → loader.generationScopes へ蓄積
WorldCodexYamlLoader.buildAndReset()
  └─ buildGenerationDefs(loader, objectDefsByGlobalId)     src/loader/parseGeneration.ts(object_def/axis/location_type の相互参照を検証)
       └─ new GenerationDefs(axes, locationTypes, scopes)  src/domain/generation/GenerationDefs.ts
            → WorldCodex.generation プロパティへ格納        src/domain/WorldCodex.ts

[生成・実体化]
startNewGame(codex, characterDefName, seed, rng)          src/domain/generation/NewGame.ts  ← ゲーム開始の入口
  ├─ world・character の WorldObject を生成(worldはnew WorldObject、characterはWorldSession.createObject)
  ├─ generateIsland(codex.generation, "island", seed) → IslandMap
  │    │                                                    src/domain/generation/TerrainGenerator.ts
  │    ├─ 1. placeSites(scope, rng)                         → Site[](座標のみ)  SitePlacer.ts
  │    ├─ 2. assignAxisValues(defs.axes, sites, seed, scope) → Site.axisValues を埋める  AxisSampler.ts
  │    ├─ 3. assignTypes(defs, scope, sites)                 → Site.type を確定  LocationTypeMatcher.ts
  │    ├─ 4. triangulate(sites)                              → readonly [number, number][]  DelaunayTriangulator.ts
  │    ├─ 5. buildPathNetwork(sites, delaunayEdges, scope, defs.axes) → IslandEdge[]  PathNetworkBuilder.ts
  │    └─ 6. assignNames(sites, rng)                         → Site.name を確定  NameAssigner.ts
  ├─ spawnIslandIntoWorld(session, map) → SpawnedIsland     → 各SiteをWorldObjectとして生成し、道も生成  IslandSpawner.ts
  └─ placePlayer(island, character)                         → 開始地点へキャラクタを配置、Locationを返す  IslandSpawner.ts

generateIsland(TerrainGenerator.ts)までは WorldObject に一切触れない純粋な計算です(IslandMap/Site/ IslandEdge はただのデータ)。WorldObject の生成・配置が始まるのは IslandSpawner からです。この境界を 意識すると、「レイアウトのバグ」(src/domain/generation/ 側)と「実体化のバグ」(IslandSpawner 以降)の どちらを疑うべきかを素早く切り分けられます。この境界は検査が保ちます——実体化を担うファイル以外が WorldObject とその包みを輸入していないこと(tests/architecture/layers.test.ts)と、IslandMap が 生成の結果しか欄に持たないこと(tests/generation/terrainGenerator.test.ts。対応表は数の配列でも 書けるので、輸入を見るだけでは足りません)。

2. ロード: YAML → GenerationDefs

地形生成関連の処理は、object_defs/traits を読む本体 src/loader/WorldCodexYamlLoader.ts とは別モジュール src/loader/parseGeneration.ts に分離されています。WorldCodexYamlLoader が持つ地形生成の蓄積フィールド (generation で始まる名前のもの)は、loader 引数として parseGeneration.ts 側の関数へ渡されます。

3. generateIsland(TerrainGenerator.ts): ステップの内訳

TerrainGenerator.ts(generateIsland 1関数のみをエクスポート)は、以下のモジュールの関数を順番に呼ぶだけの オーケストレータです。各モジュールも状態を持たない関数の集まりで、Site/IslandEdge の配列を受け取って 書き換える・新しく作る、という素朴な手続きです。

3.1 placeSites(scope: GenerationScopeDef, rng: Pcg32): Site[](SitePlacer.ts)

座標だけを決めます(軸値はまだ持ちません)。

3.2 assignAxisValues(axes, sites, seed, scope)(AxisSampler.ts)

各 Site の axisValues(Map<string, number>)を埋めます。

3.3 assignTypes(defs, scope, sites)(LocationTypeMatcher.ts)

各 Site.type(LocationTypeDef)を確定します。

  1. モジュール内関数 assignGuaranteedSites が scope.guarantees の強制割当先をまとめて決めます。保証 1 件の count 個ぶんを受け口(GuaranteeOpening)に開き、assignEligibleSitesInOrder が受け口を宣言順に見て、 orderForGuarantee で軸値の最大/最小順に並べた Site のうち hard_limits を満たすものから取らせます。 取ってよいのは、その Site を抜いても残る受け口が同じ数を当てられる候補だけで、この数は maxAssignableCount(増加路を辿る=二部グラフの最大マッチング)が数えます。相手の見つからなかった 受け口は、同じ並び順の残る Site から補います(保証は絶対のため)。
  2. 残る Site を、モジュール内関数 bestDistanceIgnoringCrowding が返す最良距離の昇順に並べ、1つずつ モジュール内関数 nearestTypeAvoidingFull(types, site, scope, counts) へ渡します。選ぶ基準は、 LocationTypeDef.satisfiesHardLimits を満たす型のうち LocationTypeDef.normalizedDistanceFrom (正規化した重み付き距離)に同じ型の既存個数ぶんの割増(scope.crowdingPenaltyPerDuplicate)を掛けた 値が最小のものです。scope.maxSitesPerType まで埋まった型は避け、全型が埋まっていれば上限を無視して 選び直します。それでも該当が無ければ isFallback かつ fallbackPriority 最大の型へフォールバック します(それも無ければ例外)。

3.4 triangulate(sites): readonly [number, number][](DelaunayTriangulator.ts)

Bowyer-Watson 法によるDelaunay三角形分割です。すべての Site を包む仮想の「スーパートライアングル」から 始め、Site を1点ずつ挿入するたびに外接円判定(モジュール内関数 inCircumcircle)で無効化された三角形を 削除・再分割します。最後にスーパートライアングルの頂点を含む三角形を除いて、無向辺の集合を返します。

3.5 buildPathNetwork(sites, delaunayEdges, scope, defs): IslandEdge[](PathNetworkBuilder.ts)

3.6 assignNames(sites, rng)(NameAssigner.ts)

Site.name(LocationName)と Site.variant を確定します。Site を LocationType ごとにまとめ、 1 つだけの型は型の識別子だけを名前にし(亜種は付きません)、複数ある型はモジュール内関数 shuffled(values, rng) で並べ替えた variants を 1 つずつ配ります。亜種が尽きた分には通し番号 (LocationName.ordinal)を持たせます。表示文字列はここでは作りません——LocationName は識別子の 組み合わせで、文字列にするのは Localization.locationName(Localization.md)です。亜種の props を 実体へ書き込むのは IslandSpawner(4 節)です。

rng は配置とは別の列(Pcg32.forPurpose(seed, 'names'))です。配置の引く回数を変えても、命名の 結果は動きません。

4. 実体化: IslandSpawner

generateIsland(TerrainGenerator.ts)の結果(IslandMap、まだ WorldObject を含まないデータ)を、実際の世界 (world を根とするツリー)へ実体化します。

SpawnedIsland(src/domain/generation/SpawnedIsland.ts)は、生成結果(map)と、そこから湧いた土地の WorldObject の対応を束ねたものです。対応は実体化と同時に出来上がる(コンストラクタがすべての サイトの土地を要求する)ので、「まだ実体化していないサイト」という状態がありません。読む側は 「サイト → instanceId → WorldObject」を自分で辿らず、landOf(site)・siteOf(instanceId)・ nameOf(instanceId)・lands を使います。土地の名前(LocationName)の唯一の出所もここです ——個体ごとに決まる名前なので、型(ObjectDef)の側は知りません。

5. データの流れ(型で見る層)

層 主な型 特徴
定義(ロード後不変) GenerationDefs(AxisDef/LocationTypeDef/GenerationScopeDef、src/domain/generation/) WorldCodex.generation として1つだけ存在。YAMLの内容そのもの
生成の中間・最終結果(純粋計算) Site/IslandEdge/IslandMap(src/domain/generation/IslandMap.ts) WorldObject を一切含まない。generateIsland(TerrainGenerator.ts)が返す。座標・軸値・確定した LocationTypeDef・命名・辺を持つだけの、ただのデータ
実体化後(実行時状態) WorldObject(Location/Path でラップ、src/domain/wrappers/) IslandSpawner が Site/IslandEdge を読んで生成する、実際にゲームが動かす対象
生成結果と実体の対応 SpawnedIsland(src/domain/generation/SpawnedIsland.ts) spawnIslandIntoWorld が返す。サイトとその土地の対応を持ち、実体化と同時に出来上がる(4節)

IslandMap(中間層)を経由することで、generateIsland(TerrainGenerator.ts)は完全に決定的な純粋関数として単体テスト でき(tests/generation/terrainGenerator.test.ts)、IslandSpawner 以降の実体化のテスト (tests/generation/islandSpawner.test.ts)と関心事が分離されています。

6. 決定性の仕組み: 用途ごとの乱数列

1つの seed から、用途ごとに独立した列を作ります(Pcg32.forPurpose、RandomPurpose)。

用途 引く場所 何を決めるか
sites placeSites(SitePlacer.ts) サイト総数と座標
names assignNames(NameAssigner.ts) 亜種の配り方
play WorldSession.rng(Rng.seededRng) 初期値ロール・pick の抽選・開始時刻

AxisSampler は列を引かず、seed を直接 ValueNoise のハッシュへ渡します(状態を持たない純関数)。 LocationTypeMatcher・DelaunayTriangulator・PathNetworkBuilder は乱数を一切使いません(Site の 座標・軸値が決まった時点で結果は一意に決まります)。

列を分けて守れるのは「他の用途が何回引いたか」の変化に対してだけです。上流が出した値が変われば 下流は動きます——placeSites を変えれば、軸のノイズを別に引いていても軸値は変わり、型も名前も変わります。 play の列だけは下流を持たないので、「同じ seed なら WorldSession.rng に何を渡しても島のレイアウトは 変わらない」という契約が成り立ちます(tests/generation/islandSpawner.test.ts が検証しています)。

7. エンジン拡張との接点

地形生成の実装にあわせて GameElementDefinition.md へ追加した汎用エンジン拡張(duration/move、 ExplorationSystem.md 4節)は、以下のコードに対応します。

8. ファイル一覧(索引)

ファイル 役割
src/domain/generation/AxisDef.ts 軸1本の定義(層を積んで軸値を作る手順、3.2節)
src/domain/generation/LocationTypeDef.ts LocationType 1件の定義(軸の好み・上限は3.3節、亜種は3.6節・4節)
src/domain/generation/GenerationScopeDef.ts 生成スコープ1件の定義(サイト数・偏り・網羅の保証、3.1節・3.3節)
src/domain/generation/GenerationDefs.ts 上記の束(WorldCodex.generation の中身、2節)
src/loader/parseGeneration.ts YAML → 上記Defsのパース(2節)
src/domain/Pcg32.ts 用途ごとの列を作る決定的RNG(6節)
src/domain/generation/ValueNoise.ts シード付き格子値ノイズ(3.2節)
src/domain/generation/IslandMap.ts 生成結果のデータ(座標・軸値・型・命名・辺、5節)
src/domain/generation/SitePlacer.ts 3.1節: 座標配置
src/domain/generation/AxisSampler.ts 3.2節: 軸値サンプリング
src/domain/generation/LocationTypeMatcher.ts 3.3節: LocationTypeマッチング
src/domain/generation/DelaunayTriangulator.ts 3.4節: Delaunay三角形分割
src/domain/generation/PathNetworkBuilder.ts 3.5節: MST+辺復活+移動時間
src/domain/generation/NameAssigner.ts 3.6節: 命名
src/domain/generation/TerrainGenerator.ts 3節全体のオーケストレータ(generateIsland)
src/domain/generation/IslandSpawner.ts 4節: 実体化(spawnIslandIntoWorld/placePlayer/placePlayerAt)
src/domain/generation/StartSiteSelection.ts 開始地点の選抜(ContentSkeleton.md 2.3節)
src/domain/generation/SpawnedIsland.ts 4節: 実体化された島(サイトと土地の対応)
src/domain/generation/NewGame.ts ゲーム開始の入口(startNewGame)・StartedGame
src/domain/MoveEffect.ts 7節: move 効果動詞
src/domain/InteractionDef.ts 7節: duration フィールド
src/domain/wrappers/Location.ts 7節: 探索の入口(explore/revealDueFixtures)
src/domain/wrappers/Path.ts 7節: 道のビュー(travel)

対応するテストは以下のとおりです。

テストファイル 対象
tests/loader/generationYamlLoader.test.ts 2節(parseGeneration.ts)
tests/world-codex/terrainGenerationYaml.test.ts 実ファイル terrain_generation.yaml と locations.yaml の対応
tests/generation/terrainGenerator.test.ts 3節(generateIsland の不変条件)
tests/generation/islandSpawner.test.ts 4節・6節(IslandSpawner/NewGame、決定性)
tests/domain/actionDuration.test.ts 7節(duration)
tests/domain/moveEffect.test.ts 7節(move)