Skip to content

[Engine] Split material identity from cell state behind a property table#14

Open
Kakapio wants to merge 1 commit into
core-sim-overhaulfrom
material-property-table
Open

[Engine] Split material identity from cell state behind a property table#14
Kakapio wants to merge 1 commit into
core-sim-overhaulfrom
material-property-table

Conversation

@Kakapio

@Kakapio Kakapio commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Stacked on #13 — review that first; this PR's diff is only the material/property-table change.

Summary

Item 4 from the engine review: the biggest extensibility lever. The nested Particle enum tree (Common/Special(Ore|Gem)/Liquid(Direction)/Solid) conflated what a particle is with how it currently behaves, and adding a particle meant touching five-plus files (enum variant, sprite match, depth methods, spawn chance, interaction rules). This PR collapses all of it into a single registry. Net −157 lines.

What changed

Material + one properties() row per material

A flat identity enum whose entire static behavior lives in one const fn table: sprite index, Phase (Static or Liquid { viscosity }), and an optional WorldGenRule (Common depth band, or Special spawn weight + vein flag). Adding a particle is now one variant + one row + an atlas sprite. The generator, renderer, and simulator all read the table; the ParticleType/WorldGenType traits, the duplicated inherent depth methods on Common/Special, and the dead Liquid world-gen impl are deleted.

Particle { material, state } — honest equality

Flow direction moves out of the enum variants into CellState. Equality and hashing are now exact (derived), killing the footgun where Water(Left) == Water(Right) for HashMaps but not for pattern matching. Code that wants identity semantics — rule lookups, change tracking, deferred-step re-validation — now compares .material explicitly, which is what it always meant.

Directional interaction rules, no more HashMap

interaction_rule(source, target) is a const fn match on material pairs, replacing the LazyLock<HashMap> whose commutative key spun up two DefaultHashers per lookup in the hot path. Symmetry is now spelled out ((Water, Lava) | (Lava, Water)), and the asymmetric acid-into-water no-op — previously an accident of commutative lookup against directional Preserve semantics — is explicit and documented, with a note pointing at source-transforming rules as the future fix.

Phase-driven dispatch

Chunk::simulate picks a simulator by material.is_liquid() (the one dispatch point), and FluidSimulator reads viscosity from the table via let Phase::Liquid { viscosity } = …. A future powder or gas phase plugs in here without touching scattered match arms.

Behavior parity — verified, not hoped

  • Both insta snapshots pass unchanged (byte-identical files in git): same-seed terrain generation is bit-for-bit identical (RNG draw order preserved — Material::iter() keeps Gold before Ruby), and the settled-pool simulation endpoint is identical.
  • All conservation, interaction, sleep/wake, and determinism regression tests from [Engine] Overhaul simulation correctness, world generation, and chunk sleeping #13 pass untouched.
  • Game smoke-run clean.

Tests

27 passing. New in this PR:

  • sprite_indices_are_unique_and_nonzero — the atlas-collision insurance the old scattered indices never had
  • particle_equality_is_exact — documents the new equality semantics
  • Directional rule tests (water+lava both ways, water→acid, no-rule pairs)
  • common_bands_cover_all_depths — terrain generation can never panic on a depth gap

🤖 Generated with Claude Code

Replace the nested Particle enum tree (Common/Special/Liquid/Solid with
Direction embedded in liquid variants) with:

- Material: a flat identity enum. All static behavior lives in one
  const properties() row per material: sprite index, phase (with
  per-liquid viscosity), and world-gen rule (common depth band, or
  special spawn weight + vein flag). Adding a particle is now one
  variant + one row + a sprite.
- CellState + Particle { material, state }: dynamic state (flow
  direction) is separate, so equality and hashing are exact again.
  The old discriminant-only Eq/Hash hack on Liquid - where
  Water(Left) == Water(Right) for maps but not for matching - is gone;
  identity comparisons now say `.material` explicitly.
- Directional interaction rules: a const match on (source, target)
  material pairs replaces the HashMap with commutative double-hash
  keys. Symmetric behavior (water+lava both ways) is spelled out, and
  the acid-into-water no-op is now explicit and documented instead of
  an accident of commutative lookup.
- Phase-driven dispatch: Chunk::simulate picks a simulator by
  Material::phase(), and FluidSimulator reads viscosity from the
  table - a new phase (powder, gas) plugs in without touching match
  arms scattered across files. The ParticleType and WorldGenType
  traits, the duplicated inherent depth methods, and the dead
  Liquid world-gen impl are all deleted.

Behavior-preserving: both insta snapshots (seeded terrain, settled
pool) pass unchanged, and RNG draw order is identical, so same-seed
worlds are bit-for-bit the same as before.

Tests: 27 passing; new unit tests for sprite-index uniqueness, exact
particle equality, directional rules, and full-depth common-band
coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant