Imported from mikelindo2000/rogue (
.agents/AGENTS.md). Install upstream withnpx skills add mikelindo2000/rogue --skill .agents. Copyright stays with the author.
Rogue: DungeonMaster Agent Guidelines
Guidelines and rules for modifying the Rogue: DungeonMaster codebase.
Version Control
-
Commit as you work:
- Make small, focused commits as you complete coherent units of work (a feature, a refactor, a fix) rather than one giant commit at the end.
- Run
npx tsc --noEmit(ornpm run build) before committing so every commit type-checks and builds.
-
Commit messages:
- Write a concise imperative subject line describing the change (e.g. "Add custom dropdown component", "Render classic Rogue walls").
- Add a short body when the why isn't obvious from the subject.
-
Pre-commit hook:
- A repo hook runs
npm run checkbefore each commit. Enable it once per clone:git config core.hooksPath .githooks.
- A repo hook runs
-
Changelog:
- If you make notable changes, document them under the appropriate section in CHANGELOG.md.
Testing & Determinism
-
Run
npm run check(tsc --noEmit && vitest run) before committing.npm testwatches;npm run test:runruns once. -
Seedable RNG: game logic draws randomness from an injected
RNG(src/rng.ts), neverMath.random()directly. Generation, loot, and combat take anrngparameter. This keeps everything reproducible — when adding a randomized system, thread therngthrough rather than reaching forMath.random(). -
Test the pure core: combat math (
src/combat.ts), loot (src/items.ts), leveling (src/player.ts), and map generation (src/map.ts) are pure/seedable and have unit tests insrc/*.test.ts. When changing balance or generation, add/adjust a test. Derive expected values fromBALANCE(see below) instead of hardcoding, so tests survive retuning. -
Keep logic pure where practical: prefer functions that take state +
rngand return a result (the engine applies it) over functions that mutate and log inline. Pure functions are what the test suite can pin down. -
Assert invariants, fail loud: use
assert(cond, msg)fromsrc/assert.tsfor cheap conditions that must always hold (e.g. the generator asserts a room was placed and the player/stairs land on walkable tiles). UsedevAssert(() => check(), msg)for deeper/expensive checks that should run in dev and tests but never risk throwing in a player's production session (e.g. the map generator's reachability flood-fill). A violated invariant should surface immediately, not corrupt state for a later mysterious crash.
Game Balance
All game-balance settings are centralized in config.ts. Do not hardcode magic numbers in game logic.
1. Where Balance Files Live
- Static Constants: Designer-set constants (spawn rates, room sizes, combat formulas, status durations, loot scaling, FOV) live in the
BALANCEobject insrc/config.ts. - Tunable Configuration: Player-adjustable values (sliders/knobs) live in
TunableConfigandDEFAULT_TUNABLESinsrc/config.ts. - Monster Database: Base stats, floor requirements, symbols, colors, and special tags (e.g.
'boss') for all monsters are defined inMONSTER_DATABASE. - Gear Pool: Item stats (defense, damage, category, rarity) are defined in
GEAR_POOL.
2. How to Make Adjustments
A. Adjusting Static Values
If you need to change a core game balance constant (e.g. food spawn rate, level-up HP multiplier, trap damage):
- Locate the setting inside the
BALANCEobject insrc/config.tsand modify it. - Ensure that any relevant unit tests (e.g. in
src/combat.test.tsorsrc/map.test.ts) are updated to reflect the new expected outcomes (prefer deriving assertions fromBALANCEinstead of hardcoding).
B. Adding a New Tunable Slider
To add a new setting that players can adjust dynamically:
- Define the property type in the
TunableConfiginterface insrc/config.ts. - Provide a default value in the
DEFAULT_TUNABLESobject insrc/config.ts. - If the setting alters already-spawned game objects or player state (like starting HP), add logic to handle it in
handleBalanceUpdate()withinsrc/engine.ts. - Update the visual tweaking panel HTML and events to render the new slider and bind it to
saveConfig.
C. Special Monster Overrides (e.g., Tutorial/Floor 1 Spawns)
- If a high-level monster or boss (such as
Marcus the Brave) is spawned on a lower floor for testing/tutorial purposes, do not nerf their base stats inMONSTER_DATABASE(as that would weaken them on their normal depths). - Instead, scale their stats down dynamically at spawn-time within map.ts (e.g., overriding
hpandatkfields when pushed to themonsterslist).
D. Verifying and Testing Changes
- After any balance adjustment, run
npm run check(tsc --noEmit && vitest run) to ensure no type errors or broken test assertions were introduced. - If a formula changes, update the corresponding tests (e.g. in
src/combat.test.tsorsrc/leveling.test.ts) to maintain 100% coverage of pure mechanics.
UI Component Architecture
The UI chrome (everything around the dungeon canvas) is built in Svelte 5.
The dungeon board itself stays on <canvas> and is rendered imperatively by
GameUI.render() — do not move the board into Svelte or change how it draws.
-
Svelte 5, runes, light components:
- Build reusable UI in
src/ui/components/(shared building blocks insrc/ui/components/primitives/). Use runes:$props(),$state,$derived,$effect. Use theonclick=event syntax (noton:click). - Keep components small and token-driven (see Styling Guidelines). The
composition root is
src/ui/App.svelte, mounted insrc/main.ts. - Avoid heavier UI frameworks (React/Vue/Angular). Svelte compiles away; the reactive layer only drives once-per-turn HUD chrome, never the canvas loop.
- Build reusable UI in
-
Engine ↔ UI state bridge (
src/ui/store.svelte.ts):- The engine is the imperative source of truth. After each turn it pushes a
plain snapshot into the reactive
uiobject viaGameUI(updateStats/updateDropdowns/renderLogs), and components render from it. Mutatingui's properties / reassigning its arrays is reactive across modules — no manual subscriptions. - User actions flow the other way through
actions(equip/usePotion/eat/…), wired to engine methods insrc/main.ts. Add new HUD data as fields onUIState; add new side effects asactionshooks.
- The engine is the imperative source of truth. After each turn it pushes a
plain snapshot into the reactive
-
Design = source of truth: components are implemented from the Claude Design project (see
design/implemented/spec.md). When changing the look, update the tokens/components to match the design rather than hardcoding values.
Keyboard Input & Controls
-
Centralized Keyboard Handler:
- Do NOT bind direct
keydownevent listeners to the window or document for game actions. - Always register key shortcuts with the global
KeyboardManagerinstance created insrc/main.ts.
- Do NOT bind direct
-
Overlay suspension:
- Movement/action shortcuts no-op while a menu or modal is open.
src/main.tsguards them withoverlayOpen(), which checks for an open[role="menu"](Popover) or[role="dialog"](Modal). Give any new overlay one of those roles so it suspends the game automatically.
- Movement/action shortcuts no-op while a menu or modal is open.
-
Form Input Focus Safety:
KeyboardManagerautomatically filters out shortcuts when typing inside form elements (like input search fields), except for theEscapekey which is allowed to bubble or trigger close actions. Keep this behavior intact when adding fields.
Styling Guidelines
-
Tokens + scoped styles:
src/styles.cssonly importssrc/ui/styles/global.css(fonts, reset, body, scrollbars), which in turn importssrc/ui/styles/tokens.css.- Per-component styling lives in each
.sveltefile's scoped<style>block. There is no global per-component CSS file anymore.
-
Design Tokens:
- All chrome colors, fonts, radii, spacing, shadows, and easing are CSS custom
properties in
:root(src/ui/styles/tokens.css), extracted from the design. Reference them (var(--surface-rail),var(--accent),var(--r-md),var(--ease), …) — never hardcode a hex value. The only acceptable raw colors are black scrims/shadows andcolor-mix()over a token; data-driven colors (monster/rarity colors from the store) are passed through as values. - Canvas (dungeon) colors can't live in CSS; they stay centralized in
src/theme.tswith the tile vocabulary insrc/tiles.ts. Do not retheme the board here.
- All chrome colors, fonts, radii, spacing, shadows, and easing are CSS custom
properties in
-
Visual Theme:
- Dark surfaces with an amber/gold accent (
var(--accent)),Geistfor body text andSpace Groteskfor headings/numbers (tabular). The dungeon view still follows the original Rogue board (rooms with-/|walls,.floor,#corridors,+doors) — unchanged.
- Dark surfaces with an amber/gold accent (
-
Accessibility & motion:
- Interactive elements are real
<button>s witharia-labels; bars userole="progressbar"with aria values; decorative SVG isaria-hidden. A global:focus-visiblering (--focus-ring) covers keyboard focus — don'toutline: nonewithout a visible replacement. - Keep micro-animations (cubic-bezier transitions, scale-ups, blur backdrops)
for menus/modals/panels. Reduced-motion is handled globally in
global.css.
- Interactive elements are real
Bestiary Art
- Generated monster art lives in
public/bestiary/<monster-id>.png, where<monster-id>must matchmonsterId()/ the slug fallback insrc/discovery.ts. - Regenerate future monster images from the recipe in
design/implemented/monster_image_generation.md. Do not assume any particular wrapper CLI or another repo exists on a developer's machine; use any available image generator that can match the documented model/style/params and output paths. - Keep bestiary images as dark, text-free, centered creature portraits so they remain legible as card backgrounds and behind the sparring preview.