glytch/fktry/src/sim/constants.ts
LANE-SIM 71c885e009 [sim] the relic, wildlife, seams, research time, grantResearch, reference v5
Round 5 (MEGA) per lanes/LANE-SIM.md.

- THE RELIC: one optical fossil per world, position a pure function of the
  seed, 18-28 tiles out, never on a seam, never under the demo. excavate
  within a tile uncovers it; it stays found through save/load forever.
  Folds into DATA's relicReserve when the seam map supplies one
- wildlife v1: piles grow from dust a machine is BACKED UP on, not dust
  merely present -- belt your dust away and nothing ever hatches. First
  cut grew from raw held and the tidy factory still bred 56 swarms; the
  asymmetry is the whole mechanic. Swarms drift to production machinery
  and slow it; traps are recognised by recipe shape (no inputs, cans a
  swarm) so DATA's real trap needs no code. SWARM_MAX caps a plague at 8
  -- a permanently jammed machine bred 29 in one demo run
- seams: extractors idle with 'no seam' off-ore. Consumes DATA's real
  seams.json shape, defensively -- GameData.seams still needs wiring
- research takes time: RESEARCH_TICKS_PER_PACK, stepped whole packs; two
  labs measurably halve the wait (200 ticks vs 260)
- grantResearch: idempotent, save/load safe, verified through the live bus
- reference v5: a second works on the eastern seam that researches a tech
  at tick 1235. Separate from the melt line by choice -- the western
  floorplan has no columns left and threading three ingredients through it
  would make the melt regression hostage to the science

Melt still first at 1247, still shipping at 20k, zero brownout ticks,
179/179 placements legal, zero extractors off-seam once seams are wired.
99 sim tests, 446 across the repo. 63x realtime at 3,500 entities.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:07:08 +10:00

137 lines
6.5 KiB
TypeScript

/**
* LANE-SIM's tuning constants — the single source of truth.
*
* These were private to index.ts until round 4. They're exported because other lanes'
* tests were hand-copying the numbers and going stale in silence: a copy of 0.7 doesn't
* fail when the sim moves to 0.65, it just quietly asserts the wrong thing forever.
* Import these rather than retyping them.
*
* Anything DATA can already set per-machine (heatPerTick, coolPerTick, bufferCap,
* beltSpeed, coolRadius) lives in data/machines.json, not here. These are the sim-wide
* rules that no machine overrides.
*/
// ------------------------------------------------------------------ belts
/** Minimum gap between two items, in belt-tiles. Enforced across the tile seam too. */
export const BELT_SPACING = 0.45;
/** Items a single belt tile may hold. */
export const BELT_CAPACITY = 2;
/** Items a splitter holds while it waits for an output to open. */
export const SPLITTER_CAPACITY = 2;
// -------------------------------------------------------------- machines
/** A machine stalls ('output full') once any output piles to this multiple of its yield. */
export const OUTPUT_STALL_FACTOR = 4;
/** A machine accepts an input up to this multiple of the recipe requirement. */
export const INPUT_BUFFER_FACTOR = 2;
// ------------------------------------------------------------------ heat
//
// heatPerTick is GROSS (contracts v4): net climb = heatPerTick - coolPerTick, so a
// machine whose heatPerTick never clears its cooling can never overheat.
/** Below this heat, throttling is purely cosmetic. */
export const HEAT_THROTTLE_START = 0.7;
/** Speed multiplier at heat 1.0 — the codex's "tick rate visibly halves". */
export const HEAT_THROTTLE_FLOOR = 0.5;
/** Heat at which a machine scrams. */
export const HEAT_SCRAM = 1;
/** Cool back below this and a scrammed machine restarts. */
export const HEAT_RESTART = 0.5;
/** Per-tick cooling when a machine's def doesn't specify `coolPerTick`. */
export const HEAT_COOL_DEFAULT = 0.004;
/**
* Ceiling on a machine's total cooling once auras stack (own rate + every cooler in
* range). At 0.05 a machine sheds a full 1.0 of heat in 20 ticks (~0.67s), which is
* already effectively instant — past this, extra coolers would buy nothing but would
* still cost bandwidth and floor space, and stacking them would look broken rather than
* generous. See NOTES round 4.
*/
export const HEAT_COOL_MAX = 0.05;
// ---------------------------------------------------------------- research
/**
* Ticks a lab spends turning ONE delivered science pack into one unit of progress.
* Research is an investment, not a purchase: at 30 tps this is ~2s per pack, so a
* 10-pack tech is ~20s of lab time on one lab, or ~10s on two (labs parallelize).
* Stepped, not fractional — a pack is consumed and progress ticks up by a whole 1 only
* when the timer completes, which keeps `research.progress` integer and the determinism
* trivial to reason about.
*/
export const RESEARCH_TICKS_PER_PACK = 60;
// ---------------------------------------------------------------- wildlife
//
// The dust→swarm loop, M2's living hazard. These item ids are the coupling to DATA: the
// pile grows from the dust a machine is HOLDING (so a factory that belts its dust away
// never hatches), and the trap is any machine whose recipe cans the swarm.
/** The item whose accumulation breeds swarms (codex: HF DUST "attracts and hatches"). */
export const DUST_ITEM = 'hf-dust';
/** The canned live-swarm item — a trap's output, and what commissions ask for. */
export const SWARM_ITEM = 'mosquito-swarm';
/**
* Pile growth per tick per unit of dust BACKED UP in a machine's output buffer — where
* "backed up" means beyond one craft's worth (see DUST_SPILL_GRACE). A line that keeps its
* dust moving never crosses the grace threshold and never breeds; a line that ignores it
* climbs to the stall cap and hatches in roughly 400 ticks (~14s). That gap is the mechanic.
*/
export const DUST_GROWTH_PER_HELD = 0.00025;
/**
* A machine may hold this multiple of its own dust yield without shedding any on the floor.
* Normal buffering between belt pushes is not neglect, and punishing it would make the
* hazard feel like weather rather than a consequence.
*/
export const DUST_SPILL_GRACE = 1;
/** A pile at or above this hatches a swarm and is consumed. */
export const DUST_PILE_MAX = 1;
/** Tiles from the emitter a new pile appears (seeded RNG picks the exact offset). */
export const DUST_PILE_SPREAD = 2;
/** Swarm drift toward its target, tiles/sec. */
export const SWARM_DRIFT_TILES_PER_SEC = 1.5;
/** Within this chebyshev range of its target's footprint, a swarm is attached and biting. */
export const SWARM_ATTACH_RANGE = 1;
/** Speed multiplier a fully-severe attached swarm inflicts (0.5 = half speed). */
export const SWARM_SLOW_FLOOR = 0.5;
/** Severity gained per tick while attached, up to 1.0 — the longer it sits, the worse. */
export const SWARM_SEVERITY_GROWTH = 0.004;
/** A trap catches swarms within this chebyshev range of its footprint. */
export const TRAP_RADIUS = 4;
/**
* Live swarms allowed at once. A machine that is permanently jammed on dust would
* otherwise breed without limit — measured 29 of them in a 20k demo run, which is a
* plague rather than a hazard. At the cap, full piles simply sit there waiting for room.
*/
export const SWARM_MAX = 8;
// ------------------------------------------------------------------ relic
/** Nearest a buried relic sits to origin (tiles, euclidean) — well past the starter area. */
export const RELIC_MIN_DIST = 18;
/** Farthest a buried relic sits from origin. */
export const RELIC_MAX_DIST = 28;
/**
* Keep-outs for relic placement, sized to enclose the reference factory's two works — the
* western melt line and the eastern research annex — so the demo can always dig its relic
* up. A real world has no factory here yet; this just keeps the secret clear of the ground
* everyone builds on. relic.test.ts checks, against the layout's real occupied tiles, that
* a spread of seeds never buries it. When DATA supplies a `relicReserve` the sim prefers
* that corner instead and these become belt-and-braces.
*/
export const RELIC_KEEPOUT = [
{ x0: -33, y0: -16, x1: 2, y1: 16 }, // the melt line
{ x0: 6, y0: -9, x1: 29, y1: 12 }, // the research annex
];
/** Chebyshev range within which an `excavate` uncovers the relic. */
export const RELIC_DIG_RANGE = 1;
// ---------------------------------------------------------------- tracing
/** Belt graphs bigger than this are treated as unterminated (cheap loop guard). */
export const MAX_TRACE = 512;