Skip to content

Latest commit

 

History

History
165 lines (146 loc) · 11 KB

File metadata and controls

165 lines (146 loc) · 11 KB

CLAUDE.md — The Master's Shadow

Guidance for Claude Code (and other agents) working in this repository.

What this is

A C++17 / raylib gothic narrative roguelike. A deterministic simulation core (ms_core, raylib-free) drives the rules; a presentation layer renders it with raylib; content lives in an OKF-style data bundle under data/.

Build: cmake --build build. Targets: ms_core (lib), masters_shadow (exe), ms_tests (doctest), ms_headless (--selftest determinism, --balance, --apex).

The two laws (do not break these)

1. Determinism is the crown jewel

ms_core guarantees: same seed + same action journal ⇒ identical state_hash (proven by ms_headless --selftest, which runs the sim twice and asserts h1==h2).

  • Never edit the determinism spine: src/core/rng.cpp, src/core/rules_engine.cpp (apply/dispatch/replay), src/core/game_state.cpp state-hash (field order + the sorted-key flag iteration). Change behaviour by adding an injection seam, never by moving logic to disk.
  • ms_core does no file I/O for rules. Content is parsed in the app/headless layer and injected as frozen POD structs before the first apply() (the mutable_state() setup window). The sim reads plain values, never ContentDB on a dispatch() path.
  • After ANY change, ms_headless --selftest must still PASS, and for presentation-only changes the hash must be unchanged (current baseline 16425e914e1636ca). A moved hash on a "text-only" edit means you wired text into a core path — stop and revert.
  • Scope of the golden hash: it locks the core journal path (seed, lineage, journal) → state_hash. Full player runs are a hybrid: many Phase 2/3 effects still mutate via mutable_state() outside apply() (inventory: docs/MUTABLE_STATE_INVENTORY.md). Continue checkpoints use MSSNAP state snapshots (src/game/run_save.cpp), not pure journal replay, for that reason. Prefer additive ActionTypes for new/journalized mechanics (hash-safe if absent from the selftest journal).

2. Externalize the numbers and the words; keep the ifs (OKF-SPIRIT)

As little as possible is hardcoded. Tables, parameters, weights, and all user-facing text belong in data/ as OKF records. Branching rules/logic stay in ms_core.

  • Content is an OKF-style bundle: data/<bundle>/*.md, flat key: value records split by a lone ---, loaded by src/core/content_db.cpp (by_type / by_id). Structured numeric tables → integer frontmatter fields; narrative → the text field.
  • Every bundle has an index.md (type: index, OKF metadata). Regenerate with python tools/gen_okf_index.py.
  • Determinism guard for content: any field a core table will consume MUST be an integer (store 0.06 as a per-mille 6std::stod is not byte-stable across libc, and a float leak breaks state_hash). Enforced by the validator.
  • Run python tools/okf_validate.py (gate G-OKF-CONFORM) before committing data edits — it must be GREEN. It checks: every record has a type:; ids are unique (ContentDB silently overwrites duplicates); core-consumed files are integer-only; grammar quirks; and index.md presence.

Conventions

  • Engine resolves mechanics; the presentation narrates. Rolls, combat, and rules go through ms_core (eng()->apply() / journaled eng()->rng()); the screens read state and draw. Presentation-only randomness (cosmetic barks, particles, shake) uses raylib's GetRandomValue, never the sim RNG.
  • Art/audio pipeline: tools/gen_*.py (Pillow/numpy) process Grok-generated source art from design/assets/** and ElevenLabs audio into assets/**. See the memory bundle and docs/.
  • Prove with tests: ms_tests (46) + ms_headless --selftest before declaring done.

OKF conversion status (in progress)

Externalizing hardcoded content into the OKF bundle, phased. Golden baseline hash 16425e914e1636ca has held through EVERY change so far (bit-identical).

  • Phase 1 (presentation text): DONE. ~364 user-facing UI + narrative strings read from data/strings/*.md (type: uistring) via tr(id, fallback) (src/core/strings.hpp); endings + chaos display read from data/; enemy display names wired from data/enemies (the name field is live, not dead). tr() falls back to the exact literal, so text is identical with data absent. Runtime-interpolated lines are stored as whole %d/%s templates (the loader trims values and keeps quotes, so never rely on boundary whitespace or wrap a value in quotes). Only truly dead items/spells display records remain unwired (the data exists; no screen reads it yet).
  • Phase 2 (core tables via injection): the framework is proven end-to-end. ms_core reads externalized tables from the frozen GameTables (src/core/game_tables.hpp), injected by app + headless via build_game_tables(ContentDB) before the first apply(). DONE (all bit-identical, hash unchanged): enemy stats (data/enemies), lineage traits (data/lineages, floats→per-mille), chaos base weights (data/chaos), tuning knobs (data/tuning), terrain movement cost + encounter base (data/world), and brew recipes (data/recipes, app-layer read). Spell + item catalogues now drive real gameplay (data/spells, data/itemsSpellRow/ItemRow): combat builds its action bar from learned spells, a shared spellbook casts field spells, and a data-driven satchel uses items. CastSpell reads the catalogue only when an action carries a spell id (the legacy id-less path is byte-identical, so the hash held); UseItem is a new additive action (absent from the selftest journal ⇒ hash unchanged). This is the sanctioned way to add mechanics: a new/extended action behind an injection seam, never a hash-breaking edit. Deliberately NOT externalized: constants.hpp — 26 of its 40 tunables are consumed inside game_state.cpp (a named determinism-spine file that Law 1 forbids editing); they stay constexpr (already centralized in one balancing file). Scattered in-logic ranges (rng.range(3,6) forage, barter/parley deltas) stay in code per OKF-SPIRIT ("keep the ifs").
  • Phase 3 (convention): DONE. validator (tools/okf_validate.py, G-OKF-CONFORM) + index.md + log.md ×13 (regen: python tools/gen_okf_index.py) + this file. Remaining: docs/MODDING.md reconciliation.

To add a core table: mirror the enemy/lineage migration — POD row in game_tables.hpp, loader in content_tables.cpp (canonical enum order, per-mille ints), a data-first read in the rule fn with the hardcoded block as fallback, add the bundle to the validator's CORE_CONSUMED, then verify --selftest hash == baseline + --apex/--balance unchanged.

The Glow-Up pass (WS0–WS6, v0.2.0)

A full quality pass; the golden hash still 16425e914e1636ca — every mechanic landed as an additive action or a difficulty-0-neutral change, and --balance/--apex stayed bit-identical. Additive actions now: UseItem=16, EndDay=17 (Phase-1 sleep: clock tick + recovery + reset day_segment and every p1d_* per-day flag), BrewCombine=18 (tag-synergy alchemy), DisarmTrap=19.

  • Phase-1 economy: the day is a segment budget (data/tuning/phase1_economy.md, core-consumed); the last segment = Exhausted (only doors + bed); the day ends ONLY via EndDay. Per-day limits are journaled p1d_* flags.
  • Difficulty: data/difficulty (Normal/Hardcore) → GameTables.difficulty[2], index stamped into GameState.difficulty in the setup window (deliberately NOT hashed). Row 0 must always mirror the canonical literals — the selftest runs difficulty 0, which is what keeps the hash pinned.
  • New core-consumed bundles: reagents (tags→bitmask + synergy pairs), traps, master_spells, reputation, difficulty — all integer-only, all in the validator's CORE_CONSUMED. Presentation bundles: lore (codex, unlock-gated).
  • User settings live at %APPDATA%\MastersShadow\settings.md via src/game/user_settings.* (user STATE, never in ContentDB). Settings render as an overlay panel (settings_panel.cpp) embedded by title + pause — never a screen swap (screens hold live run state).
  • Sprites: draw_actor_facing() discovers sprites/<base>_<walk|idle>_<facing>.png at runtime, falling back to the legacy sheets. New art arrives via tools/grok/ (manifest → browser driver → ingest slicer); keep the character-bible sentence identical across a character's sheets.
  • Pre-pass journals/saves no longer replay (EndDay + master-turn changes) — v0.2.0.

The Overhaul pass (O0–O7, v0.4.0)

A full review-and-upgrade of every system. The golden hash is still 16425e914e1636ca — a controlled re-baseline was approved for this work and proved unnecessary, because every mechanic landed as an additive action or in the screen layer. Additive action added: GrantSkill=23 (a=Skill, b=points, capped at 8).

  • Input discipline. ui::begin_frame() + ui::consume_key() (src/ui/widgets.hpp) make hotkeys frame-scoped and single-claim. This is load-bearing: immediate-mode widgets live in draw(), update() runs first in the same frame, and IsKeyPressed stays true throughout — which is exactly how the apex came to resolve every action twice per keypress. ui::input_enabled() is the single pause/transition gate.
  • Feel layer. ui/motion.hpp (dt-correct 1-exp(-k*dt) damping — never fminf(1, dt*k) — easing, Animated), ui/style.hpp (type + spacing scales), ui/juice.* (trauma² shake, hit-stop, flash; the settings gate lives inside Juice so combat, which has no AppContext, is covered), ui/toast.* (ONE notification channel), ui/coach.* (the teaching layer), game/transition.* (fades + chapter cards that can hold a line for input).
  • Persistence beyond the run. Unlocks (game/unlocks.*) now holds earned honours, cross-run knowledge (reagent: / syn: / taught: keys) and the run history the Chronicle reads. MS_UNLOCKS_PATH redirects the file — tests use it, and must, or they clobber the player's real board. game/legacy.* applies honour + fallen-vessel inheritance inside start_run's setup window (total skill grant capped at kMaxSkill), and fresh_seed()/daily_seed() give a run its own wood.
  • Art lands without code changes. apprentice_base(Lineage) (render/scene.cpp) and the NPC portrait: field both probe for the asset and fall back — drop the PNG in and it appears. Probe results are cached; Assets::has() is cached too, because it is called from draw loops.
  • Content bundles added: npcs (named visitors, presentation + closed-set offer kinds), tutorial (type: lesson). Neither is core-consumed.
  • Do NOT "clean up" the EventBus. It is write-only (emitters in rules_engine.cpp, no subscribers), but those emit calls sit inside the determinism spine that Law 1 forbids editing. An empty handler vector costs nothing; touching the spine to save it risks the crown jewel.