See README.md for how this doc relates to SPEC.md and the other
subsystem docs. See character.md for the Player entity that
occupies rooms, and persistence.md for how rooms are stored.
Superseded by engine-vs-ruleset.md: Room/Exit/
Area below are no longer dedicated classes — they're Things composed from
RoomBehavior/ExitBehavior/AreaBehavior. This doc still describes the
correct shape (exits, locking, hand-built hub vs. generated frontier); read
engine-vs-ruleset.md for how that shape is actually represented in code now.
Hybrid, per SPEC.md:
- Core/hub area (starting town, key NPCs, tutorial content): hand-authored.
- Wilderness/dungeon frontier: procedurally generated, but generated once and persisted — not regenerated per visit. Once created, a frontier room is saved and stays fixed, exactly like a hand-built room from the player's perspective. This preserves classic MUD navigation muscle memory (N N E E S W means the same thing every time).
The in-memory world model below makes no assumption about which authoring path
produced a given Room — that's what allows moving from hardcoded rooms to a
data-driven JSON format later without disturbing the rest of the engine.
public sealed class Room
{
public RoomId Id { get; init; }
public AreaId AreaId { get; init; }
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public bool IsGenerated { get; init; } // true for procedural frontier rooms
public List<Exit> Exits { get; set; } = [];
public List<ItemId> ItemsOnGround { get; set; } = [];
public List<NpcId> Npcs { get; set; } = [];
}
public sealed class Exit
{
public Direction Direction { get; set; }
public RoomId DestinationRoomId { get; set; }
public bool IsOneWay { get; set; } // no implicit return path assumed
public ExitLockState? Lock { get; set; } // null = not lockable
}
public sealed class ExitLockState
{
public bool IsLocked { get; set; }
public bool IsClosed { get; set; }
public ItemId? RequiredKeyItemId { get; set; }
}
public enum Direction { North, South, East, West, NorthEast, NorthWest, SouthEast, SouthWest, Up, Down }Bidirectional exits are a construction-time convenience, not a runtime
invariant: creating a standard exit creates the matching return Exit on the
destination room, but the model itself makes no assumption that a reverse exit
exists — this is what allows one-way exits and locked doors to be first-class
without special-casing.
public interface IWorld
{
Room? GetRoom(RoomId id);
Player? GetPlayer(PlayerId id);
IEnumerable<Player> PlayersInRoom(RoomId id);
void MovePlayer(Player player, Room from, Room to);
}IWorld.MovePlayer is the single choke point for all player-room transitions
— see the movement walkthrough in commands.md for the full
call sequence, and the frontier-generation walkthrough below for what happens
when a move target doesn't exist yet.
- Hardcoded rooms in C# (bootstrap only, to get the loop working).
- Data-driven world files (JSON/YAML) loaded at startup — natural next step, also the foundation for in-game building commands later.
- In-game building commands (
@dig,@describe, etc.) writing to the same data model.
MoveCommand(see commands.md) resolves an exit whoseDestinationRoomIdpoints into ungenerated frontier space (represented as a coordinate not yet backed by a persistedRoom).- World layer calls the (future, deferred per SPEC.md) frontier generator to
produce a
Roomfor that coordinate. - Generated
Roomis saved viaIRoomRepository.SaveAsync(see persistence.md) withIsGenerated = truebefore the player is moved into it — generation happens once, then it's a normal persisted room forever after. This is the mechanism behind the generate-once-persist decision in SPEC.md.
Grid-based overland map: frontier rooms exist on an implicit (x, y) grid; terrain assigned via noise (Perlin/simplex) for biome variety, room descriptions templated per terrain type. Matches classic MUD "wilderness outside town" convention better than a roguelike dungeon-graph generator would. Still deferred per SPEC.md until after the foundation phase — this is the confirmed approach for when that phase starts, not a v1 commitment.
- Area boundary rules: how/when a frontier coordinate gets assigned to an
AreaIdvs. spanning a borderless wilderness. - Terrain-type template variety needed to avoid repetitive descriptions.
- Noise-function parameters (scale, octaves, seed source) not yet chosen.