Skip to content

Element Crystals Ruleset Implementation Plan

Element Crystals Ruleset Implementation Plan

Section titled “Element Crystals Ruleset Implementation Plan”

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. Also follow the project’s bh-dev-chunk ritual (import → boot-check → full suite → count guard → determinism) on every sim-touching task.

Goal: Add an alternate “Crystals” element ruleset (elements as a build economy that auto-upgrades weapons) alongside the existing reactions system, behind a flag, both playable and telemetry-tagged for A/B comparison.

Architecture: A Sim.ruleset flag (null-object pattern, mirrors Sim.story) defaulting to reactions so the determinism baseline is byte-identical. In Crystals mode the reaction path short-circuits; a per-run CrystalState wallet fills from randomised level-up grants; a WeaponThresholds table auto-fires each weapon’s existing apply_mod()/evolve() when crystal counts cross thresholds (non-consuming → shared across weapons → synergy).

Tech Stack: Godot 4.6.3, typed GDScript, GUT 9.6 tests, CF Worker + D1 for telemetry.

Design spec: docs/superpowers/specs/2026-06-25-element-crystals-ruleset-design.md.

  • /sim is pure: every file extends RefCounted; NO Node / Engine / Input / Time / OS / File / JSON APIs. Loaders/render/UI live outside /sim.
  • Determinism is the keystone: Sim.ruleset MUST default to RULESET_REACTIONS and be flipped ONLY by main (render-side) via enable_crystals() — never inside the sim or any test that asserts the baseline. The survival baseline (the value currently pinned in tests/test_determinism_checksum.gd — read it, don’t hardcode a remembered number; the other agent re-pins it often) must stay byte-identical. Re-run the determinism test after EVERY sim task.
  • Crystal randomness draws from Sim.upgrade_rng, never the spawn rng (drawing from rng desyncs the spawn stream).
  • GUT gotchas: methods are assert_lte/assert_gte (NOT assert_le/assert_ge — a typo silently drops the whole file). An un-consumed push_error FAILS the test — test the non-erroring seam or consume with assert_push_error("substr").
  • After adding any class_name in a new file: run godot --headless --path . --import before tests (stale class cache → silently dropped tests + boot parse errors).
  • Trust the COUNT, not just “passed”: bash scripts/check-test-count.sh must report N/N matching tests/test_*.gd.
  • Timing: build on a CLEAN tree AFTER the other agent’s cycle-21 sim.gd work has settled — this plan edits sim.gd heavily and would otherwise conflict. git status clean before starting.
  • Reuse, don’t reinvent: weapon upgrades go through each weapon’s existing apply_mod(kind, mag) / evolve(); do not add a parallel mechanism.
  • Single test: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/<file>.gd -gexit
  • Full suite: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit
  • Count guard: bash scripts/check-test-count.sh
  • Import: godot --headless --path . --import
  • Boot check: godot --headless --path . --quit-after 90 2>&1 | grep "SCRIPT ERROR" (empty = good)
  • Tests build a sim via Sim.new(seed, SimContentFixture.db()); add an enemy with sim.enemies.add(pos, vel, radius, hp, armor, speed, contact, xp, tid, base_el, beh, flank).
  • Create sim/crystal_state.gd (CrystalState) — per-run crystal wallet (pure).
  • Create sim/weapon_thresholds.gd (WeaponThresholds) — the threshold table + lookup (pure, mirrors SimMods.TABLE/StatEffects.TABLE; chosen over bible.json for testability + no ContentLoader churn + the bible.json-drift caveat).
  • Modify sim/sim.gdruleset flag, enable_crystals(), reaction guard, crystals, _eval_thresholds(), roll_upgrade_choices/apply_upgrade/grant_weapon branches.
  • Modify net/gameplay_telemetry.gd + telemetry/src/worker.js + telemetry/schema.sqlruleset tag.
  • Modify main.gd + ui/start_menu.gd — Crystals launch entry → enable_crystals().
  • Create tests/test_crystals_ruleset.gd, tests/test_crystal_state.gd, tests/test_weapon_thresholds.gd.

Task 1: Ruleset seam — flag + enable_crystals() + reaction guard

Section titled “Task 1: Ruleset seam — flag + enable_crystals() + reaction guard”

Files:

  • Modify: sim/sim.gd (add ruleset consts/var near the other state ~line 175; enable_crystals(); guard in _apply_element)
  • Test: tests/test_crystals_ruleset.gd

Interfaces:

  • Produces: Sim.RULESET_REACTIONS (=0), Sim.RULESET_CRYSTALS (=1), Sim.ruleset: int (default RULESET_REACTIONS), Sim.enable_crystals() -> void.

  • Step 1: Write the failing testtests/test_crystals_ruleset.gd

extends GutTest
# Crystals ruleset seam: reactions OFF, enemy elements cosmetic only.
func _content() -> ContentDB:
return SimContentFixture.db()
func test_default_ruleset_is_reactions() -> void:
var sim := Sim.new(1, _content())
assert_eq(sim.ruleset, Sim.RULESET_REACTIONS, "default ruleset is reactions (determinism-safe)")
func test_enable_crystals_flips_flag() -> void:
var sim := Sim.new(1, _content())
sim.enable_crystals()
assert_eq(sim.ruleset, Sim.RULESET_CRYSTALS)
func test_crystals_mode_applies_aura_without_reacting() -> void:
var sim := Sim.new(1, _content())
sim.enable_crystals()
# enemy with a DIFFERENT innate element than the applied one -> would react in reactions mode
var e := sim.enemies.add(Vector2(50, 0), Vector2.ZERO, 14.0, 100.0, 0.0, 70.0, 12.0, 1.0,
EnemyPool.TYPE_SWARMER, -1, EnemyPool.BEHAVIOR_WALK)
var foreign := 0 # element index 0 (any valid element)
sim.zones.clear()
sim._apply_element(e, foreign)
assert_eq(sim.zones.size(), 0, "no reaction terrain zone is dropped in crystals mode")
assert_eq(sim.enemies.aura_element[e], foreign, "aura tint IS set (cosmetic) in crystals mode")
assert_eq(sim.enemies.primed[e], 0, "no priming in crystals mode")
  • Step 2: Run it to verify it fails

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_crystals_ruleset.gd -gexit Expected: FAIL — Sim.RULESET_REACTIONS / enable_crystals not defined.

  • Step 3: Add the flag + method to sim/sim.gd (near the other run-state vars, e.g. just after var game_over)
const RULESET_REACTIONS := 0
const RULESET_CRYSTALS := 1
var ruleset: int = RULESET_REACTIONS # set ONLY by main.enable_crystals(); never in /sim or determinism tests
func enable_crystals() -> void:
ruleset = RULESET_CRYSTALS
  • Step 4: Guard the reaction path in _apply_element — keep the cosmetic aura, skip the reaction. Find func _apply_element(ei: int, element_idx: int) and wrap the reaction portion:
func _apply_element(ei: int, element_idx: int) -> void:
if element_idx < 0 or enemies.data[ei] <= 0.0:
return
if ruleset == RULESET_CRYSTALS:
# Pure economy: tint only, NO reaction / burst / zone / prime / status.
enemies.aura_element[ei] = element_idx
return
# ... existing reactions-mode body unchanged (Elemental.apply -> _on_reaction -> _pop_primed) ...
  • Step 5: Run the test to verify it passes

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_crystals_ruleset.gd -gexit Expected: PASS (3/3).

  • Step 6: bh-dev-chunk gates--import (no new class_name yet, but cheap), boot-check empty, full suite green, count guard N/N, determinism baseline unchanged (default ruleset path untouched).

  • Step 7: Commit

Terminal window
git add sim/sim.gd tests/test_crystals_ruleset.gd
git commit -m "feat(crystals): ruleset flag + reaction-path guard (seam)"

Task 2: CrystalState — per-run crystal wallet

Section titled “Task 2: CrystalState — per-run crystal wallet”

Files:

  • Create: sim/crystal_state.gd
  • Modify: sim/sim.gd (add var crystals := CrystalState.new() with the other state)
  • Test: tests/test_crystal_state.gd

Interfaces:

  • Produces: CrystalState.add(element_id: String, n: int), .count(element_id: String) -> int, .total() -> int, .to_dict() -> Dictionary, .from_dict(d: Dictionary). Sim.crystals: CrystalState.

  • Step 1: Write the failing testtests/test_crystal_state.gd

extends GutTest
func test_add_accumulates_and_counts() -> void:
var c := CrystalState.new()
c.add("fire", 4)
c.add("fire", 2)
c.add("cold", 3)
assert_eq(c.count("fire"), 6)
assert_eq(c.count("cold"), 3)
assert_eq(c.count("void"), 0, "unseen element is zero")
assert_eq(c.total(), 9)
func test_dict_round_trip() -> void:
var c := CrystalState.new()
c.add("light", 5)
var d := c.to_dict()
var c2 := CrystalState.new()
c2.from_dict(d)
assert_eq(c2.count("light"), 5)
  • Step 2: Run it to verify it fails Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_crystal_state.gd -gexit Expected: FAIL — CrystalState not defined.

  • Step 3: Create sim/crystal_state.gd

class_name CrystalState
extends RefCounted
# Per-run element crystal wallet for the Crystals ruleset. Pure data; lost at game over.
var counts: Dictionary = {} # element_id (String) -> int
func add(element_id: String, n: int) -> void:
counts[element_id] = int(counts.get(element_id, 0)) + n
func count(element_id: String) -> int:
return int(counts.get(element_id, 0))
func total() -> int:
var t := 0
for k in counts:
t += int(counts[k])
return t
func to_dict() -> Dictionary:
return counts.duplicate()
func from_dict(d: Dictionary) -> void:
counts = d.duplicate()
  • Step 4: Add to sim/sim.gd (with the other run-state): var crystals: CrystalState = CrystalState.new()

  • Step 5: Run tests to verify they pass (same single-test command) — Expected PASS (2/2).

  • Step 6: gates--import (new class_name CrystalState), boot-check, full suite, count guard, determinism unchanged.

  • Step 7: Commit

Terminal window
git add sim/crystal_state.gd sim/sim.gd tests/test_crystal_state.gd
git commit -m "feat(crystals): CrystalState per-run wallet"

Task 3: WeaponThresholds table + Sim._eval_thresholds()

Section titled “Task 3: WeaponThresholds table + Sim._eval_thresholds()”

Files:

  • Create: sim/weapon_thresholds.gd
  • Modify: sim/sim.gd (_eval_thresholds() + a per-weapon applied-set _thresholds_done)
  • Test: tests/test_weapon_thresholds.gd

Interfaces:

  • Produces: WeaponThresholds.TABLE: Dictionary (weapon_id → Array of {element:String, amount:int, kind:String}; kind is an existing apply_mod kind or "evolve"), WeaponThresholds.rules_for(weapon_id) -> Array. Sim._eval_thresholds() -> void.

  • Consumes: each weapon’s existing apply_mod(kind: String, mag: float) and evolve(); Sim.active_weapon_ids, Sim._weapon_by_id, Sim.crystals.

  • Step 1: Write the failing testtests/test_weapon_thresholds.gd

extends GutTest
func _content() -> ContentDB:
return SimContentFixture.db()
# Crossing a threshold fires the weapon's apply_mod exactly once (idempotent, non-consuming).
func test_threshold_upgrades_weapon_once() -> void:
var sim := Sim.new(1, _content())
sim.enable_crystals()
# orbit is owned for the test; record its shard count
sim.grant_weapon("orbit")
var before: int = sim.orbit.shards
# give enough COLD to cross orbit's first shard threshold (see WeaponThresholds.TABLE)
sim.crystals.add("cold", 99)
sim._eval_thresholds()
var after: int = sim.orbit.shards
assert_gt(after, before, "orbit gained shards from the COLD threshold")
# non-consuming: crystals are not spent
assert_eq(sim.crystals.count("cold"), 99, "thresholds do NOT consume crystals")
# idempotent: a second eval with the same crystals does nothing further
sim._eval_thresholds()
assert_eq(sim.orbit.shards, after, "threshold applies once, not every eval")
func test_table_only_references_owned_weapons_safely() -> void:
# eval with NO owned weapons must not error even if crystals are high
var sim := Sim.new(1, _content())
sim.enable_crystals()
sim.crystals.add("fire", 99)
sim._eval_thresholds() # no crash, no-op
assert_true(true)
  • Step 2: Run it to verify it fails — Expected FAIL (WeaponThresholds / _eval_thresholds undefined). (If sim.orbit.shards is the wrong accessor, fix the test to the real var — verify in sim/weapon_orbit.gd at build time.)

  • Step 3: Create sim/weapon_thresholds.gd (starter table — TUNE later; uses real weapon ids + real apply_mod kinds from cycle 17, and the 6 core elements)

class_name WeaponThresholds
extends RefCounted
# weapon_id -> [{element, amount, kind}] ; kind is an existing apply_mod kind or "evolve".
# Non-consuming, shared across weapons. STARTER VALUES — balance via telemetry.
const TABLE := {
"blade": [{"element":"blood","amount":5,"kind":"arc"}, {"element":"fire","amount":10,"kind":"reach"}, {"element":"void","amount":18,"kind":"evolve"}],
"pulse": [{"element":"lightning","amount":5,"kind":"chain"}, {"element":"void","amount":10,"kind":"range"}, {"element":"lightning","amount":18,"kind":"evolve"}],
"nova": [{"element":"fire","amount":5,"kind":"radius"}, {"element":"fire","amount":12,"kind":"rate"}, {"element":"light","amount":18,"kind":"evolve"}],
"orbit": [{"element":"cold","amount":5,"kind":"shards"}, {"element":"cold","amount":12,"kind":"spin"}, {"element":"lightning","amount":9,"kind":"reach"}, {"element":"void","amount":18,"kind":"evolve"}],
"beam": [{"element":"light","amount":5,"kind":"width"}, {"element":"void","amount":10,"kind":"reach"}, {"element":"light","amount":18,"kind":"evolve"}],
"turret": [{"element":"blood","amount":5,"kind":"count"}, {"element":"fire","amount":12,"kind":"rate"}, {"element":"blood","amount":18,"kind":"evolve"}],
"scatter": [{"element":"blood","amount":5,"kind":"pellets"},{"element":"blood","amount":12,"kind":"spread"}, {"element":"fire","amount":18,"kind":"evolve"}],
}
static func rules_for(weapon_id: String) -> Array:
return TABLE.get(weapon_id, [])
  • Step 4: Add _eval_thresholds() to sim/sim.gd (+ a var _thresholds_done: Dictionary = {} with the run state — keys "<weapon>:<ruleIndex>")
func _eval_thresholds() -> void:
if ruleset != RULESET_CRYSTALS:
return
for wid in active_weapon_ids:
var rules: Array = WeaponThresholds.rules_for(wid)
var w = _weapon_by_id.get(wid, null)
if w == null:
continue
for i in range(rules.size()):
var key := "%s:%d" % [wid, i]
if _thresholds_done.has(key):
continue
var rule: Dictionary = rules[i]
if crystals.count(rule["element"]) >= int(rule["amount"]):
_thresholds_done[key] = true
if rule["kind"] == "evolve":
if w.has_method("evolve") and not w.evolved:
w.evolve()
else:
w.apply_mod(rule["kind"], _weapon_mod_mag(wid, rule["kind"]))

(Use the existing _weapon_mod_mag(wid, kind) helper if present; otherwise pass the same magnitude the wm: path used. Verify the helper name at build time.)

  • Step 5: Run tests to verify they pass — Expected PASS (2/2). Fix accessor names (sim.orbit.shards, weapon evolved) against the real source if needed.

  • Step 6: gates--import (new WeaponThresholds), boot, suite, count, determinism unchanged (eval is guarded by ruleset != RULESET_CRYSTALS).

  • Step 7: Commit

Terminal window
git add sim/weapon_thresholds.gd sim/sim.gd tests/test_weapon_thresholds.gd
git commit -m "feat(crystals): weapon threshold table + _eval_thresholds (reuses apply_mod/evolve)"

Task 4: Crystal grants at level-up (the economy) + RNG

Section titled “Task 4: Crystal grants at level-up (the economy) + RNG”

Files:

  • Modify: sim/sim.gd (roll_upgrade_choices crystals branch; apply_upgrade crystals: handler)
  • Test: append to tests/test_crystals_ruleset.gd

Interfaces:

  • Produces: upgrade id form "crystals:<el>=<n>,<el>=<n>" (e.g. "crystals:fire=4,light=2"); apply_upgrade parses it → crystals.add(...) then _eval_thresholds().

  • Consumes: Sim.upgrade_rng (the upgrade RNG stream, NOT spawn rng), CrystalState, _eval_thresholds.

  • Step 1: Write the failing test (append)

const CRYSTAL_ELEMENTS := ["fire", "cold", "lightning", "void", "blood", "light"]
func test_apply_crystal_grant_adds_and_evals() -> void:
var sim := Sim.new(1, _content())
sim.enable_crystals()
sim.grant_weapon("orbit")
sim.apply_upgrade("crystals:cold=99")
assert_eq(sim.crystals.count("cold"), 99, "crystal grant added to the wallet")
assert_gt(sim.orbit.shards, 0, "applying crystals re-evaluated thresholds")
func test_crystal_offers_are_deterministic() -> void:
var a := Sim.new(7, _content()); a.enable_crystals()
var b := Sim.new(7, _content()); b.enable_crystals()
# same seed -> identical upgrade_rng draws -> identical offers
for i in range(5):
assert_eq(a.roll_upgrade_choices(3), b.roll_upgrade_choices(3), "crystals offers are seed-deterministic")
func test_crystals_mode_excludes_weapon_mods() -> void:
var sim := Sim.new(3, _content()); sim.enable_crystals()
sim.grant_weapon("orbit")
for i in range(8):
for id in sim.roll_upgrade_choices(3):
assert_false(id.begins_with("wm:"), "manual weapon-mods are not offered in crystals mode")
assert_false(id.begins_with("evolve:"), "manual evolves are not offered (thresholds handle them)")
  • Step 2: Run to verify it fails — Expected FAIL (crystals: unhandled; wm: still offered).

  • Step 3: Add apply_upgrade handler (in sim/sim.gd, in the apply_upgrade(id) dispatch)

if id.begins_with("crystals:"):
var spec := id.substr("crystals:".length())
for pair in spec.split(",", false):
var kv := pair.split("=")
if kv.size() == 2:
crystals.add(kv[0], int(kv[1]))
_eval_thresholds()
return
  • Step 4: Branch roll_upgrade_choices — in crystals mode, exclude wm:/evolve: from the pool and inject a crystal grant with ~1/3 weight using upgrade_rng. Sketch (adapt to the real function body):
if ruleset == RULESET_CRYSTALS:
var out: Array[String] = []
# ~1/3 chance the first slot is a crystal grant
if upgrade_rng.randf() < 0.34:
out.append(_roll_crystal_grant())
# fill remaining slots from weapon-grant + stat-mod + transformative pools (NO wm:/evolve:)
# ... reuse existing pool-building but filter out ids starting with "wm:" / "evolve:" ...
return out
func _roll_crystal_grant() -> String:
const ELS := ["fire","cold","lightning","void","blood","light"]
var n_types := upgrade_rng.randi_range(1, 3)
var parts: Array[String] = []
for _i in range(n_types):
var el: String = ELS[upgrade_rng.randi_range(0, ELS.size() - 1)]
parts.append("%s=%d" % [el, upgrade_rng.randi_range(2, 5)])
return "crystals:" + ",".join(parts)
  • Step 5: Run tests to verify they pass — Expected PASS. (If offer determinism fails, confirm no Math/Time/randi() snuck in — only upgrade_rng.)

  • Step 6: gates — boot, suite, count, determinism unchanged (crystals branch only taken when ruleset == RULESET_CRYSTALS, which the baseline never sets).

  • Step 7: Commit

Terminal window
git add sim/sim.gd tests/test_crystals_ruleset.gd
git commit -m "feat(crystals): randomised crystal grants at level-up; exclude wm:/evolve:"

Task 5: Weapon-grant re-evaluates thresholds

Section titled “Task 5: Weapon-grant re-evaluates thresholds”

Files:

  • Modify: sim/sim.gd (grant_weapon calls _eval_thresholds() at the end)
  • Test: append to tests/test_weapon_thresholds.gd

Interfaces: Consumes grant_weapon, _eval_thresholds.

  • Step 1: Write the failing test (append)
func test_late_weapon_inherits_existing_crystals() -> void:
var sim := Sim.new(1, _content())
sim.enable_crystals()
sim.crystals.add("cold", 99) # pile exists BEFORE the weapon is owned
sim._eval_thresholds() # nothing owns it yet -> no effect
sim.grant_weapon("orbit") # granting must back-apply met thresholds
assert_gt(sim.orbit.shards, 0, "a late weapon immediately benefits from the existing crystal pile")
  • Step 2: Run to verify it fails — Expected FAIL (orbit not upgraded on grant).

  • Step 3: Add the call — at the end of func grant_weapon(...) in sim/sim.gd:

if ruleset == RULESET_CRYSTALS:
_eval_thresholds()
  • Step 4: Run to verify it passes — Expected PASS.

  • Step 5: gates — suite, count, determinism unchanged (guarded).

  • Step 6: Commit

Terminal window
git add sim/sim.gd tests/test_weapon_thresholds.gd
git commit -m "feat(crystals): grant_weapon back-applies met thresholds (synergy)"

Task 6: Launch-menu Crystals entry + main wiring

Section titled “Task 6: Launch-menu Crystals entry + main wiring”

Files:

  • Modify: ui/start_menu.gd (add a “Crystals” mode option), main.gd (_mode == "crystals"sim.enable_crystals() in _new_run, after Sim.new)
  • Test: render/UI — verified by boot + play, not GUT (no determinism impact).

Interfaces: Consumes Sim.enable_crystals().

  • Step 1: Add the menu option — in ui/start_menu.gd, add a third entry “CRYSTALS” alongside Survival/Story emitting mode_chosen.emit("crystals") (follow the existing option-building pattern; keep the tvOS-safe joypad nav).

  • Step 2: Wire main._new_run — after sim = Sim.new(run_seed, content) and before render setup:

if _mode == "crystals":
sim.enable_crystals()

(Note: _is_story() stays false for “crystals”, so it runs as a survival-shaped run with the crystals element layer.)

  • Step 3: Boot-checkgodot --headless --path . --quit-after 120 2>&1 | grep "SCRIPT ERROR" empty. Full suite + count green (no test changes, must still match). Determinism unchanged.

  • Step 4: Manual play check — open in editor, pick CRYSTALS, confirm: no reaction bursts/zones fire; the Element Crystals upgrade appears at level-ups; collecting crystals visibly upgrades weapons (e.g. orbit gains shards).

  • Step 5: Commit

Terminal window
git add ui/start_menu.gd main.gd
git commit -m "feat(crystals): launch-menu Crystals mode -> enable_crystals()"

Task 7: Telemetry ruleset tag (A/B comparison)

Section titled “Task 7: Telemetry ruleset tag (A/B comparison)”

Files:

  • Modify: net/gameplay_telemetry.gd (send ruleset), telemetry/schema.sql (+runs.ruleset), telemetry/src/worker.js (ingest + dashboard group-by)
  • Verify: live round-trip (no GUT test — render/worker side)

Interfaces: report_run body gains "ruleset": "reactions"|"crystals".

  • Step 1: Send the tag — in net/gameplay_telemetry.gd report_run, add to the body:
"ruleset": "crystals" if sim.ruleset == Sim.RULESET_CRYSTALS else "reactions",
  • Step 2: Schema — add to telemetry/schema.sql runs table ruleset TEXT, and the one-time live migration: ALTER TABLE runs ADD COLUMN ruleset TEXT; (run once via wrangler d1 execute --remote --command; ALTER is not idempotent — expected to error if re-run).

  • Step 3: Worker — in telemetry/src/worker.js /gameplay INSERT, add the ruleset column + str(s.ruleset, 12) bind; add a runRulesetAgg query (GROUP BY ruleset, mode) and render a “By ruleset (A/B)” dashboard table (run length, level, kills, build variety per ruleset). HTML-esc() every value.

  • Step 4: Deploy + round-trip verifycd telemetry && source ~/.secrets && CLOUDFLARE_API_TOKEN="$CF_LUMARA_DEPLOY_TOKEN" CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT_ID" npx --yes wrangler@latest deploy; POST a build=999 ruleset=crystals test row, confirm it stores + appears in the A/B table, then DELETE FROM runs WHERE build=999.

  • Step 5: Commit

Terminal window
git add net/gameplay_telemetry.gd telemetry/schema.sql telemetry/src/worker.js
git commit -m "feat(telemetry): tag runs by ruleset for the crystals-vs-reactions A/B"

Task 8: Crystals-mode determinism pin + first balance pass (tuning)

Section titled “Task 8: Crystals-mode determinism pin + first balance pass (tuning)”

Files:

  • Create: tests/test_determinism_crystals.gd (pin a crystals-mode trace so the new path is regression-locked)

  • Modify: sim/weapon_thresholds.gd + the crystal-grant magnitudes (tune from first telemetry)

  • Step 1: Pin a crystals trace — mirror tests/test_determinism_checksum.gd but call sim.enable_crystals() after construction, drive 600 ticks with the same scripted input, and (first run) print state_checksum(); then hardcode the printed value as the assertion. This locks the crystals path against accidental change WITHOUT touching the reactions baseline.

extends GutTest
func test_crystals_trace_is_stable() -> void:
var a := Sim.new(1234, SimContentFixture.db()); a.enable_crystals()
var b := Sim.new(1234, SimContentFixture.db()); b.enable_crystals()
for i in range(600):
var dir := Vector2(cos(float(i)*0.05), sin(float(i)*0.03)).normalized()
var inp := InputState.new(dir)
a.tick(inp); b.tick(inp)
assert_eq(a.state_checksum(), b.state_checksum(), "crystals mode is deterministic (a==b)")
# assert_eq(a.state_checksum(), <PASTE printed value>, "crystals baseline")
  • Step 2: gates + Commit (commit the pinned value once stable).

  • Step 3: Balance pass (after first ATV/web telemetry in crystals mode) — adjust WeaponThresholds.TABLE amounts, crystal bundle sizes, and (if needed) a crystals-mode enemy-HP scalar so run length/feel match reactions mode. Iterate against the dashboard “By ruleset” table. Commit per tune.


Spec coverage: ruleset seam (T1), CrystalState (T2), thresholds + eval reusing apply_mod/evolve (T3), randomised level-up grants + wm:/evolve: exclusion (T4), late-weapon synergy (T5), launch menu (T6), telemetry A/B (T7), determinism pin + tuning (T8). Pure-economy/reactions-off = T1. ~6 core elements = T3/T4 constants. Non-consuming/shared = T3 test asserts it. All spec sections map to a task.

Placeholder scan: threshold magnitudes are explicit starter values (flagged TUNE, not TBD); accessor names (sim.orbit.shards, _weapon_mod_mag, weapon evolved) are called out to verify against source at build time (the other agent is actively changing sim.gd), which is honest given the concurrency rather than a placeholder.

Type consistency: id form crystals:<el>=<n> consistent across T4 producer and apply_upgrade handler; RULESET_REACTIONS/RULESET_CRYSTALS/ruleset/enable_crystals consistent T1→T6; _eval_thresholds consistent T3→T5; WeaponThresholds.rules_for/TABLE consistent T3. Determinism guard (ruleset default + main-only flip) repeated in every sim task.