Skip to content

Commit 05d4bcd

Browse files
pruukclaude
andcommitted
docs(context): merge sim context into one coherent doc; refresh crawler and root
sim/context.md was the M1 graph doc with an M2 appendix bolted on (state.go listed twice); now one structure covering all files, the full Step pipeline, determinism, and admin mutations. crawler/context.md gets the graph-semantics notes consumers need (directed-exit weights, room-id assumption, exits-only connectivity) and a current consumers section. Root context.md documents the embedded FS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1a1023a commit 05d4bcd

3 files changed

Lines changed: 160 additions & 102 deletions

File tree

context.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ All fields of `weatherModule` are touched only from the single game-loop
1111
goroutine — no synchronization needed.
1212

1313
## Key Components
14-
- **weather.go**: `weatherModule` struct (plug, cfg, graph, started, simReady,
14+
- **weather.go**: the `files` embed.FS (`//go:embed files/*` — the config
15+
overlay plus `datafiles/` mutator specs and emote tables; the engine loads
16+
`mutators/*` from it via the plugin registry, `content` loaders read the
17+
rest). `weatherModule` struct (plug, cfg, graph, started, simReady,
1518
simCfg, climate, tables, state, nextTick, nextEmote). `init()``plugins.New`
1619
+ `AttachFileSystem` + `SetOnLoad`, then registers the `weather` command as a
1720
**player** command (not admin-only; admin subcommands are gated in-handler) and

crawler/context.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,23 @@ type WorldReader interface {
4343
## Options
4444
- `IncludeSecretExits bool`, `ExcludeZonePatterns []string`,
4545
`BuiltAtRound uint64`. `DefaultOptions()` enables secret exits and excludes
46-
`instance_*` / `ephemeral_*`.
46+
`instance_*` / `ephemeral_*`. `IncludeSecretExits` is wired to module config;
47+
`ExcludeZonePatterns` is not yet a config key (M4 follow-up) — consumers get
48+
the defaults.
49+
50+
## Graph semantics consumers should know
51+
- **Edge weight counts *directed* exits, not connections.** Every room-exit
52+
crossing a zone border adds 1, so a normal two-way border reads `weight: 2`
53+
(one exit each way). Treat weight as a "border width" proxy; halve it for a
54+
connection count.
55+
- **Room→zone resolution assumes zones don't share room ids.** Each room id
56+
maps to one zone; if an id were reported under two zones, the mapping (and a
57+
few edges) could vary between runs. Real worlds don't do this —
58+
`WorldReader.RoomIDs` returns disjoint sets per zone — noted only for
59+
`WorldReader` adapter authors.
60+
- **Exits are the only connectivity source.** Zones reachable solely by
61+
teleport or scripted movement get no edges; they form separate components
62+
with independent weather.
4763

4864
## Dependencies
4965
- `github.com/GoMudEngine/GoMud/modules/weather/sim` (the `Graph` types).
@@ -54,8 +70,10 @@ type WorldReader interface {
5470
`fakeReader`: adjacency, weights, secret-exit option, zone exclusion, node
5571
metadata, components, and an end-to-end cache round-trip.
5672

57-
## Status
58-
M1b is complete: the live `engine.WorldReader` (over `internal/rooms`), the
59-
`weather graph` / `weather rebuild` admin commands, and cache persistence are
60-
implemented (see the `engine` and root `weather` packages). Future milestones
61-
(M2+) add the weather simulation that consumes this graph.
73+
## Consumers
74+
- The module root calls `Build` (via `engine.NewWorldReader()`) on the first
75+
round after boot and from `weather rebuild`; the result is cached through
76+
`plugin.WriteBytes` and feeds the entire weather simulation (`sim.Step`,
77+
coverage queries, zone resolution). The crawler itself is milestone-complete;
78+
changes here are only needed if graph semantics change (e.g. future
79+
directional edge metadata for prevailing wind).

sim/context.md

Lines changed: 132 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
11
# sim Package Context
22

33
## Overview
4-
`sim` is the pure, engine-independent core of the weather module. It holds the
5-
geography `Graph` that the crawler produces and the weather simulation will
6-
consume. Nothing here imports the GoMud engine (`internal/*`) — a rule enforced
7-
by `arch_test.go` — so the simulation stays unit-testable without a running
8-
server and portable across GoMud and DOGMud.
4+
`sim` is the pure, engine-independent core of the weather module: the geography
5+
`Graph` (produced by `crawler`, consumed everywhere) and the deterministic
6+
weather simulation that runs on it. Nothing here imports the GoMud engine
7+
(`internal/*`) — enforced by `arch_test.go` — and the package is stdlib-only,
8+
so everything is unit-testable without a server and portable across GoMud and
9+
DOGMud. All randomness flows through a serializable PRNG carried in `State`,
10+
making every run exactly reproducible from a seed.
911

1012
## Key Components
1113
### Core Files
12-
- **graph.go**: the `Graph`, `ZoneNode`, and `Edge` types; the `GraphVersion`
13-
constant; JSON cache serialization (`ToJSON` / `FromJSON`); `Zones()`;
14-
`Neighbors` (lazy adjacency index); and `FindZone` (case-insensitive lookup).
15-
- **state.go**: `NewState` / `DeriveSeed` (FNV-1a over sorted zone names); and
16-
the persistence codec `ToJSON` / `StateFromJSON`.
17-
- **query.go**: `Coverage` and `Covering` — front-projection query that mirrors
18-
`resolveWeather`'s coverage rule.
19-
- **mutate.go**: `ForceSpawn` and `ClearZones` — pure admin-action mutators (no
20-
RNG consumed).
21-
- **arch_test.go**: architecture guardrail — fails if any file in this package
22-
imports a `GoMudEngine/GoMud/internal` path.
14+
- **graph.go**: `Graph`, `ZoneNode`, `Edge`; `GraphVersion`; JSON cache codec
15+
(`ToJSON`/`FromJSON`); `Zones()` (sorted, for deterministic iteration);
16+
`Neighbors` (lazy adjacency index); `FindZone` (case-insensitive lookup).
17+
- **rng.go**: `RNG` — serializable splitmix64 PRNG; its entire state is one
18+
`uint64` cursor (`State.RNGState`).
19+
- **weather.go**: core value types — `WeatherType` (open, data-driven; `Clear`
20+
is the calm baseline), `ZoneId` (= `string`), `FrontId`, `Front`, `State`,
21+
`Clock`, `ZoneChange`, `StateDiff`; `clamp01`.
22+
- **climate.go**: `ClimateProfile`/`WeatherInfluence`/`Climate` (biome →
23+
weather weights, terrain influence, spawn weight); `Climate.For` (falls back
24+
to the `"default"` profile); `DefaultClimate()` (built-in profiles for the
25+
standard biomes); `Config`/`DefaultConfig()` (front budget, spawn chance,
26+
history length, hard age cap, coverage falloff/threshold/radius).
27+
- **tick.go**: `Step` — the simulation tick — and its helpers (`ageAndFeedback`,
28+
`moveFronts`, `evolveTypes`, `removeDead`, `spawnFronts`, `resolveWeather`,
29+
`zonesWithin`, `diffWeather`, weighted-pick helpers).
30+
- **state.go**: `NewState`/`DeriveSeed` plus the `State` persistence codec
31+
(`ToJSON`/`StateFromJSON`).
32+
- **query.go**: `Coverage`/`Covering` — read-only front-projection query that
33+
mirrors `resolveWeather`'s coverage rule exactly (a consistency test pins
34+
them together).
35+
- **mutate.go**: `ForceSpawn`/`ClearZones` — pure admin-action mutations.
36+
- **arch_test.go**: purity guardrail — fails if any file imports a
37+
`GoMudEngine/GoMud/internal` path.
2338

2439
### Key Structures
2540
```go
@@ -38,98 +53,120 @@ type Graph struct {
3853
Nodes map[string]ZoneNode
3954
Edges []Edge
4055
Components int // connected-component count
41-
// adj is a lazy adjacency index; nil after FromJSON, rebuilt on first Neighbors call.
56+
// + unexported adj: lazy adjacency index; nil after FromJSON,
57+
// rebuilt on the first Neighbors call (not serialized).
58+
}
59+
type Front struct { // one traveling weather system
60+
Id FrontId
61+
Type WeatherType
62+
Zone ZoneId // center
63+
Intensity float64 // 0..1; <= 0 means death
64+
Moisture float64 // 0..1
65+
Age, MaxAge int // soft cap; past MaxAge decay accelerates
66+
History []ZoneId // bounded recent path (no immediate backtrack)
67+
}
68+
type State struct { // full sim state — serializable
69+
Round uint64
70+
RNGState uint64 // PRNG cursor
71+
NextID FrontId
72+
Fronts []Front
73+
Weather map[ZoneId]WeatherType // resolved per-zone weather
4274
}
4375
type Coverage struct {
4476
Front Front
4577
Effective float64 // projected intensity at the queried zone
46-
Hops int // graph distance from the front's centre
78+
Hops int // graph distance from the front's center
4779
}
4880
```
4981

5082
## Core Functions
51-
- `(*Graph) ToJSON() ([]byte, error)` / `FromJSON([]byte) (*Graph, error)`
52-
the on-disk cache format (indented JSON). `GraphVersion` lets a loader detect
53-
a stale cache and rebuild.
54-
- `(*Graph) Zones() []string` — sorted zone names for deterministic iteration.
55-
- `(*Graph) Neighbors(zone string) []Edge` — adjacent zones, each Edge oriented
56-
from the queried zone (`Edge.A == zone`). Returns a **shared slice** from a
57-
lazily-built index; callers MUST NOT mutate it — copy before sorting. Returns
58-
nil for unknown or isolated zones. The adjacency index is rebuilt lazily after
59-
`FromJSON` (the `adj` field is not serialized).
60-
- `(*Graph) FindZone(name string) (string, bool)` — case-insensitive zone
61-
lookup; exact match wins over a fold-equal match.
62-
- `NewState(seed uint64) State` — initial simulation state for a fresh run.
63-
- `DeriveSeed(g *Graph) uint64` — stable default seed from sorted zone names
64-
(FNV-1a); used when the configured `Seed` is 0 so two distinct worlds differ.
65-
- `(State) ToJSON() ([]byte, error)` / `StateFromJSON([]byte) (State, error)`
66-
persistence codec for the full simulation state.
67-
- `Covering(g, fronts, cfg, zone) []Coverage` — fronts whose area projection
68-
reaches `zone`, strongest effective intensity first (ties by front id). Mirrors
69-
`resolveWeather`'s coverage rule exactly.
70-
- `ForceSpawn(prev, g, cfg, wtype, zone, intensity, clock) (State, StateDiff, bool)`
71-
— inject a front bypassing budget and spawn chance. `intensity <= 0` defaults
72-
to 0.6. Returns ok=false for an unknown zone. Pure: input state unchanged, no
73-
RNG consumed (admin actions must not perturb the deterministic trace).
74-
- `ClearZones(prev, g, cfg, zones, clock) (State, StateDiff)` — remove fronts
75-
and re-resolve weather. No zones = clear all. With zones, uses `Covering` so a
76-
named zone clears even when its front is centred elsewhere. Pure; no RNG
77-
consumed.
83+
### Graph
84+
- `(*Graph) ToJSON / FromJSON` — on-disk cache format (indented JSON);
85+
`GraphVersion` lets a loader detect a stale cache and rebuild.
86+
- `(*Graph) Neighbors(zone) []Edge` — adjacent zones, each Edge oriented from
87+
the queried zone (`Edge.A == zone`). Returns a **shared slice** from the
88+
lazily-built index: callers MUST NOT mutate it (copy before sorting). Nil
89+
for unknown or isolated zones.
90+
- `(*Graph) FindZone(name) (string, bool)` — case-insensitive resolution to
91+
the canonical zone key; exact match wins.
92+
93+
### Simulation
94+
- `Step(prev State, g *Graph, climate Climate, cfg Config, now Clock) (State, StateDiff)`
95+
— one coarse tick, a pure function of its inputs. In order: age fronts and
96+
apply the occupied biome's influence (the weather ← biome feedback half);
97+
move fronts along edges (chance damped by `MovementResistance`, destination
98+
weighted by edge weight, no immediate backtrack via `History`); re-roll a
99+
*moved* front's type from the destination climate (biased to keep its
100+
current type, so changes read naturally); drop dead fronts (intensity ≤ 0 or
101+
past `FrontHardAge`); maybe spawn one front under `MaxActiveFronts` (origin
102+
weighted by climate `SpawnWeight`, type from the origin's weights); resolve
103+
per-zone weather with **intensity-scaled area coverage** — each front
104+
projects onto zones within `MaxFrontRadius` hops at
105+
`Intensity × CoverageFalloff^hops`, covering while `>= MinProjected`; per
106+
zone the highest effective intensity wins (ties → lowest front id);
107+
frontless zones are `Clear` — then emit the `StateDiff` of changed zones
108+
(sorted; a `From` of `""` means "previously unset", not "was clear").
109+
- `NewState(seed) State` — fresh run (NextID 1, empty non-nil Weather map).
110+
- `DeriveSeed(g) uint64` — stable default seed: FNV-1a over sorted zone names,
111+
so each world keeps its seed across boots but two worlds differ. Used when
112+
the configured seed is 0.
113+
- `(State) ToJSON / StateFromJSON` — persistence codec (the engine layer wraps
114+
it in a versioned envelope; see `engine/state.go`).
115+
116+
### Queries & admin mutations
117+
- `Covering(g, fronts, cfg, zone) []Coverage` — every front whose projection
118+
reaches `zone`, strongest first. Powers the player `weather` view and the
119+
exported `GetWeather` intensity.
120+
- `ForceSpawn(prev, g, cfg, wtype, zone, intensity, now) (State, StateDiff, bool)`
121+
— inject a front, bypassing budget and spawn chance but flowing through the
122+
same resolve+diff path as `Step`. `intensity <= 0` defaults to 0.6; fixed
123+
`MaxAge` 24; ok=false for an unknown zone. Pure — input state unmodified.
124+
- `ClearZones(prev, g, cfg, zones, now) (State, StateDiff)` — nil/empty zones
125+
removes every front; named zones remove every front whose *coverage* reaches
126+
one of them (so the zone actually clears even when the front is centered
127+
elsewhere). Pure. Kept fronts share `History` backing arrays with the input;
128+
any later mutation must go through `cloneFronts` (`Step` does).
129+
- **Neither admin mutation consumes RNG** — admin actions must not perturb the
130+
deterministic trace.
131+
132+
## Determinism
133+
All randomness comes from the `RNG` built from `State.RNGState`; the advanced
134+
cursor is written back into the next `State`. Same seed + graph + tick count ⇒
135+
byte-identical `State` (asserted by `TestStep_Deterministic` via
136+
`reflect.DeepEqual` and by a golden-trace test). Anything that would consume
137+
randomness outside `Step` (emote selection, admin spawns) deliberately does
138+
not touch this RNG.
78139

79140
## Dependencies
80-
- Standard library only (`encoding/json`, `sort`, `strings`). No engine imports (enforced).
141+
Standard library only (`encoding/json`, `sort`, `strings`). No engine imports
142+
(enforced), no third-party imports.
81143

82144
## Consumers
83-
- `crawler.Build` returns a `*Graph`.
84-
- `engine.Apply`/`engine.Reconcile` consume `StateDiff` and `State.Weather`.
85-
- The module root calls `Covering`, `ForceSpawn`, `ClearZones`, `NewState`, and
86-
`DeriveSeed` from command handlers, the tick path, and the exported API.
145+
- `crawler.Build` produces the `*Graph`; `engine.DecodeCache` round-trips it.
146+
- The module root drives `Step` from the weather tick and feeds
147+
`State.Weather` to `engine.Reconcile`; commands/exports call `Covering`,
148+
`ForceSpawn`, `ClearZones`, `NewState`, `DeriveSeed`, `FindZone`.
149+
- `content.LoadClimate` returns a `Climate`; `engine.EmitAmbient` reads
150+
`State.Weather`.
87151

88152
## Testing
89-
- `graph_test.go`: JSON round-trip, `Neighbors`, `FindZone`.
90-
- `state_test.go`: `NewState`, `DeriveSeed`, `ToJSON`/`StateFromJSON` round-trip.
91-
- `query_test.go`: `Covering` projection and ordering.
92-
- `mutate_test.go`: `ForceSpawn` and `ClearZones` (coverage-based clear).
153+
- `graph_test.go`: JSON round-trip, `Neighbors` (stability, orientation,
154+
unknown/isolated zones, post-decode rebuild), `FindZone`.
155+
- `rng_test.go`: determinism, cursor round-trip, ranges.
156+
- `weather_test.go`: `clamp01`.
157+
- `climate_test.go`: profile fallback, defaults.
158+
- `tick_test.go`: per-behavior tests (feedback, movement resistance, type
159+
evolution, death, budget, coverage), golden trace, full-state determinism,
160+
storm-dies-crossing-mountains feedback scenario.
161+
- `state_test.go`: `NewState`, `DeriveSeed`, state JSON round-trip.
162+
- `query_test.go`: `Covering` projection/ordering + consistency with
163+
`resolveWeather`.
164+
- `mutate_test.go`: `ForceSpawn`/`ClearZones` incl. purity assertions.
93165
- `arch_test.go`: purity guardrail.
94166

95-
## Weather simulation (M2)
96-
97-
Beyond the geography `Graph`, `sim` now contains the pure, deterministic weather
98-
simulation. It consumes a `*Graph` as its read-only world and produces weather
99-
state as plain data — no engine imports.
100-
101-
### Key files
102-
- **rng.go**: `RNG`, a serializable splitmix64 PRNG (cursor = one `uint64`).
103-
- **weather.go**: `WeatherType` (+ `Clear`), `Front`, `State`, `StateDiff`,
104-
`ZoneChange`, `Clock`, and `clamp01`.
105-
- **climate.go**: `ClimateProfile` / `WeatherInfluence` / `Climate` (biome →
106-
weather weights + influence + spawn weight), `Climate.For` (default fallback),
107-
`DefaultClimate`; plus `Config` / `DefaultConfig` (front budget, spawn chance,
108-
history length, hard age cap).
109-
- **tick.go**: `Step(prev, graph, climate, cfg, clock) -> (next, diff)` and its
110-
helpers (`ageAndFeedback`, `moveFronts`, `evolveTypes`, `removeDead`,
111-
`spawnFronts`, `resolveWeather`, `diffWeather`).
112-
- **state.go**: `State.ToJSON` / `StateFromJSON` (persistence codec).
113-
114-
### The tick
115-
Each `Step`: age fronts and apply the current zone's biome influence (the
116-
weather ← biome feedback), move fronts along edges (damped by `MovementResistance`,
117-
no immediate backtrack), evolve a moved front's type from the new zone's climate,
118-
drop dead fronts (intensity ≤ 0 or past the hard age cap), maybe spawn one front
119-
under budget (origin weighted by `SpawnWeight`), resolve per-zone weather with
120-
**intensity-scaled area coverage** (a front projects onto zones within
121-
`MaxFrontRadius` hops at `Intensity × CoverageFalloff^hops`, covered while
122-
`>= MinProjected`; per zone the highest *effective* intensity wins; frontless
123-
zones = `Clear`), and emit the diff.
124-
125-
### Determinism
126-
All randomness flows through the `RNG` built from `State.RNGState`; the advanced
127-
cursor is written back into the next `State`. Same seed + graph + tick count ⇒
128-
identical fronts and per-zone weather (see `TestStep_Deterministic`).
129-
130-
### Deferred (later milestones)
131-
- **Prevailing-wind direction** (a MUD owner biasing where storms originate/move,
132-
e.g. west→east) is a planned later chunk: it needs directional edge metadata,
133-
which a future crawler pass can derive from each exit's `MapDirection`.
134-
- Calm-zone variety (occasional light fog/overcast in frontless zones) and an
135-
explicit windward "orographic" precip spike are noted enrichments, not built.
167+
## Deferred (later milestones)
168+
- **Prevailing-wind direction** (biasing where fronts originate/travel, e.g.
169+
west→east) needs directional edge metadata, derivable by a future crawler
170+
pass from each exit's `MapDirection`.
171+
- Calm-zone variety (occasional light fog/overcast in frontless zones) and a
172+
windward "orographic" precipitation spike are noted enrichments, not built.

0 commit comments

Comments
 (0)