Skip to main content

World Recipe Authoring Guide

What a recipe is

A World Recipe is the declarative authoring contract for a mapv10 continent. It describes the geomorphology family the author wants the generator to produce: which orogens belong on which coasts, which cratons fill which interiors, what rainfall pattern governs the climate, how many basins and rivers of each kind the continent should carry, and which named features the author wants the generator to place. The generator runs the recipe under a chosen seed and emits a concrete continent that belongs to the requested family.

A recipe is seed-agnostic. The same recipe replayed against different seeds yields different concrete continents that share the same morphology. A South-America-like recipe under seed 1, seed 2, seed 7 produces three distinct continents, each with a coast-parallel western cordillera, an eastern shield craton, and a foreland basin between them.

A recipe is scale-agnostic. The scale preset (province-slice, regional-slice, realm-slice, continent) is owned by GeneratorConfig, not by the recipe. The same recipe runs at every scale preset; the resulting continent has the same morphology family at every preset.

A recipe is deterministic. The four-tuple (seed, recipe, intent-directives, vocabulary) produces a byte-identical content_hash across runs. The canonicalization block on every recipe makes this contract explicit: sorted JSON keys, shortest round-trip f64 representation, LF line endings, UTF-8 NFC encoding without a BOM.

A recipe is declarative. It does not encode procedural steps. It says "I want a continent with these orogens, these cratons, this rainfall, and these named features." The generator decides how to satisfy the request and reports satisfaction back to the author.

File location and CLI invocation

Recipes live under examples/map/mapv10/recipes/. The four starter presets ship there:

  • preset-south-america-like.json
  • preset-australia-like.json
  • preset-europe-like.json
  • preset-africa-like.json

Each preset's companion intent-directives file lives in examples/map/mapv10/recipes/intents/ with the same base name and an .intents.json suffix.

The bundled vocabulary instance lives at examples/map/mapv10/vocabulary/default-world-vocabulary.json. Recipes reference it implicitly when vocabularyRef is omitted, or explicitly when they want to ship their own vocabulary.

The CLI invocation that consumes a recipe is added in M11c. The planned shape:

mapv10-generator \
--scale-preset continent \
--seed 42 \
--recipe examples/map/mapv10/recipes/preset-south-america-like.json \
--intents examples/map/mapv10/recipes/intents/preset-south-america-like.intents.json \
--vocabulary examples/map/mapv10/vocabulary/default-world-vocabulary.json \
--out continent-lod6

Wave S commits the schemas, the bundled vocabulary, and the four starter presets. The CLI flags are deferred to M11c.

Top-level fields tour

Every recipe carries these top-level fields:

  • schemaVersion (const "mapv10-world-recipe-v1"). The schema version pin. Future revisions ship beside v1; recipes never silently migrate.
  • id. A short slug, lower-kebab-case, three to sixty-four characters. Used in manifests, validation output, and capture filenames.
  • displayName. Author-facing label up to 120 characters.
  • description. Optional prose explaining the recipe's intent.
  • defaultSeed. Optional integer suggestion for the canonical seed; the CLI is free to override.
  • vocabularyRef. Optional relative path to a vocabulary instance. When omitted, the generator uses its bundled default vocabulary.
  • archetype. The geomorphology declaration. See Archetype below.
  • counts. Top-level count constraints with priorities. See Counts below.
  • intentDirectivesRef. Optional relative path to a companion intent- directives file.
  • satisfactionReporting. Configures how satisfaction is reported. Three booleans: emitUnsatisfiedHardFails (always true; recipes cannot opt out), emitSoftSatisfactionScores, haltOnHardFail.
  • canonicalization. The determinism contract. Four const-valued fields; recipes cannot opt out.

Archetype

The archetype field is the heart of the recipe. It carries seven sub-fields:

  • orogens[]. A list of orogen declarations. Each declaration names an orogenKinds vocabulary token (e.g. active-margin, collisional, rift-flank, residual-eroded, transform-margin, hotspot-volcanic-arc, passive-margin), a placement mode, and per-belt parameters: length, height, age band, asymmetry factor, priority.
  • cratons[]. A list of craton declarations. Each names a cratonKinds vocabulary token (e.g. shield, platform, accreted-terrane), a placement mode, an age band, an area fraction range, a residual-highland policy, an optional interior-basin policy, and a priority.
  • rainfall. A rainfall declaration naming a rainfallPatterns token (e.g. zonal-temperate, zonal-tropical, monsoonal, orographic-dominant, arid-continental, rift-driven), an orographic-strength range, an optional rain-shadow azimuth, and an optional priority.
  • basinPolicy. Three sub-policies, one per basin class: forelandBasins, interiorBasins, endorheicBasins. Each carries a count range, a depth-meters range, and a priority.
  • coastPolicy. Coast-shape policy: jaggedness range, fjordLikelihood range, an islandPolicies token, an isthmusPolicies token, an optional priority.
  • customAnchorPolicy. How directives conflict with the archetype: conflictResolution is one of hard-directive-overrides-archetype, archetype-overrides-soft-directive, directives-suggest-only, or directives-forbidden. maxDirectiveCount caps the number of directives the recipe will accept.
  • latitudeBandDegrees. The absolute geographic latitude band the continent spans in degrees. { min, max } with both in [-90, 90], min < max, and max - min >= 5°. Stage 5 (biomes + materials) projects each per-cell world-y onto this band to drive the Köppen- Geiger climate-zone proxy (tropical / subtropical / temperate / boreal / polar). Sign convention: y = 0 (north edge of the world rectangle) maps to max; y = world_height_km (south edge) maps to min. The field is recipe geometry, not a soft request — the validator hard- fails the recipe at load time if the bounds are absent, swapped, outside [-90, 90], or span less than 5°. There is no priority field because the band cannot be partially satisfied.

The four shipped presets declare bands that span their real-world counterparts:

PresetminmaxClimate range at y = 0..1
preset-south-america-like-55+12tropical (north) → subpolar (south)
preset-australia-like-43-10tropical south → temperate south
preset-europe-like+35+70temperate (south) → polar (north)
preset-africa-like-35+38temperate north → subtropical south

Placement modes

Orogens use three placement modes:

  • coast-parallel. The orogen runs along a fraction of a named coast edge (north, east, south, west). The recipe declares coastFraction.fromFraction and coastFraction.toFraction to constrain which segment of the coast hosts the belt. An optional offshoreOffsetKm range pushes the belt slightly inland (positive) or offshore (negative, for island arcs).
  • interior-axis. The orogen runs along an interior axis at a constrained azimuth. The recipe declares azimuth.fromDeg and azimuth.toDeg (0 deg is north, 90 deg is east, 180 deg is south, 270 deg is west). An optional anchorFraction point anchors the axis at a specific fractional coordinate on the continent.
  • archetype-derived. The generator chooses placement based on the rest of the archetype. Used when the author wants the generator to make the call.

Cratons use two placement modes: archetype-derived and anchor-fraction. The latter pins the craton at a fractional coordinate.

Counts

The counts block carries seven count constraints, each on a different morphological class:

  • primaryMountainBelts - the high, named ranges (typically the orogens declared in archetype.orogens[]).
  • secondaryRanges - smaller ranges, residual ridges, branched sub-belts.
  • residualHighlands - low residual relief on the cratons.
  • forelandBasins, interiorBasins, endorheicBasins - basin classes.
  • majorRivers - the river-network trunks. Strahler order three or higher.

Each count constraint carries a range (with min and max), an optional exact value, and a priority. When exact is set, the validator hard-fails if the generated count is not exactly that number; otherwise the count must fall within the range.

Satisfaction reporting

The satisfactionReporting block has three fields:

  • emitUnsatisfiedHardFails: const true. Hard-fail records are always emitted; the field is const true to prevent recipes from opting out.
  • emitSoftSatisfactionScores. When true, the generator emits a satisfactionScore in [0, 1] for each soft request.
  • haltOnHardFail. When true, the generator stops the run on the first hard-fail. When false, the run completes and the manifest carries validationStatus: "fail".

Canonicalization

The canonicalization block has four const fields:

  • sortedKeys: const true.
  • floatRepresentation: const "shortest-roundtrip-f64".
  • lineEndings: const "lf".
  • encoding: const "utf-8-nfc-no-bom".

These are not author choices; they are determinism invariants every recipe declares to make its conformance visible.

Hard / soft / suggest priorities

Three priorities govern every count, every basin class, every directive:

  • hard. The request must be satisfied. If it cannot be, the generator emits a satisfaction record with state: unsatisfied, increments validation.satisfactionSummary.hardFailCount, sets manifest.validationStatus to fail, and (when haltOnHardFail is true) halts the run. A hard unsatisfied request is a generator-emitted failure, never a silent default.
  • soft. The generator best-efforts the request and emits a satisfactionScore in [0, 1]. state is satisfied (score 1.0), soft-satisfied (between 0 and 1), or unsatisfied (score 0.0). The run continues; manifest.validationStatus is unaffected.
  • suggest. The generator best-efforts the request and records satisfaction but does not emit a score. State is satisfied or unsatisfied. The run continues; manifest.validationStatus is unaffected.

The validation.unsatisfied[] list inside terrainGenesis.json and hydrology.json carries per-request reasons. Authors read it to understand why a soft or hard request did not land.

Example walkthrough: preset-south-america-like.json

The South-America-like preset is the simplest case of an active-margin recipe. Walk through its blocks:

"archetype": {
"orogens": [
{
"id": "western-cordillera",
"kind": "active-margin",
"placement": {
"mode": "coast-parallel",
"coastFraction": { "edge": "west", "fromFraction": 0.05, "toFraction": 0.95 },
"offshoreOffsetKm": { "min": 40, "max": 120 }
},
...
}
],
...
}

The recipe declares a single primary orogen, western-cordillera, of kind active-margin. It runs along the west coast from 5% to 95% of that edge, 40 to 120 km inland. Length is between 2,000 and 3,200 km; peak elevation is between 3,500 and 5,500 m; age is young; asymmetry is between 2.5 and 4.0 (steep ocean flank, gentler interior flank). The priority is hard - the generator must satisfy this declaration or fail loudly.

"cratons": [
{
"id": "eastern-shield",
"ageBand": "ancient",
"areaFraction": { "min": 0.40, "max": 0.60 },
"placement": {
"mode": "anchor-fraction",
"anchorFraction": { "xFraction": 0.65, "yFraction": 0.50 }
},
...
}
]

A single ancient shield craton occupies 40-60% of the continent area and is anchored at 65% east, 50% north. The shield carries residual-ranges as its residual-highland policy and wet-interior as its interior-basin policy. The priority is hard.

"rainfall": {
"pattern": "orographic-dominant",
"orographicStrength": { "min": 0.65, "max": 0.85 },
"rainShadowAzimuth": { "fromDeg": 80, "toDeg": 120 },
"priority": "soft"
}

Rainfall is orographic-dominant: the dominant control is the western cordillera's rain shadow. Strength is 0.65 to 0.85. Rain shadow azimuth is between 80 deg and 120 deg (east-ish), meaning winds come from the west and the cordillera blocks them. Priority is soft; the generator best-efforts but does not hard-fail if the chosen rainfall pattern is slightly different.

"basinPolicy": {
"forelandBasins": { "count": { "min": 1, "max": 1 }, "depthMeters": { "min": 100, "max": 400 }, "priority": "hard" },
"interiorBasins": { "count": { "min": 1, "max": 2 }, "depthMeters": { "min": 50, "max": 250 }, "priority": "soft" },
"endorheicBasins": { "count": { "min": 0, "max": 1 }, "depthMeters": { "min": 200, "max": 800 }, "priority": "suggest" }
}

Exactly one foreland basin (hard); one or two interior basins (soft); zero or one endorheic basin (suggest). The foreland basin priority is hard because the AAA reference (Andes + Brazilian Shield) always has a foreland basin between the active-margin orogen and the shield.

"counts": {
"primaryMountainBelts": { "range": { "min": 1, "max": 1 }, "exact": 1, "priority": "hard" },
...
"majorRivers": { "range": { "min": 2, "max": 4 }, "priority": "soft" }
}

The top-level counts enforce: exactly one primary mountain belt (hard); one to three secondary ranges (soft); two to four major rivers (soft).

"intentDirectivesRef": "intents/preset-south-america-like.intents.json"

The recipe defers to its companion intents file for custom-anchor requests. That file declares one directive: a soft custom-river-mouth near the east coast (Amazon analog) requesting mustBeNavigable: true.

Composing a new recipe from scratch

Five-step cookbook:

  1. Pick orogen kinds. Decide which mountain types belong on the continent. Look at the AAA references in examples/map/mapv10/vocabulary/default-world-vocabulary.json to choose tokens. Active-margin for Andean-like; collisional for Alpine-like; rift-flank for African-Rift-like; residual-eroded for old continental highlands; transform-margin for California-coast-like.

  2. Place cratons. Decide which craton kinds anchor the interior. Shield for ancient peneplain interiors; platform for sediment-covered interiors; accreted-terrane for collision-formed mosaic interiors.

  3. Declare rainfall. Choose a rainfallPatterns token that matches the latitude and dominant wind direction. Set orographic strength to control the rain-shadow contrast. Set rain-shadow azimuth to align with the wind direction.

  4. Set basin counts. Decide how many basins of each kind belong. An active-margin recipe wants exactly one foreland basin. An old craton recipe wants multiple endorheic basins. A collisional recipe wants foreland basins on both flanks.

  5. Check satisfaction. Run the recipe under several seeds. Read the validation.unsatisfied[] list to see what the generator could not satisfy. Adjust priorities: soft-fail requests that should be optional; hard-fail requests that are truly required; suggest-only requests that are aspirational.

Determinism contract

The canonicalization block declares what makes a recipe deterministic. The contract:

  • The same (seed, recipe, intent-directives, vocabulary) four-tuple produces a byte-identical content_hash across runs.
  • Changing the seed produces a different continent but the same morphology family.
  • Changing the recipe (any field) changes the content hash. Renaming a recipe id changes the content hash.
  • Changing the directives file changes the content hash even if no directive resolves differently.
  • Changing the vocabulary instance changes the content hash even if no token is added or removed (because the registry serialization changes).

What NOT to change in an existing recipe:

  • The id field. Renaming the id breaks the manifest cross-reference.
  • The schemaVersion. Bumping the version means a new schema is in play.
  • Any const-valued field in canonicalization. The fields are const for a reason; the validator rejects recipes that override them.

Common authoring mistakes

  • Soft-vs-hard miscalibration. Marking too many counts as hard produces recipes that almost always hard-fail because the generator cannot satisfy every constraint simultaneously. As a rule of thumb, mark one or two foundation declarations as hard (the primary orogen, the primary craton, the foreland basin if the AAA reference always has one); mark everything else as soft. Use suggest for aspirational features the recipe does not need.
  • Impossible coast-parallel arcs. A coast-parallel placement with coastFraction.fromFraction close to 0.0 and toFraction close to 1.0 asks for an orogen that spans nearly the entire coast. If the coast is short (small scalePreset), the orogen may not fit. Use moderate fractions (0.1-0.9) until the recipe is validated at every scale preset.
  • Over-constrained counts. Setting exact on a count constraint that the generator typically cannot satisfy produces guaranteed hard-fails. Use exact only for foundation declarations where the AAA reference is unambiguous (e.g. "exactly one primary belt for an active-margin recipe").
  • Conflicting directives and archetype. A directive with placement inside an archetype-declared orogen runs the customAnchorPolicy.conflictResolution policy. Mark the conflictResolution explicitly; do not assume the default.
  • Forgetting intentDirectivesRef. A recipe that references directives must point at them with intentDirectivesRef. The schema does not enforce the cross-reference (it is a relative path), but the validator script does.