This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
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:
6000.3.7f1 (or compatible 6000.3.x).Assets/Scenes/SampleScene.unity.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.
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/)Room — a node with Position (free Vector2, not grid-snapped), Radius, Kind (Start/Normal/Hallway/Exit),
and a list of Exits. 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/)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:
fromRoom.Position + exit.Direction * roomSpacing).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.TryRouteAroundObstruction tries
extending further out, then a single 90° bend, before falling back to FindClearPosition (a ring/spiral
search) as a last resort.BuildRoom, rolling cardinal-direction exits (PickCardinalAngles).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.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/)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/)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.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/)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.RoomView, ExitMarker, agent sprites) are intentionally flat placeholder sprites sharing
RoomView.SharedSquareSprite — swappable for real art later without touching generation logic.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.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.SetActive(false)
permanently kills the in-flight agent-spawn coroutines. Suspend the cameras instead.