# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project A Unity project (Editor version `6000.3.7f1`, Universal Render Pipeline, 2D/top-down via `Physics2D`) that procedurally generates a maze of connected rooms and populates it with autonomous agents that explore it. All project-specific code lives under `Assets/Scripts/Maze/`, namespace `MazeGen`. There is no code outside that folder worth touching except `Assets/Scenes/SampleScene.unity`, which wires up the single `MazeSceneBuilder` entry point component. ## Commands This is a pure Unity Editor project — there is no CLI build, lint, or test setup in the repo (no `package.json`, `Makefile`, `.asmdef`, or test assemblies). The only way to run/verify changes is: 1. Open the project in Unity Editor `6000.3.7f1` (or compatible 6000.3.x). 2. Open `Assets/Scenes/SampleScene.unity`. 3. Press Play. The `MazeSceneBuilder` component on the scene's root object generates the maze and spawns agents automatically. Compile errors surface in the Unity Console; there's no separate `dotnet build`/`csc` step to invoke. If you need to sanity-check C# syntax without the Editor, treat the generated `Assembly-CSharp.csproj` as a rough reference for compiled files, but it's Editor-generated and shouldn't be hand-edited. ## Architecture The maze is **not** a pre-built grid — it's a lazily-grown graph of `Room` nodes connected by `Exit` doors, generated on demand as agents actually walk through unresolved exits. Nothing beyond an unused exit exists until something tries to go through it. This is what makes "many agents, huge maze" cheap. ### Core data model (`Core/`) - `Room` — a node with `Position` (free `Vector2`, not grid-snapped), `Radius`, `Kind` (Start/Normal/Hallway/Exit), and a list of `Exit`s. `CorridorWaypoints` is set only on elbow-hallway rooms (see below) for stretched rendering. - `Exit` — one doorway. **Grid-locked to the 4 cardinal directions** (0/90/180/270°, `DirectionDegrees`), so a room has at most one exit per side and "the door facing room X" always has a single, exact answer. Exits are matched in connected pairs (`ConnectedRoomId`/`ConnectedExitId`) once resolved; `IsUsed` marks that. - `MazeGraph` — the registry of all rooms, id allocation, `UnusedExitCount` bookkeeping (must never hit 0 before an Exit room exists, or the maze could never be solvable), and spatial lookup via `SpatialRoomIndex` (a uniform grid bucket index, cell size = `roomSpacing`, used for overlap/collision queries — not a maze grid). ### Generation (`Generation/`) - `MazeGenerationSettings` — a `ScriptableObject` with all tunables (room sizes, exit counts, hallway chance, start room count/spread, collision-avoidance search parameters, exit-room eligibility odds). - `MazeGenerator` — the core algorithm, called lazily from `MazeAgentController.TryUseExit` / `AutonomousAgentBrain` whenever an agent uses an unresolved exit (`GenerateConnectedRoom`). On each call it: 1. Computes where a new room *would* go (`fromRoom.Position + exit.Direction * roomSpacing`). 2. **`TryConnectViaExistingDoor`**: if that spot overlaps an existing room, looks for (or adds on-the-fly) a door on that room facing back toward `fromRoom`, and links into it instead of creating an overlapping room — this is what turns the tree into a maze with loops. The link is either direct (`ConnectStraight`, doors face each other head-on) or via a single 90°-bend elbow hallway room (`ConnectWithElbow`, perpendicular doors). All paths run `IsPathClear` (point-to-segment distance vs. every other room) so a corridor can never cut through unrelated geometry. 3. If no door connection works but the spot still overlaps something, `TryRouteAroundObstruction` tries extending further out, then a single 90° bend, before falling back to `FindClearPosition` (a ring/spiral search) as a last resort. 4. Otherwise builds the room normally via `BuildRoom`, rolling cardinal-direction exits (`PickCardinalAngles`). - Known limitation: an elbow hallway's collision footprint is only a small circle at its bend corner, not its full rendered length — something can in rare cases still slip past a corridor without being caught. ### Visualization (`Visualization/`) - `MazeSceneBuilder` — the scene entry point (requires `MazeGenerator`, `MonsterSpawner`, `CombatDirector`, `BattleArenaView`; it get-or-adds the last two so a scene saved before they existed still works). Generates the configured start rooms, spawns `agentCount` agents on staggered coroutine delays, wires each agent's `AutonomousAgentBrain` and registers it with the `CombatDirector`, tracks win conditions (target finished-agent count and/or a time limit — at least one must be set, else it warns and defaults to requiring all agents; also ends when every agent is dead or finished), and draws the IMGUI HUD: status + simulation controls (pause/speed, pause-on-fight toggle), a clickable agent list, an "agent thoughts and info" panel for the followed agent, a live-fights panel with the join-window countdown and watch/log-only buttons, and the global combat log. It hides itself entirely while a fight is being watched, since the arena draws its own. - `MonsterSpawner` — spawns monster views per room and, via `RefreshRoom` (wired to `CombatDirector.FightResolved`), drops the views of monsters that died so a cleared room looks cleared. - `RoomView` — placeholder square sprite per room; renders elbow hallways as stretched/rotated rectangles along `CorridorWaypoints` instead of a single small square. - `ExitMarker` — placeholder door sprite. Purely informational (not clickable — movement is autonomous now): green/blue/gray coloring is relative to whichever agent the camera is currently following. - `PathVisualizer` — one `LineRenderer` per hop; only the followed agent's trail is shown, with the most recent hop bright and earlier hops dimmed. ### Agents (`Agent/`) - `MazeAgent` — plain C# (no `MonoBehaviour`) state: current room, path history, visited-room/used-exit sets, `IsFinished`/`IsDead`/`IsEngaged`, and an `AgentThoughtLog`. Cheap enough to run many of. - `AgentThoughtLog` — bounded, newest-wins journal of one agent's decisions (`ThoughtKind` categorizes and colors them). Purely an observation surface — the simulation never reads it back — and it's what the HUD's "agent thoughts and info" panel shows for the followed agent. - `MazeAgentController` — scene-facing wrapper: positions a GameObject, tints it a per-agent color, dims it on death, and drives `TryUseExit` (which calls `MazeGenerator.GenerateConnectedRoom` lazily if the exit isn't resolved yet). Refuses to move dead or fight-committed agents. - `AutonomousAgentBrain` — per-agent decision loop. Waits a randomized initial delay (so agents don't all decide in lockstep), then ticks a steady `decisionInterval` **on `SimulationClock` time**. Each tick it first deals with hostiles in its current room (`HandleRoomHostiles`: declare a fight, join/sneak past a fight already mustering, or try to slip out), then falls through to movement. `ChooseExit` is explore-biased: prefers an unused exit (fresh territory) > a used exit into a room this agent hasn't personally visited (following another agent's discovery) > backtracking into an already-visited room, with `exploreChance` adding randomness. **Every decision is narrated into the thought log** — adding a decision branch without a matching `Think()` call makes the agent unexplainable to the observer, so keep them together. ### Simulation clock (`Simulation/`) - `SimulationClock` — static, ref-counted pause for the whole maze simulation. Anything that advances maze state (agent decision timers, fight join windows, the level's elapsed clock) ticks on `SimulationClock.DeltaTime`, never `Time.deltaTime`. It deliberately does **not** touch `Time.timeScale`, because real Unity time has to keep flowing while the maze is frozen — that's what drives the watched-fight playback and its coroutines. Pauses are held by reason string so several fights (plus an arena playback) can each hold the sim without stomping on each other's resume; the user's manual pause is separate. ### Combat & fights (`Combat/`) The fight lifecycle is owned by `CombatDirector`; `CombatResolver` decides outcomes and `BattleArenaView` only ever *animates* them. - `CharacterSheet` / `CharacterStats` / `Items/` / `Monsters/Monster` / `MonsterEncounter` — stats, equipment and per-room monster groups. `MazeGenerator.BuildRoom` rolls an encounter into `room.Encounter` as rooms are generated. - `Combatant` — unifies an agent and a monster instance behind one interface so the resolver needn't branch. Wraps the **live** `CharacterSheet` (fights mutate real health, which is how results persist into the maze); `StartHealth` is snapshotted only so playback can animate health draining from where it began. - `CombatEvent` / `BattleResult` — a resolved battle is nothing but an ordered event list (hit/miss/death/ round). **This is the key seam**: the resolver runs to completion up front, log-only mode prints the list, and the arena replays the same list as animation. The view therefore cannot desync from the simulation, and switching between watching and simulating can never change who wins. - `CombatResolver` — multi-participant, all agents vs all monsters in one room. Fixed initiative order by effective speed, hit roll from the speed difference, damage from effective attack vs effective defense. Runs out to a stalemate after `maxRounds` rather than looping forever; survivors of a stalemate mark the room via `MazeAgent.NoteFledFrom` so their brains don't immediately re-declare the same unwinnable fight. - `PendingFight` — a declared-but-not-started fight and its join window. This type exists for the window: declaring a fight commits an agent and *then* waits, which is what lets others pile in. - `CombatDirector` — the orchestrator. Declaration drags in every agent already standing in that room; during the join window (`joinWindowSeconds`, default 5) arriving agents either join (better odds) or roll to sneak past, with a failed sneak forcing them in; agents elsewhere in the maze can't join. Committed agents are marked `IsEngaged`, which parks their brain. On expiry it resolves the fight — log-only, or handed to the arena — then pushes results back into maze state (dead agents `Die()`, survivors released with their damage intact). `pauseOnFightDeclared` holds the simulation the moment a fight starts so the observer can zoom in. ### Battle arena (`Visualization/BattleArenaView.cs`) Plays a fight out on a grid in its **own additively-created scene** (`SceneManager.CreateScene`, unloaded on exit — no scene asset or Build Settings wiring involved), built at a far world offset so its camera can't see maze geometry. It hides the maze by *suspending the maze cameras*, never by deactivating maze GameObjects — `SetActive(false)` on the maze root would permanently kill the agent-spawn coroutines. Because the outcome is already decided before playback and health bars run off a private display copy, this class never touches a room, agent or character sheet; returning to the maze therefore restores it exactly, with only the fight's result changed. ### Camera & input (`Camera/`, `Input/`) - `MazeCameraController` — manual WASD pan / middle-mouse drag, plus `FocusOn` (smooth glide, used only while following an agent) and `FrameBounds` (instant zoom-to-fit, used by the HUD button). The camera does nothing on its own unless something calls one of those. - `MazeInput` — input-backend-agnostic helpers (`ENABLE_INPUT_SYSTEM` vs legacy `Input`), since this project has the Input System package installed and legacy APIs like `OnMouseDown`/`Input.GetKey` silently break if Active Input Handling is set to "Input System Package (New)" only. ## Conventions worth preserving - Rooms/exits are **cardinal-only** by design (not free-angle) — this is what makes "which side faces which room" and 90°-bend hallway routing well-defined. Don't reintroduce arbitrary exit angles without reworking the routing/merge logic that depends on exact cardinal matching. - All visuals (`RoomView`, `ExitMarker`, agent sprites) are intentionally flat placeholder sprites sharing `RoomView.SharedSquareSprite` — swappable for real art later without touching generation logic. - Generation must stay lazy: don't pre-generate rooms beyond an unused exit "just in case." - Anything that advances maze state must tick on `SimulationClock.DeltaTime`, not `Time.deltaTime` — a half-paused simulation is worse than none. Equally, don't "fix" pausing by setting `Time.timeScale`: the watched-fight playback depends on real time still running. - Fight outcomes are decided once, up front, by `CombatResolver` and then replayed. Don't move outcome decisions into `BattleArenaView` or make it mutate character sheets — that's what keeps watched and simulated fights identical, and what guarantees the maze is untouched when a fight ends. - Don't hide the maze during a watched fight by deactivating the maze root GameObject: `SetActive(false)` permanently kills the in-flight agent-spawn coroutines. Suspend the cameras instead.