diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..13357b66 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,3 @@ +{ + "enableAllProjectMcpServers": true +} diff --git a/.gitignore b/.gitignore index 07dfaa8d..ba738563 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ obj/ *.csproj.user *.suo *.tmp -.vscode \ No newline at end of file +.vscode + +# BenchmarkDotNet +BenchmarkDotNet.Artifacts/ \ No newline at end of file diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..a511ba2d --- /dev/null +++ b/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "rogueelemens": { + "type": "stdio", + "command": "node", + "args": ["mcp-server/dist/index.js"] + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..8ef23c24 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,138 @@ +# RogueElements + +C# library for procedural roguelike map generation using a pipeline architecture. + +## Claude Code Rules + +- **Do not commit without explicit user consent** - Always ask before running `git commit` +- **Do not push without explicit user consent** - Always ask before running `git push` + +## Quick Start + +```bash +# Build +dotnet build RogueElements.sln + +# Test +dotnet test RogueElements.Tests/RogueElements.Tests.csproj + +# Run examples +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +## Architecture + +Pipeline pattern: `MapGen` orchestrates `GenStep` passes that modify `IGenContext`. + +``` +MapGen.GenMap(seed) + → GenStep.Apply(context) // repeated for each step + → GenStep.Apply(context) + → ... + → returns IGenContext +``` + +### Core Abstractions + +| Class/Interface | Purpose | +|-----------------|---------| +| `MapGen` | Orchestrator - holds priority-ordered GenSteps, calls `GenMap(seed)` | +| `GenStep` | Base class for generation passes - implement `Apply(T map)` | +| `IGenContext` | Base interface for map state - provides `Rand`, `InitSeed()`, `FinishGen()` | +| `Priority` | Ordering mechanism for GenSteps (lower = earlier) | +| `PriorityList` | Container holding GenSteps by Priority | + +### Key Context Interfaces + +Implement these to enable specific GenStep types: + +| Interface | Enables | +|-----------|---------| +| `ITiledGenContext` | Tile-based operations (get/set tiles, wall detection) | +| `IFloorPlanGenContext` | Freeform room placement via `FloorPlan` | +| `IRoomGridGenContext` | Grid-based room layouts via `GridPlan` | +| `IPlaceableGenContext` | Spawning entities (items, stairs, mobs) | + +## Key Entry Points + +- `RogueElements/MapGen/MapGen.cs` - Main orchestrator +- `RogueElements/MapGen/GenStep.cs` - Base step class +- `RogueElements.Examples/Program.cs` - Interactive examples runner + +## Directory Guide + +| Directory | Purpose | +|-----------|---------| +| `RogueElements/` | Core library | +| `RogueElements/MapGen/` | Generation pipeline (GenStep, MapGen, contexts) | +| `RogueElements/MapGen/FloorPlan/` | Freeform room-based generation | +| `RogueElements/MapGen/Grid/` | Grid-based room layouts | +| `RogueElements/MapGen/Rooms/` | Room shape generators (RoomGenSquare, RoomGenCave, etc.) | +| `RogueElements/MapGen/Spawning/` | Entity placement (items, stairs, mobs) | +| `RogueElements/MapGen/Tiles/` | Tile manipulation and water generation | +| `RogueElements/Rand/` | RNG utilities (RandRange, SpawnList, noise) | +| `RogueElements/Priority/` | Priority queue for step ordering | +| `RogueElements.Examples/` | 8 progressive examples (Ex1-Ex8) | +| `RogueElements.Tests/` | NUnit tests with Moq | + +## Examples Progression + +| Example | Concept | +|---------|---------| +| Ex1_Tiles | Static tiles, `InitTilesStep` | +| Ex2_Rooms | Freeform rooms via `FloorPlan` | +| Ex3_Grid | Grid-based layouts via `GridPlan` | +| Ex4_Stairs | Stair placement | +| Ex5_Terrain | Water/terrain via Perlin noise | +| Ex6_Items | Item spawning | +| Ex7_Special | Special room placement | +| Ex8_Integration | Full pipeline combining all concepts | + +## Patterns & Conventions + +- **Naming**: PascalCase for all public members, `Step` suffix for GenStep subclasses +- **Generics**: GenSteps constrain `T` to required context interfaces +- **Serialization**: All GenSteps are `[Serializable]` for save/load support +- **Testing**: NUnit + Moq, test files mirror source structure +- **Style**: StyleCop + CodeCracker analyzers enforced + +## Creating Custom Steps + +1. Inherit from `GenStep` where T implements needed interfaces +2. Override `Apply(T map)` with generation logic +3. Add to MapGen via `layout.GenSteps.Add(priority, step)` + +```csharp +public class MyStep : GenStep +{ + public override void Apply(ITiledGenContext map) + { + // modify map.Tiles, use map.Rand for randomness + } +} +``` + +## Creating Custom Contexts + +1. Inherit from `IGenContext` (minimum) +2. Add interfaces as needed (ITiledGenContext, IFloorPlanGenContext, etc.) +3. See `RogueElements.Examples/Common/BaseMapGenContext.cs` for reference + +## Deep Dive Documentation + +For detailed architecture and code flow documentation, see `docs/claude/`: + +| Document | Purpose | +|----------|---------| +| [architecture.md](docs/claude/architecture.md) | Interface hierarchy, GenStep categories, data flow diagrams | +| [flows.md](docs/claude/flows.md) | Traced code paths for key operations | +| [patterns.md](docs/claude/patterns.md) | Step-by-step recipes for common modifications | + +## Debug Support + +```csharp +GenContextDebug.OnInit += handler; // Map initialization +GenContextDebug.OnStep += handler; // Each step execution +GenContextDebug.OnStepIn += handler; // Step entry +GenContextDebug.OnStepOut += handler; // Step exit +``` diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..2ed3b6dc --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,88 @@ +# Code Of Conduct + +## 1. Overview + +The developers elected to govern their interactions with each other, with their clients, and with the larger user community in accordance with the "instruments of good works" from chapter 4 of The Rule of St. Benedict (hereafter: "The Rule"). This Rule has proven its mettle in thousands of diverse communities for over 1,500 years, and has served as a baseline for many civil law codes since the time of Charlemagne. + +The Rule is strict, and none are able to comply perfectly. Grace is readily granted for minor transgressions. All are encouraged to follow this rule closely, as in so doing they may expect to live happier, healthier, and more productive lives. The entire Rule is good and wholesome, and yet we make no enforcement of the more introspective aspects. + +We view The Rule as our promise to all users of this project of how the developers are expected to behave. This is a one-way promise, or covenant. In other words, the developers are saying: "We will treat you this way regardless of how you treat us." + +## 2. The Rule + +1. First of all, love the Lord God with your whole heart, your whole soul, and your whole strength. +2. Then, love your neighbor as yourself. +3. Do not murder. +4. Do not commit adultery. +5. Do not steal. +6. Do not covet. +7. Do not bear false witness. +8. Honor all. +9. Do not do to another what you would not have done to yourself. +10. Deny oneself in order to follow Christ. +11. Chastise the body. +12. Do not become attached to pleasures. +13. Love fasting. +14. Relieve the poor. +15. Clothe the naked. +16. Visit the sick. +17. Bury the dead. +18. Be a help in times of trouble. +19. Console the sorrowing. +20. Be a stranger to the world's ways. +21. Prefer nothing more than the love of Christ. +22. Do not give way to anger. +23. Do not nurse a grudge. +24. Do not entertain deceit in your heart. +25. Do not give a false peace. +26. Do not forsake charity. +27. Do not swear, for fear of perjuring yourself. +28. Utter only truth from heart and mouth. +29. Do not return evil for evil. +30. Do no wrong to anyone, and bear patiently wrongs done to yourself. +31. Love your enemies. +32. Do not curse those who curse you, but rather bless them. +33. Bear persecution for justice's sake. +34. Be not proud. +35. Be not addicted to wine. +36. Be not a great eater. +37. Be not drowsy. +38. Be not lazy. +39. Be not a grumbler. +40. Be not a detractor. +41. Put your hope in God. +42. Attribute to God, and not to self, whatever good you see in yourself. +43. Recognize always that evil is your own doing, and to impute it to yourself. +44. Fear the Day of Judgment. +45. Be in dread of hell. +46. Desire eternal life with all the passion of the spirit. +47. Keep death daily before your eyes. +48. Keep constant guard over the actions of your life. +49. Know for certain that God sees you everywhere. +50. When wrongful thoughts come into your heart, dash them against Christ immediately. +51. Disclose wrongful thoughts to your spiritual mentor. +52. Guard your tongue against evil and depraved speech. +53. Do not love much talking. +54. Speak no useless words or words that move to laughter. +55. Do not love much or boisterous laughter. +56. Listen willingly to holy reading. +57. Devote yourself frequently to prayer. +58. Daily in your prayers, with tears and sighs, confess your past sins to God, and amend them for the future. +59. Fulfill not the desires of the flesh; hate your own will. +60. Obey in all things the commands of those whom God has placed in authority over you even though they (which God forbid) should act otherwise, mindful of the Lord's precept, "Do what they say, but not what they do." +61. Do not wish to be called holy before one is holy; but first to be holy, that you may be truly so called. +62. Fulfill God's commandments daily in your deeds. +63. Love chastity. +64. Hate no one. +65. Be not jealous, nor harbor envy. +66. Do not love quarreling. +67. Shun arrogance. +68. Respect your seniors. +69. Love your juniors. +70. Pray for your enemies in the love of Christ. +71. Make peace with your adversary before the sun sets. +72. Never despair of God's mercy. + +## 3. Attribution + +This code of conduct was taken from [The rule of St. Benedict, as your Code of Conduct](https://github.com/saint-benedict/code-of-conduct) which in turn was adapted from [SQLite's Code of conduct](https://web.archive.org/web/20181024103452/https://sqlite.org/codeofconduct.html) and now [Code of Ethics](https://sqlite.org/codeofethics.html). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..30a5d430 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing to RogueElements + +First off, **thank you** for considering contributing! I truly believe in open source and the power of community collaboration. Unlike many repositories, I actively welcome contributions of all kinds - from bug fixes to new features. + +## My Promise to Contributors + +- **I will respond to every PR and issue** - I guarantee feedback on all contributions +- **Bug fixes are obvious accepts** - If it fixes a bug, it's getting merged +- **New features are welcome** - I'm genuinely open to new ideas and enhancements +- **Direct line of communication** - If I'm not responding to a PR or issue, email me directly at johnvondrashek@gmail.com + +## Ways to Contribute + +### Bug Fixes +Found a bug in dungeon generation? Room placement acting strange? Stairs spawning in walls? Open a PR - these are obvious accepts. + +### New Features +Ideas that would be great additions: +- New `GenStep` implementations (terrain types, room shapes, spawning strategies) +- Additional `RoomGen` variants for different dungeon aesthetics +- Improved corridor algorithms +- New utility classes for procedural generation + +### Documentation +- Example tutorials (Ex9 and beyond!) +- Integration guides for game engines (Unity, Godot, MonoGame) +- API documentation improvements + +### Tests +More test coverage is always welcome. The project uses NUnit with Moq. + +## Getting Started + +```bash +# Clone your fork +git clone https://github.com/YOUR_USERNAME/RogueElements.git + +# Build +dotnet build RogueElements.sln + +# Run tests +dotnet test RogueElements.Tests/RogueElements.Tests.csproj + +# Run examples to understand the library +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +## Pull Request Guidelines + +1. **Target `master` branch** for most contributions +2. **Include tests** for new functionality when possible +3. **Follow existing code style** - the project uses StyleCop and CodeCracker analyzers +4. **Keep commits focused** - one logical change per commit +5. **Write clear commit messages** describing the "why" + +## Creating Custom GenSteps + +If you're adding a new generation step, follow this pattern: + +```csharp +[Serializable] +public class MyCustomStep : GenStep + where T : /* required context interfaces */ +{ + public override void Apply(T map) + { + // Your generation logic here + // Use map.Rand for deterministic randomness + } +} +``` + +## Code of Conduct + +This project follows the [Rule of St. Benedict](CODE_OF_CONDUCT.md) as its code of conduct. + +## Questions? + +- Open an issue +- Email: johnvondrashek@gmail.com diff --git a/README.md b/README.md index d91da1db..f57bfb04 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,148 @@ -# RogueElements # -[![Build Status](https://travis-ci.org/audinowho/RogueElements.svg?branch=master)](https://travis-ci.org/audinowho/RogueElements) +# RogueElements -RogueElements is a C# library that allows the user to randomly generate maps for use in roguelikes. Generation is implemented in a series of interchangeable steps, similar to shader passes. These steps all share a base class, which the user can inherit to make their own steps. Additionally, RogueElements contains a collection of functions designed to make working with 4-directional and 8-directional tile maps more convenient. +[![NuGet](https://img.shields.io/nuget/v/RogueElements.svg)](https://www.nuget.org/packages/RogueElements/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-blue.svg)](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) -There exist a large base of unit tests that serve to cover the basic functions of the library. +**Procedural roguelike map generation library for C#.** Generate dungeons with rooms, corridors, items, enemies, and terrain using a flexible pipeline architecture. -RogueElements does NOT provide a base engine for the gameplay of an actual roguelike; that's for the game developers themselves to decide on. A map generation context specified by the developer is all that is needed to integrate the library with their game. It will inherit all interfaces that the developer is interested in to allow the correct steps to apply to it. +

RogueElements Debug View

+## Features -The presence of diagnostic methods also makes it easy to set breakpoints and view the entire map state at a given time: +- **Pipeline Architecture** - Chain generation steps like shader passes +- **Game-Agnostic** - Integrate with any game engine (Unity, MonoGame, Godot, etc.) +- **Two Layout Modes** - Freeform rooms or grid-based dungeons +- **Extensible** - Create custom room shapes, paths, and spawning logic +- **Deterministic** - Seed-based generation for reproducible maps +- **Well-Tested** - Comprehensive unit test coverage -

+## Quick Start -# Overview # +```bash +dotnet add package RogueElements +``` +```csharp +// 1. Create your map context implementing IGenContext interfaces +public class MyMapContext : ITiledGenContext, IFloorPlanGenContext +{ + // ... implement required members +} -The library revolves around 3 major classes: -`MapGen`, `GenStep`, and `IGenContext`. +// 2. Build a generation pipeline +var mapGen = new MapGen(); -`IGenContext` is an interface that represents the map you wish to generate. Implement it with your own user-defined `MapGenContext` class so that it can be passed into RogueElements's other classes. Other interfaces in RogueElements inherit from `IGenContext`, and specify more features that RogueElements will be allowed to interact with. For example, implementing `ITiledGenContext` indicates that your `MapGenContext` class has tiles that can be get and set. +// Initialize tiles +mapGen.GenSteps.Add(-1, new InitTilesStep(50, 50)); -`GenStep` is a class with a single Apply function, which will perform an operation on any IGenContext passed in (specified by its class parameter). Many `GenStep`s have constraints on what kind of `IGenContext` they will accept. For example, `PerlinWaterStep` will randomly generate user-specified water terrain on the map using Perlin Noise, but it only allows classes implementing `ITiledGenContext` as its parameter. +// Create room layout +mapGen.GenSteps.Add(0, new InitFloorPlanStep()); +mapGen.GenSteps.Add(1, new FloorPathBranch()); +mapGen.GenSteps.Add(2, new ConnectRoomStep()); -`MapGen` is the class that generates the map. Add `GenStep`s to the GenSteps list, then call the method `GenMap()` to output a `MapGenContext`. +// Render to tiles +mapGen.GenSteps.Add(3, new DrawFloorToTileStep()); +// 3. Generate! +MyMapContext map = mapGen.GenMap(seed: 12345); +``` -The flow of map generation resembles a shader pipeline: +## How It Works -

+RogueElements uses a **pipeline pattern** where `GenStep` operations progressively build up map state: -An example map generation pipeline. The `GenStep`s can be swapped in and out. +

Pipeline Diagram

-* `InitFloorPlanStep`: Initializes a list of rooms (A `FloorPlan`). -* `FloorPathBranch`: Creates the shape of the path of rooms in the grid as a minimum spanning tree. -* `ConnectRoomStep`: Randomly connects adjacent rooms in the `FloorPlan`. -* `DrawFloorToTileStep`: Draws the list of freehand rooms onto the actual map tiles. -* `FloorStairsStep`: For adding an up and down stairs to your map. You must provide the StairsUp and StairsDown classes. -* `PerlinWaterStep`: For generating water patterns on your map using Perlin Noise. -* `RandomSpawnStep`: For distributing items across the floor in a random pattern. You must provide the Item class. -* `RandomSpawnStep`: For distributing items across the floor in a random pattern. You must provide the Mob class. +### Core Classes +| Class | Purpose | +|-------|---------| +| `MapGen` | Orchestrates the generation pipeline | +| `GenStep` | Base class for generation operations | +| `IGenContext` | Interface for map state containers | -RogueElements.Examples contains examples of how to set up a `MapGen`. Each example builds on the previous one. +### Generation Steps +| Step | Description | +|------|-------------| +| `InitFloorPlanStep` | Initialize room list | +| `FloorPathBranch` | Create branching room paths | +| `ConnectRoomStep` | Add extra room connections | +| `DrawFloorToTileStep` | Render rooms to tiles | +| `FloorStairsStep` | Place entrance/exit stairs | +| `PerlinWaterStep` | Generate water terrain | +| `RandomSpawnStep` | Distribute items and enemies | +## Layout Modes -# Credits # +### Freeform (FloorPlan) +Rooms placed freely with flexible positioning. Best for organic, cave-like dungeons. -- [Brogue](https://sites.google.com/site/broguegame/): A major inspiration in itemizing steps to generate dungeon maps. -- [Spike Chunsoft Mystery Dungeon Series](http://www.spike-chunsoft.co.jp/) - Several floor layouts used as a reference for grid-based floor steps. -- [RogueSharp](https://bitbucket.org/FaronBracy/roguesharp) - A C# library dedicated to creating a full roguelike, used as an example for integrating RogueElements. +### Grid-Based (GridPlan) +Rooms aligned to a grid with cardinal connections. Best for structured, traditional dungeons. +``` +Grid Layout Example: +┌───┐ ┌───┐ ┌───┐ +│ A │───│ B │───│ C │ +└───┘ └─┬─┘ └───┘ + │ + ┌─┴─┐ + │ D │ + └───┘ +``` + +## Examples + +The `RogueElements.Examples` project contains 8 progressive tutorials: + +```bash +dotnet run --project RogueElements.Examples +``` + +| Example | Concept | +|---------|---------| +| Ex1 | Basic tiles | +| Ex2 | Freeform rooms | +| Ex3 | Grid layouts | +| Ex4 | Stairs placement | +| Ex5 | Water/terrain | +| Ex6 | Item spawning | +| Ex7 | Special rooms | +| Ex8 | Full integration | + +See [RogueElements.Examples/README.md](RogueElements.Examples/README.md) for detailed walkthroughs. + +## Documentation + +Each folder contains its own README with detailed documentation: + +- [**RogueElements/**](RogueElements/README.md) - Core library architecture +- [**MapGen/**](RogueElements/MapGen/README.md) - Pipeline and GenStep system +- [**FloorPlan/**](RogueElements/MapGen/FloorPlan/README.md) - Freeform room generation +- [**Grid/**](RogueElements/MapGen/Grid/README.md) - Grid-based layouts +- [**Spawning/**](RogueElements/MapGen/Spawning/README.md) - Entity placement +- [**Rand/**](RogueElements/Rand/README.md) - RNG and weighted selection + +## Integration Examples + +RogueElements integrates with popular game libraries: + +- **[RogueSharp](https://bitbucket.org/FaronBracy/roguesharp)** - See Ex8_Integration for IMapCreationStrategy pattern +- **Unity** - Implement context interfaces in MonoBehaviour +- **MonoGame** - Direct integration with tile-based rendering + +## Credits + +- [**Brogue**](https://sites.google.com/site/broguegame/) - Inspiration for step-based dungeon generation +- [**Spike Chunsoft Mystery Dungeon**](http://www.spike-chunsoft.co.jp/) - Reference for grid-based floor layouts +- [**RogueSharp**](https://bitbucket.org/FaronBracy/roguesharp) - C# roguelike library integration example + +## License + +MIT License - see [LICENSE](LICENSE) for details. + +--- + +![Repobeats](https://repobeats.axiom.co/api/embed/your-hash-here.svg "Repobeats analytics image") diff --git a/RogueElements.Benchmarks/BenchmarkContext.cs b/RogueElements.Benchmarks/BenchmarkContext.cs new file mode 100644 index 00000000..a1a8bb36 --- /dev/null +++ b/RogueElements.Benchmarks/BenchmarkContext.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) Audino +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace RogueElements.Benchmarks; + +/// +/// Simple tile wrapper for benchmarking. +/// +public class BenchmarkTile : ITile +{ + public const int WALL_ID = 0; + public const int ROOM_ID = 1; + public const int WATER_ID = 2; + + public int ID { get; set; } + + public BenchmarkTile() + { + ID = WALL_ID; + } + + public BenchmarkTile(int id) + { + ID = id; + } + + public ITile Copy() => new BenchmarkTile(ID); + + public bool TileEquivalent(ITile other) => other is BenchmarkTile tile && tile.ID == ID; +} + +/// +/// Map storage for benchmarking. +/// +public class BenchmarkMap +{ + public BenchmarkTile[][] Tiles { get; private set; } = null!; + public int Width => Tiles?.Length ?? 0; + public int Height => Tiles?.Length > 0 ? Tiles[0].Length : 0; + public ReRandom Rand { get; set; } = null!; + + public void InitializeTiles(int width, int height) + { + Tiles = new BenchmarkTile[width][]; + for (int x = 0; x < width; x++) + { + Tiles[x] = new BenchmarkTile[height]; + for (int y = 0; y < height; y++) + { + Tiles[x][y] = new BenchmarkTile(BenchmarkTile.WALL_ID); + } + } + } +} + +/// +/// Full-featured map generation context for benchmarking grid-based generation. +/// Implements IRoomGridGenContext to support the complete generation pipeline. +/// +public class BenchmarkMapGenContext : ITiledGenContext, IRoomGridGenContext +{ + public BenchmarkMap Map { get; set; } + + public ITile RoomTerrain => new BenchmarkTile(BenchmarkTile.ROOM_ID); + public ITile WallTerrain => new BenchmarkTile(BenchmarkTile.WALL_ID); + public bool TilesInitialized => Map.Tiles != null; + public int Width => Map.Width; + public int Height => Map.Height; + public bool Wrap => false; + public IRandom Rand => Map.Rand; + public FloorPlan RoomPlan { get; private set; } = null!; + public GridPlan GridPlan { get; private set; } = null!; + + public BenchmarkMapGenContext() + { + Map = new BenchmarkMap(); + } + + public ITile GetTile(Loc loc) => Map.Tiles[loc.X][loc.Y]; + + public bool CanSetTile(Loc loc, ITile tile) => true; + + public bool TrySetTile(Loc loc, ITile tile) + { + if (!CanSetTile(loc, tile)) + return false; + Map.Tiles[loc.X][loc.Y] = (BenchmarkTile)tile; + return true; + } + + public void SetTile(Loc loc, ITile tile) + { + if (!TrySetTile(loc, tile)) + throw new InvalidOperationException("Can't place tile!"); + } + + public void InitSeed(ulong seed) + { + Map.Rand = new ReRandom(seed); + } + + public bool TileBlocked(Loc loc) => Map.Tiles[loc.X][loc.Y].ID == BenchmarkTile.WALL_ID; + + public bool TileBlocked(Loc loc, bool diagonal) => Map.Tiles[loc.X][loc.Y].ID == BenchmarkTile.WALL_ID; + + public void CreateNew(int width, int height, bool wrap = false) + { + Map.InitializeTiles(width, height); + } + + public void FinishGen() + { + } + + public void InitPlan(FloorPlan plan) + { + RoomPlan = plan; + } + + public void InitGrid(GridPlan plan) + { + GridPlan = plan; + } +} diff --git a/RogueElements.Benchmarks/CollisionBenchmarks.cs b/RogueElements.Benchmarks/CollisionBenchmarks.cs new file mode 100644 index 00000000..6a938df8 --- /dev/null +++ b/RogueElements.Benchmarks/CollisionBenchmarks.cs @@ -0,0 +1,160 @@ +// +// Copyright (c) Audino +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using BenchmarkDotNet.Attributes; + +namespace RogueElements.Benchmarks; + +/// +/// Benchmarks for FloorPlan collision detection. +/// Measures how collision checking scales with room count. +/// This is a CRITICAL hotspot - currently O(rooms + halls) per check. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class CollisionBenchmarks +{ + private FloorPlan _smallFloorPlan = null!; + private FloorPlan _mediumFloorPlan = null!; + private FloorPlan _largeFloorPlan = null!; + private Rect _testRect; + + [GlobalSetup] + public void Setup() + { + _testRect = new Rect(50, 50, 5, 5); + + // Small: 10 rooms + _smallFloorPlan = CreateFloorPlanWithRooms(10); + + // Medium: 50 rooms + _mediumFloorPlan = CreateFloorPlanWithRooms(50); + + // Large: 200 rooms + _largeFloorPlan = CreateFloorPlanWithRooms(200); + } + + private static FloorPlan CreateFloorPlanWithRooms(int roomCount) + { + var plan = new FloorPlan(); + plan.InitSize(new Loc(500, 500)); + + var rand = new ReRandom(12345UL); + + // Add rooms in a grid pattern to avoid collisions during setup + int gridSize = (int)Math.Ceiling(Math.Sqrt(roomCount)); + int cellSize = 500 / gridSize; + + for (int i = 0; i < roomCount; i++) + { + int gridX = i % gridSize; + int gridY = i / gridSize; + + int x = gridX * cellSize + 2; + int y = gridY * cellSize + 2; + int width = Math.Min(cellSize - 4, rand.Next(4, 10)); + int height = Math.Min(cellSize - 4, rand.Next(4, 10)); + + var roomGen = new RoomGenSquare( + new RandRange(width, width + 1), + new RandRange(height, height + 1)); + roomGen.PrepareSize(rand, new Loc(width, height)); + roomGen.SetLoc(new Loc(x, y)); + + plan.AddRoom(roomGen, new ComponentCollection()); + } + + return plan; + } + + [Benchmark(Baseline = true)] + public List CheckCollision_10Rooms() + { + return _smallFloorPlan.CheckCollision(_testRect); + } + + [Benchmark] + public List CheckCollision_50Rooms() + { + return _mediumFloorPlan.CheckCollision(_testRect); + } + + [Benchmark] + public List CheckCollision_200Rooms() + { + return _largeFloorPlan.CheckCollision(_testRect); + } +} + +/// +/// Benchmarks for AddRoom collision validation. +/// Each AddRoom call checks against ALL existing rooms. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class AddRoomCollisionBenchmarks +{ + private ReRandom _rand = null!; + + [GlobalSetup] + public void Setup() + { + _rand = new ReRandom(12345UL); + } + + [Benchmark(Baseline = true)] + public FloorPlan AddRooms_10() + { + return AddRoomsToFloorPlan(10); + } + + [Benchmark] + public FloorPlan AddRooms_25() + { + return AddRoomsToFloorPlan(25); + } + + [Benchmark] + public FloorPlan AddRooms_50() + { + return AddRoomsToFloorPlan(50); + } + + [Benchmark] + public FloorPlan AddRooms_100() + { + return AddRoomsToFloorPlan(100); + } + + private FloorPlan AddRoomsToFloorPlan(int roomCount) + { + var plan = new FloorPlan(); + plan.InitSize(new Loc(500, 500)); + + int gridSize = (int)Math.Ceiling(Math.Sqrt(roomCount)); + int cellSize = 500 / gridSize; + + for (int i = 0; i < roomCount; i++) + { + int gridX = i % gridSize; + int gridY = i / gridSize; + + int x = gridX * cellSize + 2; + int y = gridY * cellSize + 2; + int width = Math.Min(cellSize - 4, 6); + int height = Math.Min(cellSize - 4, 6); + + var roomGen = new RoomGenSquare( + new RandRange(width, width + 1), + new RandRange(height, height + 1)); + roomGen.PrepareSize(_rand, new Loc(width, height)); + roomGen.SetLoc(new Loc(x, y)); + + plan.AddRoom(roomGen, new ComponentCollection()); + } + + return plan; + } +} diff --git a/RogueElements.Benchmarks/FloorPlanBenchmarks.cs b/RogueElements.Benchmarks/FloorPlanBenchmarks.cs new file mode 100644 index 00000000..38115efe --- /dev/null +++ b/RogueElements.Benchmarks/FloorPlanBenchmarks.cs @@ -0,0 +1,307 @@ +// +// Copyright (c) Audino +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using BenchmarkDotNet.Attributes; + +namespace RogueElements.Benchmarks; + +/// +/// Benchmarks for FloorPlan room erasure operations. +/// EraseRoomHall updates ALL room/hall indices - O(rooms + halls) per erasure. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class EraseRoomBenchmarks +{ + private ReRandom _rand = null!; + + [GlobalSetup] + public void Setup() + { + _rand = new ReRandom(12345UL); + } + + /// + /// Erase 5 rooms from a 20-room floor plan. + /// Each erase is O(remaining rooms + halls). + /// + [Benchmark(Baseline = true)] + public FloorPlan EraseRooms_5from20() + { + var plan = CreateFloorPlanWithConnectedRooms(20); + // Erase from end to avoid index shifting issues in test + for (int i = 0; i < 5; i++) + { + plan.EraseRoomHall(new RoomHallIndex(plan.RoomCount - 1, false)); + } + return plan; + } + + /// + /// Erase 10 rooms from a 50-room floor plan. + /// + [Benchmark] + public FloorPlan EraseRooms_10from50() + { + var plan = CreateFloorPlanWithConnectedRooms(50); + for (int i = 0; i < 10; i++) + { + plan.EraseRoomHall(new RoomHallIndex(plan.RoomCount - 1, false)); + } + return plan; + } + + /// + /// Erase 20 rooms from a 100-room floor plan. + /// This demonstrates quadratic behavior: 20 × O(80..100) = O(1800) operations. + /// + [Benchmark] + public FloorPlan EraseRooms_20from100() + { + var plan = CreateFloorPlanWithConnectedRooms(100); + for (int i = 0; i < 20; i++) + { + plan.EraseRoomHall(new RoomHallIndex(plan.RoomCount - 1, false)); + } + return plan; + } + + private FloorPlan CreateFloorPlanWithConnectedRooms(int roomCount) + { + var plan = new FloorPlan(); + plan.InitSize(new Loc(500, 500)); + + int gridSize = (int)Math.Ceiling(Math.Sqrt(roomCount)); + int cellSize = 500 / gridSize; + int roomSize = Math.Max(4, cellSize / 3); + + for (int i = 0; i < roomCount; i++) + { + int gridX = i % gridSize; + int gridY = i / gridSize; + + int x = gridX * cellSize + (cellSize - roomSize) / 2; + int y = gridY * cellSize + (cellSize - roomSize) / 2; + + var roomGen = new RoomGenSquare( + new RandRange(roomSize, roomSize + 1), + new RandRange(roomSize, roomSize + 1)); + roomGen.PrepareSize(_rand, new Loc(roomSize, roomSize)); + roomGen.SetLoc(new Loc(x, y)); + + // Connect to previous room if exists + if (i > 0) + { + plan.AddRoom(roomGen, new ComponentCollection(), new RoomHallIndex(i - 1, false)); + } + else + { + plan.AddRoom(roomGen, new ComponentCollection()); + } + } + + return plan; + } +} + +/// +/// Benchmarks for FloorPlan.DrawOnMap which iterates all rooms and halls. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class DrawFloorPlanBenchmarks +{ + private BenchmarkMapGenContext _context = null!; + private FloorPlan _smallPlan = null!; + private FloorPlan _mediumPlan = null!; + private FloorPlan _largePlan = null!; + + [GlobalSetup] + public void Setup() + { + _context = new BenchmarkMapGenContext(); + _context.InitSeed(12345UL); + _context.CreateNew(200, 200); + + var rand = new ReRandom(12345UL); + _smallPlan = CreateDrawableFloorPlan(rand, 10, 200, 200); + _mediumPlan = CreateDrawableFloorPlan(rand, 30, 200, 200); + _largePlan = CreateDrawableFloorPlan(rand, 60, 200, 200); + } + + private static FloorPlan CreateDrawableFloorPlan(ReRandom rand, int roomCount, int width, int height) + { + var plan = new FloorPlan(); + plan.InitSize(new Loc(width, height)); + + int gridSize = (int)Math.Ceiling(Math.Sqrt(roomCount)); + int cellSize = Math.Min(width, height) / gridSize; + int roomSize = Math.Max(4, cellSize / 2); + + for (int i = 0; i < roomCount; i++) + { + int gridX = i % gridSize; + int gridY = i / gridSize; + + int x = gridX * cellSize + 2; + int y = gridY * cellSize + 2; + + var roomGen = new RoomGenSquare( + new RandRange(roomSize, roomSize + 1), + new RandRange(roomSize, roomSize + 1)); + roomGen.PrepareSize(rand, new Loc(roomSize, roomSize)); + roomGen.SetLoc(new Loc(x, y)); + + if (i > 0) + { + // Add hall connecting to previous room + int prevGridX = (i - 1) % gridSize; + int prevGridY = (i - 1) / gridSize; + + if (gridX != prevGridX || gridY != prevGridY) + { + // Create a connecting hall + int hallX = Math.Min(x, prevGridX * cellSize + 2 + roomSize); + int hallY = Math.Min(y, prevGridY * cellSize + 2 + roomSize); + + var hallGen = new RoomGenSquare( + new RandRange(2, 3), + new RandRange(2, 3)); + hallGen.PrepareSize(rand, new Loc(2, 2)); + hallGen.SetLoc(new Loc(hallX, hallY)); + + plan.AddHall(hallGen, new ComponentCollection(), new RoomHallIndex(i - 1, false)); + } + + plan.AddRoom(roomGen, new ComponentCollection(), new RoomHallIndex(i - 1, false)); + } + else + { + plan.AddRoom(roomGen, new ComponentCollection()); + } + } + + return plan; + } + + [Benchmark(Baseline = true)] + public void DrawFloorPlan_10Rooms() + { + ResetContext(); + _smallPlan.DrawOnMap(_context); + } + + [Benchmark] + public void DrawFloorPlan_30Rooms() + { + ResetContext(); + _mediumPlan.DrawOnMap(_context); + } + + [Benchmark] + public void DrawFloorPlan_60Rooms() + { + ResetContext(); + _largePlan.DrawOnMap(_context); + } + + private void ResetContext() + { + // Fill with walls before each draw + for (int x = 0; x < _context.Width; x++) + { + for (int y = 0; y < _context.Height; y++) + { + _context.Map.Tiles[x][y] = new BenchmarkTile(BenchmarkTile.WALL_ID); + } + } + } +} + +/// +/// Benchmarks for GetDirAdjacent which tries all 4 directions. +/// Called repeatedly during DrawOnMap for border negotiation. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class AdjacencyLookupBenchmarks +{ + private FloorPlan _plan = null!; + private RoomHallIndex _centerRoom; + + [GlobalSetup] + public void Setup() + { + _plan = CreateConnectedFloorPlan(25); + _centerRoom = new RoomHallIndex(12, false); // Center room in 5x5 grid + } + + private static FloorPlan CreateConnectedFloorPlan(int roomCount) + { + var plan = new FloorPlan(); + plan.InitSize(new Loc(200, 200)); + var rand = new ReRandom(12345UL); + + int gridSize = (int)Math.Ceiling(Math.Sqrt(roomCount)); + int cellSize = 200 / gridSize; + int roomSize = cellSize - 4; + + for (int i = 0; i < roomCount; i++) + { + int gridX = i % gridSize; + int gridY = i / gridSize; + + int x = gridX * cellSize + 2; + int y = gridY * cellSize + 2; + + var roomGen = new RoomGenSquare( + new RandRange(roomSize, roomSize + 1), + new RandRange(roomSize, roomSize + 1)); + roomGen.PrepareSize(rand, new Loc(roomSize, roomSize)); + roomGen.SetLoc(new Loc(x, y)); + + // Connect horizontally + if (gridX > 0) + { + plan.AddRoom(roomGen, new ComponentCollection(), new RoomHallIndex(i - 1, false)); + } + // Connect vertically + else if (gridY > 0) + { + plan.AddRoom(roomGen, new ComponentCollection(), new RoomHallIndex(i - gridSize, false)); + } + else + { + plan.AddRoom(roomGen, new ComponentCollection()); + } + } + + return plan; + } + + [Benchmark] + public int GetAdjacents_AllRooms() + { + int totalAdjacents = 0; + for (int i = 0; i < _plan.RoomCount; i++) + { + var room = _plan.GetRoomHall(new RoomHallIndex(i, false)); + totalAdjacents += room.Adjacents.Count; + } + return totalAdjacents; + } + + [Benchmark] + public List GetAdjacentRoomHalls_SingleRoom() + { + var adjacents = new List(); + var room = _plan.GetRoomHall(_centerRoom); + foreach (var adj in room.Adjacents) + { + adjacents.Add(adj.Index); + } + return adjacents; + } +} diff --git a/RogueElements.Benchmarks/GridPlanBenchmarks.cs b/RogueElements.Benchmarks/GridPlanBenchmarks.cs new file mode 100644 index 00000000..241a38df --- /dev/null +++ b/RogueElements.Benchmarks/GridPlanBenchmarks.cs @@ -0,0 +1,325 @@ +// +// Copyright (c) Audino +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using BenchmarkDotNet.Attributes; + +namespace RogueElements.Benchmarks; + +/// +/// Benchmarks for GridPlan.GetAdjacentRooms. +/// Currently uses List.Contains which is O(n) - should use HashSet for O(1). +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class GridAdjacencyBenchmarks +{ + private GridPlan _smallGrid = null!; + private GridPlan _mediumGrid = null!; + private GridPlan _largeGrid = null!; + + [GlobalSetup] + public void Setup() + { + // Small: 4x4 grid + _smallGrid = CreateGridPlan(4, 4); + + // Medium: 8x8 grid + _mediumGrid = CreateGridPlan(8, 8); + + // Large: 12x12 grid + _largeGrid = CreateGridPlan(12, 12); + } + + private static GridPlan CreateGridPlan(int width, int height) + { + var plan = new GridPlan(); + plan.InitSize(width, height, 8, 8, 1); + + var rand = new ReRandom(12345UL); + var roomGen = new RoomGenSquare( + new RandRange(4, 6), new RandRange(4, 6)); + var hallGen = new RoomGenAngledHall(50); + + // Add rooms in a connected pattern + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + plan.AddRoom(new Loc(x, y), roomGen.Copy(), new ComponentCollection()); + + // Add horizontal halls + if (x > 0) + { + plan.SetHall(new LocRay4(x, y, Dir4.Left), (IPermissiveRoomGen)hallGen.Copy(), new ComponentCollection()); + } + + // Add vertical halls + if (y > 0) + { + plan.SetHall(new LocRay4(x, y, Dir4.Up), (IPermissiveRoomGen)hallGen.Copy(), new ComponentCollection()); + } + } + } + + return plan; + } + + [Benchmark(Baseline = true)] + public int GetAdjacentRooms_4x4Grid() + { + int total = 0; + for (int i = 0; i < _smallGrid.RoomCount; i++) + { + total += _smallGrid.GetAdjacentRooms(i).Count; + } + return total; + } + + [Benchmark] + public int GetAdjacentRooms_8x8Grid() + { + int total = 0; + for (int i = 0; i < _mediumGrid.RoomCount; i++) + { + total += _mediumGrid.GetAdjacentRooms(i).Count; + } + return total; + } + + [Benchmark] + public int GetAdjacentRooms_12x12Grid() + { + int total = 0; + for (int i = 0; i < _largeGrid.RoomCount; i++) + { + total += _largeGrid.GetAdjacentRooms(i).Count; + } + return total; + } + + /// + /// Simulates the current List.Contains approach for comparison. + /// + [Benchmark] + public List GetAdjacentRooms_ListContains_SingleRoom() + { + return _mediumGrid.GetAdjacentRooms(_mediumGrid.RoomCount / 2); + } + + /// + /// Simulates optimized HashSet approach. + /// + [Benchmark] + public HashSet GetAdjacentRooms_HashSet_SingleRoom() + { + return GetAdjacentRoomsWithHashSet(_mediumGrid, _mediumGrid.RoomCount / 2); + } + + private static HashSet GetAdjacentRoomsWithHashSet(GridPlan plan, int roomIndex) + { + var returnSet = new HashSet(); + var room = plan.GetRoomPlan(roomIndex); + if (room == null) + return returnSet; + + var bounds = room.Bounds; + + for (int ii = 0; ii < bounds.Size.X; ii++) + { + // above + int up = plan.GetRoomIndex(new LocRay4(bounds.X + ii, bounds.Y, Dir4.Up)); + if (up > -1) + returnSet.Add(up); + + // below + int down = plan.GetRoomIndex(new LocRay4(bounds.X + ii, bounds.End.Y - 1, Dir4.Down)); + if (down > -1) + returnSet.Add(down); + } + + for (int ii = 0; ii < bounds.Size.Y; ii++) + { + // left + int left = plan.GetRoomIndex(new LocRay4(bounds.X, bounds.Y + ii, Dir4.Left)); + if (left > -1) + returnSet.Add(left); + + // right + int right = plan.GetRoomIndex(new LocRay4(bounds.End.X - 1, bounds.Y + ii, Dir4.Right)); + if (right > -1) + returnSet.Add(right); + } + + return returnSet; + } +} + +/// +/// Benchmarks for GridPlan room erasure operations. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class GridEraseRoomBenchmarks +{ + private ReRandom _rand = null!; + + [GlobalSetup] + public void Setup() + { + _rand = new ReRandom(12345UL); + } + + [Benchmark(Baseline = true)] + public GridPlan EraseRooms_4from16() + { + var plan = CreateFullGrid(4, 4); + // Erase corner rooms + plan.EraseRoom(new Loc(3, 3)); + plan.EraseRoom(new Loc(3, 0)); + plan.EraseRoom(new Loc(0, 3)); + plan.EraseRoom(new Loc(0, 0)); + return plan; + } + + [Benchmark] + public GridPlan EraseRooms_9from64() + { + var plan = CreateFullGrid(8, 8); + // Erase a 3x3 section + for (int x = 2; x < 5; x++) + { + for (int y = 2; y < 5; y++) + { + plan.EraseRoom(new Loc(x, y)); + } + } + return plan; + } + + [Benchmark] + public GridPlan EraseRooms_16from144() + { + var plan = CreateFullGrid(12, 12); + // Erase a 4x4 section + for (int x = 4; x < 8; x++) + { + for (int y = 4; y < 8; y++) + { + plan.EraseRoom(new Loc(x, y)); + } + } + return plan; + } + + private GridPlan CreateFullGrid(int width, int height) + { + var plan = new GridPlan(); + plan.InitSize(width, height, 8, 8, 1); + + var roomGen = new RoomGenSquare( + new RandRange(4, 6), new RandRange(4, 6)); + + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + plan.AddRoom(new Loc(x, y), roomGen.Copy(), new ComponentCollection()); + } + } + + return plan; + } +} + +/// +/// Benchmarks for GridPlan to FloorPlan conversion. +/// PlaceRoomsOnFloor is a complex operation with multiple phases. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class GridToFloorConversionBenchmarks +{ + private BenchmarkMapGenContext _smallContext = null!; + private BenchmarkMapGenContext _mediumContext = null!; + private BenchmarkMapGenContext _largeContext = null!; + + [GlobalSetup] + public void Setup() + { + _smallContext = CreateContextWithGrid(4, 4); + _mediumContext = CreateContextWithGrid(8, 6); + _largeContext = CreateContextWithGrid(12, 10); + } + + private static BenchmarkMapGenContext CreateContextWithGrid(int gridWidth, int gridHeight) + { + var context = new BenchmarkMapGenContext(); + context.InitSeed(12345UL); + + var gridPlan = new GridPlan(); + gridPlan.InitSize(gridWidth, gridHeight, 10, 10, 2); + context.InitGrid(gridPlan); + + var rand = new ReRandom(12345UL); + var roomGen = new RoomGenSquare( + new RandRange(5, 8), new RandRange(5, 8)); + var hallGen = new RoomGenAngledHall(50); + + // Create connected grid + for (int x = 0; x < gridWidth; x++) + { + for (int y = 0; y < gridHeight; y++) + { + gridPlan.AddRoom(new Loc(x, y), roomGen.Copy(), new ComponentCollection()); + + if (x > 0) + gridPlan.SetHall(new LocRay4(x, y, Dir4.Left), (IPermissiveRoomGen)hallGen.Copy(), new ComponentCollection()); + if (y > 0) + gridPlan.SetHall(new LocRay4(x, y, Dir4.Up), (IPermissiveRoomGen)hallGen.Copy(), new ComponentCollection()); + } + } + + return context; + } + + [Benchmark(Baseline = true)] + public FloorPlan ConvertGridToFloor_4x4() + { + var context = CloneContext(_smallContext); + var floorPlan = new FloorPlan(); + floorPlan.InitSize(context.GridPlan.Size); + context.InitPlan(floorPlan); + context.GridPlan.PlaceRoomsOnFloor(context); + return floorPlan; + } + + [Benchmark] + public FloorPlan ConvertGridToFloor_8x6() + { + var context = CloneContext(_mediumContext); + var floorPlan = new FloorPlan(); + floorPlan.InitSize(context.GridPlan.Size); + context.InitPlan(floorPlan); + context.GridPlan.PlaceRoomsOnFloor(context); + return floorPlan; + } + + [Benchmark] + public FloorPlan ConvertGridToFloor_12x10() + { + var context = CloneContext(_largeContext); + var floorPlan = new FloorPlan(); + floorPlan.InitSize(context.GridPlan.Size); + context.InitPlan(floorPlan); + context.GridPlan.PlaceRoomsOnFloor(context); + return floorPlan; + } + + private static BenchmarkMapGenContext CloneContext(BenchmarkMapGenContext source) + { + // Re-create context with same grid structure + return CreateContextWithGrid(source.GridPlan.GridWidth, source.GridPlan.GridHeight); + } +} diff --git a/RogueElements.Benchmarks/MapGenerationBenchmarks.cs b/RogueElements.Benchmarks/MapGenerationBenchmarks.cs new file mode 100644 index 00000000..6b1e7ddc --- /dev/null +++ b/RogueElements.Benchmarks/MapGenerationBenchmarks.cs @@ -0,0 +1,191 @@ +// +// Copyright (c) Audino +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using BenchmarkDotNet.Attributes; + +namespace RogueElements.Benchmarks; + +/// +/// Benchmarks for map generation operations. +/// Tests various map sizes and generation pipeline configurations. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class MapGenerationBenchmarks +{ + private MapGen _smallGridLayout = null!; + private MapGen _mediumGridLayout = null!; + private MapGen _largeGridLayout = null!; + private ulong _seed; + + [GlobalSetup] + public void Setup() + { + _seed = 12345UL; + + // Small grid: 4x3 cells, 7x7 each (~28x21 tiles) + _smallGridLayout = CreateGridLayout(4, 3, 7, 7); + + // Medium grid: 6x4 cells, 9x9 each (~54x36 tiles) + _mediumGridLayout = CreateGridLayout(6, 4, 9, 9); + + // Large grid: 10x8 cells, 11x11 each (~110x88 tiles) + _largeGridLayout = CreateGridLayout(10, 8, 11, 11); + } + + private static MapGen CreateGridLayout(int cellX, int cellY, int cellWidth, int cellHeight) + { + var layout = new MapGen(); + + // Initialize grid + var startGen = new InitGridPlanStep(1) + { + CellX = cellX, + CellY = cellY, + CellWidth = cellWidth, + CellHeight = cellHeight, + }; + layout.GenSteps.Add(-4, startGen); + + // Create branching path + var path = new GridPathBranch + { + RoomRatio = new RandRange(70), + BranchRatio = new RandRange(0, 50), + }; + + var genericRooms = new SpawnList> + { + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, + }; + path.GenericRooms = genericRooms; + + var genericHalls = new SpawnList> + { + { new RoomGenAngledHall(50), 10 }, + }; + path.GenericHalls = genericHalls; + + layout.GenSteps.Add(-4, path); + + // Convert to floor plan + layout.GenSteps.Add(-2, new DrawGridToFloorStep()); + + // Draw to tiles + layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + + return layout; + } + + [Benchmark(Baseline = true)] + public BenchmarkMapGenContext SmallGrid_4x3() + { + return _smallGridLayout.GenMap(_seed++); + } + + [Benchmark] + public BenchmarkMapGenContext MediumGrid_6x4() + { + return _mediumGridLayout.GenMap(_seed++); + } + + [Benchmark] + public BenchmarkMapGenContext LargeGrid_10x8() + { + return _largeGridLayout.GenMap(_seed++); + } +} + +/// +/// Benchmarks for individual room generation operations. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class RoomGenBenchmarks +{ + private BenchmarkMapGenContext _context = null!; + private RoomGenSquare _squareGen = null!; + private RoomGenRound _roundGen = null!; + private RoomGenCave _caveGen = null!; + + [GlobalSetup] + public void Setup() + { + _context = new BenchmarkMapGenContext(); + _context.InitSeed(12345UL); + _context.CreateNew(100, 100); + + _squareGen = new RoomGenSquare(new RandRange(5, 10), new RandRange(5, 10)); + _roundGen = new RoomGenRound(new RandRange(5, 10), new RandRange(5, 10)); + _caveGen = new RoomGenCave(new RandRange(5, 10), new RandRange(5, 10)); + } + + [Benchmark(Baseline = true)] + public Rect SquareRoom() + { + _squareGen.PrepareSize(_context.Rand, new Loc(10, 10)); + _squareGen.DrawOnMap(_context); + return _squareGen.Draw; + } + + [Benchmark] + public Rect RoundRoom() + { + _roundGen.PrepareSize(_context.Rand, new Loc(10, 10)); + _roundGen.DrawOnMap(_context); + return _roundGen.Draw; + } + + [Benchmark] + public Rect CaveRoom() + { + _caveGen.PrepareSize(_context.Rand, new Loc(10, 10)); + _caveGen.DrawOnMap(_context); + return _caveGen.Draw; + } +} + +/// +/// Benchmarks for random number generation operations. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class RngBenchmarks +{ + private ReRandom _reRandom = null!; + private RandRange _range; + + [GlobalSetup] + public void Setup() + { + _reRandom = new ReRandom(12345UL); + _range = new RandRange(1, 100); + } + + [Benchmark] + public int ReRandom_Next() + { + return _reRandom.Next(); + } + + [Benchmark] + public int ReRandom_NextRange() + { + return _reRandom.Next(1, 100); + } + + [Benchmark] + public int RandRange_Pick() + { + return _range.Pick(_reRandom); + } + + [Benchmark] + public ulong ReRandom_NextUInt64() + { + return _reRandom.NextUInt64(); + } +} diff --git a/RogueElements.Benchmarks/Program.cs b/RogueElements.Benchmarks/Program.cs new file mode 100644 index 00000000..78059d31 --- /dev/null +++ b/RogueElements.Benchmarks/Program.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Audino +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using BenchmarkDotNet.Running; +using RogueElements.Benchmarks; + +// Run all benchmarks when no args provided, or specific ones based on args +if (args.Length == 0) +{ + // Core generation benchmarks + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + + // Performance hotspot benchmarks + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); +} +else +{ + BenchmarkSwitcher.FromAssembly(typeof(MapGenerationBenchmarks).Assembly).Run(args); +} diff --git a/RogueElements.Benchmarks/RogueElements.Benchmarks.csproj b/RogueElements.Benchmarks/RogueElements.Benchmarks.csproj new file mode 100644 index 00000000..624462af --- /dev/null +++ b/RogueElements.Benchmarks/RogueElements.Benchmarks.csproj @@ -0,0 +1,21 @@ + + + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B} + Exe + RogueElements.Benchmarks + RogueElements.Benchmarks + net10.0 + enable + enable + false + + + + {a777bc3d-2635-401d-96d7-162178d8dfc4} + RogueElements + + + + + + diff --git a/RogueElements.Benchmarks/SUGGESTIONS.md b/RogueElements.Benchmarks/SUGGESTIONS.md new file mode 100644 index 00000000..117eee51 --- /dev/null +++ b/RogueElements.Benchmarks/SUGGESTIONS.md @@ -0,0 +1,179 @@ +# Performance Optimization Suggestions + +Performance audit conducted 2025-12-31. Benchmarks added to measure hotspots. + +## Executive Summary + +**Current performance is excellent.** A large 80-room map generates in ~0.5ms. These suggestions are for future reference if scaling to very large maps (200+ rooms) or bulk generation (100+ floors at startup). + +--- + +## Quick Wins + +### 1. SwapPop for Spawn Distribution + +**File:** `RogueElements/MapGen/Spawning/RandomSpawnStep.cs:54` + +**Current code:** +```csharp +freeTiles.RemoveAt(randIndex); // O(n) - shifts all elements after index +``` + +**Suggested fix:** +```csharp +// O(1) removal: swap with last element, then remove last +int lastIndex = freeTiles.Count - 1; +if (randIndex != lastIndex) +{ + freeTiles[randIndex] = freeTiles[lastIndex]; +} +freeTiles.RemoveAt(lastIndex); +``` + +**Benchmark result:** 21% faster for 100 items (908µs → 715µs) + +**When it matters:** Games spawning 500+ entities per floor + +--- + +## Medium-Term Improvements + +### 2. Spatial Indexing for Collision Detection + +**Files:** +- `RogueElements/MapGen/FloorPlan/FloorPlan.cs:310-320` (AddRoom) +- `RogueElements/MapGen/FloorPlan/FloorPlan.cs:757-776` (CheckCollision) + +**Current behavior:** Linear scan through all rooms/halls for every collision check - O(n) per check, O(n²) total for n rooms. + +**Suggested approach:** Implement grid-based spatial hashing or quad-tree. + +```csharp +// Example: Grid-based spatial hash +private Dictionary<(int, int), List> _spatialGrid; +private const int CellSize = 32; + +private (int, int) GetCell(Loc loc) => (loc.X / CellSize, loc.Y / CellSize); + +public List CheckCollision(Rect rect) +{ + var results = new List(); + // Only check rooms in overlapping cells instead of all rooms + for (int x = rect.X / CellSize; x <= rect.End.X / CellSize; x++) + { + for (int y = rect.Y / CellSize; y <= rect.End.Y / CellSize; y++) + { + if (_spatialGrid.TryGetValue((x, y), out var candidates)) + { + // Check only candidates in this cell + } + } + } + return results; +} +``` + +**Estimated improvement:** O(n) → O(1) average case for collision checks + +**When it matters:** Maps with 200+ rooms, or algorithms that attempt many placements + +--- + +### 3. Deferred Room Erasure Index Updates + +**File:** `RogueElements/MapGen/FloorPlan/FloorPlan.cs:386-424` + +**Current behavior:** After removing a room/hall, scans ALL rooms and ALL halls to update adjacency indices. O(n) per erasure. + +**Suggested approach:** Batch erasures and update indices once, or use stable IDs instead of array indices. + +**When it matters:** Algorithms that remove many rooms during generation + +--- + +## Not Worth Implementing + +### HashSet for GetAdjacentRooms + +**File:** `RogueElements/MapGen/Grid/GridPlan.cs:837-868` + +**Benchmark result:** HashSet was actually 12% *slower* (45ns vs 41ns) due to overhead. For typical room sizes with ~4 neighbors, List.Contains is faster. + +**Recommendation:** Keep current implementation. + +--- + +## Benchmark Coverage + +The following benchmarks were added to measure these hotspots: + +| Benchmark Class | File | What It Measures | +|-----------------|------|------------------| +| `CollisionBenchmarks` | CollisionBenchmarks.cs | FloorPlan.CheckCollision scaling | +| `AddRoomCollisionBenchmarks` | CollisionBenchmarks.cs | Cumulative AddRoom cost | +| `SpawnBenchmarks` | SpawnBenchmarks.cs | RemoveAt vs SwapPop comparison | +| `FreeTilesBenchmarks` | SpawnBenchmarks.cs | GetAllFreeTiles allocation | +| `EraseRoomBenchmarks` | FloorPlanBenchmarks.cs | Room erasure cost | +| `DrawFloorPlanBenchmarks` | FloorPlanBenchmarks.cs | DrawOnMap performance | +| `AdjacencyLookupBenchmarks` | FloorPlanBenchmarks.cs | FloorPlan adjacency iteration | +| `GridAdjacencyBenchmarks` | GridPlanBenchmarks.cs | List vs HashSet for adjacency | +| `GridEraseRoomBenchmarks` | GridPlanBenchmarks.cs | Grid room erasure | +| `GridToFloorConversionBenchmarks` | GridPlanBenchmarks.cs | PlaceRoomsOnFloor | + +### Running Benchmarks + +```bash +# Run all benchmarks +dotnet run --project RogueElements.Benchmarks -c Release + +# Run specific benchmark +dotnet run --project RogueElements.Benchmarks -c Release -- --filter "*SpawnBenchmarks*" + +# Quick dry run +dotnet run --project RogueElements.Benchmarks -c Release -- --filter "*Collision*" --job Dry +``` + +--- + +## Baseline Results (2025-12-31) + +**Hardware:** Apple M3 Pro, .NET 10.0.1 + +### Map Generation (End-to-End) + +| Map Size | Rooms | Time | Memory | +|----------|-------|------|--------| +| Small (4×3) | 12 | 0.04 ms | 152 KB | +| Medium (6×4) | 24 | 0.10 ms | 378 KB | +| Large (10×8) | 80 | 0.55 ms | 1.5 MB | + +### AddRoom Collision Scaling + +| Rooms | Time | Ratio | +|-------|------|-------| +| 10 | 4.6 µs | 1.0x | +| 25 | 13.8 µs | 3.0x | +| 50 | 33.8 µs | 7.4x | +| 100 | 75.9 µs | 16.6x | + +### Spawn Distribution + +| Items | RemoveAt | SwapPop | Improvement | +|-------|----------|---------|-------------| +| 10 | 32.2 µs | 30.9 µs | 4% | +| 50 | 148.9 µs | 131.4 µs | 12% | +| 100 | 908.3 µs | 715.1 µs | 21% | + +--- + +## When to Revisit + +Consider implementing optimizations if: + +1. Users report slow map generation +2. Targeting maps with 200+ rooms +3. Pre-generating 100+ floors at startup +4. Targeting low-end mobile devices +5. Adding real-time streaming generation + +Until then, the library performs well for typical roguelike use cases. diff --git a/RogueElements.Benchmarks/SpawnBenchmarks.cs b/RogueElements.Benchmarks/SpawnBenchmarks.cs new file mode 100644 index 00000000..ae63e503 --- /dev/null +++ b/RogueElements.Benchmarks/SpawnBenchmarks.cs @@ -0,0 +1,294 @@ +// +// Copyright (c) Audino +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using BenchmarkDotNet.Attributes; + +namespace RogueElements.Benchmarks; + +/// +/// Benchmarks for spawn distribution operations. +/// Measures the O(n²) impact of RemoveAt in RandomSpawnStep. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class SpawnBenchmarks +{ + private SpawnableBenchmarkContext _smallContext = null!; + private SpawnableBenchmarkContext _mediumContext = null!; + private SpawnableBenchmarkContext _largeContext = null!; + private List _spawns10 = null!; + private List _spawns50 = null!; + private List _spawns100 = null!; + + [GlobalSetup] + public void Setup() + { + // Small: 50x50 = 2,500 tiles + _smallContext = CreateSpawnableContext(50, 50); + + // Medium: 100x100 = 10,000 tiles + _mediumContext = CreateSpawnableContext(100, 100); + + // Large: 200x200 = 40,000 tiles + _largeContext = CreateSpawnableContext(200, 200); + + // Pre-create spawn lists + _spawns10 = CreateSpawnList(10); + _spawns50 = CreateSpawnList(50); + _spawns100 = CreateSpawnList(100); + } + + private static SpawnableBenchmarkContext CreateSpawnableContext(int width, int height) + { + var context = new SpawnableBenchmarkContext(); + context.InitSeed(12345UL); + context.CreateNew(width, height); + + // Create some floor tiles (checkerboard pattern for variety) + for (int x = 1; x < width - 1; x++) + { + for (int y = 1; y < height - 1; y++) + { + if ((x + y) % 3 != 0) // ~66% floor tiles + { + context.SetTile(new Loc(x, y), new BenchmarkTile(BenchmarkTile.ROOM_ID)); + } + } + } + + return context; + } + + private static List CreateSpawnList(int count) + { + var spawns = new List(); + for (int i = 0; i < count; i++) + { + spawns.Add(new BenchmarkSpawnable { ID = i }); + } + return spawns; + } + + // Benchmark: Current RemoveAt approach + [Benchmark(Baseline = true)] + public int Spawn_10Items_SmallMap_RemoveAt() + { + return DistributeWithRemoveAt(_smallContext.Clone(), new List(_spawns10)); + } + + [Benchmark] + public int Spawn_50Items_MediumMap_RemoveAt() + { + return DistributeWithRemoveAt(_mediumContext.Clone(), new List(_spawns50)); + } + + [Benchmark] + public int Spawn_100Items_LargeMap_RemoveAt() + { + return DistributeWithRemoveAt(_largeContext.Clone(), new List(_spawns100)); + } + + // Benchmark: Optimized swap-and-pop approach + [Benchmark] + public int Spawn_10Items_SmallMap_SwapPop() + { + return DistributeWithSwapPop(_smallContext.Clone(), new List(_spawns10)); + } + + [Benchmark] + public int Spawn_50Items_MediumMap_SwapPop() + { + return DistributeWithSwapPop(_mediumContext.Clone(), new List(_spawns50)); + } + + [Benchmark] + public int Spawn_100Items_LargeMap_SwapPop() + { + return DistributeWithSwapPop(_largeContext.Clone(), new List(_spawns100)); + } + + /// + /// Current implementation: O(n) RemoveAt for each spawn. + /// + private static int DistributeWithRemoveAt(SpawnableBenchmarkContext map, List spawns) + { + List freeTiles = map.GetAllFreeTiles(); + int placed = 0; + + for (int ii = 0; ii < spawns.Count && freeTiles.Count > 0; ii++) + { + int randIndex = map.Rand.Next(freeTiles.Count); + map.PlaceItem(freeTiles[randIndex], spawns[ii]); + freeTiles.RemoveAt(randIndex); // O(n) shift + placed++; + } + + return placed; + } + + /// + /// Optimized: O(1) swap-and-pop for each spawn. + /// + private static int DistributeWithSwapPop(SpawnableBenchmarkContext map, List spawns) + { + List freeTiles = map.GetAllFreeTiles(); + int placed = 0; + + for (int ii = 0; ii < spawns.Count && freeTiles.Count > 0; ii++) + { + int randIndex = map.Rand.Next(freeTiles.Count); + map.PlaceItem(freeTiles[randIndex], spawns[ii]); + + // O(1) removal: swap with last, then remove last + int lastIndex = freeTiles.Count - 1; + if (randIndex != lastIndex) + { + freeTiles[randIndex] = freeTiles[lastIndex]; + } + freeTiles.RemoveAt(lastIndex); + placed++; + } + + return placed; + } +} + +/// +/// Benchmarks for GetAllFreeTiles allocation. +/// +[MemoryDiagnoser] +[SimpleJob(warmupCount: 3, iterationCount: 10)] +public class FreeTilesBenchmarks +{ + private SpawnableBenchmarkContext _smallContext = null!; + private SpawnableBenchmarkContext _mediumContext = null!; + private SpawnableBenchmarkContext _largeContext = null!; + + [GlobalSetup] + public void Setup() + { + _smallContext = CreateContext(50, 50); + _mediumContext = CreateContext(100, 100); + _largeContext = CreateContext(200, 200); + } + + private static SpawnableBenchmarkContext CreateContext(int width, int height) + { + var context = new SpawnableBenchmarkContext(); + context.InitSeed(12345UL); + context.CreateNew(width, height); + + // Create floor tiles + for (int x = 1; x < width - 1; x++) + { + for (int y = 1; y < height - 1; y++) + { + context.SetTile(new Loc(x, y), new BenchmarkTile(BenchmarkTile.ROOM_ID)); + } + } + + return context; + } + + [Benchmark(Baseline = true)] + public List GetFreeTiles_50x50() + { + return _smallContext.GetAllFreeTiles(); + } + + [Benchmark] + public List GetFreeTiles_100x100() + { + return _mediumContext.GetAllFreeTiles(); + } + + [Benchmark] + public List GetFreeTiles_200x200() + { + return _largeContext.GetAllFreeTiles(); + } +} + +/// +/// Simple spawnable for benchmarking. +/// +public class BenchmarkSpawnable : ISpawnable +{ + public int ID { get; set; } + public Loc Loc { get; set; } + + public ISpawnable Copy() => new BenchmarkSpawnable { ID = ID, Loc = Loc }; +} + +/// +/// Context that supports spawning for benchmarks. +/// +public class SpawnableBenchmarkContext : BenchmarkMapGenContext, IPlaceableGenContext +{ + private readonly List _spawnedItems = new(); + + public List GetAllFreeTiles() + { + var freeTiles = new List(); + for (int x = 0; x < Width; x++) + { + for (int y = 0; y < Height; y++) + { + var loc = new Loc(x, y); + if (CanPlaceItem(loc)) + freeTiles.Add(loc); + } + } + return freeTiles; + } + + public List GetFreeTiles(Rect rect) + { + var freeTiles = new List(); + for (int x = rect.X; x < rect.End.X && x < Width; x++) + { + for (int y = rect.Y; y < rect.End.Y && y < Height; y++) + { + var loc = new Loc(x, y); + if (CanPlaceItem(loc)) + freeTiles.Add(loc); + } + } + return freeTiles; + } + + public bool CanPlaceItem(Loc loc) + { + if (loc.X < 0 || loc.X >= Width || loc.Y < 0 || loc.Y >= Height) + return false; + + // Can only place on floor tiles + return !TileBlocked(loc); + } + + public void PlaceItem(Loc loc, BenchmarkSpawnable item) + { + item.Loc = loc; + _spawnedItems.Add(item); + } + + public SpawnableBenchmarkContext Clone() + { + var clone = new SpawnableBenchmarkContext(); + clone.InitSeed((ulong)Rand.Next()); + clone.CreateNew(Width, Height); + + // Copy tiles + for (int x = 0; x < Width; x++) + { + for (int y = 0; y < Height; y++) + { + clone.Map.Tiles[x][y] = new BenchmarkTile(Map.Tiles[x][y].ID); + } + } + + return clone; + } +} diff --git a/RogueElements.Examples/Common/BaseMap.cs b/RogueElements.Examples/Common/BaseMap.cs index eb50799c..97041d7e 100644 --- a/RogueElements.Examples/Common/BaseMap.cs +++ b/RogueElements.Examples/Common/BaseMap.cs @@ -8,20 +8,90 @@ namespace RogueElements.Examples { + /// + /// Abstract base class representing a roguelike dungeon map. + /// + /// + /// + /// This class provides the fundamental data structure for storing map tiles and + /// serves as a reference implementation for games using RogueElements. It defines + /// terrain type constants and manages the 2D tile array that represents the dungeon. + /// + /// + /// Subclasses can extend this to add game-specific data such as entity lists, + /// lighting information, or additional terrain layers. + /// + /// public abstract class BaseMap { + /// + /// Terrain ID representing impassable wall tiles. + /// + /// + /// Walls are the default terrain type when tiles are initialized. + /// Generation steps carve rooms and hallways by replacing walls with floor tiles. + /// public const int WALL_TERRAIN_ID = 0; + + /// + /// Terrain ID representing passable floor/room tiles. + /// + /// + /// Floor tiles are walkable areas where entities can be placed. + /// Rooms and hallways are carved from walls by setting tiles to this ID. + /// public const int ROOM_TERRAIN_ID = 1; + + /// + /// Terrain ID representing water terrain tiles. + /// + /// + /// Water tiles are typically generated using Perlin noise in terrain generation steps. + /// Whether water is passable depends on your game's mechanics. + /// public const int WATER_TERRAIN_ID = 2; + /// + /// Gets or sets the random number generator used for map generation. + /// + /// + /// This is initialized by with the + /// generation seed, ensuring deterministic map generation for the same seed value. + /// public ReRandom Rand { get; set; } + /// + /// Gets or sets the 2D array of tiles representing the map. + /// + /// + /// Tiles are stored in column-major order: Tiles[x][y] where x is the + /// horizontal position and y is the vertical position. Initialize this array + /// using before generation begins. + /// public Tile[][] Tiles { get; set; } + /// + /// Gets the width of the map in tiles. + /// + /// The number of tile columns in the map. public int Width => this.Tiles.Length; + /// + /// Gets the height of the map in tiles. + /// + /// The number of tile rows in the map. public int Height => this.Tiles[0].Length; + /// + /// Initializes the tile array with the specified dimensions. + /// + /// The width of the map in tiles. + /// The height of the map in tiles. + /// + /// All tiles are initialized as wall terrain (). + /// This is typically called by + /// during map generation initialization. + /// public void InitializeTiles(int width, int height) { this.Tiles = new Tile[width][]; diff --git a/RogueElements.Examples/Common/BaseMapGenContext.cs b/RogueElements.Examples/Common/BaseMapGenContext.cs index 6dba5a21..17d86047 100644 --- a/RogueElements.Examples/Common/BaseMapGenContext.cs +++ b/RogueElements.Examples/Common/BaseMapGenContext.cs @@ -10,34 +10,168 @@ namespace RogueElements.Examples { + /// + /// Abstract base class providing a reference implementation of + /// for tile-based map generation. + /// + /// + /// The concrete map type that stores generation results. Must inherit from + /// and have a parameterless constructor. + /// + /// + /// + /// This class bridges the RogueElements generation pipeline with your game's map representation. + /// It implements to enable tile-based generation steps such as + /// room carving, hallway drawing, and terrain placement. + /// + /// + /// Extension Points: + /// + /// + /// + /// Override to add placement restrictions (e.g., protecting certain areas). + /// + /// + /// Override to initialize additional map data structures. + /// + /// + /// Override to perform post-generation cleanup or validation. + /// + /// + /// + /// For spawning support, extend this class and implement additional interfaces such as + /// for items, stairs, or mobs. + /// + /// + /// + /// + /// // Create a concrete context for your game + /// public class MyMapGenContext : BaseMapGenContext<MyMap>, IPlaceableGenContext<Item> + /// { + /// // Add spawning support, custom interfaces, etc. + /// } + /// + /// public abstract class BaseMapGenContext : ITiledGenContext where TMap : BaseMap, new() { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new empty map instance. The map tiles are not initialized until + /// is called during generation. + /// protected BaseMapGenContext() { this.Map = new TMap(); } + /// + /// Gets or sets the underlying map data being generated. + /// + /// The map instance that stores tiles and generation results. + /// + /// After generation completes, access this property to retrieve the finished map + /// for use in your game. + /// public TMap Map { get; set; } + /// + /// Gets a tile instance representing passable floor/room terrain. + /// + /// A new with . + /// + /// Used by generation steps when carving rooms and hallways. + /// Returns a new instance each call to avoid shared state issues. + /// public ITile RoomTerrain => new Tile(BaseMap.ROOM_TERRAIN_ID); + /// + /// Gets a tile instance representing impassable wall terrain. + /// + /// A new with . + /// + /// Used by generation steps when filling areas with walls. + /// Returns a new instance each call to avoid shared state issues. + /// public ITile WallTerrain => new Tile(BaseMap.WALL_TERRAIN_ID); + /// + /// Gets a value indicating whether the tile array has been initialized. + /// + /// true if has been called; otherwise, false. + /// + /// Generation steps may check this to ensure the map is ready for modification. + /// public bool TilesInitialized => this.Map.Tiles != null; + /// + /// Gets the width of the map in tiles. + /// + /// The number of tile columns in the map. public int Width => this.Map.Width; + /// + /// Gets the height of the map in tiles. + /// + /// The number of tile rows in the map. public int Height => this.Map.Height; + /// + /// Gets a value indicating whether the map wraps at edges (toroidal topology). + /// + /// Always false in this implementation (no wrapping). + /// + /// Override this property and return true to enable wraparound maps + /// where walking off one edge appears on the opposite side. + /// public bool Wrap => false; + /// + /// Gets the random number generator for this generation context. + /// + /// The instance seeded during . + /// + /// All generation steps should use this RNG to ensure deterministic generation. + /// Using the same seed will produce identical maps. + /// public IRandom Rand => this.Map.Rand; + /// + /// Gets the tile at the specified location. + /// + /// The map coordinates to query. + /// The at the specified location. + /// + /// Does not perform bounds checking. Ensure is within + /// the map dimensions before calling. + /// public ITile GetTile(Loc loc) => this.Map.Tiles[loc.X][loc.Y]; + /// + /// Determines whether a tile can be placed at the specified location. + /// + /// The map coordinates to check. + /// The tile to potentially place. + /// true if the tile can be placed; otherwise, false. + /// + /// Override this method to implement placement restrictions, such as protecting + /// certain areas from modification or enforcing terrain rules. + /// The default implementation always returns true. + /// public virtual bool CanSetTile(Loc loc, ITile tile) => true; + /// + /// Attempts to place a tile at the specified location. + /// + /// The map coordinates where the tile should be placed. + /// The tile to place. + /// true if the tile was successfully placed; false if placement was blocked by . + /// + /// This is the safe way to modify tiles, respecting any placement restrictions. + /// Prefer this over direct array access. + /// public bool TrySetTile(Loc loc, ITile tile) { if (!this.CanSetTile(loc, tile)) @@ -46,32 +180,98 @@ public bool TrySetTile(Loc loc, ITile tile) return true; } + /// + /// Places a tile at the specified location, throwing if placement is blocked. + /// + /// The map coordinates where the tile should be placed. + /// The tile to place. + /// + /// Thrown when returns false for this location and tile. + /// + /// + /// Use this when tile placement is required and failure indicates a generation error. + /// For optional placement, use instead. + /// public void SetTile(Loc loc, ITile tile) { if (!this.TrySetTile(loc, tile)) throw new InvalidOperationException("Can't place tile!"); } + /// + /// Initializes the random number generator with the specified seed. + /// + /// The seed value for deterministic generation. + /// + /// Called by at the start of generation. Using the same + /// seed will produce identical maps, enabling reproducible generation for + /// testing or sharing map seeds with players. + /// public void InitSeed(ulong seed) { this.Map.Rand = new ReRandom(seed); } + /// + /// Determines whether the specified tile blocks movement. + /// + /// The map coordinates to check. + /// true if the tile is a wall; otherwise, false. + /// + /// Used by pathfinding and connectivity algorithms in generation steps. + /// This implementation treats only as blocking. + /// bool ITiledGenContext.TileBlocked(Loc loc) { return this.Map.Tiles[loc.X][loc.Y].ID == BaseMap.WALL_TERRAIN_ID; } + /// + /// Determines whether the specified tile blocks movement, with diagonal consideration. + /// + /// The map coordinates to check. + /// Whether this is a diagonal movement check. + /// true if the tile is a wall; otherwise, false. + /// + /// This implementation does not distinguish between cardinal and diagonal movement. + /// Override to implement different blocking rules for diagonal movement + /// (e.g., requiring adjacent tiles to also be passable). + /// bool ITiledGenContext.TileBlocked(Loc loc, bool diagonal) { return this.Map.Tiles[loc.X][loc.Y].ID == BaseMap.WALL_TERRAIN_ID; } + /// + /// Creates and initializes a new map with the specified dimensions. + /// + /// The width of the map in tiles. + /// The height of the map in tiles. + /// Whether the map should wrap at edges (ignored in this implementation). + /// + /// Called by initialization steps (e.g., InitTilesStep) at the start of generation. + /// Override to initialize additional data structures alongside the tile array. + /// All tiles are initialized as walls. + /// public virtual void CreateNew(int width, int height, bool wrap = false) { this.Map.InitializeTiles(width, height); } + /// + /// Performs any final cleanup or validation after generation completes. + /// + /// + /// Called by after all generation steps have executed. + /// Override to perform post-generation tasks such as: + /// + /// Validating map connectivity + /// Computing pathfinding data + /// Generating minimap data + /// Releasing temporary generation resources + /// + /// The default implementation does nothing. + /// public virtual void FinishGen() { } diff --git a/RogueElements.Examples/Common/MainHallComponent.cs b/RogueElements.Examples/Common/MainHallComponent.cs index 8fdbf937..a4f0edb3 100644 --- a/RogueElements.Examples/Common/MainHallComponent.cs +++ b/RogueElements.Examples/Common/MainHallComponent.cs @@ -8,8 +8,55 @@ namespace RogueElements.Examples { + /// + /// A room component marker that identifies hallways as "main" hallways in the dungeon layout. + /// + /// + /// + /// Hall components work similarly to room components but are specifically used to tag + /// hallways (corridors) connecting rooms. In RogueElements, hallways are also represented + /// as rooms in the and can have components attached. + /// + /// + /// Common uses for MainHallComponent: + /// + /// + /// + /// Distinguishing primary connection corridors from secret passages + /// + /// + /// Controlling where hallway-specific spawns can occur (traps, ambushes) + /// + /// + /// Filtering hallways for terrain modifications (adding puddles, debris) + /// + /// + /// + /// Apply this component during hallway generation using steps like SetGridDefaultsStep + /// with hall-specific settings. + /// + /// + /// + /// + /// // Set default hall component during grid initialization + /// var defaultsStep = new SetGridDefaultsStep<MyContext>() + /// { + /// DefaultHallComponent = new MainHallComponent() + /// }; + /// + /// + /// + /// public class MainHallComponent : RoomComponent { + /// + /// Creates a copy of this hall component. + /// + /// A new instance. + /// + /// Required by base class. Hall components are cloned + /// when hallways are copied during floor plan manipulation. + /// public override RoomComponent Clone() { return new MainHallComponent(); diff --git a/RogueElements.Examples/Common/MainRoomComponent.cs b/RogueElements.Examples/Common/MainRoomComponent.cs index 1de4c2a1..a1ca776d 100644 --- a/RogueElements.Examples/Common/MainRoomComponent.cs +++ b/RogueElements.Examples/Common/MainRoomComponent.cs @@ -8,8 +8,59 @@ namespace RogueElements.Examples { + /// + /// A room component marker that identifies rooms as "main" rooms in the dungeon layout. + /// + /// + /// + /// Room components in RogueElements are metadata tags attached to rooms in a + /// or . They enable generation steps to filter and identify specific + /// rooms for targeted operations. + /// + /// + /// Common uses for MainRoomComponent: + /// + /// + /// + /// Identifying rooms that should receive standard enemy spawns + /// + /// + /// Distinguishing regular rooms from special rooms (treasure rooms, boss rooms) + /// + /// + /// Filtering rooms for item placement that should only appear in main areas + /// + /// + /// + /// Apply this component during room generation using steps like SetGridDefaultsStep + /// or by adding it directly to room generators. + /// + /// + /// + /// + /// // Filter for main rooms when spawning enemies + /// var spawnStep = new RandomSpawnStep<MyContext, Mob>(mobList) + /// { + /// Filters = new List<BaseRoomFilter> + /// { + /// new RoomFilterComponent(true, new MainRoomComponent()) + /// } + /// }; + /// + /// + /// + /// + /// public class MainRoomComponent : RoomComponent { + /// + /// Creates a copy of this room component. + /// + /// A new instance. + /// + /// Required by base class. Room components are cloned + /// when rooms are copied during floor plan manipulation. + /// public override RoomComponent Clone() { return new MainRoomComponent(); diff --git a/RogueElements.Examples/Common/README.md b/RogueElements.Examples/Common/README.md new file mode 100644 index 00000000..f86128f7 --- /dev/null +++ b/RogueElements.Examples/Common/README.md @@ -0,0 +1,132 @@ +# Common + +Shared infrastructure classes used across all RogueElements examples. + +## Overview + +The Common folder provides base classes and reusable components that all examples build upon. This establishes a consistent foundation for map representation, context management, and spawnable entities. + +## Classes + +### BaseMap + +The abstract base class for all map data structures. + +```csharp +public abstract class BaseMap +{ + public const int WALL_TERRAIN_ID = 0; + public const int ROOM_TERRAIN_ID = 1; + public const int WATER_TERRAIN_ID = 2; + + public ReRandom Rand { get; set; } + public Tile[][] Tiles { get; set; } + public int Width => this.Tiles.Length; + public int Height => this.Tiles[0].Length; + + public void InitializeTiles(int width, int height); +} +``` + +**Key Points:** +- Defines terrain ID constants used throughout all examples +- Holds the 2D tile array and RNG instance +- Provides `InitializeTiles()` for creating the tile grid + +### BaseMapGenContext + +The abstract base context that implements `ITiledGenContext` for all examples. + +```csharp +public abstract class BaseMapGenContext : ITiledGenContext + where TMap : BaseMap, new() +{ + public TMap Map { get; set; } + public ITile RoomTerrain => new Tile(BaseMap.ROOM_TERRAIN_ID); + public ITile WallTerrain => new Tile(BaseMap.WALL_TERRAIN_ID); + + public ITile GetTile(Loc loc); + public bool TrySetTile(Loc loc, ITile tile); + public void SetTile(Loc loc, ITile tile); + public void CreateNew(int width, int height, bool wrap = false); + public void InitSeed(ulong seed); +} +``` + +**Key Points:** +- Generic over the Map type, allowing each example to extend BaseMap +- Implements core tile operations required by `ITiledGenContext` +- Initializes the RNG with `ReRandom` for deterministic generation + +### Tile + +A simple tile implementation of `ITile`. + +```csharp +public class Tile : ITile +{ + public int ID { get; set; } + public ITile Copy(); + public bool TileEquivalent(ITile other); +} +``` + +### Stairs, StairsUp, StairsDown + +Spawnable stair entities for floor transitions. + +```csharp +public abstract class Stairs : ISpawnable +{ + public Loc Loc { get; set; } + public abstract ISpawnable Copy(); +} + +public class StairsUp : Stairs, IEntrance { } +public class StairsDown : Stairs, IExit { } +``` + +**Key Points:** +- `StairsUp` implements `IEntrance` - marks the player spawn point +- `StairsDown` implements `IExit` - marks the floor exit +- Used by `FloorStairsStep` for automatic placement + +### Room Components + +Tags for marking rooms with special purposes: + +| Class | Purpose | +|-------|---------| +| `MainRoomComponent` | Marks rooms as part of the main path | +| `MainHallComponent` | Marks hallways as part of the main path | +| `TreasureRoomComponent` | Marks rooms for special treasure spawning | + +These components enable filtered spawning - for example, placing special loot only in treasure rooms. + +## Architecture Pattern + +Each example follows this inheritance pattern: + +``` +BaseMap (Common) + | + +-- Map (Ex1, Ex2, etc.) - adds example-specific data + +BaseMapGenContext (Common) + | + +-- MapGenContext (Ex1, Ex2, etc.) - adds example-specific interfaces +``` + +This allows examples to progressively add capabilities while reusing core infrastructure. + +## Usage + +When creating a new example: + +1. Create a `Map` class extending `BaseMap` +2. Create a `MapGenContext` class extending `BaseMapGenContext` +3. Add interfaces to `MapGenContext` as needed (e.g., `IFloorPlanGenContext`, `IPlaceableGenContext`) + +## Next Steps + +Proceed to [Example 1: Static Tiles](../Ex1_Tiles/README.md) to see these classes in action. diff --git a/RogueElements.Examples/Common/Stairs.cs b/RogueElements.Examples/Common/Stairs.cs index 278dd14a..0790effb 100644 --- a/RogueElements.Examples/Common/Stairs.cs +++ b/RogueElements.Examples/Common/Stairs.cs @@ -9,19 +9,71 @@ namespace RogueElements.Examples { + /// + /// Abstract base class for stair entities that connect dungeon floors. + /// + /// + /// + /// Stairs implement to participate in RogueElements' spawning system. + /// This allows them to be placed using spawn steps like FloorStairsStep which + /// handles stair placement according to floor plan or grid plan room locations. + /// + /// + /// In roguelike games, stairs typically represent: + /// + /// + /// Entrances from the previous floor (upstairs) + /// Exits to the next floor (downstairs) + /// Shortcuts, teleporters, or ladders in some games + /// + /// + /// See and for concrete implementations + /// that also implement and respectively. + /// + /// + /// + /// + /// public abstract class Stairs : ISpawnable { + /// + /// Initializes a new instance of the class. + /// protected Stairs() { } + /// + /// Initializes a new instance of the class by copying another. + /// + /// The stairs instance to copy. + /// + /// Used by implementations in derived classes to create + /// independent copies for spawning. + /// protected Stairs(Stairs other) { this.Loc = other.Loc; } + /// + /// Gets or sets the map location where this stair is placed. + /// + /// The tile coordinates of the stairs on the map. + /// + /// Set by spawn steps during map generation. Your game logic uses this + /// to determine where to place the player when entering/exiting floors. + /// public Loc Loc { get; set; } + /// + /// Creates a deep copy of this stair entity. + /// + /// A new instance that is a copy of this stair. + /// + /// Required by interface. The spawning system uses this + /// to create instances from templates in spawn lists. + /// public abstract ISpawnable Copy(); } } diff --git a/RogueElements.Examples/Common/StairsDown.cs b/RogueElements.Examples/Common/StairsDown.cs index c9cff693..a2841e49 100644 --- a/RogueElements.Examples/Common/StairsDown.cs +++ b/RogueElements.Examples/Common/StairsDown.cs @@ -8,18 +8,61 @@ namespace RogueElements.Examples { + /// + /// Represents downward stairs serving as the exit point for a dungeon floor. + /// + /// + /// + /// This class implements both (inherited from ) + /// and , making it suitable for use with exit placement steps. + /// The marker interface tells RogueElements that this entity + /// represents where the player leaves the floor to descend deeper. + /// + /// + /// In typical roguelike conventions: + /// + /// + /// Downstairs lead to the next (deeper) floor + /// Reaching downstairs is often the floor's objective + /// Often displayed as '>' in ASCII roguelikes + /// + /// + /// + /// + /// // Register downstairs in a spawn list for exit placement + /// var exitList = new SpawnList<IExit>(); + /// exitList.Add(new StairsDown(), 10); + /// + /// // Add to generation pipeline + /// layout.GenSteps.Add(new FloorStairsStep<MyContext, IEntrance, IExit>(entranceList, exitList)); + /// + /// + /// + /// + /// public class StairsDown : Stairs, IExit { + /// + /// Initializes a new instance of the class. + /// public StairsDown() : base() { } + /// + /// Initializes a new instance of the class by copying another. + /// + /// The stairs instance to copy. protected StairsDown(StairsDown other) : base(other) { } + /// + /// Creates a deep copy of this stair entity. + /// + /// A new instance with the same location. public override ISpawnable Copy() => new StairsDown(this); } } \ No newline at end of file diff --git a/RogueElements.Examples/Common/StairsUp.cs b/RogueElements.Examples/Common/StairsUp.cs index 7f3be8de..ba6ce9d1 100644 --- a/RogueElements.Examples/Common/StairsUp.cs +++ b/RogueElements.Examples/Common/StairsUp.cs @@ -8,18 +8,61 @@ namespace RogueElements.Examples { + /// + /// Represents upward stairs serving as the entrance point for a dungeon floor. + /// + /// + /// + /// This class implements both (inherited from ) + /// and , making it suitable for use with entrance placement steps. + /// The marker interface tells RogueElements that this entity + /// represents where the player enters the floor. + /// + /// + /// In typical roguelike conventions: + /// + /// + /// Upstairs lead back to the previous (shallower) floor + /// The player starts on upstairs when entering a new floor from above + /// Often displayed as '<' in ASCII roguelikes + /// + /// + /// + /// + /// // Register upstairs in a spawn list for entrance placement + /// var entranceList = new SpawnList<IEntrance>(); + /// entranceList.Add(new StairsUp(), 10); + /// + /// // Add to generation pipeline + /// layout.GenSteps.Add(new FloorStairsStep<MyContext, IEntrance, IExit>(entranceList, exitList)); + /// + /// + /// + /// + /// public class StairsUp : Stairs, IEntrance { + /// + /// Initializes a new instance of the class. + /// public StairsUp() : base() { } + /// + /// Initializes a new instance of the class by copying another. + /// + /// The stairs instance to copy. protected StairsUp(StairsUp other) : base(other) { } + /// + /// Creates a deep copy of this stair entity. + /// + /// A new instance with the same location. public override ISpawnable Copy() => new StairsUp(this); } } \ No newline at end of file diff --git a/RogueElements.Examples/Common/Tile.cs b/RogueElements.Examples/Common/Tile.cs index 8d6fc71d..14d43bec 100644 --- a/RogueElements.Examples/Common/Tile.cs +++ b/RogueElements.Examples/Common/Tile.cs @@ -9,27 +9,105 @@ namespace RogueElements.Examples { + /// + /// Reference implementation of representing a single map tile. + /// + /// + /// + /// This simple tile implementation uses an integer ID to distinguish terrain types. + /// It serves as a starting point for games using RogueElements. Extend this class + /// or create your own implementation to add game-specific + /// tile properties such as: + /// + /// + /// Visual variants or sprite indices + /// Movement costs for pathfinding + /// Damage or status effects (lava, poison, etc.) + /// Light blocking/transmitting properties + /// Interactive elements (doors, traps, switches) + /// + /// + /// + /// + /// public class Tile : ITile { + /// + /// Initializes a new instance of the class with wall terrain. + /// + /// + /// Default tiles are walls, which are then carved into rooms and hallways + /// during map generation. + /// public Tile() { this.ID = BaseMap.WALL_TERRAIN_ID; } + /// + /// Initializes a new instance of the class with the specified terrain ID. + /// + /// + /// The terrain type ID. Use constants from : + /// , , + /// or . + /// public Tile(int id) { this.ID = id; } + /// + /// Initializes a new instance of the class by copying another tile. + /// + /// The tile to copy. + /// + /// Used by to create independent tile instances. + /// protected Tile(Tile other) { this.ID = other.ID; } + /// + /// Gets or sets the terrain type identifier. + /// + /// + /// An integer identifying the terrain type. Standard values are defined in . + /// + /// + /// Generation steps use this ID to determine tile behavior: + /// + /// (0): Impassable wall + /// (1): Walkable floor + /// (2): Water terrain + /// + /// public int ID { get; set; } + /// + /// Creates a deep copy of this tile. + /// + /// A new instance with the same . + /// + /// Required by interface. RogueElements uses this to copy + /// template tiles when setting terrain, ensuring each map location has an + /// independent tile instance. + /// public ITile Copy() => new Tile(this); + /// + /// Determines whether this tile is equivalent to another tile. + /// + /// The tile to compare with. + /// + /// true if is a with the same + /// ; otherwise, false. + /// + /// + /// Used by generation algorithms to compare terrain types without reference equality. + /// Two tiles with the same ID are considered equivalent regardless of other properties. + /// public bool TileEquivalent(ITile other) { if (!(other is Tile tile)) diff --git a/RogueElements.Examples/Common/TreasureRoomComponent.cs b/RogueElements.Examples/Common/TreasureRoomComponent.cs index 4da9a78f..fe40d713 100644 --- a/RogueElements.Examples/Common/TreasureRoomComponent.cs +++ b/RogueElements.Examples/Common/TreasureRoomComponent.cs @@ -8,8 +8,68 @@ namespace RogueElements.Examples { + /// + /// A room component marker that identifies rooms as treasure rooms containing valuable loot. + /// + /// + /// + /// Treasure rooms are special rooms that typically contain better items, chests, or rewards. + /// This component allows generation steps to identify these rooms for specialized spawning + /// behavior. + /// + /// + /// Common uses for TreasureRoomComponent: + /// + /// + /// + /// Spawning high-value items or rare equipment only in treasure rooms + /// + /// + /// Adding treasure chests or containers to marked rooms + /// + /// + /// Excluding treasure rooms from regular enemy spawns + /// + /// + /// Applying special visual theming (gold piles, gem decorations) + /// + /// + /// + /// In RogueElements examples, this component is used with SetSpecialRoomStep to + /// create distinct treasure chambers off the main dungeon path. + /// + /// + /// + /// + /// // Create a special treasure room with custom spawning + /// var treasureRoomStep = new SetSpecialRoomStep<MyContext>() + /// { + /// RoomComponents = new List<RoomComponent> { new TreasureRoomComponent() }, + /// Room = new RoomGenSquare<MyContext>(new RandRange(6, 8), new RandRange(6, 8)) + /// }; + /// + /// // Later, spawn items only in treasure rooms + /// var itemStep = new RandomSpawnStep<MyContext, Item>(treasureItems) + /// { + /// Filters = new List<BaseRoomFilter> + /// { + /// new RoomFilterComponent(true, new TreasureRoomComponent()) + /// } + /// }; + /// + /// + /// + /// public class TreasureRoomComponent : RoomComponent { + /// + /// Creates a copy of this room component. + /// + /// A new instance. + /// + /// Required by base class. Room components are cloned + /// when rooms are copied during floor plan manipulation. + /// public override RoomComponent Clone() { return new TreasureRoomComponent(); diff --git a/RogueElements.Examples/Ex1_Tiles/Example1.cs b/RogueElements.Examples/Ex1_Tiles/Example1.cs index 3b684627..b5108ad3 100644 --- a/RogueElements.Examples/Ex1_Tiles/Example1.cs +++ b/RogueElements.Examples/Ex1_Tiles/Example1.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -8,19 +8,66 @@ namespace RogueElements.Examples.Ex1_Tiles { + /// + /// Example 1: Introduction to the RogueElements Map Generation Pipeline. + /// + /// This is the simplest possible example demonstrating the core concepts: + /// - MapGen<T> as the orchestrator that runs the generation pipeline + /// - GenStep<T> as the base class for all generation passes + /// - Priority-based ordering of generation steps + /// - Seed-based reproducibility for deterministic map generation + /// + /// This example creates a static, hand-designed map to illustrate the basic + /// pipeline mechanics before introducing procedural generation in later examples. + /// public static class Example1 { + /// + /// Runs Example 1, demonstrating a static tile-based map generation. + /// + /// Key RogueElements Concepts Introduced: + /// 1. MapGen<T> - The orchestrator that holds and executes GenSteps + /// 2. GenStep<T> - Base class for generation passes (InitTilesStep, SpecificTilesStep) + /// 3. Priority - Numeric ordering that determines step execution sequence + /// 4. Seed - A 64-bit value ensuring reproducible generation results + /// public static void Run() { Console.Clear(); const string title = "1: A Static Map Example"; + + // ============================================================ + // STEP 1: Create the MapGen orchestrator + // ============================================================ + // MapGen is the central orchestrator of the generation pipeline. + // The generic parameter T specifies the context type that GenSteps will operate on. + // MapGenContext must implement IGenContext (minimum) and typically ITiledGenContext + // for tile-based operations. var layout = new MapGen(); - // Initialize a 30x25 blank map full of Wall tiles + // ============================================================ + // STEP 2: Add an InitTilesStep to create the blank tile array + // ============================================================ + // InitTilesStep is a built-in GenStep that: + // - Creates the tile array with specified dimensions (30 wide x 25 tall) + // - Fills all tiles with the "wall" terrain (ID 0) by default + // + // The step is added with Priority 0. Priority determines execution order: + // - Lower numbers execute first + // - Steps with the same priority execute in the order they were added + // - Negative priorities are valid (see Ex2 which uses -2, -1, 0) InitTilesStep startStep = new InitTilesStep(30, 25); layout.GenSteps.Add(0, startStep); - // Draw a specific array of tiles onto the map at offset X2,Y3 + // ============================================================ + // STEP 3: Prepare a hand-designed tile pattern + // ============================================================ + // This array represents our desired map layout as ASCII art. + // '.' = floor/room terrain (passable) + // '#' = wall terrain (impassable) + // + // In procedural generation, you would use RoomGen classes instead, + // but this demonstrates how tiles work at the lowest level. string[] level = { ".........................", @@ -45,12 +92,19 @@ public static void Run() "....###...###...###......", "...........#.............", }; + + // Convert the string array to a 2D ITile array. + // ITile is the interface for tile data; Tile is the concrete implementation. + // The array is indexed as tiles[x][y] (column-major order). ITile[][] tiles = new ITile[level[0].Length][]; for (int xx = 0; xx < level[0].Length; xx++) { tiles[xx] = new ITile[level.Length]; for (int yy = 0; yy < level.Length; yy++) { + // Map.WALL_TERRAIN_ID (0) = wall/impassable + // Map.ROOM_TERRAIN_ID (1) = floor/passable + // These constants are defined in BaseMap for consistency across examples. int id = Map.WALL_TERRAIN_ID; if (level[yy][xx] == '.') id = Map.ROOM_TERRAIN_ID; @@ -58,30 +112,76 @@ public static void Run() } } + // ============================================================ + // STEP 4: Add a SpecificTilesStep to draw the pattern + // ============================================================ + // SpecificTilesStep is a built-in GenStep that stamps a pre-defined + // tile array onto the map at a specified offset (Loc is a 2D point). + // + // Parameters: + // - tiles: The 2D array of ITile to stamp + // - new Loc(2, 3): The X,Y offset where stamping begins (2 tiles from left, 3 from top) + // + // This step also has Priority 0, so it executes after InitTilesStep + // (which was added first at the same priority). var drawStep = new SpecificTilesStep(tiles, new Loc(2, 3)); layout.GenSteps.Add(0, drawStep); - // Run the generator and print + // ============================================================ + // STEP 5: Run the generation pipeline + // ============================================================ + // GenMap(seed) executes all GenSteps in priority order and returns the context. + // + // The seed (a 64-bit unsigned integer) initializes the random number generator. + // Using the same seed always produces the same map - this is crucial for: + // - Debugging: reproduce exact scenarios + // - Multiplayer: all clients generate identical maps + // - Seeded runs: players can share interesting map seeds + // + // MathUtils.Rand.NextUInt64() generates a random seed for variety. + // For reproducibility, you could use a fixed seed like: layout.GenMap(12345); MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); + + // The generated map is accessible through context.Map Print(context.Map, title); } + /// + /// Prints the generated map to the console using ASCII characters. + /// + /// The generated map containing the tile data. + /// The title to display above the map. + /// + /// This is a simple visualization helper. In a real game, you would render + /// the map using your game engine's graphics system instead. + /// + /// Tile ID interpretation: + /// - ID <= 0: Wall (rendered as '#') + /// - ID == 1: Floor (rendered as '.') + /// - Other: Unknown (rendered as '?') + /// public static void Print(Map map, string title) { var topString = new StringBuilder(string.Empty); string turnString = title; topString.Append($"{turnString,-82}"); topString.Append('\n'); + + // Draw a separator line for (int i = 0; i < map.Width + 1; i++) topString.Append("="); topString.Append('\n'); + // Iterate through all tiles and convert to ASCII characters. + // Map dimensions come from the Tiles array created by InitTilesStep. for (int y = 0; y < map.Height; y++) { for (int x = 0; x < map.Width; x++) { char tileChar; Tile tile = map.Tiles[x][y]; + + // Convert tile ID to display character if (tile.ID <= 0) // wall tileChar = '#'; else if (tile.ID == 1) // floor diff --git a/RogueElements.Examples/Ex1_Tiles/Map.cs b/RogueElements.Examples/Ex1_Tiles/Map.cs index 10177982..75e347f4 100644 --- a/RogueElements.Examples/Ex1_Tiles/Map.cs +++ b/RogueElements.Examples/Ex1_Tiles/Map.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -11,7 +11,56 @@ namespace RogueElements.Examples.Ex1_Tiles { + /// + /// The map data structure for Example 1 (Tiles). + /// + /// This class represents the final output of map generation - the actual game map + /// that will be used at runtime. It holds the tile data and any other game-specific + /// information your map needs. + /// + /// Key Concepts: + /// - Map is separate from MapGenContext (context is for generation, map is the result) + /// - The context holds a Map instance and populates it during generation + /// - After generation, you extract the Map from the context for use in your game + /// + /// Separation of Concerns: + /// - MapGenContext: Implements RogueElements interfaces, manages generation state + /// - Map: Your game's map class, holds runtime data (tiles, entities, etc.) + /// + /// This separation allows RogueElements to work with any game's map format. + /// + /// + /// BaseMap provides the common functionality: + /// - Tiles[x][y]: 2D array of Tile objects (column-major order) + /// - Width/Height: Dimensions derived from Tiles array + /// - Rand: Random number generator (ReRandom) for reproducible generation + /// - InitializeTiles(): Creates and initializes the tile array + /// - Terrain ID constants: WALL_TERRAIN_ID (0), ROOM_TERRAIN_ID (1), WATER_TERRAIN_ID (2) + /// + /// In a real game, you would add: + /// - Entity lists (monsters, items, NPCs) + /// - Spawn points + /// - Lighting/visibility data + /// - Any other map metadata + /// public class Map : BaseMap { + // This example uses BaseMap as-is with no additions. + // BaseMap provides everything needed for basic tile-based maps: + // + // From BaseMap: + // - public Tile[][] Tiles { get; set; } // The tile data + // - public int Width => Tiles.Length // Map width + // - public int Height => Tiles[0].Length // Map height + // - public ReRandom Rand { get; set; } // RNG for generation + // - public void InitializeTiles(w, h) // Create tile array + // + // Constants for tile IDs: + // - WALL_TERRAIN_ID = 0 // Impassable wall + // - ROOM_TERRAIN_ID = 1 // Passable floor + // - WATER_TERRAIN_ID = 2 // Water terrain (see Ex5) + // + // Later examples will add more data to their Map classes, + // such as item spawn locations (Ex6) and special rooms (Ex7). } } diff --git a/RogueElements.Examples/Ex1_Tiles/MapGenContext.cs b/RogueElements.Examples/Ex1_Tiles/MapGenContext.cs index 4b532ad4..feb0a948 100644 --- a/RogueElements.Examples/Ex1_Tiles/MapGenContext.cs +++ b/RogueElements.Examples/Ex1_Tiles/MapGenContext.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -10,11 +10,62 @@ namespace RogueElements.Examples.Ex1_Tiles { + /// + /// The map generation context for Example 1 (Tiles). + /// + /// A "context" in RogueElements is the state object that GenSteps operate on. + /// It holds all the data being built up during generation (tiles, rooms, spawns, etc.). + /// + /// Key Concepts: + /// - Every MapGen<T> requires a context type T that implements IGenContext + /// - The context is created fresh for each GenMap() call + /// - GenSteps read from and write to the context during Apply() + /// + /// Interface Hierarchy: + /// - IGenContext: Minimum interface (Rand, InitSeed, FinishGen) + /// - ITiledGenContext: Adds tile operations (GetTile, SetTile, CreateNew, etc.) + /// - IFloorPlanGenContext: Adds FloorPlan for room-based generation (see Ex2) + /// - IRoomGridGenContext: Adds GridPlan for grid-based layouts (see Ex3) + /// + /// This example uses the simplest setup: just ITiledGenContext for basic tile operations. + /// The heavy lifting is done by BaseMapGenContext<T> in the Common folder. + /// + /// + /// Why create a separate context class for each example? + /// - Different examples need different interfaces (IFloorPlanGenContext, etc.) + /// - Keeps each example self-contained and easy to understand + /// - Allows customization of context behavior per example + /// + /// In a real game, you would typically have one context class that implements + /// all the interfaces your generation pipeline needs. + /// public class MapGenContext : BaseMapGenContext { + /// + /// Initializes a new instance of the class. + /// + /// The base class constructor (BaseMapGenContext) creates a new Map instance. + /// MapGen.GenMap() will call InitSeed() to set up the random number generator + /// before any GenSteps are executed. + /// public MapGenContext() : base() { + // The base class handles all initialization: + // - Creates a new Map instance (this.Map = new TMap()) + // - Provides ITiledGenContext implementation (GetTile, SetTile, etc.) + // - Provides IGenContext implementation (Rand, InitSeed, FinishGen) } + + // Note: This context only implements ITiledGenContext (via BaseMapGenContext). + // That's sufficient for Example 1's tile-based operations. + // + // GenSteps like InitTilesStep and SpecificTilesStep require + // ITiledGenContext, which provides: + // - CreateNew(width, height): Initialize the tile array + // - GetTile(loc): Read a tile at a position + // - SetTile(loc, tile): Write a tile at a position + // - TileBlocked(loc): Check if a tile is impassable + // - RoomTerrain/WallTerrain: Default tile types } } diff --git a/RogueElements.Examples/Ex1_Tiles/README.md b/RogueElements.Examples/Ex1_Tiles/README.md new file mode 100644 index 00000000..9075243b --- /dev/null +++ b/RogueElements.Examples/Ex1_Tiles/README.md @@ -0,0 +1,148 @@ +# Example 1: Static Tiles + +A minimal example demonstrating direct tile manipulation without procedural generation. + +## What You'll Learn + +- How to set up a basic `MapGen` pipeline +- Using `InitTilesStep` to create a blank map +- Using `SpecificTilesStep` to draw a predefined pattern +- Understanding the generation context and tile system + +## Prerequisites + +- Read the [Common](../Common/README.md) folder documentation to understand the base classes + +## Concepts + +This example creates a **static map** - one that looks the same every time. While not useful for actual roguelike gameplay, it demonstrates the fundamental pipeline architecture before adding randomness. + +### The Pipeline Architecture + +RogueElements uses a pipeline of `GenStep` objects orchestrated by `MapGen`: + +``` +MapGen.GenMap(seed) + --> GenStep 1: InitTilesStep (creates blank tile grid) + --> GenStep 2: SpecificTilesStep (draws pattern onto grid) + --> returns MapGenContext +``` + +## Code Walkthrough + +### Step 1: Create the MapGen Instance + +```csharp +var layout = new MapGen(); +``` + +The `MapGen` is the orchestrator that holds all generation steps and executes them in priority order. + +### Step 2: Initialize the Tile Grid + +```csharp +InitTilesStep startStep = new InitTilesStep(30, 25); +layout.GenSteps.Add(0, startStep); +``` + +`InitTilesStep` creates a 30x25 grid filled with wall tiles (ID 0). The priority `0` determines execution order. + +### Step 3: Define the Pattern + +```csharp +string[] level = +{ + ".........................", + ".........................", + "...........#.............", + "....###...###...###......", + // ... more rows ... +}; +``` + +The pattern uses `.` for floor tiles and `#` for walls. This creates a decorative cross/diamond design. + +### Step 4: Convert to Tile Array + +```csharp +ITile[][] tiles = new ITile[level[0].Length][]; +for (int xx = 0; xx < level[0].Length; xx++) +{ + tiles[xx] = new ITile[level.Length]; + for (int yy = 0; yy < level.Length; yy++) + { + int id = Map.WALL_TERRAIN_ID; + if (level[yy][xx] == '.') + id = Map.ROOM_TERRAIN_ID; + tiles[xx][yy] = new Tile(id); + } +} +``` + +The string array is converted to a 2D tile array, mapping characters to terrain IDs. + +### Step 5: Draw the Pattern + +```csharp +var drawStep = new SpecificTilesStep(tiles, new Loc(2, 3)); +layout.GenSteps.Add(0, drawStep); +``` + +`SpecificTilesStep` places the tile array at offset (2, 3) on the map. Using priority `0` means it runs after (or with) the initialization step. + +### Step 6: Generate and Display + +```csharp +MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); +Print(context.Map, title); +``` + +`GenMap()` executes all steps in priority order and returns the completed context. + +## Try It + +```bash +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +Press `1` to run Example 1. + +**What to observe:** +- The same pattern appears every time (static map) +- The pattern is offset from the top-left corner by (2, 3) tiles +- Wall tiles (`#`) surround the pattern area + +**Expected output:** +``` +1: A Static Map Example +=============================== +############################## +############################## +############################## +##.........................### +##.........................### +##...........#.............### +##....###...###...###......### +... (decorative pattern continues) +``` + +## Key Classes Used + +| Class | Purpose | +|-------|---------| +| `MapGen` | Orchestrates the generation pipeline | +| `InitTilesStep` | Creates a blank tile grid of specified dimensions | +| `SpecificTilesStep` | Draws a predefined tile pattern at an offset | +| `MapGenContext` | Holds map state during generation | +| `Tile` | Simple tile implementation with an ID | + +## Key Takeaways + +1. **Pipeline Pattern**: Generation is a series of steps, each modifying the context +2. **Priority Ordering**: Steps are executed by priority (lower = earlier) +3. **Context Pattern**: The context holds all state and is passed through steps +4. **Tile IDs**: Terrain types are represented by integer IDs (0=wall, 1=floor) + +## Next Steps + +[Example 2: Freeform Rooms](../Ex2_Rooms/README.md) introduces procedural room generation using `FloorPlan`. diff --git a/RogueElements.Examples/Ex2_Rooms/Example2.cs b/RogueElements.Examples/Ex2_Rooms/Example2.cs index 97088d4c..143ef2c0 100644 --- a/RogueElements.Examples/Ex2_Rooms/Example2.cs +++ b/RogueElements.Examples/Ex2_Rooms/Example2.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -8,67 +8,211 @@ namespace RogueElements.Examples.Ex2_Rooms { + /// + /// Example 2: Introduction to FloorPlan-based Room Generation. + /// + /// This example introduces the "freeform" approach to procedural room generation + /// using FloorPlan. Unlike the static tiles in Example 1, rooms are placed + /// dynamically based on configurable parameters. + /// + /// Key Concepts Introduced: + /// - FloorPlan: An abstract representation of rooms and halls before tile conversion + /// - RoomGen: Classes that define room shapes (square, round, cave, etc.) + /// - FloorPathBranch: A path generator that creates branching dungeon layouts + /// - SpawnList: Weighted random selection for room/hall types + /// - DrawFloorToTileStep: Converts FloorPlan to actual tiles + /// + /// Two-Phase Generation: + /// 1. PLANNING: Create abstract room/hall layout in FloorPlan (no tiles yet) + /// 2. DRAWING: Convert FloorPlan to actual tiles + /// + /// This separation allows: + /// - Complex room placement algorithms without worrying about tiles + /// - Multiple drawing strategies from the same plan + /// - Validation of room connectivity before committing to tiles + /// public static class Example2 { + /// + /// Runs Example 2, demonstrating FloorPlan-based procedural room generation. + /// + /// Generation Pipeline: + /// Priority -2: InitFloorPlanStep - Create empty FloorPlan + /// Priority -1: FloorPathBranch - Place rooms and halls + /// Priority 0: DrawFloorToTileStep - Convert to tiles + /// + /// The negative priorities ensure planning happens before drawing. + /// public static void Run() { Console.Clear(); const string title = "2: A Map Made with Rooms and Halls"; + // ============================================================ + // STEP 1: Create the MapGen orchestrator + // ============================================================ + // Same as Example 1, but our context now implements IFloorPlanGenContext + // in addition to ITiledGenContext. var layout = new MapGen(); - // Initialize a 54x40 floorplan with which to populate with rectangular floor and halls. + // ============================================================ + // STEP 2: Initialize the FloorPlan + // ============================================================ + // InitFloorPlanStep creates an empty FloorPlan with the specified dimensions. + // The FloorPlan is a planning structure that holds abstract room and hall + // information - no actual tiles are created yet. + // + // Think of FloorPlan as a blueprint: it describes WHERE rooms go and + // HOW they connect, but doesn't fill in the actual tile data. + // + // Priority -2 ensures this runs first (before room placement at -1). InitFloorPlanStep startGen = new InitFloorPlanStep(54, 40); layout.GenSteps.Add(-2, startGen); - // Create some room types to place + // ============================================================ + // STEP 3: Define room types with weighted probabilities + // ============================================================ + // SpawnList is a weighted random selection container. + // Each entry has: + // - The item (a RoomGen that defines a room shape) + // - A weight (higher = more likely to be chosen) + // + // RoomGen is the base class for room shape generators: + // - RoomGenSquare: Rectangular rooms (most common) + // - RoomGenRound: Circular/oval rooms + // - RoomGenCave: Organic cave-like shapes (see Ex3) + // - RoomGenCross: Cross-shaped rooms + // - And many more... + // + // RandRange specifies a random range: RandRange(4, 8) means 4-7 inclusive. var genericRooms = new SpawnList> { - { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, // cross - { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, // round + // Square rooms: 4-7 tiles wide, 4-7 tiles tall, weight 10 + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + + // Round rooms: 5-8 tiles wide, 5-8 tiles tall, weight 10 + // Equal weights mean equal probability of selection. + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, }; - // Create some hall types to place + // ============================================================ + // STEP 4: Define hall types with weighted probabilities + // ============================================================ + // Halls connect rooms together. They use PermissiveRoomGen which + // is a RoomGen that can be stretched to fit between connection points. + // + // Hall types shown: + // - RoomGenAngledHall: L-shaped or straight halls with variable sizes + // - RoomGenSquare: Simple 1x1 halls (just a doorway connection) var genericHalls = new SpawnList> { + // Angled halls: Can bend around obstacles + // Parameters: turnBias (0 = no preference), width range, height range { new RoomGenAngledHall(0, new RandRange(3, 7), new RandRange(3, 7)), 10 }, + + // 1x1 "halls" - essentially direct doorway connections between rooms + // Weight 20 makes these twice as likely as angled halls. { new RoomGenSquare(new RandRange(1), new RandRange(1)), 20 }, }; - // Feed the room and hall types to a path that is composed of a branching tree + // ============================================================ + // STEP 5: Create the room placement path + // ============================================================ + // FloorPathBranch is a path generator that creates a branching tree of rooms. + // It's one of several path generators available: + // - FloorPathBranch: Branching tree (most natural for dungeons) + // - FloorPathStartStepGeneric: Linear path from start to end + // - FloorPathGridGeneric: Grid-based placement (see Ex3) + // + // The path generator determines the overall dungeon structure: + // - How rooms connect to each other + // - The branching pattern + // - Dead ends vs loops FloorPathBranch path = new FloorPathBranch(genericRooms, genericHalls) { + // HallPercent: Chance (0-100) that a hall room is placed between rooms. + // 50 = 50% chance of halls, creating a mix of direct and hallway connections. HallPercent = 50, + + // FillPercent: Target percentage of the map area to fill with rooms. + // RandRange(45) = exactly 45%. Higher values = more rooms, denser maps. FillPercent = new RandRange(45), + + // BranchRatio: Chance (0-100) to branch off the main path. + // RandRange(0, 25) = 0-24% chance. Higher = more dead ends and side branches. BranchRatio = new RandRange(0, 25), }; + // Priority -1 ensures room placement happens after FloorPlan initialization (-2) + // but before tile drawing (0). layout.GenSteps.Add(-1, path); - // Draw the rooms onto the tiled map, with 1 TILE padded on each side + // ============================================================ + // STEP 6: Convert FloorPlan to tiles + // ============================================================ + // DrawFloorToTileStep is the bridge between planning and tile data. + // It iterates through all rooms and halls in the FloorPlan and + // "draws" them onto the tile grid. + // + // Parameter: padding (1 = one tile of wall between rooms and map edge) + // Padding prevents rooms from touching the map boundary. + // + // This step requires both: + // - IFloorPlanGenContext (to read the FloorPlan) + // - ITiledGenContext (to write the tiles) layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); - // Run the generator and print + // ============================================================ + // STEP 7: Run the generation pipeline + // ============================================================ + // Execution order: + // 1. Priority -2: InitFloorPlanStep creates empty FloorPlan + // 2. Priority -1: FloorPathBranch places rooms and halls + // 3. Priority 0: DrawFloorToTileStep converts plan to tiles + // + // The seed ensures reproducibility - same seed = same map. MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); + + // The context now contains: + // - context.RoomPlan: The FloorPlan with room/hall layout + // - context.Map: The tile data ready for gameplay Print(context.Map, title); } + /// + /// Prints the generated map to the console using ASCII characters. + /// + /// The generated map containing the tile data. + /// The title to display above the map. + /// + /// Note how the output looks similar to Example 1, but the layout + /// was generated procedurally rather than hand-designed. + /// + /// The FloorPlan abstraction means we get varied, interesting layouts + /// every time we run with a different seed, while maintaining + /// connectivity guarantees (all rooms are reachable). + /// public static void Print(Map map, string title) { var topString = new StringBuilder(string.Empty); string turnString = title; topString.Append($"{turnString,-82}"); topString.Append('\n'); + + // Draw separator for (int i = 0; i < map.Width + 1; i++) topString.Append("="); topString.Append('\n'); + // Convert tiles to ASCII for (int y = 0; y < map.Height; y++) { for (int x = 0; x < map.Width; x++) { char tileChar; Tile tile = map.Tiles[x][y]; + + // Use BaseMap constants for consistency across examples switch (tile.ID) { case BaseMap.WALL_TERRAIN_ID: diff --git a/RogueElements.Examples/Ex2_Rooms/Map.cs b/RogueElements.Examples/Ex2_Rooms/Map.cs index 57bf4297..36a6d16b 100644 --- a/RogueElements.Examples/Ex2_Rooms/Map.cs +++ b/RogueElements.Examples/Ex2_Rooms/Map.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -11,7 +11,57 @@ namespace RogueElements.Examples.Ex2_Rooms { + /// + /// The map data structure for Example 2 (Rooms). + /// + /// This Map class is identical to Example 1's Map. The procedural room generation + /// doesn't require any changes to the Map itself - all the new functionality + /// is in MapGenContext (which implements IFloorPlanGenContext). + /// + /// Key Insight: The Map class represents your game's runtime map data. + /// It doesn't need to know HOW the map was generated (static tiles vs rooms), + /// only WHAT the final result is (the tile array). + /// + /// This separation means: + /// - Your game code works with Map, not generation internals + /// - You can swap generation strategies without changing game code + /// - The Map format is stable even as generation evolves + /// + /// + /// In a real game, your Map class would likely contain additional data: + /// + /// Runtime Data: + /// - List of spawned monsters/NPCs + /// - List of items on the ground + /// - Player spawn point + /// - Exit/stairs locations + /// - Fog of war / visibility state + /// + /// Metadata: + /// - Dungeon floor number + /// - Difficulty level + /// - Theme/tileset to use for rendering + /// - Music track to play + /// + /// The BaseMap class provides the core tile storage that all examples share. + /// public class Map : BaseMap { + // Inherits from BaseMap without additions (same as Example 1). + // + // The interesting changes in Example 2 are: + // 1. MapGenContext now implements IFloorPlanGenContext + // 2. Example2.cs uses FloorPlan-based generation steps + // + // The Map output format remains the same - just tiles. + // This demonstrates how RogueElements separates the generation + // process from the output format. + // + // From BaseMap: + // - Tile[][] Tiles: The 2D tile array + // - int Width, Height: Map dimensions + // - ReRandom Rand: Random number generator + // - InitializeTiles(w, h): Create tile array + // - WALL_TERRAIN_ID, ROOM_TERRAIN_ID, WATER_TERRAIN_ID: Tile type constants } } diff --git a/RogueElements.Examples/Ex2_Rooms/MapGenContext.cs b/RogueElements.Examples/Ex2_Rooms/MapGenContext.cs index 8040b206..5b9b1a06 100644 --- a/RogueElements.Examples/Ex2_Rooms/MapGenContext.cs +++ b/RogueElements.Examples/Ex2_Rooms/MapGenContext.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -10,17 +10,100 @@ namespace RogueElements.Examples.Ex2_Rooms { + /// + /// The map generation context for Example 2 (Rooms). + /// + /// This context adds IFloorPlanGenContext to enable FloorPlan-based room generation. + /// FloorPlan is the core abstraction for "freeform" room placement, where rooms + /// can be placed anywhere on the map (as opposed to grid-based placement in Ex3). + /// + /// Interface Progression: + /// - Ex1: ITiledGenContext only (basic tile operations) + /// - Ex2: ITiledGenContext + IFloorPlanGenContext (room-based generation) + /// - Ex3: ITiledGenContext + IRoomGridGenContext (grid-based generation) + /// + /// IFloorPlanGenContext provides: + /// - RoomPlan property: Access to the FloorPlan being built + /// - InitPlan(plan): Initialize the FloorPlan (called by InitFloorPlanStep) + /// + /// FloorPlan contains: + /// - Room list: All rooms with their positions and shapes + /// - Hall list: Connections between rooms + /// - Adjacency information: Which rooms connect to which + /// + /// + /// Why separate IFloorPlanGenContext from ITiledGenContext? + /// + /// 1. Separation of concerns: Planning (FloorPlan) vs Drawing (Tiles) + /// 2. Flexibility: Some GenSteps only need FloorPlan, some only need tiles + /// 3. Composability: You can have multiple planning phases before drawing + /// + /// The generation flow is: + /// 1. InitFloorPlanStep creates empty FloorPlan (needs IFloorPlanGenContext) + /// 2. Path generators add rooms/halls to FloorPlan (needs IFloorPlanGenContext) + /// 3. DrawFloorToTileStep converts FloorPlan to tiles (needs both interfaces) + /// public class MapGenContext : BaseMapGenContext, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public MapGenContext() : base() { + // BaseMapGenContext provides ITiledGenContext implementation. + // This class adds IFloorPlanGenContext on top. + // + // Note: RoomPlan starts as null and is initialized by InitFloorPlanStep. } + /// + /// Gets the FloorPlan containing the abstract room and hall layout. + /// + /// FloorPlan is the central data structure for room-based generation: + /// - Holds all rooms as RoomGen instances with positions + /// - Holds all halls connecting rooms + /// - Tracks adjacency (which rooms connect to which) + /// - Provides queries like "get all rooms adjacent to room X" + /// + /// This is populated by path generators (FloorPathBranch, etc.) + /// and consumed by DrawFloorToTileStep to create actual tiles. + /// + /// + /// FloorPlan methods you might use in custom GenSteps: + /// - GetRoom(index): Get a specific room + /// - GetHall(index): Get a specific hall + /// - GetAdjacents(index): Get indices of adjacent rooms + /// - RoomCount: Total number of rooms + /// - HallCount: Total number of halls + /// - DrawOnMap(): Render to tiles (used by DrawFloorToTileStep) + /// public FloorPlan RoomPlan { get; private set; } + /// + /// Initializes the FloorPlan for this generation context. + /// + /// This is called by InitFloorPlanStep to set up the FloorPlan + /// before room placement begins. The FloorPlan is created with + /// the map dimensions and stored here for subsequent GenSteps to use. + /// + /// The FloorPlan instance to use for this generation. + /// + /// This method is part of the IFloorPlanGenContext interface. + /// It's called once at the start of generation (by InitFloorPlanStep) + /// and the plan is then modified by subsequent room placement steps. + /// + /// The 'private set' on RoomPlan ensures only InitPlan can set it, + /// maintaining proper initialization order. + /// public void InitPlan(FloorPlan plan) { + // Store the FloorPlan for use by room placement and drawing steps. + // After this call, GenSteps can access context.RoomPlan to: + // - Add rooms (path generators) + // - Add halls (path generators) + // - Query room positions (spawning steps) + // - Convert to tiles (DrawFloorToTileStep) this.RoomPlan = plan; } } diff --git a/RogueElements.Examples/Ex2_Rooms/README.md b/RogueElements.Examples/Ex2_Rooms/README.md new file mode 100644 index 00000000..c283c296 --- /dev/null +++ b/RogueElements.Examples/Ex2_Rooms/README.md @@ -0,0 +1,192 @@ +# Example 2: Freeform Rooms + +Generate maps with randomly placed rooms connected by hallways using `FloorPlan`. + +## What You'll Learn + +- How to use `FloorPlan` for freeform room placement +- Creating room and hall generators with `SpawnList` +- Using `FloorPathBranch` to create branching dungeon layouts +- Converting a `FloorPlan` to actual tiles + +## Prerequisites + +- [Example 1: Static Tiles](../Ex1_Tiles/README.md) +- Understanding of the pipeline architecture + +## Concepts + +### FloorPlan Architecture + +`FloorPlan` is an intermediate representation that defines rooms and hallways **before** they become tiles: + +``` +FloorPlan (abstract room positions) + --> DrawFloorToTileStep + --> Tile Grid (concrete tiles) +``` + +This separation allows the algorithm to focus on room connectivity without worrying about tile details. + +### Freeform vs Grid-Based + +This example uses **freeform** placement where rooms can be positioned anywhere. This contrasts with [Example 3](../Ex3_Grid/README.md) which constrains rooms to a grid. + +## Code Walkthrough + +### Step 1: Initialize the FloorPlan + +```csharp +InitFloorPlanStep startGen = new InitFloorPlanStep(54, 40); +layout.GenSteps.Add(-2, startGen); +``` + +Creates a 54x40 `FloorPlan`. Note the negative priority (`-2`) - this ensures it runs before the path generation step. + +### Step 2: Define Room Types + +```csharp +var genericRooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, +}; +``` + +`SpawnList` is a weighted random picker: +- **RoomGenSquare**: Creates rectangular rooms (4-8 tiles wide/tall) +- **RoomGenRound**: Creates rounded/elliptical rooms (5-9 tiles wide/tall) +- Weight `10` gives each equal probability + +### Step 3: Define Hall Types + +```csharp +var genericHalls = new SpawnList> +{ + { new RoomGenAngledHall(0, new RandRange(3, 7), new RandRange(3, 7)), 10 }, + { new RoomGenSquare(new RandRange(1), new RandRange(1)), 20 }, +}; +``` + +Hallways connect rooms: +- **RoomGenAngledHall**: L-shaped or straight hallways (3-7 tiles) +- **RoomGenSquare(1,1)**: Single-tile connection points (weight 20 = more common) + +### Step 4: Create the Path Generator + +```csharp +FloorPathBranch path = new FloorPathBranch(genericRooms, genericHalls) +{ + HallPercent = 50, + FillPercent = new RandRange(45), + BranchRatio = new RandRange(0, 25), +}; + +layout.GenSteps.Add(-1, path); +``` + +`FloorPathBranch` creates a tree-like dungeon structure: + +| Property | Purpose | +|----------|---------| +| `HallPercent` | Chance (%) that connections include visible halls (vs direct room adjacency) | +| `FillPercent` | Target percentage of the map to fill with rooms | +| `BranchRatio` | Chance of creating branches vs extending the main path | + +### Step 5: Convert to Tiles + +```csharp +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); +``` + +`DrawFloorToTileStep` converts the abstract `FloorPlan` to actual tiles: +- Parameter `1` = padding (1 tile of wall around each room) +- This ensures rooms don't touch each other directly + +### Step 6: Generate + +```csharp +MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); +``` + +The seed ensures reproducibility - same seed = same map. + +## MapGenContext Changes + +The context must implement `IFloorPlanGenContext`: + +```csharp +public class MapGenContext : BaseMapGenContext, IFloorPlanGenContext +{ + public FloorPlan RoomPlan { get; private set; } + + public void InitPlan(FloorPlan plan) + { + this.RoomPlan = plan; + } +} +``` + +This allows `FloorPlan`-based steps to work with the context. + +## Try It + +```bash +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +Press `2` to run Example 2. + +**What to observe:** +- Different layout each run (procedural generation!) +- Mix of square and round rooms +- Hallways connecting rooms in a tree structure +- Some rooms at dead ends, others along the main path + +**Example output:** +``` +2: A Map Made with Rooms and Halls +======================================================= +###################################################### +###########.........################################## +###########.........#####################............# +###########.........#####################............# +###########............................##............# +###########.........###################..............# +###########.........########...........############## +###........#........########...........############## +... (procedurally generated layout) +``` + +## Key Classes Used + +| Class | Purpose | +|-------|---------| +| `InitFloorPlanStep` | Creates the FloorPlan structure | +| `FloorPathBranch` | Generates branching room layouts | +| `RoomGenSquare` | Creates rectangular rooms | +| `RoomGenRound` | Creates elliptical rooms | +| `RoomGenAngledHall` | Creates L-shaped or straight hallways | +| `SpawnList` | Weighted random selection | +| `DrawFloorToTileStep` | Converts FloorPlan to tiles | +| `RandRange` | Random range (min, max) | + +## Key Takeaways + +1. **FloorPlan Abstraction**: Design rooms abstractly, then render to tiles +2. **Weighted Spawning**: `SpawnList` enables controlled randomness +3. **Path Algorithms**: `FloorPathBranch` creates natural dungeon layouts +4. **Padding**: The `1` in `DrawFloorToTileStep(1)` prevents rooms from merging + +## Comparison: Freeform vs Grid + +| Aspect | FloorPlan (Freeform) | GridPlan (Grid-Based) | +|--------|---------------------|----------------------| +| Room placement | Anywhere | Locked to grid cells | +| Hall length | Variable | Determined by cell spacing | +| Layout feel | Organic, sprawling | Structured, orderly | +| Use case | Natural caves, forests | Traditional dungeons | + +## Next Steps + +[Example 3: Grid-Based Layouts](../Ex3_Grid/README.md) introduces `GridPlan` for more structured dungeon generation. diff --git a/RogueElements.Examples/Ex3_Grid/Example3.cs b/RogueElements.Examples/Ex3_Grid/Example3.cs index 6297f7fb..eb73cc94 100644 --- a/RogueElements.Examples/Ex3_Grid/Example3.cs +++ b/RogueElements.Examples/Ex3_Grid/Example3.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -8,8 +8,29 @@ namespace RogueElements.Examples.Ex3_Grid { + /// + /// Example 3: Grid-Based Room Generation. + /// + /// This example introduces the GridPlan system, which arranges rooms in a regular grid + /// structure. This is a higher-level abstraction than FloorPlan (Ex2), providing more + /// control over room distribution and connectivity. + /// + /// Key concepts introduced: + /// - GridPlan: A grid of cells where each cell can contain a room + /// - IRoomGridGenContext: Interface that provides GridPlan access + /// - InitGridPlanStep: Creates the initial grid structure + /// - GridPathBranch: Creates branching room layouts within the grid + /// - DrawGridToFloorStep: Converts GridPlan to FloorPlan + /// + /// The generation pipeline flows: GridPlan -> FloorPlan -> Tiles + /// This layered approach allows flexible room placement at multiple abstraction levels. + /// public static class Example3 { + /// + /// Runs the grid-based room generation example. + /// Demonstrates how to use GridPlan to create structured room layouts. + /// public static void Run() { Console.Clear(); @@ -17,39 +38,98 @@ public static void Run() var layout = new MapGen(); - // Initialize a 6x4 grid of 10x10 cells. + // ============================================================ + // STEP 1: Initialize the GridPlan + // ============================================================ + // InitGridPlanStep creates a grid structure that will hold our rooms. + // Unlike FloorPlan which places rooms at exact pixel coordinates, + // GridPlan divides the map into a regular grid of cells. + // + // GridPlan Properties: + // - CellX/CellY: Number of grid cells horizontally/vertically (6x4 = 24 potential rooms) + // - CellWidth/CellHeight: Size of each cell in tiles (9x9 tiles per cell) + // + // The constructor parameter (1) is the default wall thickness between cells. + // Total map size = (CellX * CellWidth) + borders, (CellY * CellHeight) + borders var startGen = new InitGridPlanStep(1) { - CellX = 6, - CellY = 4, - CellWidth = 9, - CellHeight = 9, + CellX = 6, // 6 columns of cells + CellY = 4, // 4 rows of cells + CellWidth = 9, // Each cell is 9 tiles wide + CellHeight = 9, // Each cell is 9 tiles tall }; layout.GenSteps.Add(-4, startGen); - // Create a path that is composed of branches in grid lock + // ============================================================ + // STEP 2: Create the Room Layout Path + // ============================================================ + // GridPathBranch creates a branching tree of rooms within the grid. + // It starts from a random cell and grows outward, creating branches. + // + // GridPathBranch Properties: + // - RoomRatio: Percentage of grid cells that will contain rooms (70% = ~17 of 24 cells) + // - BranchRatio: How much the path branches (0-50% chance per expansion to branch) + // + // Other path types available: + // - GridPathTwoSides: Rooms on opposite sides connected + // - GridPathCircle: Rooms arranged in a ring + // - GridPathGrid: Full grid connectivity var path = new GridPathBranch { - RoomRatio = new RandRange(70), - BranchRatio = new RandRange(0, 50), + RoomRatio = new RandRange(70), // Fill 70% of cells with rooms + BranchRatio = new RandRange(0, 50), // Random branching factor 0-50% }; + // Define which room generators can be used in grid cells. + // SpawnList is a weighted random selection list. + // Each entry has a generator and a weight (10 = equal probability). var genericRooms = new SpawnList> { - { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, // cross - { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, // round + // RoomGenSquare: Rectangular rooms with random dimensions + // RandRange(4, 8) means width/height between 4-7 tiles + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + + // RoomGenRound: Elliptical/circular rooms + // RandRange(5, 9) means diameter between 5-8 tiles + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, }; path.GenericRooms = genericRooms; + // Define hall generators for connections between rooms. + // PermissiveRoomGen is a base class for halls that can connect any two rooms. + // RoomGenAngledHall creates L-shaped or straight halls. + // The parameter (50) is the chance of creating an angled (L-shaped) hall vs straight. var genericHalls = new SpawnList> { { new RoomGenAngledHall(50), 10 } }; path.GenericHalls = genericHalls; + // Add the path generator at the same priority as grid initialization. + // Steps at the same priority run in the order they were added. layout.GenSteps.Add(-4, path); - // Output the rooms into a FloorPlan + // ============================================================ + // STEP 3: Convert GridPlan to FloorPlan + // ============================================================ + // DrawGridToFloorStep transforms the abstract grid layout into + // concrete room placements in a FloorPlan. + // + // This is where grid cells become actual room rectangles with + // exact positions and dimensions. The FloorPlan then handles + // the detailed room boundaries and hall connections. + // + // Pipeline so far: GridPlan (grid cells) -> FloorPlan (room bounds) layout.GenSteps.Add(-2, new DrawGridToFloorStep()); - // Draw the rooms of the FloorPlan onto the tiled map, with 1 TILE padded on each side + // ============================================================ + // STEP 4: Render FloorPlan to Tiles + // ============================================================ + // DrawFloorToTileStep converts the FloorPlan's room definitions + // into actual tile data in the map. + // + // The parameter (1) specifies padding - how many tiles of wall + // to maintain around each room and hall. This creates visual + // separation between adjacent rooms. + // + // Final pipeline: GridPlan -> FloorPlan -> Tiles (the actual map!) layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); // Run the generator and print @@ -57,6 +137,11 @@ public static void Run() Print(context.Map, title); } + /// + /// Prints the generated map to the console. + /// + /// The map to print. + /// Title to display above the map. public static void Print(Map map, string title) { var topString = new StringBuilder(string.Empty); diff --git a/RogueElements.Examples/Ex3_Grid/Map.cs b/RogueElements.Examples/Ex3_Grid/Map.cs index 92cda1e6..409fe441 100644 --- a/RogueElements.Examples/Ex3_Grid/Map.cs +++ b/RogueElements.Examples/Ex3_Grid/Map.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -11,6 +11,16 @@ namespace RogueElements.Examples.Ex3_Grid { + /// + /// Map class for grid-based room generation example. + /// + /// This map is identical to Ex2's Map because grid-based generation + /// doesn't require additional map features - it's purely a different + /// approach to room placement that produces the same tile output. + /// + /// The grid structure exists only during generation (in GridPlan/FloorPlan) + /// and is not stored in the final map. The map only contains the rendered tiles. + /// public class Map : BaseMap { } diff --git a/RogueElements.Examples/Ex3_Grid/MapGenContext.cs b/RogueElements.Examples/Ex3_Grid/MapGenContext.cs index 6280cba4..11919650 100644 --- a/RogueElements.Examples/Ex3_Grid/MapGenContext.cs +++ b/RogueElements.Examples/Ex3_Grid/MapGenContext.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -10,22 +10,74 @@ namespace RogueElements.Examples.Ex3_Grid { + /// + /// Map generation context that supports grid-based room layouts. + /// + /// This context implements IRoomGridGenContext, which combines: + /// - IFloorPlanGenContext: Access to FloorPlan for room placement + /// - IGridPlanGenContext: Access to GridPlan for grid-based layouts + /// + /// The interface hierarchy enables a two-stage generation process: + /// 1. GridPlan defines the high-level room arrangement in a grid + /// 2. FloorPlan receives the converted room definitions for rendering + /// + /// IRoomGridGenContext is the key interface for grid-based generation. + /// GenSteps that require grid functionality constrain their type parameter + /// to this interface (e.g., GridPathBranch<T> where T : IRoomGridGenContext). + /// public class MapGenContext : BaseMapGenContext, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public MapGenContext() : base() { } + /// + /// Gets the FloorPlan containing room and hall definitions. + /// FloorPlan is populated when DrawGridToFloorStep converts the GridPlan. + /// Required by IFloorPlanGenContext. + /// public FloorPlan RoomPlan { get; private set; } + /// + /// Gets the GridPlan containing the grid-based room layout. + /// GridPlan is initialized by InitGridPlanStep and populated by path generators + /// like GridPathBranch. It represents rooms as cells in a regular grid. + /// Required by IGridPlanGenContext. + /// + /// + /// GridPlan stores: + /// - Grid dimensions (cells and cell sizes) + /// - Which cells contain rooms + /// - Connections between adjacent cells (halls) + /// - Room generators assigned to each occupied cell + /// + /// This is a higher-level abstraction than FloorPlan. While FloorPlan + /// stores exact room boundaries, GridPlan stores logical positions + /// in a grid structure. + /// public GridPlan GridPlan { get; private set; } + /// + /// Initializes the FloorPlan for this context. + /// Called by DrawGridToFloorStep when converting from GridPlan. + /// Required by IFloorPlanGenContext. + /// + /// The FloorPlan to use for room placement. public void InitPlan(FloorPlan plan) { this.RoomPlan = plan; } + /// + /// Initializes the GridPlan for this context. + /// Called by InitGridPlanStep at the start of grid-based generation. + /// Required by IGridPlanGenContext. + /// + /// The GridPlan defining the grid structure. public void InitGrid(GridPlan plan) { this.GridPlan = plan; diff --git a/RogueElements.Examples/Ex3_Grid/README.md b/RogueElements.Examples/Ex3_Grid/README.md new file mode 100644 index 00000000..75190c9e --- /dev/null +++ b/RogueElements.Examples/Ex3_Grid/README.md @@ -0,0 +1,201 @@ +# Example 3: Grid-Based Layouts + +Generate dungeons using a grid structure where rooms occupy fixed cells. + +## What You'll Learn + +- How to use `GridPlan` for structured room placement +- Configuring grid cells and dimensions +- Using `GridPathBranch` for grid-constrained paths +- The two-step conversion: GridPlan --> FloorPlan --> Tiles + +## Prerequisites + +- [Example 2: Freeform Rooms](../Ex2_Rooms/README.md) +- Understanding of FloorPlan concepts + +## Concepts + +### Grid-Based Generation + +`GridPlan` divides the map into a grid of cells. Each cell can contain: +- A room +- Part of a hallway +- Empty space (wall) + +This creates more structured, traditional dungeon layouts compared to freeform placement. + +### The Two-Step Conversion + +``` +GridPlan (cells with room assignments) + --> DrawGridToFloorStep + --> FloorPlan (concrete room positions) + --> DrawFloorToTileStep + --> Tile Grid +``` + +Grid-based generation adds an extra abstraction layer for structured control. + +## Code Walkthrough + +### Step 1: Initialize the Grid + +```csharp +var startGen = new InitGridPlanStep(1) +{ + CellX = 6, + CellY = 4, + CellWidth = 9, + CellHeight = 9, +}; +layout.GenSteps.Add(-4, startGen); +``` + +Creates a 6x4 grid of cells, each 9x9 tiles: + +| Property | Value | Purpose | +|----------|-------|---------| +| `CellX` | 6 | Number of cells horizontally | +| `CellY` | 4 | Number of cells vertically | +| `CellWidth` | 9 | Tile width of each cell | +| `CellHeight` | 9 | Tile height of each cell | +| Constructor arg `1` | 1 | Default hall width | + +Total map size = (6 * 9) x (4 * 9) = 54 x 36 tiles. + +### Step 2: Create the Grid Path + +```csharp +var path = new GridPathBranch +{ + RoomRatio = new RandRange(70), + BranchRatio = new RandRange(0, 50), +}; +``` + +`GridPathBranch` creates paths within the grid: + +| Property | Purpose | +|----------|---------| +| `RoomRatio` | Percentage of cells to fill with rooms (70%) | +| `BranchRatio` | Chance of branching vs extending (0-50%) | + +### Step 3: Define Rooms and Halls + +```csharp +var genericRooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, +}; +path.GenericRooms = genericRooms; + +var genericHalls = new SpawnList> +{ + { new RoomGenAngledHall(50), 10 }, +}; +path.GenericHalls = genericHalls; +``` + +Same room types as Example 2, but constrained to fit within grid cells. + +Note: `RoomGenAngledHall(50)` means 50% chance of an angled (L-shaped) hall vs straight. + +### Step 4: Convert Grid to FloorPlan + +```csharp +layout.GenSteps.Add(-2, new DrawGridToFloorStep()); +``` + +`DrawGridToFloorStep` converts the abstract grid into a concrete `FloorPlan` with positioned rooms. + +### Step 5: Convert FloorPlan to Tiles + +```csharp +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); +``` + +Same as Example 2 - converts the FloorPlan to actual tile data. + +## MapGenContext Changes + +The context must implement `IRoomGridGenContext` (which extends `IFloorPlanGenContext`): + +```csharp +public class MapGenContext : BaseMapGenContext, IRoomGridGenContext +{ + public FloorPlan RoomPlan { get; private set; } + public GridPlan GridPlan { get; private set; } + + public void InitPlan(FloorPlan plan) { this.RoomPlan = plan; } + public void InitGrid(GridPlan plan) { this.GridPlan = plan; } +} +``` + +## Try It + +```bash +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +Press `3` to run Example 3. + +**What to observe:** +- Rooms are aligned to a grid pattern +- Hallways run between adjacent cells +- More structured layout than Example 2 +- Regular spacing between rooms + +**Example output:** +``` +3: A Map made with Rooms and Halls arranged in a grid. +======================================================= +###################################################### +########.......####################################### +########.......####.......############################ +########.......####.......############################ +########.......####.......#####........############### +########..............#########........############### +########.......####...#########........############### +########.......####...#########........############### +... (grid-aligned layout) +``` + +## Key Classes Used + +| Class | Purpose | +|-------|---------| +| `InitGridPlanStep` | Creates the grid structure | +| `GridPathBranch` | Generates branching paths on the grid | +| `DrawGridToFloorStep` | Converts GridPlan to FloorPlan | +| `DrawFloorToTileStep` | Converts FloorPlan to tiles | +| `IRoomGridGenContext` | Context interface for grid-based generation | + +## Grid vs FloorPlan: When to Use Which + +| Use Case | Approach | +|----------|----------| +| Traditional dungeon crawler | GridPlan | +| Roguelike with regular room spacing | GridPlan | +| Cave systems, organic layouts | FloorPlan | +| Need precise room placement control | GridPlan | +| Want rooms to vary in position freely | FloorPlan | + +## Hybrid Approaches + +You can combine both: +1. Start with `GridPlan` for the main structure +2. Add `FloorPlan` modifications for special areas +3. Both convert to the same tile format + +## Key Takeaways + +1. **Grid Abstraction**: Cells provide structure before room generation +2. **Two-Step Conversion**: Grid --> FloorPlan --> Tiles +3. **Cell Sizing**: Room sizes are constrained by cell dimensions +4. **Structured Layouts**: Grid-based maps feel more "designed" + +## Next Steps + +[Example 4: Stair Placement](../Ex4_Stairs/README.md) adds entrance and exit stairs to the map. diff --git a/RogueElements.Examples/Ex4_Stairs/Example4.cs b/RogueElements.Examples/Ex4_Stairs/Example4.cs index 800d7f30..435ea71b 100644 --- a/RogueElements.Examples/Ex4_Stairs/Example4.cs +++ b/RogueElements.Examples/Ex4_Stairs/Example4.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -8,15 +8,40 @@ namespace RogueElements.Examples.Ex4_Stairs { + /// + /// Example 4: Entity Spawning with Stairs. + /// + /// This example introduces the entity spawning system, using stairs as the example. + /// Stairs are "spawnable" entities that can be placed on valid floor tiles. + /// + /// Key concepts introduced: + /// - IPlaceableGenContext<T>: Interface for spawning typed entities + /// - IViewPlaceableGenContext<T>: Extended interface for viewing spawned entities + /// - GetAllFreeTiles(): Finds valid spawn locations + /// - PlaceItem(): Adds an entity at a specific location + /// - FloorStairsStep: Built-in step for placing entrance/exit stairs + /// + /// Important: You must implement IPlaceableGenContext separately for EACH + /// spawnable type. This example implements it for both StairsUp and StairsDown. + /// public static class Example4 { + /// + /// Runs the stair spawning example. + /// Demonstrates how to place typed entities on the generated map. + /// public static void Run() { Console.Clear(); const string title = "4: A Map with Stairs Up and Down"; var layout = new MapGen(); - // Initialize a 3x2 grid of 10x10 cells. + // ============================================================ + // STEPS 1-4: Grid-based room generation (same as Ex3) + // ============================================================ + // We use a smaller 3x2 grid for this example to focus on stairs. + + // Initialize a 3x2 grid of 9x9 cells (smaller than Ex3 for clarity) var startGen = new InitGridPlanStep(1) { CellX = 3, @@ -26,7 +51,7 @@ public static void Run() }; layout.GenSteps.Add(-4, startGen); - // Create a path that is composed of a ring around the edge + // Create a branching room layout (see Ex3 for detailed explanation) var path = new GridPathBranch { RoomRatio = new RandRange(70), @@ -35,8 +60,8 @@ public static void Run() var genericRooms = new SpawnList> { - { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, // cross - { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, // round + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, }; path.GenericRooms = genericRooms; @@ -48,13 +73,34 @@ public static void Run() layout.GenSteps.Add(-4, path); - // Output the rooms into a FloorPlan + // Convert GridPlan to FloorPlan, then to tiles layout.GenSteps.Add(-2, new DrawGridToFloorStep()); - - // Draw the rooms of the FloorPlan onto the tiled map, with 1 TILE padded on each side layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); - // Add the stairs up and down + // ============================================================ + // STEP 5: Place Stairs (NEW IN THIS EXAMPLE) + // ============================================================ + // FloorStairsStep places entrance and exit stairs. + // It finds valid spawn locations and places the stairs entities. + // + // Type parameters: + // - TContext: The map context (must implement IPlaceableGenContext for both stair types) + // - TEntrance: The entrance stair type (StairsUp - implements IEntrance) + // - TExit: The exit stair type (StairsDown - implements IExit) + // + // Constructor parameters: + // - filterIndex: Which spawn filter to use (0 = default) + // - entrance: Template for entrance stairs (copied when placed) + // - exit: Template for exit stairs (copied when placed) + // + // The step works by: + // 1. Calling GetAllFreeTiles() on the context to find valid locations + // 2. Selecting appropriate locations (maximally separated) + // 3. Calling PlaceItem() to spawn the stairs at those locations + // + // This requires MapGenContext to implement: + // - IPlaceableGenContext + // - IPlaceableGenContext layout.GenSteps.Add(2, new FloorStairsStep(0, new StairsUp(), new StairsDown())); // Run the generator and print @@ -62,6 +108,11 @@ public static void Run() Print(context.Map, title); } + /// + /// Prints the generated map to the console, including stair markers. + /// + /// The map to print. + /// Title to display above the map. public static void Print(Map map, string title) { var topString = new StringBuilder(string.Empty); @@ -80,20 +131,23 @@ public static void Print(Map map, string title) Tile tile = map.Tiles[x][y]; char tileChar = tile.ID <= BaseMap.WALL_TERRAIN_ID ? '#' : tile.ID == BaseMap.ROOM_TERRAIN_ID ? '.' : '?'; + // Check if this location has an entrance stair (StairsUp) + // Stairs are stored in the Map's entity lists, not in the tile data foreach (StairsUp entrance in map.GenEntrances) { if (entrance.Loc == loc) { - tileChar = '<'; + tileChar = '<'; // Traditional roguelike symbol for stairs up break; } } + // Check if this location has an exit stair (StairsDown) foreach (StairsDown entrance in map.GenExits) { if (entrance.Loc == loc) { - tileChar = '>'; + tileChar = '>'; // Traditional roguelike symbol for stairs down break; } } diff --git a/RogueElements.Examples/Ex4_Stairs/Map.cs b/RogueElements.Examples/Ex4_Stairs/Map.cs index ece3a837..f5b3da75 100644 --- a/RogueElements.Examples/Ex4_Stairs/Map.cs +++ b/RogueElements.Examples/Ex4_Stairs/Map.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -9,16 +9,60 @@ namespace RogueElements.Examples.Ex4_Stairs { + /// + /// Map class that stores spawned stair entities. + /// + /// This map extends BaseMap with collections for entrance and exit stairs. + /// Spawned entities are stored separately from tiles because: + /// 1. Entities have additional properties beyond just tile type (e.g., Loc) + /// 2. Multiple entity types can occupy the same conceptual "layer" + /// 3. Entity data may need to be serialized/saved differently than tiles + /// + /// The Map is the final output of generation. It contains: + /// - Tiles (inherited from BaseMap) + /// - GenEntrances (StairsUp entities) + /// - GenExits (StairsDown entities) + /// + /// Note: The naming convention "Gen" prefix indicates these collections + /// are populated during generation. Your game might have separate + /// runtime collections for entities that move or change. + /// public class Map : BaseMap { + /// + /// Initializes a new instance of the class. + /// Creates empty collections for entrance and exit stairs. + /// public Map() { this.GenEntrances = new List(); this.GenExits = new List(); } + /// + /// Gets or sets the list of entrance stairs (StairsUp) on this map. + /// These are typically where the player enters the level. + /// StairsUp implements IEntrance, marking it as an entry point. + /// + /// + /// In a typical roguelike: + /// - StairsUp leads to the previous floor (going "up" toward the surface) + /// - Multiple entrances are possible for multi-entrance dungeons + /// - The first entrance is usually the player's spawn point + /// public List GenEntrances { get; set; } + /// + /// Gets or sets the list of exit stairs (StairsDown) on this map. + /// These are typically where the player exits to the next level. + /// StairsDown implements IExit, marking it as an exit point. + /// + /// + /// In a typical roguelike: + /// - StairsDown leads to the next floor (going "down" deeper into the dungeon) + /// - Multiple exits allow for branching dungeon paths + /// - Some games place the exit far from the entrance for exploration + /// public List GenExits { get; set; } } } diff --git a/RogueElements.Examples/Ex4_Stairs/MapGenContext.cs b/RogueElements.Examples/Ex4_Stairs/MapGenContext.cs index 8dc59c25..0505a5b4 100644 --- a/RogueElements.Examples/Ex4_Stairs/MapGenContext.cs +++ b/RogueElements.Examples/Ex4_Stairs/MapGenContext.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -10,35 +10,107 @@ namespace RogueElements.Examples.Ex4_Stairs { + /// + /// Map generation context that supports entity spawning. + /// + /// This context extends Ex3's grid support with entity placement capabilities. + /// It implements IViewPlaceableGenContext for both StairsUp and StairsDown, + /// enabling spawn steps to place these entities on the map. + /// + /// KEY CONCEPT: IPlaceableGenContext<T> + /// ======================================== + /// This interface must be implemented SEPARATELY for each spawnable entity type. + /// There is no generic "spawn anything" mechanism - each type needs explicit support. + /// + /// The interface requires: + /// - GetAllFreeTiles(): Returns all valid spawn locations + /// - GetFreeTiles(Rect): Returns valid spawn locations within a rectangle + /// - CanPlaceItem(Loc): Checks if a specific location is valid + /// - PlaceItem(Loc, T): Actually spawns the entity at the location + /// + /// IViewPlaceableGenContext<T> extends this with: + /// - Count: Number of spawned entities + /// - GetItem(int): Get entity by index + /// - GetLoc(int): Get location by index + /// + /// This design allows different entity types to have different spawn rules. + /// For example, stairs might only spawn on floor tiles, while items might + /// spawn on any non-wall tile including water. + /// public class MapGenContext : BaseMapGenContext, IRoomGridGenContext, IViewPlaceableGenContext, IViewPlaceableGenContext { + /// + /// Initializes a new instance of the class. + /// public MapGenContext() : base() { } + /// + /// Delegate type for finding open tiles within a rectangle. + /// Used internally to share tile-finding logic between stair types. + /// + /// The rectangular area to search. + /// List of valid spawn locations. protected delegate List GetOpen(Rect rect); + /// + /// Gets the FloorPlan containing room and hall definitions. + /// public FloorPlan RoomPlan { get; private set; } + /// + /// Gets the GridPlan containing the grid-based room layout. + /// public GridPlan GridPlan { get; private set; } + /// + /// Gets the list of entrance stairs (StairsUp) on the map. + /// Delegates to the Map's GenEntrances collection. + /// public List GenEntrances => this.Map.GenEntrances; + /// + /// Gets the list of exit stairs (StairsDown) on the map. + /// Delegates to the Map's GenExits collection. + /// public List GenExits => this.Map.GenExits; + // ============================================================ + // IViewPlaceableGenContext.Count implementations + // ============================================================ + // Explicit interface implementations allow the same property name + // with different return values for different type parameters. + + /// + /// Gets the count of entrance stairs. + /// int IViewPlaceableGenContext.Count => this.GenEntrances.Count; + /// + /// Gets the count of exit stairs. + /// int IViewPlaceableGenContext.Count => this.GenExits.Count; + /// + /// Determines whether a tile can be modified at the given location. + /// Prevents tiles from being changed where stairs have been placed. + /// This protects spawned entities from being overwritten by later generation steps. + /// + /// The location to check. + /// The tile that would be set (unused in this check). + /// True if the tile can be set; false if stairs occupy this location. public override bool CanSetTile(Loc loc, ITile tile) { + // Check all entrance stairs for (int ii = 0; ii < this.GenEntrances.Count; ii++) { if (this.GenEntrances[ii].Loc == loc) return false; } + // Check all exit stairs for (int ii = 0; ii < this.GenExits.Count; ii++) { if (this.GenExits[ii].Loc == loc) @@ -48,62 +120,173 @@ public override bool CanSetTile(Loc loc, ITile tile) return true; } + /// + /// Initializes the FloorPlan for this context. + /// + /// The FloorPlan to use for room placement. public void InitPlan(FloorPlan plan) { this.RoomPlan = plan; } + /// + /// Initializes the GridPlan for this context. + /// + /// The GridPlan defining the grid structure. public void InitGrid(GridPlan plan) { this.GridPlan = plan; } - List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + // ============================================================ + // IPlaceableGenContext implementation + // ============================================================ + // These methods define how StairsUp entities can be spawned. - List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// + /// Gets all free tiles where StairsUp can be placed. + /// Called by spawn steps to find valid spawn locations. + /// + /// List of all valid spawn locations for entrance stairs. + List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// + /// Gets free tiles within a specific rectangle for StairsUp. + /// + /// The area to search. + /// List of valid spawn locations within the rectangle. List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); - List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); - + /// + /// Checks if StairsUp can be placed at a specific location. + /// + /// The location to check. + /// True if the location is a valid spawn point. bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); - bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); - + /// + /// Places a StairsUp entity at the specified location. + /// Creates a copy of the template and assigns the location. + /// + /// Where to place the stairs. + /// Template stairs to copy. void IPlaceableGenContext.PlaceItem(Loc loc, StairsUp item) { + // Copy the template - never modify the original var stairs = (StairsUp)item.Copy(); stairs.Loc = loc; this.GenEntrances.Add(stairs); } + // ============================================================ + // IPlaceableGenContext implementation + // ============================================================ + // These methods define how StairsDown entities can be spawned. + // Note: Nearly identical to StairsUp, but uses GenExits list. + + /// + /// Gets all free tiles where StairsDown can be placed. + /// + /// List of all valid spawn locations for exit stairs. + List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + + /// + /// Gets free tiles within a specific rectangle for StairsDown. + /// + /// The area to search. + /// List of valid spawn locations within the rectangle. + List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + + /// + /// Checks if StairsDown can be placed at a specific location. + /// + /// The location to check. + /// True if the location is a valid spawn point. + bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + + /// + /// Places a StairsDown entity at the specified location. + /// + /// Where to place the stairs. + /// Template stairs to copy. void IPlaceableGenContext.PlaceItem(Loc loc, StairsDown item) { + // Copy the template - never modify the original var stairs = (StairsDown)item.Copy(); stairs.Loc = loc; this.GenExits.Add(stairs); } + // ============================================================ + // IViewPlaceableGenContext implementations + // ============================================================ + // These methods allow reading back spawned entities by index. + + /// + /// Gets a StairsUp entity by index. + /// + /// The index of the stairs. + /// The stairs at the given index. StairsUp IViewPlaceableGenContext.GetItem(int index) => this.GenEntrances[index]; + /// + /// Gets the location of a StairsUp entity by index. + /// + /// The index of the stairs. + /// The location of the stairs. Loc IViewPlaceableGenContext.GetLoc(int index) => this.GenEntrances[index].Loc; + /// + /// Gets a StairsDown entity by index. + /// + /// The index of the stairs. + /// The stairs at the given index. StairsDown IViewPlaceableGenContext.GetItem(int index) => this.GenExits[index]; + /// + /// Gets the location of a StairsDown entity by index. + /// + /// The index of the stairs. + /// The location of the stairs. Loc IViewPlaceableGenContext.GetLoc(int index) => this.GenExits[index].Loc; + // ============================================================ + // Helper methods for finding spawn locations + // ============================================================ + + /// + /// Gets all free tiles on the entire map using the provided search function. + /// + /// Function to find open tiles within a rectangle. + /// List of all valid spawn locations. protected virtual List GetAllFreeTiles(GetOpen func) { + // Search the entire map bounds return func?.Invoke(new Rect(0, 0, this.Width, this.Height)); } + /// + /// Finds open tiles within a rectangular area. + /// A tile is "open" if it's a floor tile (ROOM_TERRAIN_ID). + /// + /// The area to search. + /// List of locations that are valid spawn points. protected List GetOpenTiles(Rect rect) { + // Define what makes a tile "open" for spawning bool CheckOp(Loc loc) => !this.IsTileOccupied(loc); + // Grid.FindTilesInBox is a utility that iterates all tiles in the rect + // and returns those that pass the check function return Grid.FindTilesInBox(rect.Start, rect.Size, CheckOp); } + /// + /// Checks if a tile is occupied (not a floor tile). + /// Used to determine valid spawn locations. + /// + /// The location to check. + /// True if the tile is not a floor tile. private bool IsTileOccupied(Loc loc) => this.Map.Tiles[loc.X][loc.Y].ID != Map.ROOM_TERRAIN_ID; } } diff --git a/RogueElements.Examples/Ex4_Stairs/README.md b/RogueElements.Examples/Ex4_Stairs/README.md new file mode 100644 index 00000000..4b8bb6af --- /dev/null +++ b/RogueElements.Examples/Ex4_Stairs/README.md @@ -0,0 +1,241 @@ +# Example 4: Stair Placement + +Add entrance and exit stairs to connect dungeon floors. + +## What You'll Learn + +- How to place spawnable entities on the map +- Using `FloorStairsStep` for stair placement +- Implementing `IPlaceableGenContext` for custom spawnables +- Understanding the `IEntrance` and `IExit` interfaces + +## Prerequisites + +- [Example 3: Grid-Based Layouts](../Ex3_Grid/README.md) +- Understanding of grid-based generation + +## Concepts + +### Spawnable Entities + +Stairs are **spawnable entities** - objects placed on the map after tile generation. RogueElements uses a generic spawning system: + +``` +ISpawnable (base interface) + --> Stairs (abstract, has Loc property) + --> StairsUp : IEntrance (player spawn point) + --> StairsDown : IExit (floor exit) +``` + +### FloorStairsStep + +`FloorStairsStep` automatically places exactly one entrance and one exit: +- Entrance is placed first +- Exit is placed at maximum distance from entrance +- Both avoid walls and existing entities + +## Code Walkthrough + +### Step 1: Standard Grid Setup + +```csharp +// Smaller grid for this example: 3x2 cells +var startGen = new InitGridPlanStep(1) +{ + CellX = 3, + CellY = 2, + CellWidth = 9, + CellHeight = 9, +}; +layout.GenSteps.Add(-4, startGen); + +// Standard path generation +var path = new GridPathBranch { /* ... */ }; +layout.GenSteps.Add(-4, path); + +// Grid to FloorPlan to Tiles +layout.GenSteps.Add(-2, new DrawGridToFloorStep()); +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); +``` + +Same setup as Example 3, but with a smaller 3x2 grid. + +### Step 2: Add Stair Placement + +```csharp +layout.GenSteps.Add(2, new FloorStairsStep( + 0, // reserved parameter + new StairsUp(), // entrance template + new StairsDown() // exit template +)); +``` + +`FloorStairsStep` parameters: +- First arg: Reserved (pass 0) +- Second arg: Template for entrance stairs +- Third arg: Template for exit stairs + +**Priority 2** ensures this runs after tile drawing (priority 0). + +## Map Class Changes + +The Map must store the placed stairs: + +```csharp +public class Map : BaseMap +{ + public Map() + { + this.GenEntrances = new List(); + this.GenExits = new List(); + } + + public List GenEntrances { get; set; } + public List GenExits { get; set; } +} +``` + +## MapGenContext Changes + +The context must implement `IViewPlaceableGenContext` for both stair types: + +```csharp +public class MapGenContext : BaseMapGenContext, IRoomGridGenContext, + IViewPlaceableGenContext, IViewPlaceableGenContext +{ + // Protect stairs from being overwritten + public override bool CanSetTile(Loc loc, ITile tile) + { + foreach (var entrance in this.GenEntrances) + if (entrance.Loc == loc) return false; + foreach (var exit in this.GenExits) + if (exit.Loc == loc) return false; + return true; + } + + // Placement implementation + void IPlaceableGenContext.PlaceItem(Loc loc, StairsUp item) + { + var stairs = (StairsUp)item.Copy(); + stairs.Loc = loc; + this.GenEntrances.Add(stairs); + } + + // Query methods for finding valid placement locations + List IPlaceableGenContext.GetAllFreeTiles() + => this.GetAllFreeTiles(this.GetOpenTiles); + + bool IPlaceableGenContext.CanPlaceItem(Loc loc) + => !this.IsTileOccupied(loc); +} +``` + +Key interface requirements: + +| Method | Purpose | +|--------|---------| +| `GetAllFreeTiles()` | Returns all valid placement locations | +| `GetFreeTiles(Rect)` | Returns valid locations within a rectangle | +| `CanPlaceItem(Loc)` | Checks if a specific location is valid | +| `PlaceItem(Loc, T)` | Places the entity at the location | + +## Rendering Stairs + +The Print method checks for stairs and displays them: + +```csharp +foreach (StairsUp entrance in map.GenEntrances) +{ + if (entrance.Loc == loc) + { + tileChar = '<'; // Up stairs + break; + } +} + +foreach (StairsDown exit in map.GenExits) +{ + if (exit.Loc == loc) + { + tileChar = '>'; // Down stairs + break; + } +} +``` + +## Try It + +```bash +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +Press `4` to run Example 4. + +**What to observe:** +- `<` symbol marks the entrance (stairs up) +- `>` symbol marks the exit (stairs down) +- Stairs are always placed on floor tiles +- Entrance and exit are typically in different rooms + +**Example output:** +``` +4: A Map with Stairs Up and Down +============================== +############################## +#####.........################ +#####.........################ +#####.<.......################ +#####.........#.......######## +#####.........#.......######## +##############........######## +##############........######## +##############........######## +##############.>......######## +##############........######## +############################## +``` + +## Key Classes Used + +| Class | Purpose | +|-------|---------| +| `FloorStairsStep` | Places entrance and exit stairs | +| `StairsUp` | Entrance stair entity (implements `IEntrance`) | +| `StairsDown` | Exit stair entity (implements `IExit`) | +| `IViewPlaceableGenContext` | Interface for placeable entity contexts | +| `ISpawnable` | Base interface for spawnable entities | + +## Placement Algorithm + +`FloorStairsStep` uses this algorithm: + +1. Find all valid floor tiles +2. Place entrance at a random valid tile +3. Calculate distances from entrance to all other tiles +4. Place exit at the tile with maximum distance +5. This ensures the player must traverse most of the floor + +## Creating Custom Spawnables + +To create your own spawnable entities: + +```csharp +public class MyEntity : ISpawnable +{ + public Loc Loc { get; set; } + public ISpawnable Copy() => new MyEntity(this); +} +``` + +Then implement `IPlaceableGenContext` in your context. + +## Key Takeaways + +1. **Spawnable Pattern**: Entities are templates that get copied and placed +2. **Interface Segregation**: Different spawn types use different interfaces +3. **Tile Protection**: `CanSetTile` prevents overwriting placed entities +4. **Distance Maximization**: Exit placement maximizes path length + +## Next Steps + +[Example 5: Terrain Features](../Ex5_Terrain/README.md) adds water and other terrain using Perlin noise. diff --git a/RogueElements.Examples/Ex5_Terrain/Example5.cs b/RogueElements.Examples/Ex5_Terrain/Example5.cs index e7446d7d..d9e1757b 100644 --- a/RogueElements.Examples/Ex5_Terrain/Example5.cs +++ b/RogueElements.Examples/Ex5_Terrain/Example5.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -8,8 +8,23 @@ namespace RogueElements.Examples.Ex5_Terrain { + /// + /// Example 5: Demonstrates terrain generation using Perlin noise and post-processing. + /// This example introduces water terrain that overlays the basic floor tiles, + /// showing how terrain steps modify existing tile data rather than replacing it. + /// + /// + /// Key concepts introduced: + /// - PerlinWaterStep: Uses Perlin noise for natural-looking terrain distribution + /// - MapTerrainStencil: Controls which tiles can be modified by terrain steps + /// - Post-processing steps: DropDiagonalBlockStep and EraseIsolatedStep clean up artifacts + /// - Terrain layering: Water terrain (ID=2) coexists with walls (ID=0) and floors (ID=1) + /// public static class Example5 { + /// + /// Runs the terrain generation example, creating a map with Perlin noise-based water. + /// public static void Run() { Console.Clear(); @@ -57,15 +72,45 @@ public static void Run() // Add the stairs up and down layout.GenSteps.Add(2, new FloorStairsStep(0, new StairsUp(), new StairsDown())); - // Generate water (specified by user as Terrain 2) with a frequency of 35%, using Perlin Noise in an order of 3, softness 1. + // =================================================================================== + // TERRAIN GENERATION - NEW CONCEPT + // =================================================================================== + // Terrain steps add secondary tile types (like water) to an already-generated map. + // They work by modifying existing tiles, not by creating the initial room structure. + + // Define the terrain ID for water (using a constant makes the code more readable) + // This matches WATER_TERRAIN_ID in BaseMap (ID=2) const int terrain = 2; + + // PerlinWaterStep uses Perlin noise to create natural-looking terrain distribution. + // Parameters explained: + // - RandRange(35): Target 35% of eligible tiles to become water + // - 3: Perlin noise "order" (octaves) - higher = more detail/complexity + // - new Tile(terrain): The tile type to place (water with ID=2) + // - MapTerrainStencil: Controls WHERE water can be placed (see below) + // - 1: "Softness" - smooths transitions at water edges + // + // The stencil (false, true, false, false) means: + // - false: Don't place water on impassable terrain (walls) + // - true: Allow placing water on floor tiles + // - false: Don't place water on existing water + // - false: Don't place water on blocked tiles var waterPostProc = new PerlinWaterStep(new RandRange(35), 3, new Tile(terrain), new MapTerrainStencil(false, true, false, false), 1); layout.GenSteps.Add(3, waterPostProc); - // Remove walls where diagonals of water exist and replace with water + // =================================================================================== + // POST-PROCESSING CLEANUP STEPS + // =================================================================================== + // After placing terrain, some cleanup is often needed to fix visual artifacts. + + // DropDiagonalBlockStep fixes "diagonal wall" issues. + // When water exists diagonally across a wall corner, it can look unnatural. + // This step removes such wall tiles and replaces them with water for better flow. layout.GenSteps.Add(4, new DropDiagonalBlockStep(new Tile(terrain))); - // Remove water stuck in the walls + // EraseIsolatedStep removes terrain that got "stuck" inside walls. + // Perlin noise doesn't understand room boundaries, so some water tiles + // may end up completely surrounded by walls. This step erases them. layout.GenSteps.Add(4, new EraseIsolatedStep(new Tile(terrain))); // Run the generator and print @@ -73,6 +118,14 @@ public static void Run() Print(context.Map, title); } + /// + /// Prints the generated map to the console with terrain visualization. + /// + /// The generated map containing tiles, stairs, and terrain. + /// Title to display above the map. + /// + /// Terrain rendering: Walls='#', Floor='.', Water='~', Stairs='<' and '>'. + /// public static void Print(Map map, string title) { var topString = new StringBuilder(string.Empty); @@ -90,6 +143,9 @@ public static void Print(Map map, string title) Loc loc = new Loc(x, y); char tileChar; Tile tile = map.Tiles[x][y]; + + // Map tile IDs to display characters + // Note the new WATER_TERRAIN_ID case for terrain display switch (tile.ID) { case BaseMap.WALL_TERRAIN_ID: diff --git a/RogueElements.Examples/Ex5_Terrain/Map.cs b/RogueElements.Examples/Ex5_Terrain/Map.cs index e9406412..60af08d1 100644 --- a/RogueElements.Examples/Ex5_Terrain/Map.cs +++ b/RogueElements.Examples/Ex5_Terrain/Map.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -11,16 +11,42 @@ namespace RogueElements.Examples.Ex5_Terrain { + /// + /// Map data structure for Example 5 with support for terrain features. + /// Extends BaseMap which already supports multiple terrain types via tile IDs. + /// + /// + /// + /// Terrain in RogueElements is represented by tile IDs: + /// - WALL_TERRAIN_ID (0): Impassable walls + /// - ROOM_TERRAIN_ID (1): Walkable floor + /// - WATER_TERRAIN_ID (2): Special terrain (water, lava, etc.) + /// + /// + /// The terrain system is tile-based, not entity-based. Water tiles replace + /// floor tiles rather than being placed on top of them. This is different + /// from items or mobs which are entities placed at locations. + /// + /// public class Map : BaseMap { + /// + /// Initializes a new instance of the class. + /// public Map() { this.GenEntrances = new List(); this.GenExits = new List(); } + /// + /// Gets or sets the list of upward stairs (level entrances). + /// public List GenEntrances { get; set; } + /// + /// Gets or sets the list of downward stairs (level exits). + /// public List GenExits { get; set; } } } diff --git a/RogueElements.Examples/Ex5_Terrain/MapGenContext.cs b/RogueElements.Examples/Ex5_Terrain/MapGenContext.cs index c7190740..37066606 100644 --- a/RogueElements.Examples/Ex5_Terrain/MapGenContext.cs +++ b/RogueElements.Examples/Ex5_Terrain/MapGenContext.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -10,35 +10,87 @@ namespace RogueElements.Examples.Ex5_Terrain { + /// + /// Generation context for Example 5 that supports terrain modification. + /// Identical to Ex4's context - terrain steps work on ITiledGenContext + /// which is already provided by BaseMapGenContext. + /// + /// + /// + /// Terrain steps like PerlinWaterStep only require ITiledGenContext to function. + /// They use SetTile() to modify existing tiles, which is why CanSetTile() is + /// important - it prevents terrain from overwriting stairs and other placed entities. + /// + /// + /// The MapTerrainStencil passed to terrain steps provides additional filtering + /// beyond CanSetTile(), controlling which tile types are eligible for modification. + /// + /// public class MapGenContext : BaseMapGenContext, IRoomGridGenContext, IViewPlaceableGenContext, IViewPlaceableGenContext { + /// + /// Initializes a new instance of the class. + /// public MapGenContext() : base() { } + /// + /// Delegate type for methods that find open tiles within a rectangular area. + /// + /// The rectangular area to search. + /// List of valid tile locations. protected delegate List GetOpen(Rect rect); + /// + /// Gets the floor plan for freeform room-based generation. + /// public FloorPlan RoomPlan { get; private set; } + /// + /// Gets the grid plan for grid-based room layouts. + /// public GridPlan GridPlan { get; private set; } + /// + /// Gets the list of upward stairs (entrances) placed on this map. + /// public List GenEntrances => this.Map.GenEntrances; + /// + /// Gets the list of downward stairs (exits) placed on this map. + /// public List GenExits => this.Map.GenExits; + /// int IViewPlaceableGenContext.Count => this.GenEntrances.Count; + /// int IViewPlaceableGenContext.Count => this.GenExits.Count; + /// + /// Determines whether a tile can be set at the specified location. + /// Prevents terrain steps from overwriting stairs. + /// + /// The location to check. + /// The tile to potentially place. + /// True if the tile can be set; false if blocked by stairs. + /// + /// This is crucial for terrain generation: without this check, PerlinWaterStep + /// could place water tiles on top of stairs, making them inaccessible. + /// The terrain step calls this before each SetTile() operation. + /// public override bool CanSetTile(Loc loc, ITile tile) { + // Don't allow terrain to overwrite entrance stairs for (int ii = 0; ii < this.GenEntrances.Count; ii++) { if (this.GenEntrances[ii].Loc == loc) return false; } + // Don't allow terrain to overwrite exit stairs for (int ii = 0; ii < this.GenExits.Count; ii++) { if (this.GenExits[ii].Loc == loc) @@ -48,28 +100,43 @@ public override bool CanSetTile(Loc loc, ITile tile) return true; } + /// + /// Initializes the floor plan for this context. + /// + /// The floor plan to use. public void InitPlan(FloorPlan plan) { this.RoomPlan = plan; } + /// + /// Initializes the grid plan for this context. + /// + /// The grid plan to use. public void InitGrid(GridPlan plan) { this.GridPlan = plan; } + /// List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + /// List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + /// bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + /// bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + /// void IPlaceableGenContext.PlaceItem(Loc loc, StairsUp item) { var stairs = (StairsUp)item.Copy(); @@ -77,6 +144,7 @@ void IPlaceableGenContext.PlaceItem(Loc loc, StairsUp item) this.GenEntrances.Add(stairs); } + /// void IPlaceableGenContext.PlaceItem(Loc loc, StairsDown item) { var stairs = (StairsDown)item.Copy(); @@ -84,19 +152,33 @@ void IPlaceableGenContext.PlaceItem(Loc loc, StairsDown item) this.GenExits.Add(stairs); } + /// StairsUp IViewPlaceableGenContext.GetItem(int index) => this.GenEntrances[index]; + /// Loc IViewPlaceableGenContext.GetLoc(int index) => this.GenEntrances[index].Loc; + /// StairsDown IViewPlaceableGenContext.GetItem(int index) => this.GenExits[index]; + /// Loc IViewPlaceableGenContext.GetLoc(int index) => this.GenExits[index].Loc; + /// + /// Gets all free tiles across the entire map using the specified function. + /// + /// Function to find open tiles in a rectangle. + /// List of all free tile locations. protected virtual List GetAllFreeTiles(GetOpen func) { return func?.Invoke(new Rect(0, 0, this.Width, this.Height)); } + /// + /// Gets all open tiles within the specified rectangular area. + /// + /// The area to search. + /// List of unoccupied tile locations. protected List GetOpenTiles(Rect rect) { bool CheckOp(Loc loc) => !this.IsTileOccupied(loc); @@ -104,6 +186,11 @@ protected List GetOpenTiles(Rect rect) return Grid.FindTilesInBox(rect.Start, rect.Size, CheckOp); } + /// + /// Checks if a tile is occupied (not a valid floor tile). + /// + /// The location to check. + /// True if the tile is occupied or not a floor tile. private bool IsTileOccupied(Loc loc) => this.Map.Tiles[loc.X][loc.Y].ID != Map.ROOM_TERRAIN_ID; } } diff --git a/RogueElements.Examples/Ex5_Terrain/README.md b/RogueElements.Examples/Ex5_Terrain/README.md new file mode 100644 index 00000000..1c9225ff --- /dev/null +++ b/RogueElements.Examples/Ex5_Terrain/README.md @@ -0,0 +1,224 @@ +# Example 5: Terrain Features + +Add water and other terrain features using Perlin noise. + +## What You'll Learn + +- How to generate terrain with `PerlinWaterStep` +- Using stencils to control where terrain can appear +- Post-processing steps for terrain cleanup +- Working with multiple terrain types + +## Prerequisites + +- [Example 4: Stair Placement](../Ex4_Stairs/README.md) +- Understanding of spawnable entities + +## Concepts + +### Terrain Generation + +RogueElements uses **Perlin noise** to create natural-looking terrain distributions. The water appears in organic blobs rather than random scattered tiles. + +### Stencils + +A **stencil** defines which tiles can be modified by a step. `MapTerrainStencil` filters based on current tile types: + +```csharp +new MapTerrainStencil(wall: false, floor: true, water: false, other: false) +``` + +This means: "only modify floor tiles, leave everything else alone." + +### Post-Processing + +After placing water, cleanup steps improve the result: +1. `DropDiagonalBlockStep` - Removes wall corners that would cause visual artifacts +2. `EraseIsolatedStep` - Removes water tiles disconnected from larger bodies + +## Code Walkthrough + +### Step 1: Standard Setup with Stairs + +```csharp +// Grid generation (same as Example 4) +var startGen = new InitGridPlanStep(1) { /* ... */ }; +layout.GenSteps.Add(-4, startGen); +layout.GenSteps.Add(-4, path); +layout.GenSteps.Add(-2, new DrawGridToFloorStep()); +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + +// Stairs (same as Example 4) +layout.GenSteps.Add(2, new FloorStairsStep(0, new StairsUp(), new StairsDown())); +``` + +### Step 2: Generate Water with Perlin Noise + +```csharp +const int terrain = 2; // WATER_TERRAIN_ID + +var waterPostProc = new PerlinWaterStep( + new RandRange(35), // 35% coverage + 3, // noise order (complexity) + new Tile(terrain), // tile to place + new MapTerrainStencil(false, true, false, false), // only on floor + 1 // softness +); +layout.GenSteps.Add(3, waterPostProc); +``` + +`PerlinWaterStep` parameters: + +| Parameter | Value | Purpose | +|-----------|-------|---------| +| Coverage | `RandRange(35)` | Target 35% of valid tiles | +| Order | `3` | Noise complexity (higher = more detailed) | +| Terrain | `new Tile(2)` | Water tile to place | +| Stencil | `MapTerrainStencil(F,T,F,F)` | Only replace floor tiles | +| Softness | `1` | Edge smoothness | + +### Step 3: Cleanup Diagonal Artifacts + +```csharp +layout.GenSteps.Add(4, new DropDiagonalBlockStep(new Tile(terrain))); +``` + +**Problem**: When water touches walls diagonally, it can create visual artifacts: +``` +#~ +~# +``` + +**Solution**: `DropDiagonalBlockStep` converts blocking walls to water: +``` +~~ +~~ +``` + +### Step 4: Remove Isolated Water + +```csharp +layout.GenSteps.Add(4, new EraseIsolatedStep(new Tile(terrain))); +``` + +Removes single water tiles that aren't connected to larger bodies. This prevents awkward single-tile puddles. + +## Stencil Deep Dive + +`MapTerrainStencil` constructor parameters: + +```csharp +MapTerrainStencil(bool wall, bool floor, bool water, bool otherTerrain) +``` + +Common configurations: + +| Stencil | Effect | +|---------|--------| +| `(F, T, F, F)` | Only floor tiles (add water) | +| `(T, F, F, F)` | Only wall tiles (carve caves) | +| `(F, T, T, F)` | Floor and water (expand water) | +| `(T, T, F, F)` | Wall and floor (replace anything) | + +## Try It + +```bash +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +Press `5` to run Example 5. + +**What to observe:** +- `~` symbols represent water +- Water forms organic blobs, not random dots +- Water never appears on walls or stairs +- Different seeds produce different water patterns + +**Example output:** +``` +5: A Map with Terrain Features +======================================================= +###################################################### +#######.........###################################### +#######...~.....###################################### +#######..<~~~...######~~~....######################### +#######...~~~...#####~~~~....######################### +#######~........#####.~~~....######################### +#######~~~~~....#####........######################### +########~~~~~~~~~~~~~~~......#########................ +#################~~~~~~~~~~~~~~~~~~~~................. +#################..~~~~~~......#####.................. +#################........>.....#####....~~............ +###################################################### +``` + +## Key Classes Used + +| Class | Purpose | +|-------|---------| +| `PerlinWaterStep` | Places terrain using Perlin noise | +| `MapTerrainStencil` | Filters which tiles can be modified | +| `DropDiagonalBlockStep` | Fixes diagonal wall/terrain conflicts | +| `EraseIsolatedStep` | Removes disconnected terrain tiles | + +## Perlin Noise Parameters + +The **order** parameter controls noise detail: + +| Order | Effect | +|-------|--------| +| 1 | Large, simple blobs | +| 2 | Medium-sized features | +| 3 | Detailed, organic shapes | +| 4+ | Very detailed, may look noisy | + +The **softness** parameter affects edge transitions: + +| Softness | Effect | +|----------|--------| +| 0 | Sharp, pixelated edges | +| 1 | Slightly smoothed edges | +| 2+ | Very soft, gradual transitions | + +## Tile Protection + +Note that stairs are protected from water replacement: + +```csharp +public override bool CanSetTile(Loc loc, ITile tile) +{ + // Check if stairs are at this location + foreach (var entrance in this.GenEntrances) + if (entrance.Loc == loc) return false; + // ... + return true; +} +``` + +This ensures water generation respects previously placed entities. + +## Key Takeaways + +1. **Perlin Noise**: Creates natural-looking terrain distributions +2. **Stencil Filtering**: Control which tiles can be modified +3. **Post-Processing**: Cleanup steps improve visual quality +4. **Priority Ordering**: Water runs after stairs to respect placement + +## Advanced Usage + +You can chain multiple terrain steps: + +```csharp +// Add water +layout.GenSteps.Add(3, new PerlinWaterStep(35%, 3, waterTile, floorStencil, 1)); + +// Add lava (on remaining floor) +layout.GenSteps.Add(3, new PerlinWaterStep(10%, 2, lavaTile, floorStencil, 0)); + +// Add grass (on remaining floor) +layout.GenSteps.Add(3, new PerlinWaterStep(20%, 4, grassTile, floorStencil, 2)); +``` + +## Next Steps + +[Example 6: Item Spawning](../Ex6_Items/README.md) adds randomly placed items and monsters to the map. diff --git a/RogueElements.Examples/Ex6_Items/Example6.cs b/RogueElements.Examples/Ex6_Items/Example6.cs index 7f48fa7f..0acfe0b7 100644 --- a/RogueElements.Examples/Ex6_Items/Example6.cs +++ b/RogueElements.Examples/Ex6_Items/Example6.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -8,8 +8,25 @@ namespace RogueElements.Examples.Ex6_Items { + /// + /// Example 6: Demonstrates entity spawning with weighted random selection. + /// This example shows how to place items and mobs using SpawnList for + /// weighted randomization and RandomSpawnStep for automatic placement. + /// + /// + /// Key concepts introduced: + /// - ISpawnable: Interface for entities that can be spawned (requires Copy() method) + /// - SpawnList<T>: Collection with weighted random selection + /// - RandomSpawnStep: Places spawnable entities at valid map locations + /// - PickerSpawner: Determines WHAT to spawn using a SpawnList + /// - LoopedRand: Determines HOW MANY times to spawn from the list + /// - Multiple IPlaceableGenContext<T>: Same context can spawn different entity types + /// public static class Example6 { + /// + /// Runs the item/mob spawning example, creating a map populated with entities. + /// public static void Run() { Console.Clear(); @@ -57,7 +74,7 @@ public static void Run() // Add the stairs up and down layout.GenSteps.Add(2, new FloorStairsStep(0, new StairsUp(), new StairsDown())); - // Generate water (specified by user as Terrain 2) with a frequency of 35%, using Perlin Noise in an order of 3, softness 1. + // Generate water (covered in Ex5) const int terrain = 2; var waterPostProc = new PerlinWaterStep(new RandRange(35), 3, new Tile(terrain), new MapTerrainStencil(false, true, false, false), 1); layout.GenSteps.Add(3, waterPostProc); @@ -68,27 +85,54 @@ public static void Run() // Remove water stuck in the walls layout.GenSteps.Add(4, new EraseIsolatedStep(new Tile(terrain))); - // Apply Items + // =================================================================================== + // ITEM SPAWNING - NEW CONCEPT + // =================================================================================== + + // SpawnList provides weighted random selection. + // Each item has a "spawn rate" (weight) that affects its selection probability. + // Higher weight = more likely to be selected when picking randomly. + // + // The weight is relative: if item A has weight 10 and item B has weight 50, + // B is 5x more likely to be selected than A. var itemSpawns = new SpawnList { - { new Item((int)'!'), 10 }, - { new Item((int)']'), 10 }, - { new Item((int)'='), 10 }, - { new Item((int)'?'), 10 }, - { new Item((int)'$'), 10 }, - { new Item((int)'/'), 10 }, - { new Item((int)'*'), 50 }, + // Common roguelike item symbols with their spawn weights: + { new Item((int)'!'), 10 }, // Potion - weight 10 (uncommon) + { new Item((int)']'), 10 }, // Armor - weight 10 (uncommon) + { new Item((int)'='), 10 }, // Ring - weight 10 (uncommon) + { new Item((int)'?'), 10 }, // Scroll - weight 10 (uncommon) + { new Item((int)'$'), 10 }, // Gold - weight 10 (uncommon) + { new Item((int)'/'), 10 }, // Wand - weight 10 (uncommon) + { new Item((int)'*'), 50 }, // Rock/gem - weight 50 (5x more common!) }; + + // RandomSpawnStep places entities at random valid locations on the map. + // It requires: + // 1. A "spawner" that decides WHAT to spawn (PickerSpawner) + // 2. The spawner uses a "picker" that decides HOW MANY (LoopedRand) + // + // PickerSpawner: Wraps a randomizer to provide spawnable instances + // LoopedRand: Picks from itemSpawns a random number of times (10-18 items) RandomSpawnStep itemPlacement = new RandomSpawnStep(new PickerSpawner(new LoopedRand(itemSpawns, new RandRange(10, 19)))); layout.GenSteps.Add(6, itemPlacement); - // Apply Mobs + // =================================================================================== + // MOB SPAWNING - SAME PATTERN, DIFFERENT TYPE + // =================================================================================== + + // Mobs use the exact same spawning system as items. + // The context implements IPlaceableGenContext to support this. var mobSpawns = new SpawnList { - { new Mob((int)'r'), 20 }, - { new Mob((int)'T'), 10 }, - { new Mob((int)'D'), 5 }, + // Classic roguelike monster symbols with spawn weights: + { new Mob((int)'r'), 20 }, // Rat - weight 20 (common) + { new Mob((int)'T'), 10 }, // Troll - weight 10 (uncommon) + { new Mob((int)'D'), 5 }, // Dragon - weight 5 (rare, half as likely as Troll) }; + + // Same pattern: RandomSpawnStep + PickerSpawner + LoopedRand + // Spawns 10-18 mobs at random valid locations RandomSpawnStep mobPlacement = new RandomSpawnStep(new PickerSpawner(new LoopedRand(mobSpawns, new RandRange(10, 19)))); layout.GenSteps.Add(6, mobPlacement); @@ -97,6 +141,15 @@ public static void Run() Print(context.Map, title); } + /// + /// Prints the generated map to the console with items and mobs. + /// + /// The generated map containing tiles, stairs, items, and mobs. + /// Title to display above the map. + /// + /// Rendering priority (highest to lowest): Mobs, Items, Stairs, Terrain. + /// Each entity type can override the display of lower-priority elements. + /// public static void Print(Map map, string title) { var topString = new StringBuilder(string.Empty); @@ -148,6 +201,7 @@ public static void Print(Map map, string title) } } + // Items render on top of terrain but below mobs foreach (Item item in map.Items) { if (item.Loc == loc) @@ -157,6 +211,7 @@ public static void Print(Map map, string title) } } + // Mobs render on top of everything foreach (Mob item in map.Mobs) { if (item.Loc == loc) diff --git a/RogueElements.Examples/Ex6_Items/Item.cs b/RogueElements.Examples/Ex6_Items/Item.cs index f501f841..33b3d55f 100644 --- a/RogueElements.Examples/Ex6_Items/Item.cs +++ b/RogueElements.Examples/Ex6_Items/Item.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -9,32 +9,93 @@ namespace RogueElements.Examples.Ex6_Items { + /// + /// Represents a spawnable item that can be placed on the map. + /// Implements ISpawnable to work with RogueElements' spawning system. + /// + /// + /// + /// ISpawnable is a simple interface with just one requirement: Copy(). + /// This allows the spawning system to create copies of template items + /// without knowing their concrete type. + /// + /// + /// The spawning pattern works like this: + /// 1. Define template items in a SpawnList (no location set) + /// 2. RandomSpawnStep picks items from the list using weighted random + /// 3. For each pick, Copy() creates a new instance + /// 4. PlaceItem() sets the location on the copy and adds it to the map + /// + /// + /// In a real game, Item would have properties like: + /// - Name, Description + /// - ItemType (weapon, armor, consumable, etc.) + /// - Stats, effects, durability + /// + /// public class Item : ISpawnable { + /// + /// Initializes a new instance of the class. + /// Default constructor required for serialization. + /// public Item() { } + /// + /// Initializes a new instance of the class with an ID. + /// Used when defining item templates in a SpawnList. + /// + /// The item's identifier (used as display character in this example). public Item(int id) { this.ID = id; } + /// + /// Initializes a new instance of the class with ID and location. + /// Used when placing an item on the map. + /// + /// The item's identifier. + /// The map location where this item is placed. public Item(int id, Loc loc) { this.ID = id; this.Loc = loc; } + /// + /// Initializes a new instance of the class as a copy. + /// Protected to encourage using Copy() method. + /// + /// The item to copy. protected Item(Item other) : this(other.ID, other.Loc) { } + /// + /// Gets or sets the item's identifier. + /// In this example, the ID is used as the ASCII display character. + /// public int ID { get; set; } + /// + /// Gets or sets the item's map location. + /// Set when the item is placed via PlaceItem(). + /// public Loc Loc { get; set; } + /// + /// Creates a copy of this item for spawning. + /// + /// A new Item instance with the same properties. + /// + /// This is the ISpawnable interface requirement. The spawning system + /// calls this to create instances from templates without knowing the + /// concrete type. The copy constructor handles the actual copying. + /// public ISpawnable Copy() => new Item(this); } } diff --git a/RogueElements.Examples/Ex6_Items/Map.cs b/RogueElements.Examples/Ex6_Items/Map.cs index a26d9f0d..942d6d3f 100644 --- a/RogueElements.Examples/Ex6_Items/Map.cs +++ b/RogueElements.Examples/Ex6_Items/Map.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -11,8 +11,28 @@ namespace RogueElements.Examples.Ex6_Items { + /// + /// Map data structure for Example 6 with support for spawned entities. + /// Extends BaseMap to include collections for items and mobs. + /// + /// + /// + /// Unlike tiles (which are stored in a 2D array), spawned entities are stored + /// in lists. Each entity knows its own location via a Loc property. + /// + /// + /// This separation allows: + /// - Multiple entities at the same location (if desired) + /// - Easy iteration over all entities of a type + /// - Entity properties beyond just position (ID, stats, etc.) + /// + /// public class Map : BaseMap { + /// + /// Initializes a new instance of the class. + /// Creates empty collections for all entity types. + /// public Map() { this.GenEntrances = new List(); @@ -21,12 +41,32 @@ public Map() this.Mobs = new List(); } + /// + /// Gets or sets the list of upward stairs (level entrances). + /// public List GenEntrances { get; set; } + /// + /// Gets or sets the list of downward stairs (level exits). + /// public List GenExits { get; set; } + /// + /// Gets or sets the list of items spawned on this map. + /// + /// + /// Items are spawned by RandomSpawnStep using the IPlaceableGenContext<Item> + /// interface. Each Item stores its own position via the Loc property. + /// public List Items { get; set; } + /// + /// Gets or sets the list of mobs (monsters/NPCs) spawned on this map. + /// + /// + /// Mobs use the same spawning system as items but through the + /// IPlaceableGenContext<Mob> interface. + /// public List Mobs { get; set; } } } diff --git a/RogueElements.Examples/Ex6_Items/MapGenContext.cs b/RogueElements.Examples/Ex6_Items/MapGenContext.cs index 0f23632e..4e1bf552 100644 --- a/RogueElements.Examples/Ex6_Items/MapGenContext.cs +++ b/RogueElements.Examples/Ex6_Items/MapGenContext.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -10,29 +10,81 @@ namespace RogueElements.Examples.Ex6_Items { + /// + /// Generation context for Example 6 that supports spawning multiple entity types. + /// Demonstrates implementing multiple IPlaceableGenContext<T> interfaces + /// to enable spawning different types of entities (items, mobs, stairs). + /// + /// + /// + /// Key pattern: A single context can implement IPlaceableGenContext<T> for + /// multiple types. This allows different RandomSpawnStep instances to place + /// different entity types using the same context. + /// + /// + /// Each IPlaceableGenContext<T> implementation requires: + /// - GetAllFreeTiles(): Returns all valid spawn locations + /// - GetFreeTiles(Rect): Returns valid spawn locations within an area + /// - CanPlaceItem(Loc): Checks if a specific location is valid + /// - PlaceItem(Loc, T): Actually places the entity at the location + /// + /// + /// The IsTileOccupied() method is extended in this example to prevent + /// stacking - items and mobs cannot be placed on the same tile. + /// + /// public class MapGenContext : BaseMapGenContext, IRoomGridGenContext, IViewPlaceableGenContext, IViewPlaceableGenContext, IPlaceableGenContext, IPlaceableGenContext { + /// + /// Initializes a new instance of the class. + /// public MapGenContext() : base() { } + /// + /// Delegate type for methods that find open tiles within a rectangular area. + /// + /// The rectangular area to search. + /// List of valid tile locations. protected delegate List GetOpen(Rect rect); + /// + /// Gets the floor plan for freeform room-based generation. + /// public FloorPlan RoomPlan { get; private set; } + /// + /// Gets the grid plan for grid-based room layouts. + /// public GridPlan GridPlan { get; private set; } + /// + /// Gets the list of upward stairs (entrances) placed on this map. + /// public List GenEntrances => this.Map.GenEntrances; + /// + /// Gets the list of downward stairs (exits) placed on this map. + /// public List GenExits => this.Map.GenExits; + /// int IViewPlaceableGenContext.Count => this.GenEntrances.Count; + /// int IViewPlaceableGenContext.Count => this.GenExits.Count; + /// + /// Determines whether a tile can be set at the specified location. + /// Prevents terrain from overwriting stairs. + /// + /// The location to check. + /// The tile to potentially place. + /// True if the tile can be set; false if blocked by stairs. public override bool CanSetTile(Loc loc, ITile tile) { for (int ii = 0; ii < this.GenEntrances.Count; ii++) @@ -50,52 +102,101 @@ public override bool CanSetTile(Loc loc, ITile tile) return true; } + /// + /// Initializes the floor plan for this context. + /// + /// The floor plan to use. public void InitPlan(FloorPlan plan) { this.RoomPlan = plan; } + /// + /// Initializes the grid plan for this context. + /// + /// The grid plan to use. public void InitGrid(GridPlan plan) { this.GridPlan = plan; } + // =================================================================================== + // IPlaceableGenContext IMPLEMENTATIONS + // =================================================================================== + // Each spawnable type needs its own set of interface methods. + // These tell RandomSpawnStep where entities CAN go and how to place them. + + /// + /// Returns all valid item spawn locations on the map. List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// + /// Returns all valid mob spawn locations on the map. List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + /// List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + /// List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + /// List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + /// bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + /// bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + /// bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + /// bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + // =================================================================================== + // PlaceItem METHODS - Actually add entities to the map + // =================================================================================== + + /// + /// Places an item at the specified location. + /// + /// The location to place the item. + /// The item template to copy and place. + /// + /// Creates a NEW Item instance at the location rather than modifying the template. + /// This is important because the same Item template may be used for multiple spawns. + /// void IPlaceableGenContext.PlaceItem(Loc loc, Item item) { + // Create a new item with the location set + // Don't modify the template - it may be reused! Item newItem = new Item(item.ID, loc); this.Map.Items.Add(newItem); } + /// + /// Places a mob at the specified location. + /// + /// The location to place the mob. + /// The mob template to copy and place. void IPlaceableGenContext.PlaceItem(Loc loc, Mob item) { Mob newItem = new Mob(item.ID, loc); this.Map.Mobs.Add(newItem); } + /// void IPlaceableGenContext.PlaceItem(Loc loc, StairsUp item) { var stairs = (StairsUp)item.Copy(); @@ -103,6 +204,7 @@ void IPlaceableGenContext.PlaceItem(Loc loc, StairsUp item) this.GenEntrances.Add(stairs); } + /// void IPlaceableGenContext.PlaceItem(Loc loc, StairsDown item) { var stairs = (StairsDown)item.Copy(); @@ -110,19 +212,33 @@ void IPlaceableGenContext.PlaceItem(Loc loc, StairsDown item) this.GenExits.Add(stairs); } + /// StairsUp IViewPlaceableGenContext.GetItem(int index) => this.GenEntrances[index]; + /// Loc IViewPlaceableGenContext.GetLoc(int index) => this.GenEntrances[index].Loc; + /// StairsDown IViewPlaceableGenContext.GetItem(int index) => this.GenExits[index]; + /// Loc IViewPlaceableGenContext.GetLoc(int index) => this.GenExits[index].Loc; + /// + /// Gets all free tiles across the entire map using the specified function. + /// + /// Function to find open tiles in a rectangle. + /// List of all free tile locations. protected virtual List GetAllFreeTiles(GetOpen func) { return func?.Invoke(new Rect(0, 0, this.Width, this.Height)); } + /// + /// Gets all open tiles within the specified rectangular area. + /// + /// The area to search. + /// List of unoccupied tile locations. protected List GetOpenTiles(Rect rect) { bool CheckOp(Loc loc) => !this.IsTileOccupied(loc); @@ -130,17 +246,33 @@ protected List GetOpenTiles(Rect rect) return Grid.FindTilesInBox(rect.Start, rect.Size, CheckOp); } + /// + /// Checks if a tile is occupied and cannot accept new entities. + /// + /// The location to check. + /// True if the tile is occupied; false if available for spawning. + /// + /// This method prevents entity stacking by checking: + /// 1. Is the tile walkable floor (not wall or water)? + /// 2. Is there already an item at this location? + /// 3. Is there already a mob at this location? + /// + /// This ensures items and mobs don't overlap, making the map more readable. + /// private bool IsTileOccupied(Loc loc) { + // Can only spawn on floor tiles (ROOM_TERRAIN_ID) if (this.Map.Tiles[loc.X][loc.Y].ID != Map.ROOM_TERRAIN_ID) return true; + // Check for existing items at this location foreach (Item item in this.Map.Items) { if (item.Loc == loc) return true; } + // Check for existing mobs at this location foreach (Mob item in this.Map.Mobs) { if (item.Loc == loc) diff --git a/RogueElements.Examples/Ex6_Items/Mob.cs b/RogueElements.Examples/Ex6_Items/Mob.cs index 719eafe4..a6faac5d 100644 --- a/RogueElements.Examples/Ex6_Items/Mob.cs +++ b/RogueElements.Examples/Ex6_Items/Mob.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -9,32 +9,94 @@ namespace RogueElements.Examples.Ex6_Items { + /// + /// Represents a spawnable mob (monster/NPC) that can be placed on the map. + /// Implements ISpawnable using the same pattern as Item. + /// + /// + /// + /// Mobs demonstrate that the ISpawnable/IPlaceableGenContext pattern + /// works identically for any entity type. The same RandomSpawnStep + /// and SpawnList classes work with mobs just as they do with items. + /// + /// + /// The key insight: RogueElements separates WHAT gets spawned (ISpawnable) + /// from WHERE it can go (IPlaceableGenContext) from HOW MANY spawn + /// (LoopedRand/RandRange). This separation allows mixing and matching + /// different spawn logic, placement rules, and quantity calculations. + /// + /// + /// In a real game, Mob would have properties like: + /// - Name, AI type + /// - Health, attack, defense + /// - Special abilities, drop tables + /// - Movement speed, vision range + /// + /// public class Mob : ISpawnable { + /// + /// Initializes a new instance of the class. + /// Default constructor required for serialization. + /// public Mob() { } + /// + /// Initializes a new instance of the class with an ID. + /// Used when defining mob templates in a SpawnList. + /// + /// The mob's identifier (used as display character in this example). public Mob(int id) { this.ID = id; } + /// + /// Initializes a new instance of the class with ID and location. + /// Used when placing a mob on the map. + /// + /// The mob's identifier. + /// The map location where this mob is placed. public Mob(int id, Loc loc) { this.ID = id; this.Loc = loc; } + /// + /// Initializes a new instance of the class as a copy. + /// Protected to encourage using Copy() method. + /// + /// The mob to copy. protected Mob(Mob other) : this(other.ID, other.Loc) { } + /// + /// Gets or sets the mob's identifier. + /// In this example, the ID is used as the ASCII display character + /// (e.g., 'r' for rat, 'D' for dragon). + /// public int ID { get; set; } + /// + /// Gets or sets the mob's map location. + /// Set when the mob is placed via PlaceItem(). + /// public Loc Loc { get; set; } + /// + /// Creates a copy of this mob for spawning. + /// + /// A new Mob instance with the same properties. + /// + /// Identical pattern to Item.Copy(). The spawning system + /// doesn't care about the concrete type - it just calls Copy() + /// through the ISpawnable interface. + /// public ISpawnable Copy() => new Mob(this); } } diff --git a/RogueElements.Examples/Ex6_Items/README.md b/RogueElements.Examples/Ex6_Items/README.md new file mode 100644 index 00000000..e3d8d045 --- /dev/null +++ b/RogueElements.Examples/Ex6_Items/README.md @@ -0,0 +1,285 @@ +# Example 6: Item Spawning + +Place items and monsters randomly across the map. + +## What You'll Learn + +- How to define spawnable item and mob types +- Using `RandomSpawnStep` for entity placement +- Configuring spawn lists with weighted probabilities +- Using `LoopedRand` for quantity control + +## Prerequisites + +- [Example 5: Terrain Features](../Ex5_Terrain/README.md) +- Understanding of the ISpawnable pattern + +## Concepts + +### Random Spawning + +`RandomSpawnStep` places entities at random valid locations across the entire map. It uses: +- A **spawner** to generate entities +- The context's placement methods to find valid tiles + +### Spawn Lists + +`SpawnList` provides weighted random selection. Higher weights = more likely to spawn. + +### LoopedRand + +`LoopedRand` repeatedly picks from a spawn list to generate multiple items from a single step. + +## Code Walkthrough + +### Step 1: Standard Setup + +```csharp +// Grid + stairs + water (same as Example 5) +// ... setup code ... +layout.GenSteps.Add(4, new DropDiagonalBlockStep(new Tile(terrain))); +layout.GenSteps.Add(4, new EraseIsolatedStep(new Tile(terrain))); +``` + +### Step 2: Define Item Types + +```csharp +var itemSpawns = new SpawnList +{ + { new Item((int)'!'), 10 }, // Potion + { new Item((int)']'), 10 }, // Armor + { new Item((int)'='), 10 }, // Ring + { new Item((int)'?'), 10 }, // Scroll + { new Item((int)'$'), 10 }, // Gold + { new Item((int)'/'), 10 }, // Wand + { new Item((int)'*'), 50 }, // Gem (5x more common) +}; +``` + +Each item uses its ASCII character as an ID for easy display. Weights determine spawn probability: +- Gems (`*`) have weight 50 = 5x more likely than others +- All others have weight 10 = equal probability among themselves + +### Step 3: Create Item Spawner + +```csharp +RandomSpawnStep itemPlacement = new RandomSpawnStep( + new PickerSpawner( + new LoopedRand(itemSpawns, new RandRange(10, 19)) + ) +); +layout.GenSteps.Add(6, itemPlacement); +``` + +Breaking this down: +- `LoopedRand(itemSpawns, RandRange(10, 19))`: Pick 10-18 items from the spawn list +- `PickerSpawner`: Wraps the random picker into a spawner +- `RandomSpawnStep`: Places each spawned item at a random valid location + +### Step 4: Define Mob Types + +```csharp +var mobSpawns = new SpawnList +{ + { new Mob((int)'r'), 20 }, // Rat (common) + { new Mob((int)'T'), 10 }, // Troll (medium) + { new Mob((int)'D'), 5 }, // Dragon (rare) +}; +``` + +Weighted so rats are 4x more common than dragons. + +### Step 5: Create Mob Spawner + +```csharp +RandomSpawnStep mobPlacement = new RandomSpawnStep( + new PickerSpawner( + new LoopedRand(mobSpawns, new RandRange(10, 19)) + ) +); +layout.GenSteps.Add(6, mobPlacement); +``` + +Same pattern as items, spawning 10-18 mobs. + +## Entity Classes + +### Item + +```csharp +public class Item : ISpawnable +{ + public Item(int id) { this.ID = id; } + public Item(int id, Loc loc) { this.ID = id; this.Loc = loc; } + + public int ID { get; set; } + public Loc Loc { get; set; } + public ISpawnable Copy() => new Item(this); +} +``` + +### Mob + +```csharp +public class Mob : ISpawnable +{ + public Mob(int id) { this.ID = id; } + public Mob(int id, Loc loc) { this.ID = id; this.Loc = loc; } + + public int ID { get; set; } + public Loc Loc { get; set; } + public ISpawnable Copy() => new Mob(this); +} +``` + +## Map Class Changes + +```csharp +public class Map : BaseMap +{ + public Map() + { + this.GenEntrances = new List(); + this.GenExits = new List(); + this.Items = new List(); // NEW + this.Mobs = new List(); // NEW + } + + public List Items { get; set; } + public List Mobs { get; set; } +} +``` + +## MapGenContext Changes + +Implement `IPlaceableGenContext` for both Item and Mob: + +```csharp +public class MapGenContext : BaseMapGenContext, IRoomGridGenContext, + IViewPlaceableGenContext, IViewPlaceableGenContext, + IPlaceableGenContext, IPlaceableGenContext // NEW +{ + void IPlaceableGenContext.PlaceItem(Loc loc, Item item) + { + Item newItem = new Item(item.ID, loc); + this.Map.Items.Add(newItem); + } + + void IPlaceableGenContext.PlaceItem(Loc loc, Mob item) + { + Mob newItem = new Mob(item.ID, loc); + this.Map.Mobs.Add(newItem); + } + + // Check if tile is occupied by existing items/mobs + private bool IsTileOccupied(Loc loc) + { + if (this.Map.Tiles[loc.X][loc.Y].ID != Map.ROOM_TERRAIN_ID) + return true; + + foreach (Item item in this.Map.Items) + if (item.Loc == loc) return true; + + foreach (Mob mob in this.Map.Mobs) + if (mob.Loc == loc) return true; + + return false; + } +} +``` + +## Try It + +```bash +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +Press `6` to run Example 6. + +**What to observe:** +- Items displayed as `!`, `]`, `=`, `?`, `$`, `/`, `*` +- Mobs displayed as `r`, `T`, `D` +- Gems (`*`) appear more frequently than other items +- Rats (`r`) appear more frequently than dragons (`D`) +- Nothing spawns on water, walls, or stairs + +**Example output:** +``` +6: A Map with Randomly Placed Items/Mobs +======================================================= +###################################################### +#######..r......###################################### +#######...*~....####*################################# +#######..<~~~...######~~~....#####r################### +#######..T~~~...#####~~~~....###########*############# +#######~.....r..#####.~~~....######################### +#######~~~~~....#####........##############!.......... +########~~~~~~~~~~~~~~~......#########.........r*D.... +#################~~~~~~~~~~~~~~~~~~~~.......]......... +#################..~~~~~~......#####.........?........ +#################........>.....#####....~~....*....... +###################################################### +``` + +## Key Classes Used + +| Class | Purpose | +|-------|---------| +| `RandomSpawnStep` | Places entities at random valid locations | +| `PickerSpawner` | Generates entities from a random picker | +| `LoopedRand` | Picks multiple items from a spawn list | +| `SpawnList` | Weighted random selection | +| `Item`, `Mob` | Custom spawnable entity classes | +| `IPlaceableGenContext` | Interface for entity placement | + +## Spawn Weight Math + +With these weights: + +```csharp +{ new Mob((int)'r'), 20 }, // Rat +{ new Mob((int)'T'), 10 }, // Troll +{ new Mob((int)'D'), 5 }, // Dragon +``` + +Total weight = 20 + 10 + 5 = 35 + +Probabilities: +- Rat: 20/35 = 57% +- Troll: 10/35 = 29% +- Dragon: 5/35 = 14% + +## Collision Handling + +The `IsTileOccupied` check ensures: +1. Only floor tiles receive spawns +2. Existing items/mobs block new spawns +3. Stairs are protected +4. Water tiles are skipped + +## Key Takeaways + +1. **Weighted Spawning**: Control rarity with spawn list weights +2. **Quantity Control**: `LoopedRand` sets min/max spawn counts +3. **Collision Prevention**: Check occupied tiles before placing +4. **Extensible Pattern**: Same pattern works for any spawnable type + +## Advanced Usage + +Different spawn strategies: + +```csharp +// Random placement (this example) +new RandomSpawnStep(spawner) + +// Room-based placement (Example 7) +new RandomRoomSpawnStep(spawner) + +// Specific room placement (with filters) +var step = new RandomRoomSpawnStep(spawner); +step.Filters.Add(new RoomFilterComponent(false, new TreasureRoomComponent())); +``` + +## Next Steps + +[Example 7: Special Rooms](../Ex7_Special/README.md) adds special hand-crafted rooms with targeted item spawning. diff --git a/RogueElements.Examples/Ex7_Special/Example7.cs b/RogueElements.Examples/Ex7_Special/Example7.cs index e371cb59..e54768d9 100644 --- a/RogueElements.Examples/Ex7_Special/Example7.cs +++ b/RogueElements.Examples/Ex7_Special/Example7.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -8,8 +8,23 @@ namespace RogueElements.Examples.Ex7_Special { + /// + /// Demonstrates special room placement using RoomComponents for tagging and filtering. + /// Special rooms allow you to designate specific rooms for unique purposes (treasure, boss, etc.) + /// and control which entities spawn in them using RoomFilterComponent. + /// + /// + /// Key concepts introduced: + /// - RoomComponent: Tags rooms with metadata (MainRoomComponent, TreasureRoomComponent, etc.) + /// - SetSpecialRoomStep: Designates a room with custom layout and components + /// - RoomFilterComponent: Filters spawn steps to only affect rooms with specific components + /// - RoomGenSpecific: Creates rooms from predefined ASCII art layouts + /// public static class Example7 { + /// + /// Runs the special room example demonstrating tagged room placement and filtered spawning. + /// public static void Run() { Console.Clear(); @@ -42,11 +57,16 @@ public static void Run() BranchRatio = new RandRange(0, 25), }; + // Tag main path rooms/halls with components for identification + // These components allow spawn steps to distinguish room types path.RoomComponents.Set(new MainRoomComponent()); path.HallComponents.Set(new MainHallComponent()); layout.GenSteps.Add(-1, path); + // Define a custom room layout using ASCII art + // '.' = floor, '#' = wall, '~' = water + // This creates a moat-surrounded treasure room string[] custom = new string[] { "~~~..~~~", @@ -59,11 +79,18 @@ public static void Run() "~~~..~~~", }; + // SetSpecialRoomStep places a single special room that replaces an existing room + // This is useful for boss rooms, treasure vaults, shops, etc. SetSpecialRoomStep listSpecialStep = new SetSpecialRoomStep { + // PresetPicker ensures this exact room layout is used Rooms = new PresetPicker>(CreateRoomGenSpecific(custom)), }; + + // Tag this room as a treasure room - used for spawn filtering below listSpecialStep.RoomComponents.Set(new TreasureRoomComponent()); + + // Define hall type connecting the special room to the main layout PresetPicker> picker = new PresetPicker> { ToSpawn = new RoomGenAngledHall(0), @@ -77,7 +104,7 @@ public static void Run() // Add the stairs up and down layout.GenSteps.Add(2, new FloorStairsStep(0, new StairsUp(), new StairsDown())); - // Apply Items + // Apply Items - these spawn in ALL rooms (no filter) var itemSpawns = new SpawnList { { new Item((int)'!'), 10 }, @@ -91,13 +118,16 @@ public static void Run() RandomRoomSpawnStep itemPlacement = new RandomRoomSpawnStep(new PickerSpawner(new LoopedRand(itemSpawns, new RandRange(10, 19)))); layout.GenSteps.Add(6, itemPlacement); - // Apply Treasure Items + // Apply Treasure Items - ONLY spawn in rooms with TreasureRoomComponent var treasureSpawns = new SpawnList { { new Item((int)'!'), 10 }, { new Item((int)'*'), 50 }, }; RandomRoomSpawnStep treasurePlacement = new RandomRoomSpawnStep(new PickerSpawner(new LoopedRand(treasureSpawns, new RandRange(7, 10)))); + + // RoomFilterComponent restricts this spawn step to rooms with TreasureRoomComponent + // The 'false' parameter means "require this component" (true would mean "exclude") treasurePlacement.Filters.Add(new RoomFilterComponent(false, new TreasureRoomComponent())); layout.GenSteps.Add(6, treasurePlacement); @@ -106,6 +136,18 @@ public static void Run() Print(context.Map, title); } + /// + /// Creates a RoomGenSpecific from an ASCII art string array. + /// + /// The context type implementing ITiledGenContext. + /// ASCII art where '.' = floor, '#' = wall, '~' = water. + /// A room generator that produces the exact specified layout. + /// + /// RoomGenSpecific allows precise control over room layout, useful for: + /// - Boss arenas with specific geometry + /// - Puzzle rooms with predetermined tile patterns + /// - Shops or special encounter areas + /// public static RoomGenSpecific CreateRoomGenSpecific(string[] level) where T : class, ITiledGenContext { @@ -130,6 +172,11 @@ public static RoomGenSpecific CreateRoomGenSpecific(string[] level) return roomGen; } + /// + /// Prints the generated map to the console with all entities rendered. + /// + /// The map to print. + /// Title to display above the map. public static void Print(Map map, string title) { var topString = new StringBuilder(string.Empty); diff --git a/RogueElements.Examples/Ex7_Special/Map.cs b/RogueElements.Examples/Ex7_Special/Map.cs index a7c6218f..d4690f45 100644 --- a/RogueElements.Examples/Ex7_Special/Map.cs +++ b/RogueElements.Examples/Ex7_Special/Map.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -11,8 +11,19 @@ namespace RogueElements.Examples.Ex7_Special { + /// + /// Map class that stores tiles, stairs, and items for the special room example. + /// Extends BaseMap with entity storage for demonstrating filtered spawning. + /// + /// + /// This map combines features from Ex4_Stairs (stairs) and Ex6_Items (items). + /// See Ex7_Special.Example7 for how RoomComponents enable filtered spawning. + /// public class Map : BaseMap { + /// + /// Initializes a new instance of the class. + /// public Map() { this.GenEntrances = new List(); @@ -20,10 +31,20 @@ public Map() this.Items = new List(); } + /// + /// Gets or sets the list of upward stairs (level entrances). + /// public List GenEntrances { get; set; } + /// + /// Gets or sets the list of downward stairs (level exits). + /// public List GenExits { get; set; } + /// + /// Gets or sets the list of items placed on the map. + /// Items in treasure rooms are filtered separately from general items. + /// public List Items { get; set; } } } diff --git a/RogueElements.Examples/Ex7_Special/MapGenContext.cs b/RogueElements.Examples/Ex7_Special/MapGenContext.cs index c98a66e6..716ac13f 100644 --- a/RogueElements.Examples/Ex7_Special/MapGenContext.cs +++ b/RogueElements.Examples/Ex7_Special/MapGenContext.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -10,27 +10,62 @@ namespace RogueElements.Examples.Ex7_Special { + /// + /// Generation context that combines FloorPlan support with item and stair placement. + /// Extends BaseMapGenContext with IFloorPlanGenContext for room-based generation. + /// + /// + /// See Ex2_Rooms for IFloorPlanGenContext basics, Ex4_Stairs for stair placement, + /// and Ex6_Items for item placement. This context combines all three. + /// public class MapGenContext : BaseMapGenContext, IFloorPlanGenContext, IViewPlaceableGenContext, IViewPlaceableGenContext, IPlaceableGenContext { + /// + /// Initializes a new instance of the class. + /// public MapGenContext() : base() { } + /// + /// Delegate for obtaining open tiles within a rectangular area. + /// + /// The area to search for open tiles. + /// List of open tile locations. protected delegate List GetOpen(Rect rect); + /// + /// Gets the FloorPlan containing room layout information. + /// Used by SetSpecialRoomStep to find rooms to replace. + /// public FloorPlan RoomPlan { get; private set; } + /// + /// Gets the list of upward stairs (entrances) on the map. + /// public List GenEntrances => this.Map.GenEntrances; + /// + /// Gets the list of downward stairs (exits) on the map. + /// public List GenExits => this.Map.GenExits; + /// int IViewPlaceableGenContext.Count => this.GenEntrances.Count; + /// int IViewPlaceableGenContext.Count => this.GenExits.Count; + /// + /// Checks if a tile can be set at the specified location. + /// Prevents overwriting tiles occupied by stairs. + /// + /// The location to check. + /// The tile to potentially place. + /// True if the tile can be placed; false if blocked by stairs. public override bool CanSetTile(Loc loc, ITile tile) { for (int ii = 0; ii < this.GenEntrances.Count; ii++) @@ -48,35 +83,51 @@ public override bool CanSetTile(Loc loc, ITile tile) return true; } + /// + /// Initializes the floor plan for this context. + /// Called by InitFloorPlanStep before room placement. + /// + /// The floor plan to use. public void InitPlan(FloorPlan plan) { this.RoomPlan = plan; } + /// List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// List IPlaceableGenContext.GetAllFreeTiles() => this.GetAllFreeTiles(this.GetOpenTiles); + /// List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + /// List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + /// List IPlaceableGenContext.GetFreeTiles(Rect rect) => this.GetOpenTiles(rect); + /// bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + /// bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + /// bool IPlaceableGenContext.CanPlaceItem(Loc loc) => !this.IsTileOccupied(loc); + /// void IPlaceableGenContext.PlaceItem(Loc loc, Item item) { Item newItem = new Item(item.ID, loc); this.Map.Items.Add(newItem); } + /// void IPlaceableGenContext.PlaceItem(Loc loc, StairsUp item) { var stairs = (StairsUp)item.Copy(); @@ -84,6 +135,7 @@ void IPlaceableGenContext.PlaceItem(Loc loc, StairsUp item) this.GenEntrances.Add(stairs); } + /// void IPlaceableGenContext.PlaceItem(Loc loc, StairsDown item) { var stairs = (StairsDown)item.Copy(); @@ -91,19 +143,33 @@ void IPlaceableGenContext.PlaceItem(Loc loc, StairsDown item) this.GenExits.Add(stairs); } + /// StairsUp IViewPlaceableGenContext.GetItem(int index) => this.GenEntrances[index]; + /// Loc IViewPlaceableGenContext.GetLoc(int index) => this.GenEntrances[index].Loc; + /// StairsDown IViewPlaceableGenContext.GetItem(int index) => this.GenExits[index]; + /// Loc IViewPlaceableGenContext.GetLoc(int index) => this.GenExits[index].Loc; + /// + /// Gets all free tiles on the entire map using the specified tile finder. + /// + /// The function to use for finding open tiles. + /// List of all free tile locations. protected virtual List GetAllFreeTiles(GetOpen func) { return func?.Invoke(new Rect(0, 0, this.Width, this.Height)); } + /// + /// Gets open tiles within a rectangular area. + /// + /// The area to search. + /// List of unoccupied floor tile locations. protected List GetOpenTiles(Rect rect) { bool CheckOp(Loc loc) => !this.IsTileOccupied(loc); @@ -111,6 +177,11 @@ protected List GetOpenTiles(Rect rect) return Grid.FindTilesInBox(rect.Start, rect.Size, CheckOp); } + /// + /// Checks if a tile is occupied by non-floor terrain or an item. + /// + /// The location to check. + /// True if occupied; false if available for placement. private bool IsTileOccupied(Loc loc) { if (this.Map.Tiles[loc.X][loc.Y].ID != Map.ROOM_TERRAIN_ID) diff --git a/RogueElements.Examples/Ex7_Special/README.md b/RogueElements.Examples/Ex7_Special/README.md new file mode 100644 index 00000000..4a427155 --- /dev/null +++ b/RogueElements.Examples/Ex7_Special/README.md @@ -0,0 +1,313 @@ +# Example 7: Special Rooms + +Add hand-crafted special rooms with filtered item spawning. + +## What You'll Learn + +- Creating custom room layouts with `RoomGenSpecific` +- Adding special rooms using `SetSpecialRoomStep` +- Tagging rooms with components for identification +- Filtering spawns to target specific room types + +## Prerequisites + +- [Example 6: Item Spawning](../Ex6_Items/README.md) +- Understanding of room components + +## Concepts + +### Special Rooms + +A **special room** is a hand-designed room added to the procedural layout. It could be: +- A treasure vault +- A boss arena +- A shrine +- A trap room + +### Room Components + +**Components** are tags attached to rooms during generation. They enable: +- Room identification (is this a treasure room?) +- Filtered spawning (spawn items only in treasure rooms) +- Conditional logic (connect boss room to main path) + +### RoomGenSpecific + +`RoomGenSpecific` creates a room from a predefined tile pattern, allowing complete control over room shape and terrain. + +## Code Walkthrough + +### Step 1: Freeform Room Setup + +```csharp +InitFloorPlanStep startGen = new InitFloorPlanStep(54, 40); +layout.GenSteps.Add(-2, startGen); + +// Main path with room components +FloorPathBranch path = new FloorPathBranch(genericRooms, genericHalls) +{ + HallPercent = 50, + FillPercent = new RandRange(40), + BranchRatio = new RandRange(0, 25), +}; + +// Tag rooms and halls on the main path +path.RoomComponents.Set(new MainRoomComponent()); +path.HallComponents.Set(new MainHallComponent()); + +layout.GenSteps.Add(-1, path); +``` + +Main path rooms get `MainRoomComponent`, halls get `MainHallComponent`. + +### Step 2: Define the Special Room Pattern + +```csharp +string[] custom = new string[] +{ + "~~~..~~~", + "~~~..~~~", + "~~#..#~~", + "........", + "........", + "~~#..#~~", + "~~~..~~~", + "~~~..~~~", +}; +``` + +A treasure room with: +- Water (`~`) around the edges +- Small pillars (`#`) in the corners +- Open floor (`.`) for loot placement + +### Step 3: Create RoomGenSpecific + +```csharp +public static RoomGenSpecific CreateRoomGenSpecific(string[] level) + where T : class, ITiledGenContext +{ + RoomGenSpecific roomGen = new RoomGenSpecific( + level[0].Length, // width + level.Length, // height + new Tile(BaseMap.ROOM_TERRAIN_ID) // default tile + ); + + roomGen.Tiles = new Tile[level[0].Length][]; + for (int xx = 0; xx < level[0].Length; xx++) + { + roomGen.Tiles[xx] = new Tile[level.Length]; + for (int yy = 0; yy < level.Length; yy++) + { + if (level[yy][xx] == '#') + roomGen.Tiles[xx][yy] = new Tile(BaseMap.WALL_TERRAIN_ID); + else if (level[yy][xx] == '~') + roomGen.Tiles[xx][yy] = new Tile(BaseMap.WATER_TERRAIN_ID); + else + roomGen.Tiles[xx][yy] = new Tile(BaseMap.ROOM_TERRAIN_ID); + } + } + return roomGen; +} +``` + +### Step 4: Add the Special Room + +```csharp +SetSpecialRoomStep listSpecialStep = new SetSpecialRoomStep +{ + Rooms = new PresetPicker>( + CreateRoomGenSpecific(custom) + ), +}; + +// Tag with TreasureRoomComponent +listSpecialStep.RoomComponents.Set(new TreasureRoomComponent()); + +// Configure hall connection +PresetPicker> picker = new PresetPicker> +{ + ToSpawn = new RoomGenAngledHall(0), +}; +listSpecialStep.Halls = picker; + +layout.GenSteps.Add(-1, listSpecialStep); +``` + +The special room gets tagged with `TreasureRoomComponent` for later filtering. + +### Step 5: Standard Item Spawning + +```csharp +// Regular items throughout the map +var itemSpawns = new SpawnList +{ + { new Item((int)'!'), 10 }, + { new Item((int)']'), 10 }, + // ... other items ... + { new Item((int)'*'), 50 }, +}; + +RandomRoomSpawnStep itemPlacement = new RandomRoomSpawnStep( + new PickerSpawner( + new LoopedRand(itemSpawns, new RandRange(10, 19)) + ) +); +layout.GenSteps.Add(6, itemPlacement); +``` + +Note: Using `RandomRoomSpawnStep` instead of `RandomSpawnStep` - this distributes items across rooms. + +### Step 6: Filtered Treasure Spawning + +```csharp +// Treasure items only in the special room +var treasureSpawns = new SpawnList +{ + { new Item((int)'!'), 10 }, // Potions + { new Item((int)'*'), 50 }, // Gems +}; + +RandomRoomSpawnStep treasurePlacement = new RandomRoomSpawnStep( + new PickerSpawner( + new LoopedRand(treasureSpawns, new RandRange(7, 10)) + ) +); + +// Filter: only spawn in rooms with TreasureRoomComponent +treasurePlacement.Filters.Add( + new RoomFilterComponent(false, new TreasureRoomComponent()) +); + +layout.GenSteps.Add(6, treasurePlacement); +``` + +The key is the filter: +- `false` = spawn in rooms that DO have the component +- `new TreasureRoomComponent()` = the component to check for + +## Room Component Classes + +```csharp +public class MainRoomComponent : RoomComponent +{ + public override RoomComponent Clone() => new MainRoomComponent(); +} + +public class MainHallComponent : RoomComponent +{ + public override RoomComponent Clone() => new MainHallComponent(); +} + +public class TreasureRoomComponent : RoomComponent +{ + public override RoomComponent Clone() => new TreasureRoomComponent(); +} +``` + +## Try It + +```bash +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +Press `7` to run Example 7. + +**What to observe:** +- One room with water edges and pillars (the treasure room) +- Extra items clustered in the treasure room +- Regular items distributed elsewhere +- The special room is connected to the main dungeon + +**Example output:** +``` +7: A Map with Special Rooms +======================================================= +###################################################### +###.......############################################ +###.......######~~~..~~~############################## +###.......######~~~..~~~#######......################# +###...........##~~#..#~~#######......################# +###.......#####..***..**#######......################# +####......#####..***..*.##............................. +####......#####~~#..#~~##.............................. +###############~~~..~~~#####......##################### +###############~~~..~~~#####......##################### +###############........#####.<....##################### +###################################################### +``` + +## Key Classes Used + +| Class | Purpose | +|-------|---------| +| `SetSpecialRoomStep` | Adds a special room to the layout | +| `RoomGenSpecific` | Creates a room from predefined tiles | +| `RoomComponent` | Base class for room tags | +| `TreasureRoomComponent` | Tags treasure rooms | +| `RoomFilterComponent` | Filters spawn steps by room component | +| `RandomRoomSpawnStep` | Spawns items distributed across rooms | +| `PresetPicker` | Always picks the same item | + +## Filter Logic + +The `RoomFilterComponent` constructor: + +```csharp +RoomFilterComponent(bool invert, params RoomComponent[] components) +``` + +| Invert | Meaning | +|--------|---------| +| `false` | Spawn in rooms WITH the component | +| `true` | Spawn in rooms WITHOUT the component | + +Examples: +```csharp +// Only treasure rooms +new RoomFilterComponent(false, new TreasureRoomComponent()) + +// Everything EXCEPT treasure rooms +new RoomFilterComponent(true, new TreasureRoomComponent()) + +// Only main path rooms (not side rooms) +new RoomFilterComponent(false, new MainRoomComponent()) +``` + +## Multiple Special Rooms + +You can add multiple special room types: + +```csharp +// Treasure room +var treasureStep = new SetSpecialRoomStep { /* ... */ }; +treasureStep.RoomComponents.Set(new TreasureRoomComponent()); + +// Boss room +var bossStep = new SetSpecialRoomStep { /* ... */ }; +bossStep.RoomComponents.Set(new BossRoomComponent()); + +// Shrine room +var shrineStep = new SetSpecialRoomStep { /* ... */ }; +shrineStep.RoomComponents.Set(new ShrineRoomComponent()); +``` + +## Key Takeaways + +1. **Hand-Crafted Rooms**: Use `RoomGenSpecific` for custom designs +2. **Component Tagging**: Label rooms during generation for later reference +3. **Filtered Spawning**: Target specific room types with filters +4. **Separation of Concerns**: Room generation and item placement are decoupled + +## Design Patterns + +| Pattern | Use Case | +|---------|----------| +| Special treasure room | High-value loot in risky area | +| Boss arena | Large open room for boss fights | +| Puzzle room | Custom layout with traps/switches | +| Shrine room | Safe zone with healing/buffs | + +## Next Steps + +[Example 8: Integration](../Ex8_Integration/README.md) shows how to integrate RogueElements with the RogueSharp library. diff --git a/RogueElements.Examples/Ex8_Integration/CellTile.cs b/RogueElements.Examples/Ex8_Integration/CellTile.cs index efcb823f..4bc65f8a 100644 --- a/RogueElements.Examples/Ex8_Integration/CellTile.cs +++ b/RogueElements.Examples/Ex8_Integration/CellTile.cs @@ -7,27 +7,79 @@ namespace RogueElements { + /// + /// Adapter that wraps RogueSharp's Cell to implement RogueElements' ITile interface. + /// Enables RogueElements to work with RogueSharp's map representation. + /// + /// + /// This is the bridge between RogueElements and RogueSharp type systems. + /// RogueElements uses ITile for tile operations; RogueSharp uses ICell/Cell. + /// CellTile inherits from Cell (for RogueSharp compatibility) and implements + /// ITile (for RogueElements compatibility). + /// + /// Key mapping: + /// - ITile.TileEquivalent -> Compares IsWalkable (floor vs wall) + /// - ITile.Copy -> Creates a new CellTile with same properties + /// public class CellTile : Cell, ITile { + /// + /// Initializes a new instance of the class. + /// + /// X coordinate. + /// Y coordinate. + /// Whether light passes through. + /// Whether entities can walk on this tile. + /// Whether currently in field of view. public CellTile(int x, int y, bool isTransparent, bool isWalkable, bool isInFov) : base(x, y, isTransparent, isWalkable, isInFov) { } + /// + /// Initializes a new instance of the class with exploration state. + /// + /// X coordinate. + /// Y coordinate. + /// Whether light passes through. + /// Whether entities can walk on this tile. + /// Whether currently in field of view. + /// Whether the player has seen this tile. public CellTile(int x, int y, bool isTransparent, bool isWalkable, bool isInFov, bool isExplored) : base(x, y, isTransparent, isWalkable, isInFov, isExplored) { } + /// + /// Initializes a new instance of the class by copying another cell. + /// + /// The cell to copy properties from. protected CellTile(ICell other) : base(other.X, other.Y, other.IsTransparent, other.IsWalkable, other.IsInFov, other.IsExplored) { } + /// + /// Creates a CellTile from a RogueSharp ICell. + /// Factory method for wrapping existing cells. + /// + /// The RogueSharp cell to wrap. + /// A new CellTile with the same properties. public static CellTile FromCell(ICell other) => new CellTile(other); + /// + /// Checks if this tile is equivalent to another for generation purposes. + /// Compares walkability - the key distinction between floor and wall. + /// + /// The tile to compare against. + /// True if both tiles have the same walkability. public bool TileEquivalent(ITile other) => (other is ICell cell) && cell?.IsWalkable == this.IsWalkable; + /// + /// Creates a copy of this tile. + /// Required by ITile for tile operations that need independent copies. + /// + /// A new CellTile with the same properties. public ITile Copy() => new CellTile(this); } -} \ No newline at end of file +} diff --git a/RogueElements.Examples/Ex8_Integration/Example8.cs b/RogueElements.Examples/Ex8_Integration/Example8.cs index dc795e42..daaabe15 100644 --- a/RogueElements.Examples/Ex8_Integration/Example8.cs +++ b/RogueElements.Examples/Ex8_Integration/Example8.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -9,15 +9,35 @@ namespace RogueElements.Examples.Ex8_Integration { + /// + /// Reference implementation demonstrating RogueElements integration with RogueSharp. + /// Shows how to use RogueElements as a MapCreationStrategy for the RogueSharp library. + /// + /// + /// This example combines concepts from earlier examples: + /// - Grid-based generation (Ex3_Grid): InitGridPlanStep, GridPathBranch + /// - Room types (Ex2_Rooms): RoomGenSquare, RoomGenRound + /// - Hall types (Ex2_Rooms): RoomGenAngledHall + /// - Drawing steps (Ex2_Rooms, Ex3_Grid): DrawGridToFloorStep, DrawFloorToTileStep + /// + /// The key integration point is ExampleCreationStrategy, which implements + /// RogueSharp's IMapCreationStrategy interface using RogueElements' MapGen pipeline. + /// public static class Example8 { + /// + /// Runs the RogueSharp integration example. + /// public static void Run() { Console.Clear(); const string title = "8: Implementation as a MapCreationStrategy in RogueSharp"; + + // Create strategy that wraps MapGen for use with RogueSharp's Map.Create() ExampleCreationStrategy exampleCreation = new ExampleCreationStrategy(); - // Initialize a 6x4 grid of 10x10 cells. + // Grid initialization (see Ex3_Grid for details) + // 6x4 grid cells, each 9x9 tiles var startGen = new InitGridPlanStep(1) { CellX = 6, @@ -27,13 +47,14 @@ public static void Run() }; exampleCreation.Layout.GenSteps.Add(-4, startGen); - // Create a path that is composed of a ring around the edge + // Branching path through grid (see Ex3_Grid for GridPath variants) var path = new GridPathBranch { RoomRatio = new RandRange(70), BranchRatio = new RandRange(0, 50), }; + // Room types (see Ex2_Rooms for RoomGen variants) var genericRooms = new SpawnList> { { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, // cross @@ -41,6 +62,7 @@ public static void Run() }; path.GenericRooms = genericRooms; + // Hall types (see Ex2_Rooms for hall options) var genericHalls = new SpawnList> { { new RoomGenAngledHall(50), 10 }, @@ -49,18 +71,23 @@ public static void Run() exampleCreation.Layout.GenSteps.Add(-4, path); - // Output the rooms into a FloorPlan + // Convert GridPlan to FloorPlan (see Ex3_Grid) exampleCreation.Layout.GenSteps.Add(-2, new DrawGridToFloorStep()); - // Draw the rooms of the FloorPlan onto the tiled map, with 1 TILE padded on each side + // Draw FloorPlan to tiles (see Ex2_Rooms) exampleCreation.Layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); - // Run the generator and print + // Generate via RogueSharp's Map.Create() using our strategy exampleCreation.Seed = MathUtils.Rand.NextUInt64(); Map map = Map.Create(exampleCreation); Print(map, title); } + /// + /// Prints the generated RogueSharp map to the console. + /// + /// The RogueSharp Map to print. + /// Title to display above the map. public static void Print(Map map, string title) { var topString = new StringBuilder(string.Empty); @@ -72,6 +99,8 @@ public static void Print(Map map, string title) topString.Append('\n'); Console.Write(topString.ToString()); + + // RogueSharp Map has built-in ToString() for rendering Console.Write(map.ToString()); Console.WriteLine(); } diff --git a/RogueElements.Examples/Ex8_Integration/ExampleCreationStrategy.cs b/RogueElements.Examples/Ex8_Integration/ExampleCreationStrategy.cs index bb0c423a..f0d31983 100644 --- a/RogueElements.Examples/Ex8_Integration/ExampleCreationStrategy.cs +++ b/RogueElements.Examples/Ex8_Integration/ExampleCreationStrategy.cs @@ -9,26 +9,62 @@ namespace RogueElements.Examples.Ex8_Integration { + /// + /// Adapts RogueElements' MapGen pipeline to RogueSharp's IMapCreationStrategy interface. + /// Allows using RogueElements generation with RogueSharp's Map.Create() factory. + /// + /// The RogueSharp Map type to create. + /// + /// RogueSharp uses the Strategy pattern for map creation via IMapCreationStrategy. + /// This class wraps RogueElements' MapGen to act as a RogueSharp strategy. + /// + /// Usage pattern: + /// 1. Create ExampleCreationStrategy instance + /// 2. Configure Layout.GenSteps with RogueElements steps + /// 3. Set Seed for reproducible generation + /// 4. Call Map.Create(strategy) to generate + /// + /// This demonstrates how RogueElements can integrate with other game frameworks + /// that have their own map representations. + /// public class ExampleCreationStrategy : IMapCreationStrategy where T : Map, new() { + /// + /// Initializes a new instance of the class. + /// public ExampleCreationStrategy() { this.Layout = new MapGen(); } + /// + /// Gets or sets the random seed for reproducible generation. + /// public ulong Seed { get; set; } + /// + /// Gets or sets the MapGen layout containing generation steps. + /// Configure this with GenSteps before calling CreateMap(). + /// public MapGen Layout { get; set; } /// - /// Creates a new IMap of the specified type. + /// Creates a new IMap of the specified type using the configured pipeline. + /// Called by RogueSharp's Map.Create() factory method. /// - /// An IMap of the specified type + /// An IMap of the specified type. + /// + /// This bridges the two APIs: + /// - Calls MapGen.GenMap() to run the RogueElements pipeline + /// - Returns the generated Map from the context + /// public T CreateMap() { + // Run the RogueElements pipeline MapGenContext context = this.Layout.GenMap(MathUtils.Rand.NextUInt64()); + // Return the RogueSharp Map from the context return (T)context.Map; } } diff --git a/RogueElements.Examples/Ex8_Integration/MapGenContext.cs b/RogueElements.Examples/Ex8_Integration/MapGenContext.cs index b642510c..912fdfd3 100644 --- a/RogueElements.Examples/Ex8_Integration/MapGenContext.cs +++ b/RogueElements.Examples/Ex8_Integration/MapGenContext.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Audino // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -8,81 +8,187 @@ namespace RogueElements.Examples.Ex8_Integration { + /// + /// Complete context implementation bridging RogueElements with RogueSharp. + /// Implements both ITiledGenContext and IRoomGridGenContext to support full pipeline. + /// + /// + /// This context demonstrates integrating with an external map library (RogueSharp). + /// Key differences from BaseMapGenContext: + /// - Uses RogueSharp's Map instead of custom tile storage + /// - Tiles are RogueSharp Cells wrapped in CellTile + /// - No inheritance from BaseMapGenContext - implements interfaces directly + /// + /// Interface breakdown (see earlier examples for details): + /// - ITiledGenContext (Ex1_Tiles): Tile operations, dimensions, terrain types + /// - IRoomGridGenContext (Ex3_Grid): Grid-based room layout support + /// - IFloorPlanGenContext (Ex2_Rooms): Freeform room placement (via IRoomGridGenContext) + /// public class MapGenContext : ITiledGenContext, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public MapGenContext() { this.Map = new Map(); } + /// + /// Gets or sets the RogueSharp Map being generated. + /// public Map Map { get; set; } + /// + /// Gets the random number generator for this generation run. + /// Initialized via InitSeed() at generation start. + /// public IRandom Rand { get; private set; } + /// + /// Gets the FloorPlan for room-based operations. + /// Populated by DrawGridToFloorStep from the GridPlan. + /// public FloorPlan RoomPlan { get; private set; } + /// + /// Gets the GridPlan for grid-based room layout. + /// Populated by InitGridPlanStep at generation start. + /// public GridPlan GridPlan { get; private set; } + /// + /// Gets a value indicating whether tiles have been initialized. + /// public bool TilesInitialized => this.Map.Width > 0 && this.Map.Height > 0; + /// + /// Gets the map width in tiles. + /// public int Width => this.Map.Width; + /// + /// Gets the map height in tiles. + /// public int Height => this.Map.Height; + /// + /// Gets a value indicating whether the map wraps at edges. + /// public bool Wrap => false; + /// + /// Gets the default floor/room terrain (walkable, transparent). + /// public ITile RoomTerrain => new CellTile(0, 0, true, true, false); + /// + /// Gets the default wall terrain (non-walkable, non-transparent). + /// public ITile WallTerrain => new CellTile(0, 0, false, false, false); + /// + /// Gets the tile at the specified location. + /// + /// The tile location. + /// The tile as a CellTile wrapper around RogueSharp's Cell. public ITile GetTile(Loc loc) => CellTile.FromCell(this.Map.GetCell(loc.X, loc.Y)); + /// + /// Checks if a tile can be placed at the specified location. + /// Always returns true - RogueSharp maps have no placement restrictions. + /// + /// The location to check. + /// The tile to potentially place. + /// Always true for this implementation. public bool CanSetTile(Loc loc, ITile tile) => true; + /// + /// Attempts to set a tile at the specified location. + /// + /// The location to set. + /// The tile to place (must be castable to Cell). + /// True if successful; false if CanSetTile returns false. public bool TrySetTile(Loc loc, ITile tile) { if (!this.CanSetTile(loc, tile)) return false; + + // Cast ITile to RogueSharp Cell and apply properties Cell cell = (Cell)tile; this.Map.SetCellProperties(loc.X, loc.Y, cell.IsTransparent, cell.IsWalkable, cell.IsExplored); return true; } + /// + /// Sets a tile at the specified location, throwing if placement fails. + /// + /// The location to set. + /// The tile to place. + /// Thrown if tile cannot be placed. public void SetTile(Loc loc, ITile tile) { if (!this.TrySetTile(loc, tile)) throw new InvalidOperationException("Can't place tile!"); } + /// + /// Initializes the random seed for this generation run. + /// Called by MapGen at the start of GenMap(). + /// + /// The seed value for reproducible generation. public void InitSeed(ulong seed) { this.Rand = new ReRandom(seed); } + /// bool ITiledGenContext.TileBlocked(Loc loc) { return !this.Map.IsWalkable(loc.X, loc.Y); } + /// bool ITiledGenContext.TileBlocked(Loc loc, bool diagonal) { return !this.Map.IsWalkable(loc.X, loc.Y); } + /// + /// Creates the tile storage with the specified dimensions. + /// Initializes the RogueSharp Map. + /// + /// Map width in tiles. + /// Map height in tiles. + /// Whether map wraps (ignored for RogueSharp). public virtual void CreateNew(int width, int height, bool wrap = false) { this.Map.Initialize(width, height); } + /// + /// Called when generation is complete. + /// No-op for this implementation. + /// public void FinishGen() { } + /// + /// Initializes the FloorPlan for room-based operations. + /// Called by DrawGridToFloorStep or InitFloorPlanStep. + /// + /// The floor plan to use. public void InitPlan(FloorPlan plan) { this.RoomPlan = plan; } + /// + /// Initializes the GridPlan for grid-based layout. + /// Called by InitGridPlanStep. + /// + /// The grid plan to use. public void InitGrid(GridPlan plan) { this.GridPlan = plan; diff --git a/RogueElements.Examples/Ex8_Integration/README.md b/RogueElements.Examples/Ex8_Integration/README.md new file mode 100644 index 00000000..9f05c4fe --- /dev/null +++ b/RogueElements.Examples/Ex8_Integration/README.md @@ -0,0 +1,322 @@ +# Example 8: Integration + +Integrate RogueElements with external libraries using the MapCreationStrategy pattern. + +## What You'll Learn + +- How to integrate RogueElements with RogueSharp +- Creating a custom `IMapCreationStrategy` +- Adapting the tile system for external libraries +- Building a complete, reusable generation pipeline + +## Prerequisites + +- All previous examples +- Basic understanding of RogueSharp (optional) + +## Concepts + +### Integration Strategy + +RogueElements is designed to be **library-agnostic**. This example shows integration with **RogueSharp**, but the same pattern works for any game framework: + +``` +RogueElements (procedural generation) + --> Adapter Layer + --> External Library (RogueSharp, MonoGame, Unity, etc.) +``` + +### IMapCreationStrategy + +RogueSharp uses the Strategy pattern for map generation. By implementing `IMapCreationStrategy`, we can plug RogueElements into RogueSharp's ecosystem. + +### CellTile Adapter + +RogueSharp uses `Cell` objects instead of our `Tile` class. `CellTile` bridges the gap by implementing both `Cell` and `ITile`. + +## Code Walkthrough + +### Step 1: Create the MapCreationStrategy + +```csharp +public class ExampleCreationStrategy : IMapCreationStrategy + where T : Map, new() +{ + public ExampleCreationStrategy() + { + this.Layout = new MapGen(); + } + + public ulong Seed { get; set; } + public MapGen Layout { get; set; } + + public T CreateMap() + { + MapGenContext context = this.Layout.GenMap(this.Seed); + return (T)context.Map; + } +} +``` + +Key points: +- Holds the `MapGen` pipeline as a property +- `Seed` allows deterministic generation +- `CreateMap()` executes the pipeline and returns the RogueSharp Map + +### Step 2: Create the CellTile Adapter + +```csharp +public class CellTile : Cell, ITile +{ + public CellTile(int x, int y, bool isTransparent, bool isWalkable, bool isInFov) + : base(x, y, isTransparent, isWalkable, isInFov) + { + } + + public static CellTile FromCell(ICell other) => new CellTile(other); + + public bool TileEquivalent(ITile other) + => (other is ICell cell) && cell?.IsWalkable == this.IsWalkable; + + public ITile Copy() => new CellTile(this); +} +``` + +This adapter: +- Extends RogueSharp's `Cell` +- Implements RogueElements' `ITile` +- Enables both libraries to work with the same object + +### Step 3: Create a RogueSharp-Compatible Context + +```csharp +public class MapGenContext : ITiledGenContext, IRoomGridGenContext +{ + public MapGenContext() + { + this.Map = new Map(); // RogueSharp.Map + } + + public Map Map { get; set; } // RogueSharp.Map (not BaseMap!) + + // ITile implementations using CellTile + public ITile RoomTerrain => new CellTile(0, 0, true, true, false); + public ITile WallTerrain => new CellTile(0, 0, false, false, false); + + public ITile GetTile(Loc loc) + => CellTile.FromCell(this.Map.GetCell(loc.X, loc.Y)); + + public bool TrySetTile(Loc loc, ITile tile) + { + Cell cell = (Cell)tile; + this.Map.SetCellProperties(loc.X, loc.Y, + cell.IsTransparent, cell.IsWalkable, cell.IsExplored); + return true; + } + + public void CreateNew(int width, int height, bool wrap = false) + { + this.Map.Initialize(width, height); + } + + // ... other interface implementations +} +``` + +Key differences from previous examples: +- Uses `RogueSharp.Map` instead of custom `BaseMap` +- `RoomTerrain`/`WallTerrain` are `CellTile` objects +- Tile operations translate to RogueSharp's cell system + +### Step 4: Configure the Pipeline + +```csharp +public static void Run() +{ + ExampleCreationStrategy exampleCreation = new ExampleCreationStrategy(); + + // Standard grid setup + var startGen = new InitGridPlanStep(1) + { + CellX = 6, CellY = 4, + CellWidth = 9, CellHeight = 9, + }; + exampleCreation.Layout.GenSteps.Add(-4, startGen); + + // Branching path + var path = new GridPathBranch + { + RoomRatio = new RandRange(70), + BranchRatio = new RandRange(0, 50), + }; + // ... room and hall setup ... + exampleCreation.Layout.GenSteps.Add(-4, path); + + // Grid -> FloorPlan -> Tiles + exampleCreation.Layout.GenSteps.Add(-2, new DrawGridToFloorStep()); + exampleCreation.Layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + + // Generate using RogueSharp's Map.Create pattern + exampleCreation.Seed = MathUtils.Rand.NextUInt64(); + Map map = Map.Create(exampleCreation); +} +``` + +### Step 5: Use RogueSharp's Native Rendering + +```csharp +public static void Print(Map map, string title) +{ + // ... header ... + Console.Write(map.ToString()); // RogueSharp's built-in rendering! +} +``` + +RogueSharp's `Map.ToString()` automatically renders the map using its own format. + +## Try It + +```bash +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj +``` + +Press `8` to run Example 8. + +**What to observe:** +- Output uses RogueSharp's rendering format +- Same procedural generation, different output library +- Seamless integration between the two libraries + +**Example output:** +``` +8: Implementation as a MapCreationStrategy in RogueSharp +======================================================= +###################################################### +#######.........###################################### +#######.........###....############################### +#######.........###....############################### +#######.............#..#####........################## +###############.....####............................## +###############.........#############................. +###############.........#############................. +``` + +## Key Classes Used + +| Class | Purpose | +|-------|---------| +| `ExampleCreationStrategy` | Implements RogueSharp's IMapCreationStrategy | +| `CellTile` | Adapter between Cell and ITile | +| `MapGenContext` | RogueSharp-compatible generation context | +| `RogueSharp.Map` | RogueSharp's native map class | + +## Integration Patterns + +### For RogueSharp + +```csharp +public class MyCreationStrategy : IMapCreationStrategy +{ + public MapGen Layout { get; set; } + + public Map CreateMap() + { + var context = Layout.GenMap(seed); + return (Map)context.Map; + } +} + +// Usage +Map map = Map.Create(new MyCreationStrategy()); +``` + +### For MonoGame/FNA + +```csharp +public class MonoGameMapGenerator +{ + public MapGen Layout { get; set; } + + public Texture2D GenerateMapTexture(GraphicsDevice device, ulong seed) + { + var context = Layout.GenMap(seed); + return RenderToTexture(device, context.Map); + } +} +``` + +### For Unity + +```csharp +public class UnityMapGenerator : MonoBehaviour +{ + public MapGen Layout { get; set; } + + public void GenerateMap(ulong seed) + { + var context = Layout.GenMap(seed); + InstantiateTilemap(context.Map); + } +} +``` + +## Creating Your Own Integration + +1. **Create a Context Class** + - Implement required interfaces (`ITiledGenContext`, etc.) + - Use your framework's tile/cell representation + +2. **Create a Tile Adapter** (if needed) + - Implement `ITile` + - Bridge to your framework's tile system + +3. **Create a Generation Entry Point** + - Hold the `MapGen` pipeline + - Provide seed/configuration options + - Return your framework's map type + +4. **Configure the Pipeline** + - Add generation steps as needed + - Reuse steps across different integrations + +## Key Takeaways + +1. **Library Agnostic**: RogueElements works with any framework +2. **Adapter Pattern**: Bridge interfaces when needed +3. **Strategy Pattern**: Plug into existing generation frameworks +4. **Reusable Pipelines**: Same steps work across integrations + +## Reference Implementation + +This example serves as a **reference implementation** for integrating RogueElements. Key patterns: + +| Pattern | Implementation | +|---------|---------------| +| Interface adaptation | `CellTile` class | +| Generation entry point | `ExampleCreationStrategy` | +| Context customization | `MapGenContext` with RogueSharp.Map | +| Pipeline reuse | Standard GenSteps work unchanged | + +## Complete Pipeline Summary + +All 8 examples build upon each other: + +| Example | Adds | +|---------|------| +| 1 | Basic pipeline, static tiles | +| 2 | FloorPlan, procedural rooms | +| 3 | GridPlan, structured layouts | +| 4 | Stair spawning | +| 5 | Perlin terrain | +| 6 | Random items/mobs | +| 7 | Special rooms, filtered spawning | +| 8 | External library integration | + +## Next Steps + +You now have all the tools to: +- Create custom generation pipelines +- Integrate with your game framework +- Build procedural roguelike maps + +Explore the main [RogueElements library](../../RogueElements/) for additional steps and features not covered in these examples. diff --git a/RogueElements.Examples/ExampleDebug.cs b/RogueElements.Examples/ExampleDebug.cs index 148d3e72..5403da89 100644 --- a/RogueElements.Examples/ExampleDebug.cs +++ b/RogueElements.Examples/ExampleDebug.cs @@ -12,6 +12,10 @@ namespace RogueElements.Examples { + /// + /// Debug visualization utility for step-by-step map generation inspection. + /// Hook into events to enable interactive debugging. + /// [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Methods grouped for documentation purposes")] public static class ExampleDebug { @@ -24,10 +28,20 @@ public static class ExampleDebug private static int currentDepth; private static IGenContext curMap; + /// + /// Gets or sets the depth level for printing. Set to -1 to disable, 0+ to print at that depth. + /// public static int Printing { get; set; } + /// + /// Gets or sets a value indicating whether to step into the next generation step. + /// public static bool SteppingIn { get; set; } + /// + /// Initializes debug state for a new map generation. Hook to . + /// + /// The map context being initialized. public static void Init(IGenContext newMap) { curMap = newMap; @@ -43,6 +57,10 @@ public static void Init(IGenContext newMap) tileDebugString.Add(new DebugState()); } + /// + /// Called when entering a generation step. Hook to . + /// + /// The step description message. public static void StepIn(string msg) { currentDepth++; @@ -55,6 +73,9 @@ public static void StepIn(string msg) Printing = Math.Max(Printing, currentDepth + 1); } + /// + /// Called when exiting a generation step. Hook to . + /// [SuppressMessage("Microsoft.Diagnostics.CodeAnalysis", "IDE0059:ValueAssignedIsUnused", Justification="Variable present for example")] public static void StepOut() { @@ -74,6 +95,10 @@ public static void StepOut() PrintStep(CreateStackString() + "<" + stepOutName + "<"); } + /// + /// Called for each generation step. Hook to . + /// + /// The step description message. public static void OnStep(string msg) { PrintStep(CreateStackString() + ">" + msg); @@ -107,6 +132,10 @@ private static string CreateStackString() /* Code below is specific to the map gen context; it can be tweaked to vary by game implementation */ + /// + /// Prints the current step state to console with interactive navigation. + /// + /// The step message to display. public static void PrintStep(string msg) { bool printDebug = false; @@ -158,6 +187,14 @@ public static void PrintStep(string msg) } } + /// + /// Prints tile-based map visualization for contexts implementing . + /// + /// The map context. + /// The message to display. + /// Whether to write to debug output. + /// Whether to display interactive console viewer. + /// The key pressed to exit the viewer, or Enter if no viewer shown. public static ConsoleKey PrintTiles(IGenContext map, string msg, bool printDebug, bool printViewer) { if (!(map is ITiledGenContext context)) @@ -250,6 +287,14 @@ public static ConsoleKey PrintTiles(IGenContext map, string msg, bool printDebug } } + /// + /// Prints floor plan visualization for contexts implementing . + /// + /// The map context. + /// The message to display. + /// Whether to write to debug output. + /// Whether to display interactive console viewer. + /// The key pressed to exit the viewer, or Enter if no viewer shown. public static ConsoleKey PrintListRoomHalls(IGenContext map, string msg, bool printDebug, bool printViewer) { if (!(map is IFloorPlanGenContext context)) @@ -416,6 +461,14 @@ public static ConsoleKey PrintListRoomHalls(IGenContext map, string msg, bool pr } } + /// + /// Prints grid plan visualization for contexts implementing . + /// + /// The map context. + /// The message to display. + /// Whether to write to debug output. + /// Whether to display interactive console viewer. + /// The key pressed to exit the viewer, or Enter if no viewer shown. public static ConsoleKey PrintGridRoomHalls(IGenContext map, string msg, bool printDebug, bool printViewer) { if (!(map is IRoomGridGenContext context)) diff --git a/RogueElements.Examples/Program.cs b/RogueElements.Examples/Program.cs index c7d0b8e3..8963fe5b 100644 --- a/RogueElements.Examples/Program.cs +++ b/RogueElements.Examples/Program.cs @@ -10,8 +10,14 @@ namespace RogueElements.Examples { + /// + /// Entry point and interactive menu for RogueElements examples. + /// public class Program { + /// + /// Runs the interactive example menu. Press 1-8 to run examples, F4 for debug mode. + /// public static void Main() { #if DEBUG @@ -115,6 +121,11 @@ public static void Main() Console.ReadKey(); } + /// + /// Reads an integer from console input. + /// + /// When true, F2 returns -2 for bulk generation mode. + /// The entered integer, -1 for escape, or -2 for F2 if enabled. public static int GetInt(bool includeAmt) { int result = 0; @@ -145,6 +156,12 @@ public static int GetInt(bool includeAmt) return result; } + /// + /// Runs map generation multiple times for performance benchmarking. + /// + /// The map context type. + /// The map generator to test. + /// Number of maps to generate. public static void StressTest(MapGen layout, int amount) where T : class, IGenContext { @@ -190,6 +207,10 @@ public static void StressTest(MapGen layout, int amount) } } + /// + /// Prints an exception and all inner exceptions to console. + /// + /// The exception to print. public static void PrintError(Exception ex) { Exception innerException = ex; diff --git a/RogueElements.Examples/README.md b/RogueElements.Examples/README.md new file mode 100644 index 00000000..01797a17 --- /dev/null +++ b/RogueElements.Examples/README.md @@ -0,0 +1,323 @@ +# RogueElements.Examples + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![.NET Core](https://img.shields.io/badge/.NET%20Core-2.1-blue.svg)](https://dotnet.microsoft.com/) + +Interactive examples demonstrating RogueElements library features, from basic tiles to complete dungeon generation. Each example builds on previous concepts, providing a structured learning path. + +## Overview + +This project contains 8 progressive examples that teach RogueElements concepts incrementally. Run the examples interactively and watch maps generate in your terminal. + +## Running the Examples + +```bash +# From repository root +dotnet run --project RogueElements.Examples/RogueElements.Examples.csproj + +# Or from this directory +cd RogueElements.Examples +dotnet run +``` + +### Interactive Controls + +``` +1-8 Run example 1-8 +F4 Enable debug mode (step-by-step generation) +F5 Step into (while debugging) +F6 Step out (while debugging) +ESC Exit +``` + +## Learning Progression + +Follow this roadmap to learn RogueElements from fundamentals to advanced usage: + +``` +Ex1_Tiles ──> Ex2_Rooms ──> Ex3_Grid ──> Ex4_Stairs + │ │ │ │ + v v v v + Basics FloorPlan GridPlan Spawning + │ + v +Ex5_Terrain <── Ex6_Items <── Ex7_Special <── Ex8_Integration + │ │ │ │ + v v v v + Water Items/Mobs Components RogueSharp +``` + +## Examples Reference + +| Example | Title | Key Concepts | GenSteps Used | +|---------|-------|--------------|---------------| +| **Ex1** | Static Tiles | Tile grids, `MapGen`, basic pipeline | `InitTilesStep`, `SpecificTilesStep` | +| **Ex2** | Rooms & Halls | Freeform room placement, `FloorPlan` | `InitFloorPlanStep`, `FloorPathBranch`, `DrawFloorToTileStep` | +| **Ex3** | Grid Layout | Grid-based generation, `GridPlan` | `InitGridPlanStep`, `GridPathBranch`, `DrawGridToFloorStep` | +| **Ex4** | Stairs | Entity spawning, entrance/exit placement | `FloorStairsStep` | +| **Ex5** | Terrain | Perlin noise, water generation, cleanup | `PerlinWaterStep`, `DropDiagonalBlockStep`, `EraseIsolatedStep` | +| **Ex6** | Items & Mobs | `SpawnList`, random placement, weighted distribution | `RandomSpawnStep`, `PickerSpawner`, `LoopedRand` | +| **Ex7** | Special Rooms | Room components, filters, custom room shapes | `SetSpecialRoomStep`, `RoomFilterComponent`, `RoomGenSpecific` | +| **Ex8** | Integration | RogueSharp interop, external library usage | `IMapCreationStrategy` pattern | + +## Example Details + +### Ex1: Static Tiles + +**Concepts**: Basic pipeline setup, tile initialization, direct tile manipulation + +```csharp +var layout = new MapGen(); + +// Initialize 30x25 grid of walls +layout.GenSteps.Add(0, new InitTilesStep(30, 25)); + +// Draw specific tiles at offset (2, 3) +var drawStep = new SpecificTilesStep(tiles, new Loc(2, 3)); +layout.GenSteps.Add(0, drawStep); + +MapGenContext context = layout.GenMap(seed); +``` + +### Ex2: Rooms & Halls (FloorPlan) + +**Concepts**: Freeform room placement, room/hall types, branching paths + +```csharp +// Initialize FloorPlan +layout.GenSteps.Add(-2, new InitFloorPlanStep(54, 40)); + +// Define room types with weights +var rooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 } +}; + +// Create branching path +var path = new FloorPathBranch(rooms, halls) +{ + HallPercent = 50, + FillPercent = new RandRange(45), + BranchRatio = new RandRange(0, 25) +}; +layout.GenSteps.Add(-1, path); + +// Render to tiles +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); +``` + +### Ex3: Grid Layout (GridPlan) + +**Concepts**: Cell-based layout, grid constraints, two-phase rendering + +```csharp +// Initialize 6x4 grid of 9x9 cells +var startGen = new InitGridPlanStep(1) +{ + CellX = 6, CellY = 4, + CellWidth = 9, CellHeight = 9 +}; +layout.GenSteps.Add(-4, startGen); + +// Grid-constrained path +var path = new GridPathBranch +{ + RoomRatio = new RandRange(70), + BranchRatio = new RandRange(0, 50) +}; +layout.GenSteps.Add(-4, path); + +// Grid -> FloorPlan -> Tiles (two-phase) +layout.GenSteps.Add(-2, new DrawGridToFloorStep()); +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); +``` + +### Ex4: Stairs + +**Concepts**: Entity spawning, `IPlaceableGenContext`, entrance/exit placement + +```csharp +// Place stairs after tiles are drawn +layout.GenSteps.Add(2, new FloorStairsStep( + 0, // Goal amount (0 = random valid position) + new StairsUp(), + new StairsDown() +)); +``` + +### Ex5: Terrain + +**Concepts**: Perlin noise, terrain stencils, post-processing cleanup + +```csharp +// Generate water with 35% coverage, order 3 noise, softness 1 +const int waterTerrain = 2; +var waterStep = new PerlinWaterStep( + new RandRange(35), + 3, // noise order + new Tile(waterTerrain), + new MapTerrainStencil(false, true, false, false), + 1 // softness +); +layout.GenSteps.Add(3, waterStep); + +// Cleanup steps +layout.GenSteps.Add(4, new DropDiagonalBlockStep(new Tile(waterTerrain))); +layout.GenSteps.Add(4, new EraseIsolatedStep(new Tile(waterTerrain))); +``` + +### Ex6: Items & Mobs + +**Concepts**: `SpawnList` weights, `LoopedRand`, picker spawners + +```csharp +// Weighted item spawns +var itemSpawns = new SpawnList +{ + { new Item('!'), 10 }, // Potion + { new Item('*'), 50 } // Gold (5x more common) +}; + +// Spawn 10-18 items +var itemStep = new RandomSpawnStep( + new PickerSpawner( + new LoopedRand(itemSpawns, new RandRange(10, 19)) + ) +); +layout.GenSteps.Add(6, itemStep); +``` + +### Ex7: Special Rooms + +**Concepts**: Room components, room filters, custom room definitions + +```csharp +// Define custom room shape +string[] treasureRoom = { + "~~~..~~~", + "~~#..#~~", + "........", + "~~#..#~~", + "~~~..~~~" +}; + +// Create special room step with component +var specialStep = new SetSpecialRoomStep +{ + Rooms = new PresetPicker>( + CreateRoomGenSpecific(treasureRoom) + ) +}; +specialStep.RoomComponents.Set(new TreasureRoomComponent()); +layout.GenSteps.Add(-1, specialStep); + +// Filter spawns to treasure rooms only +var treasureSpawns = new RandomRoomSpawnStep(...); +treasureSpawns.Filters.Add(new RoomFilterComponent(false, new TreasureRoomComponent())); +``` + +### Ex8: RogueSharp Integration + +**Concepts**: External library integration, `IMapCreationStrategy` pattern + +```csharp +// Implement RogueSharp's creation strategy +public class ExampleCreationStrategy : IMapCreationStrategy + where T : IMap, new() +{ + public MapGen Layout { get; } = new MapGen(); + public ulong Seed { get; set; } + + public T CreateMap() + { + MapGenContext context = Layout.GenMap(Seed); + // Convert to RogueSharp map... + return map; + } +} + +// Use with RogueSharp +Map map = Map.Create(new ExampleCreationStrategy()); +``` + +## Project Structure + +``` +RogueElements.Examples/ +├── Program.cs # Interactive example runner +├── ExampleDebug.cs # Debug visualization system +├── DebugState.cs # Debug state tracking +├── Common/ # Shared code for examples +│ ├── BaseMap.cs # Base map implementation +│ ├── BaseMapGenContext.cs # Base context with ITiledGenContext +│ ├── Tile.cs # Simple tile implementation +│ ├── Stairs.cs # Stairs base class +│ ├── StairsUp.cs # Upward stairs +│ ├── StairsDown.cs # Downward stairs +│ ├── MainRoomComponent.cs # Main path room marker +│ ├── MainHallComponent.cs # Main path hall marker +│ └── TreasureRoomComponent.cs # Treasure room marker +├── Ex1_Tiles/ # Example 1: Static tiles +│ └── Example1.cs +├── Ex2_Rooms/ # Example 2: FloorPlan rooms +│ └── Example2.cs +├── Ex3_Grid/ # Example 3: GridPlan layout +│ └── Example3.cs +├── Ex4_Stairs/ # Example 4: Stair placement +│ └── Example4.cs +├── Ex5_Terrain/ # Example 5: Water/terrain +│ └── Example5.cs +├── Ex6_Items/ # Example 6: Items and mobs +│ ├── Example6.cs +│ ├── Item.cs # Item spawnable +│ └── Mob.cs # Mob spawnable +├── Ex7_Special/ # Example 7: Special rooms +│ └── Example7.cs +└── Ex8_Integration/ # Example 8: RogueSharp integration + ├── Example8.cs + └── ExampleCreationStrategy.cs +``` + +## Debug Mode + +Press **F4** before running an example to enable step-by-step debugging: + +```csharp +// Debug hooks are attached in Program.cs +GenContextDebug.OnInit += ExampleDebug.Init; +GenContextDebug.OnStep += ExampleDebug.OnStep; +GenContextDebug.OnStepIn += ExampleDebug.StepIn; +GenContextDebug.OnStepOut += ExampleDebug.StepOut; +``` + +This allows you to see the map state after each GenStep executes. + +## Creating Your Own Context + +Use `BaseMapGenContext` as a starting point: + +```csharp +public class MapGenContext : BaseMapGenContext, + IFloorPlanGenContext, + IRoomGridGenContext, + IPlaceableGenContext +{ + public FloorPlan FloorPlan { get; set; } + public GridPlan GridPlan { get; set; } + + // Implement IPlaceableGenContext + public List Items { get; } = new List(); + // ... +} +``` + +## See Also + +- **[RogueElements/](../RogueElements/)** - Core library documentation +- **[RogueElements.Tests/](../RogueElements.Tests/)** - Unit tests for reference +- **[CLAUDE.md](../CLAUDE.md)** - Full architecture documentation + +--- + +![Repobeats analytics](https://repobeats.axiom.co/api/embed/placeholder.svg "Repobeats analytics image") diff --git a/RogueElements.Tests/MapGen/FloorPlan/README.md b/RogueElements.Tests/MapGen/FloorPlan/README.md new file mode 100644 index 00000000..e101428d --- /dev/null +++ b/RogueElements.Tests/MapGen/FloorPlan/README.md @@ -0,0 +1,99 @@ +# FloorPlan Tests + +## Overview + +Tests for freeform room-based map generation using `FloorPlan`. This covers adding rooms to floor plans, connecting rooms via hallways, and managing room adjacencies. The FloorPlan system allows arbitrary room placement without grid constraints. + +## Test Classes + +| Class | Description | +|-------|-------------| +| `FloorPlanTest.cs` | Core FloorPlan operations: room addition, adjacency, room drawing | +| `AddRoomTest.cs` | Tests for `AddRoom` operations and collision detection | +| `ConnectTest.cs` | Tests for hallway connections between rooms | +| `FloorPathBranchTest.cs` | Path branching algorithm tests for room placement | +| `TestFloorPlan.cs` | Test helper class implementing `IFloorPlanTestContext` | +| `IFloorPlanTestContext.cs` | Interface for floor plan test contexts | + +## Key Test Patterns + +### String Grid Visualization +Tests use ASCII grids to define room layouts with letters representing rooms and `#` for connections: +```csharp +string[] inGrid = +{ + "A#B#C", + ". # .", + "0#D.0", +}; +TestFloorPlan floorPlan = TestFloorPlan.InitFloorToContext(inGrid); +``` + +### Room Collision Testing +Tests verify rooms don't overlap and respect boundaries: +```csharp +[Test] +public void AddRoomCollide() +{ + // Tests that overlapping rooms throw exceptions +} +``` + +### Connection Testing +Verifies hallway connections between adjacent rooms: +```csharp +[Test] +public void ConnectRooms() +{ + // Tests bidirectional connections between rooms + // Verifies adjacency lists are updated correctly +} +``` + +### Mocking Room Generators +Room generators are mocked to return specific room shapes: +```csharp +Mock>> mockRooms = + new Mock>>(MockBehavior.Strict); +mockRooms.Setup(p => p.Pick(testRand.Object)).Returns(new TestFloorRoomGen('A')); +``` + +## Example Test + +```csharp +[Test] +public void AddRoomToEmptyPlan() +{ + Mock testRand = new Mock(MockBehavior.Strict); + var floorPlan = new TestFloorPlan(); + var roomGen = new TestFloorRoomGen('A'); + + floorPlan.AddRoom(roomGen.Draw, roomGen); + + Assert.That(floorPlan.RoomCount, Is.EqualTo(1)); + Assert.That(floorPlan.GetRoomGen(0).Draw, Is.EqualTo(roomGen.Draw)); +} +``` + +## Behaviors Tested + +- **Room Addition**: Adding rooms at valid/invalid positions +- **Collision Detection**: Preventing overlapping rooms +- **Adjacency Management**: Tracking which rooms connect +- **Border Handling**: Managing room borders and entry points +- **Path Generation**: Creating branching paths of rooms + +## Running Tests + +```bash +dotnet test --filter "FullyQualifiedName~FloorPlanTest" +dotnet test --filter "FullyQualifiedName~AddRoomTest" +dotnet test --filter "FullyQualifiedName~ConnectTest" +``` + +## Adding Tests + +1. Use `TestFloorPlan` and `TestFloorRoomGen` helper classes +2. Initialize plans with `InitFloorToContext()` string arrays +3. Mock `IRandom` for deterministic random behavior +4. Verify room counts, adjacencies, and positions after operations diff --git a/RogueElements.Tests/MapGen/GenSteps/README.md b/RogueElements.Tests/MapGen/GenSteps/README.md new file mode 100644 index 00000000..c4b51c3d --- /dev/null +++ b/RogueElements.Tests/MapGen/GenSteps/README.md @@ -0,0 +1,110 @@ +# GenSteps Tests + +## Overview + +Tests for individual generation step implementations (`GenStep`). Each GenStep performs a specific map generation task like spawning items, creating water features, or placing entry/exit points. These tests verify that steps correctly modify map contexts. + +## Test Classes + +| Class | Description | +|-------|-------------| +| `SpawnerTest.cs` | Tests for item/entity spawning on maps | +| `WaterStepTest.cs` | Tests for water blob placement and terrain modification | +| `PathStepTest.cs` | Tests for path generation steps | +| `IsolatedStepTest.cs` | Tests for handling isolated room scenarios | +| `StartEndStepTest.cs` | Tests for entrance/exit placement (TODO) | + +## Key Test Patterns + +### Mocking Map Contexts +GenSteps operate on map contexts, which are mocked for testing: +```csharp +Mock mockContext = new Mock(MockBehavior.Strict); +mockContext.Setup(p => p.Rand).Returns(testRand.Object); +mockContext.Setup(p => p.RoomPlan).Returns(floorPlan); +``` + +### SpawnList Testing +Spawners use weighted spawn lists: +```csharp +var spawnList = new SpawnList(); +spawnList.Add(new ItemSpawn("potion"), 10); +spawnList.Add(new ItemSpawn("scroll"), 5); + +Mock testRand = new Mock(MockBehavior.Strict); +testRand.Setup(p => p.Next(15)).Returns(0); // Always picks "potion" +``` + +### Verifying Step Application +Tests verify that steps modify maps correctly: +```csharp +[Test] +public void ApplyWaterStep() +{ + // Setup map with specific tiles + string[] inGrid = { "....", "....", "...." }; + var context = TestGenContext.InitGridToContext(inGrid); + + // Apply water step + var waterStep = new WaterStep(waterTerrain, 50); + waterStep.Apply(context); + + // Verify water was placed + Assert.That(context.GetTile(new Loc(1, 1)).ID, Is.EqualTo(waterTerrain.ID)); +} +``` + +## Behaviors Tested + +### Spawning +- Weighted random selection from spawn lists +- Spawn count based on room/map size +- Spawn placement validation (not on walls, etc.) + +### Water/Terrain +- Water blob placement using noise algorithms +- Terrain modification respecting boundaries +- Percentage-based coverage + +### Paths +- Path step application to room plans +- Integration with room generators + +### Isolation Handling +- Detection of isolated (unreachable) rooms +- Fallback behavior when rooms can't connect + +## Example Test + +```csharp +[Test] +public void SpawnItemAtValidLocation() +{ + Mock testRand = new Mock(MockBehavior.Strict); + testRand.Setup(p => p.Next(It.IsAny())).Returns(0); + + var context = CreateTestContext(); + var spawner = new PickerSpawner(spawnList); + + spawner.Apply(context); + + Assert.That(context.Items.Count, Is.EqualTo(1)); + testRand.Verify(p => p.Next(It.IsAny()), Times.AtLeastOnce); +} +``` + +## Running Tests + +```bash +dotnet test --filter "FullyQualifiedName~SpawnerTest" +dotnet test --filter "FullyQualifiedName~WaterStepTest" +dotnet test --filter "FullyQualifiedName~PathStepTest" +``` + +## Adding Tests + +1. Create a test class matching `{StepName}Test.cs` +2. Mock the appropriate context interface (`IFloorPlanGenContext`, `ITiledGenContext`, etc.) +3. Setup required mocks for `IRandom` and any dependencies +4. Call `Apply()` on the step and verify the context was modified correctly +5. Use `[Ignore("TODO")]` for tests under development diff --git a/RogueElements.Tests/MapGen/Grid/README.md b/RogueElements.Tests/MapGen/Grid/README.md new file mode 100644 index 00000000..e5124a75 --- /dev/null +++ b/RogueElements.Tests/MapGen/Grid/README.md @@ -0,0 +1,142 @@ +# Grid Tests + +## Overview + +Tests for grid-based room layout generation using `GridFloorPlan`. Unlike freeform FloorPlans, GridPlans organize rooms in a regular grid where each cell can contain a room and hallways connect adjacent cells. This enables more structured dungeon layouts. + +## Test Classes + +| Class | Description | +|-------|-------------| +| `GridFloorPlanTest.cs` | Core GridFloorPlan operations: room placement, grid navigation | +| `GridPathBranchTest.cs` | Path branching algorithm for grid-based room placement | +| `TestGridFloorPlan.cs` | Test helper class for grid plan initialization and comparison | +| `IGridPathTestContext.cs` | Interface for grid path test contexts | + +## Key Test Patterns + +### ASCII Grid Visualization +Tests use a unique string format where: +- Letters (`A`, `B`, `C`...) represent rooms +- `#` represents hallway connections +- `0` represents empty cells +- `.` represents potential connection points + +```csharp +string[] inGrid = +{ + "A#B#C", + "# . .", + "D.0.0", +}; +TestGridFloorPlan floorPlan = TestGridFloorPlan.InitGridToContext(inGrid); +``` + +### Floor Plan Comparison +Test helper compares expected vs actual grid states: +```csharp +TestGridFloorPlan compareFloorPlan = TestGridFloorPlan.InitGridToContext(outGrid); +TestGridFloorPlan.CompareFloorPlans(floorPlan, compareFloorPlan); +``` + +### Branch Expansion Testing +Tests verify which directions can expand from existing rooms: +```csharp +[Test] +[TestCase(false)] +[TestCase(true)] +public void GetPossibleExpansionsAlone(bool branch) +{ + string[] inGrid = + { + "0.0.0", + ". . .", + "0.A.0", + ". . .", + "0.0.0", + }; + + TestGridFloorPlan floorPlan = TestGridFloorPlan.InitGridToContext(inGrid); + List rays = GridPathBranch.GetPossibleExpansions(floorPlan, branch); + + // Verify 4 expansion directions when alone in center +} +``` + +## Example Test + +```csharp +[Test] +public void CreatePath100Percent() +{ + string[] inGrid = + { + "0.0.0", + ". . .", + "0.0.0", + ". . .", + "0.0.0", + }; + + string[] outGrid = + { + "A#B#C", + ". . #", + "F#E#D", + "# . .", + "G#H#I", + }; + + Mock testRand = new Mock(MockBehavior.Strict); + // Setup random sequence for path decisions... + + var pathGen = new GridPathBranch + { + RoomRatio = new RandRange(100), + BranchRatio = new RandRange(0), + }; + + TestGridFloorPlan floorPlan = TestGridFloorPlan.InitGridToContext(inGrid); + TestGridFloorPlan compareFloorPlan = TestGridFloorPlan.InitGridToContext(outGrid); + + pathGen.ApplyToPath(testRand.Object, floorPlan); + + TestGridFloorPlan.CompareFloorPlans(floorPlan, compareFloorPlan); +} +``` + +## Behaviors Tested + +### Grid Room Placement +- Adding rooms to specific grid cells +- Grid boundary validation +- Cell occupancy tracking + +### Path Generation +- Branching paths with configurable ratios +- Room quota fulfillment (0%, 50%, 100%) +- Forced branching vs. linear paths + +### Expansion Logic +- Valid expansion directions from terminals +- Branch point detection +- Corner and edge case handling + +### Hallway Connections +- Horizontal and vertical hallway placement +- Connection between adjacent grid cells + +## Running Tests + +```bash +dotnet test --filter "FullyQualifiedName~GridFloorPlanTest" +dotnet test --filter "FullyQualifiedName~GridPathBranchTest" +``` + +## Adding Tests + +1. Use `TestGridFloorPlan.InitGridToContext()` to create test grids +2. Define expected output using the same string format +3. Mock `IRandom` sequences for deterministic path generation +4. Use `CompareFloorPlans()` to verify results match expectations +5. Test edge cases: corners, edges, full grids, single rooms diff --git a/RogueElements.Tests/MapGen/README.md b/RogueElements.Tests/MapGen/README.md new file mode 100644 index 00000000..8b64cf29 --- /dev/null +++ b/RogueElements.Tests/MapGen/README.md @@ -0,0 +1,61 @@ +# MapGen Tests + +## Overview + +This folder contains tests for the map generation pipeline components in RogueElements. The tests are organized into subfolders mirroring the source structure: FloorPlan (freeform room placement), GenSteps (individual generation steps), Grid (grid-based layouts), and Rooms (room shape generators). + +## Directory Structure + +| Directory | Purpose | +|-----------|---------| +| `FloorPlan/` | Tests for freeform room-based map generation using `FloorPlan` | +| `GenSteps/` | Tests for individual `GenStep` implementations (spawning, paths, water) | +| `Grid/` | Tests for grid-based room layouts using `GridPlan` | +| `Rooms/` | Tests for room shape generators (`RoomGenSquare`, `RoomGenCave`, etc.) | + +## Key Test Patterns + +### Moq Mocking +All tests heavily use Moq to mock interfaces and verify behavior: +```csharp +Mock testRand = new Mock(MockBehavior.Strict); +testRand.Setup(p => p.Next(60)).Returns(roll); +``` + +### Deterministic Testing with Seeds +Random behavior is tested using mocked `IRandom` with predetermined sequences: +```csharp +Moq.Language.ISetupSequentialResult seq = testRand.SetupSequence(p => p.Next(3)); +seq = seq.Returns(0); +seq = seq.Returns(1); +``` + +### String Grid Initialization +Many tests use string arrays to define map/grid states: +```csharp +string[] inGrid = +{ + "A#B", + ". .", + "0.0", +}; +TestGridFloorPlan floorPlan = TestGridFloorPlan.InitGridToContext(inGrid); +``` + +## Running Tests + +```bash +# Run all MapGen tests +dotnet test --filter "FullyQualifiedName~RogueElements.Tests" --filter "Namespace~MapGen" + +# Run specific subfolder +dotnet test --filter "FullyQualifiedName~RogueElements.Tests.FloorPlanTest" +``` + +## Adding Tests + +1. Create test classes in the appropriate subfolder +2. Use `[TestFixture]` attribute on classes +3. Use `[Test]` for individual tests, `[TestCase]` for parameterized tests +4. Mock `IRandom` for deterministic behavior +5. Use existing test helpers (`TestFloorPlan`, `TestGridFloorPlan`, `TestGenContext`) diff --git a/RogueElements.Tests/MapGen/Rooms/README.md b/RogueElements.Tests/MapGen/Rooms/README.md new file mode 100644 index 00000000..1548f93b --- /dev/null +++ b/RogueElements.Tests/MapGen/Rooms/README.md @@ -0,0 +1,149 @@ +# Rooms Tests + +## Overview + +Tests for room shape generators (`RoomGen` implementations). Room generators are responsible for proposing room sizes, drawing room tiles onto the map, and managing border connectivity. These tests verify that rooms are generated with correct dimensions, shapes, and connection points. + +## Test Classes + +| Class | Description | +|-------|-------------| +| `RoomGenTest.cs` | Core `RoomGen` base class behavior: size, borders, connections | +| `RoomGenSquareTest.cs` | Tests for rectangular room generation | +| `RoomGenCaveTest.cs` | Tests for cave/irregular room shapes | +| `TestGenContext.cs` | Test helper implementing `ITiledGenContext` | +| `TestRoomGen.cs` | Test double exposing protected members for verification | + +## Key Test Patterns + +### String Grid Tile Maps +Tests use `X` for walls and `.` for floor tiles: +```csharp +string[] inGrid = +{ + "XXXXXXXX", + "XX.....X", + "XX.....X", + "XX.....X", + "XXXXXXXX", +}; +TestGenContext testContext = TestGenContext.InitGridToContext(inGrid); +``` + +### Size Proposal Testing +Verifies rooms propose sizes within configured ranges: +```csharp +[Test] +public void ProposeSize() +{ + Mock testRand = new Mock(MockBehavior.Strict); + testRand.Setup(p => p.Next(3, 5)).Returns(3); + testRand.Setup(p => p.Next(4, 7)).Returns(4); + + RoomGenSquare roomGen = + new RoomGenSquare(new RandRange(3, 5), new RandRange(4, 7)); + + Loc size = roomGen.ProposeSize(testRand.Object); + + Assert.That(size, Is.EqualTo(new Loc(3, 4))); +} +``` + +### Drawing on Map Testing +Verifies room tiles are placed correctly: +```csharp +[Test] +public void DrawOnMap() +{ + var roomGen = new RoomGenSquare(); + string[] outGrid = + { + "XXXXXXXX", + "XX.....X", + "XX.....X", + "XXXXXXXX", + }; + + TestGenContext resultContext = TestGenContext.InitGridToContext(outGrid); + roomGen.PrepareSize(testRand.Object, new Loc(5, 2)); + roomGen.SetLoc(new Loc(2, 1)); + + roomGen.DrawOnMap(testContext); + + Assert.That(testContext.Tiles, Is.EqualTo(resultContext.Tiles)); +} +``` + +### Border Connectivity Testing +Tests room border management for hallway connections: +```csharp +[Test] +public void ReceiveOpenedBorder() +{ + roomGenTo.AskBorderFromRoom(roomGenFrom.Draw, roomGenFrom.GetOpenedBorder, Dir4.Down); + + IntRange newRange = roomGenTo.RoomSideReqs[Dir4.Down][0]; + Assert.That(newRange, Is.EqualTo(new IntRange(expectedStart, expectedEnd))); +} +``` + +## Behaviors Tested + +### Size Management +- Size proposal within RandRange bounds +- Invalid size rejection (zero or negative) +- Size preparation with border arrays + +### Tile Drawing +- Correct tile placement for room shape +- Respecting room bounds (Draw.Start, Draw.Size) +- Different room shapes (square, cave, etc.) + +### Border Handling +- OpenedBorder: which tiles can connect +- FulfillableBorder: which tiles allow entries +- BorderToFulfill: pending connection requirements +- Border range validation and clamping + +### Room Connection +- Receiving border requests from adjacent rooms +- Fulfilling border requirements via digging +- Handling partially blocked borders + +## Example Test + +```csharp +[Test] +public void FulfillRoomBordersNoneOneMissing() +{ + Mock testRand = new Mock(MockBehavior.Strict); + testRand.Setup(p => p.Next(4)).Returns(2); // Pick middle tile + + Mock> roomGen = + new Mock> { CallBase = true }; + roomGen.Setup(p => p.DigAtBorder(It.IsAny(), It.IsAny(), It.IsAny())); + + // Setup room with unfulfilled border requirement + roomGen.Object.AskBorderRange(new IntRange(3, 7), Dir4.Down); + roomGen.Object.FulfillRoomBorders(testContext, false); + + // Verify it dug at the randomly selected tile + roomGen.Verify(p => p.DigAtBorder(testContext, Dir4.Down, 5), Times.Once()); +} +``` + +## Running Tests + +```bash +dotnet test --filter "FullyQualifiedName~RoomGenTest" +dotnet test --filter "FullyQualifiedName~RoomGenSquareTest" +dotnet test --filter "FullyQualifiedName~RoomGenCaveTest" +``` + +## Adding Tests + +1. Use `TestGenContext.InitGridToContext()` for tile map setup +2. Use `TestRoomGen` to access protected members for verification +3. Call `PrepareSize()` and `SetLoc()` before testing draw operations +4. Test edge cases: minimum sizes, corner positions, blocked borders +5. Mark incomplete tests with `[Ignore("TODO")]` diff --git a/RogueElements.Tests/Priority/README.md b/RogueElements.Tests/Priority/README.md new file mode 100644 index 00000000..7d55cb95 --- /dev/null +++ b/RogueElements.Tests/Priority/README.md @@ -0,0 +1,135 @@ +# Priority Tests + +## Overview + +Tests for the `Priority` class, which provides multi-level priority ordering for the generation pipeline. Priorities can have multiple numeric levels (e.g., `1.2.3`) enabling fine-grained ordering of generation steps. This is essential for ensuring steps execute in the correct sequence. + +## Test Classes + +| Class | Description | +|-------|-------------| +| `PriorityTest.cs` | All priority comparison, equality, and indexing tests | + +## Key Test Patterns + +### Parameterized Comparison Tests +Tests use `[TestCase]` for exhaustive operator testing: +```csharp +[Test] +[TestCase(0, false, false, 1, true)] // 0 < 1 +[TestCase(0, true, false, 1, false)] // 0 > 1 false +[TestCase(1, true, true, 1, true)] // 1 >= 1 +public void TestOp1(int lhs1, bool gt, bool eq, int rhs1, bool res) +{ + Priority lhs = new Priority(lhs1); + Priority rhs = new Priority(rhs1); + + if (eq) + { + if (gt) + Assert.That(lhs >= rhs, Is.EqualTo(res)); + else + Assert.That(lhs <= rhs, Is.EqualTo(res)); + } + // ... +} +``` + +### Multi-Level Priority Comparisons +Tests verify correct ordering of hierarchical priorities: +```csharp +[Test] +public void Test0Lt0p0p1() +{ + // Priority(0) < Priority(0, 0, 1) + Assert.IsTrue(new Priority(0) < new Priority(0, 0, 1)); +} + +[Test] +public void Test1p0p1Gt1p0p0p1() +{ + // Priority(1, 0, 1) > Priority(1, 0, 0, 1) + Assert.IsTrue(new Priority(1, 0, 1) > new Priority(1, 0, 0, 1)); +} +``` + +## Behaviors Tested + +### Comparison Operators +| Operator | Test Cases | +|----------|------------| +| `<` | Single level, multi-level, edge cases | +| `>` | Single level, multi-level, edge cases | +| `<=` | Includes equality cases | +| `>=` | Includes equality cases | +| `==` | Same values, different lengths, null/invalid | +| `!=` | Inverse of equality | + +### Special Cases +- **Negative sub-priorities**: `Priority(0, -1) < Priority(0, 0)` +- **Invalid priorities**: `Priority.Invalid == Priority.Invalid` +- **Null construction**: `Priority(null) == Priority(new int[])` (both empty/invalid) +- **Trailing zeros**: `Priority(0) == Priority(0, 0)` (normalized) + +### Indexing and Length +```csharp +[Test] +[TestCase(0, 0)] +[TestCase(1, 1)] +[TestCase(6, 13)] +public void TestIdx(int idx, int val) +{ + Priority priority = new Priority(0, 1, 2, 3, 5, 8, 13); + Assert.AreEqual(priority[idx], val); +} + +[Test] +public void TestLength() +{ + Priority priority = new Priority(0, 1, 2, 3, 5, 8, 13); + Assert.AreEqual(priority.Length, 7); +} +``` + +## Example Tests + +```csharp +// Multi-level ordering: 0.1 < 1.0 +[Test] +public void Test0p1Lt1() +{ + Assert.IsTrue(new Priority(0, 1) < new Priority(1)); +} + +// Equality with normalization +[Test] +public void Test0Eq0p0() +{ + Assert.IsTrue(new Priority(0) == new Priority(0, 0)); +} + +// Invalid priority comparison +[Test] +public void TestInvalidCp0(bool gt, bool eq) +{ + Priority lhs = Priority.Invalid; + Priority rhs = new Priority(0); + + // Invalid priorities never compare true to valid ones + Assert.IsFalse(lhs < rhs); + Assert.IsFalse(lhs > rhs); +} +``` + +## Running Tests + +```bash +dotnet test --filter "FullyQualifiedName~PriorityTest" +``` + +## Adding Tests + +1. Use `[TestCase]` for parameterized tests covering multiple scenarios +2. Test both directions of comparisons (A < B implies B > A) +3. Include edge cases: negative values, empty/invalid, different lengths +4. Test boundary conditions for indexing operations diff --git a/RogueElements.Tests/README.md b/RogueElements.Tests/README.md new file mode 100644 index 00000000..ae645659 --- /dev/null +++ b/RogueElements.Tests/README.md @@ -0,0 +1,369 @@ +# RogueElements.Tests + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![NUnit](https://img.shields.io/badge/NUnit-3.10-green.svg)](https://nunit.org/) +[![Moq](https://img.shields.io/badge/Moq-4.8-blue.svg)](https://github.com/moq/moq4) + +Unit tests for the RogueElements library using NUnit and Moq. Tests mirror the source structure and verify core algorithms, data structures, and generation logic. + +## Overview + +This test project ensures the reliability of RogueElements through comprehensive unit tests covering: + +- Grid operations and pathfinding algorithms +- Random number generation and weighted selection +- Collision detection and shape algorithms +- Room generation and floor plan construction +- Priority queue behavior + +## Running Tests + +```bash +# From repository root +dotnet test RogueElements.Tests/RogueElements.Tests.csproj + +# With detailed output +dotnet test RogueElements.Tests/RogueElements.Tests.csproj --logger "console;verbosity=detailed" + +# Run specific test class +dotnet test --filter "FullyQualifiedName~GridTest" + +# Run specific test method +dotnet test --filter "FullyQualifiedName~GridTest.FindAPathStraight" +``` + +## Testing Philosophy + +### Deterministic Testing + +Tests use deterministic seeds or mocked random sources to ensure reproducibility: + +```csharp +[Test] +public void SpawnListPickMultiple() +{ + // Use controlled randomness via Moq + var mockRand = new Mock(MockBehavior.Strict); + mockRand.Setup(r => r.Next(It.IsAny())).Returns(0); + + var spawnList = new SpawnList { { 1, 10 }, { 2, 20 } }; + int result = spawnList.Pick(mockRand.Object); + + Assert.That(result, Is.EqualTo(1)); +} +``` + +### Visual Test Grids + +Many tests use ASCII grid representations for clarity: + +```csharp +[Test] +public void FindAPathCurved() +{ + string[] inGrid = + { + ".......", + ".......", + ".......", + ".XXXXX.", + "...X...", + ".A.X.B.", + "...X...", + }; + + char[][] map = InitGrid(inGrid); + // Test pathfinding from A to B around obstacles X + List path = Grid.FindAPath(...); + Assert.That(path, Is.EquivalentTo(expectedPath)); +} +``` + +### Boundary Conditions + +Tests cover edge cases and boundary conditions: + +```csharp +[Test] +public void SpawnListPickEmpty() +{ + var spawnList = new SpawnList(); + Assert.Throws(() => spawnList.Pick(rand)); +} +``` + +## Test Structure + +Tests mirror the source directory structure: + +``` +RogueElements.Tests/ +├── MapGen/ +│ ├── FloorPlan/ # FloorPlan algorithm tests +│ │ ├── FloorPlanTest.cs +│ │ ├── PathTest.cs +│ │ └── ... +│ ├── Grid/ # GridPlan tests +│ │ ├── GridPlanTest.cs +│ │ └── ... +│ ├── Rooms/ # Room generation tests +│ │ ├── RoomGenTest.cs +│ │ └── ... +│ └── GenSteps/ # Individual GenStep tests +│ └── ... +├── Rand/ # RNG utility tests +│ ├── RandRangeTest.cs +│ ├── SpawnListTest.cs +│ ├── SpawnListExceptionTest.cs +│ └── SpawnRangeListTest.cs +├── Priority/ # Priority queue tests +│ └── PriorityListTest.cs +├── GridTest.cs # Core Grid algorithm tests +├── CollisionTest.cs # Collision detection tests +├── DetectionTest.cs # Shape detection tests +├── DirExtTest.cs # Direction extension tests +├── MathUtilsTest.cs # Math utility tests +├── NoiseGenTest.cs # Perlin noise tests +├── RectTest.cs # Rectangle operation tests +├── TypeDictTest.cs # TypeDict tests +└── WrappedCollisionTest.cs # Wrapped collision tests +``` + +## Key Test Files + +### GridTest.cs + +Tests grid algorithms including pathfinding: + +```csharp +[TestFixture] +public class GridTest +{ + [Test] + public void FindAPathStraight() + { + // Tests A* pathfinding in open space + } + + [Test] + public void FindAPathCurved() + { + // Tests pathfinding around obstacles + } + + [Test] + public void FloodFill() + { + // Tests flood fill algorithm + } +} +``` + +### SpawnListTest.cs + +Tests weighted random selection: + +```csharp +[TestFixture] +public class SpawnListTest +{ + [Test] + public void SpawnListAdd() { } + + [Test] + public void SpawnListGetSpawnRate() { } + + [Test] + public void SpawnListPick() { } + + [Test] + public void SpawnListPickMultiple() { } +} +``` + +### DetectionTest.cs + +Tests shape detection and blob analysis: + +```csharp +[TestFixture] +public class DetectionTest +{ + [Test] + public void DetectBlobs() { } + + [Test] + public void FindConnectedTiles() { } + + [Test] + public void FindHoles() { } +} +``` + +## Adding New Tests + +### 1. Create Test Class + +Create a new test file mirroring the source structure: + +```csharp +using System; +using NUnit.Framework; +using Moq; + +namespace RogueElements.Tests +{ + [TestFixture] + public class MyFeatureTest + { + [SetUp] + public void Setup() + { + // Test initialization + } + + [TearDown] + public void TearDown() + { + // Cleanup + } + + [Test] + public void MyFeature_WhenCondition_ShouldBehavior() + { + // Arrange + var input = PrepareInput(); + + // Act + var result = MyFeature.Process(input); + + // Assert + Assert.That(result, Is.EqualTo(expected)); + } + } +} +``` + +### 2. Use Test Helpers + +Leverage existing test utilities: + +```csharp +// Convert string grid to char array +char[][] map = GridTest.InitGrid(new[] +{ + "###", + "#.#", + "###" +}); + +// Convert to bool grid (. = true, # = false) +bool[][] boolMap = GridTest.InitBoolGrid(inGrid); + +// Convert to int grid (A=0, B=1, C=2, ...) +int[][] intMap = GridTest.InitIntGrid(inGrid); +``` + +### 3. Mock Dependencies + +Use Moq for isolating units: + +```csharp +[Test] +public void GenStep_AppliesCorrectly() +{ + // Mock the context + var mockContext = new Mock(); + mockContext.Setup(c => c.Width).Returns(10); + mockContext.Setup(c => c.Height).Returns(10); + mockContext.Setup(c => c.Rand).Returns(new ReRandom(12345)); + + // Create and apply step + var step = new MyGenStep(); + step.Apply(mockContext.Object); + + // Verify interactions + mockContext.Verify(c => c.SetTile(It.IsAny(), It.IsAny()), Times.AtLeastOnce); +} +``` + +### 4. Test Naming Conventions + +Follow the pattern: `MethodName_Condition_ExpectedBehavior` + +```csharp +[Test] +public void FindPath_WithBlockedRoute_ReturnsEmpty() { } + +[Test] +public void SpawnList_WhenEmpty_ThrowsException() { } + +[Test] +public void GridPlan_AddRoom_UpdatesCellCount() { } +``` + +## Test Data Patterns + +### ASCII Grid Pattern + +```csharp +string[] input = +{ + "#####", + "#...#", + "#.#.#", + "#...#", + "#####" +}; +// # = wall/blocked +// . = floor/open +// A-Z = special markers +// X = obstacle +``` + +### Expected Result Verification + +```csharp +List expectedPath = new List +{ + new Loc(5, 5), + new Loc(4, 5), + new Loc(3, 5), + new Loc(2, 5), + new Loc(1, 5) +}; + +Assert.That(actualPath, Is.EquivalentTo(expectedPath)); +``` + +## Continuous Integration + +Tests run on every commit via Travis CI: + +```yaml +# .travis.yml +script: + - dotnet build RogueElements.sln + - dotnet test RogueElements.Tests/RogueElements.Tests.csproj +``` + +## Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| NUnit | 3.10.1 | Test framework | +| NUnit3TestAdapter | 3.10.0 | VS Test adapter | +| Microsoft.NET.Test.Sdk | 15.8.0 | Test SDK | +| Moq | 4.8.0 | Mocking framework | +| StyleCop.Analyzers | 1.1.118 | Code style enforcement | +| CodeCracker.CSharp | 1.1.0 | Code analysis | + +## See Also + +- **[RogueElements/](../RogueElements/)** - Core library source +- **[RogueElements.Examples/](../RogueElements.Examples/)** - Usage examples +- **[CLAUDE.md](../CLAUDE.md)** - Full architecture documentation + +--- + +![Repobeats analytics](https://repobeats.axiom.co/api/embed/placeholder.svg "Repobeats analytics image") diff --git a/RogueElements.Tests/Rand/README.md b/RogueElements.Tests/Rand/README.md new file mode 100644 index 00000000..5fdb6c04 --- /dev/null +++ b/RogueElements.Tests/Rand/README.md @@ -0,0 +1,165 @@ +# Rand Tests + +## Overview + +Tests for random number generation utilities, weighted spawn lists, and noise generation. These components are fundamental to procedural generation, ensuring randomness is properly weighted, seeded, and reproducible for testing. + +## Test Classes + +| Class | Description | +|-------|-------------| +| `SpawnListTest.cs` | Tests for weighted random selection from `SpawnList` | +| `SpawnRangeListTest.cs` | Tests for level-range-based spawn lists | +| `RandRangeTest.cs` | Tests for `RandRange`, `LoopedRand`, `RandBag`, etc. | +| `NoiseGenTest.cs` | Tests for cellular automata and noise generation | + +## Key Test Patterns + +### Weighted Spawn Selection +Tests verify correct weighted random selection: +```csharp +[SetUp] +public void SpawnListSetUp() +{ + this.spawnList = new SpawnList + { + { "apple", 10 }, // 10/60 = 16.7% + { "orange", 20 }, // 20/60 = 33.3% + { "banana", 30 }, // 30/60 = 50% + }; +} + +[Test] +[TestCase(0, 0)] // Roll 0-9 -> apple (index 0) +[TestCase(9, 0)] // Roll 9 -> still apple +[TestCase(10, 1)] // Roll 10-29 -> orange (index 1) +[TestCase(30, 2)] // Roll 30-59 -> banana (index 2) +public void SpawnListChooseIndex(int roll, int result) +{ + Mock testRand = new Mock(MockBehavior.Strict); + testRand.Setup(p => p.Next(60)).Returns(roll); + + Assert.That(this.spawnList.PickIndex(testRand.Object), Is.EqualTo(result)); +} +``` + +### Level-Range Spawn Lists +Tests for spawns valid only in specific level ranges: +```csharp +[SetUp] +public void SpawnListSetUp() +{ + this.spawnRangeList = new SpawnRangeList(); + this.spawnRangeList.Add("apple", new IntRange(0, 5), 10); + this.spawnRangeList.Add("orange", new IntRange(3, 9), 20); +} + +[Test] +[TestCase(-1, false)] // Before first range +[TestCase(0, true)] // Apple valid +[TestCase(3, true)] // Both valid +[TestCase(5, true)] // Only orange +[TestCase(9, false)] // After all ranges +public void SpawnRangeListCanPick(int level, bool result) +{ + Assert.That(this.spawnRangeList.CanPick(level), Is.EqualTo(result)); +} +``` + +### Cellular Automata Noise +Tests for iterative automata with cell rules: +```csharp +[Test] +[TestCase(CellRule.None, false)] +[TestCase(CellRule.All, true)] +[TestCase(CellRule.Eq0, true)] // 0 neighbors = true +[TestCase(CellRule.Lt2, true)] // <2 neighbors = true +[TestCase(CellRule.Gte1, false)] // >=1 neighbors = false (has 0) +public void IterateAutomataSingle0(CellRule rule, bool expected) +{ + string[] inGrid = + { + "XXX", + "X.X", // Center cell has 0 open neighbors + "XXX", + }; + + bool[][] map = GridTest.InitBoolGrid(inGrid); + bool[][] result = NoiseGen.IterateAutomata(map, CellRule.None, rule, 1); + + Assert.That(result[1][1], Is.EqualTo(expected)); +} +``` + +## Behaviors Tested + +### SpawnList +- **Weighted Selection**: Correct probability distribution +- **Total Calculation**: `SpawnTotal` matches sum of weights +- **Index Selection**: Edge cases at weight boundaries +- **Rate Modification**: Changing spawn rates updates totals +- **Removal**: Removing items updates weights correctly + +### SpawnRangeList +- **Range Validation**: Items only spawn within their IntRange +- **Overlapping Ranges**: Multiple items valid at same level +- **Error Handling**: Invalid ranges throw ArgumentException + +### RandRange Utilities +- **LoopedRand**: Generate multiple values from a range +- **PresetPicker**: Always return predetermined value +- **RandBag**: Pick without replacement +- **RandBinomial**: Binomial distribution with offset + +### NoiseGen +- **Cellular Automata**: Cell rules (Eq0, Lt2, Gte4, etc.) +- **Iteration Count**: 0, 1, 2+ iterations shrink/grow areas +- **Neighbor Counting**: All 8 neighbors influence decisions + +## Example Tests + +```csharp +// RandBag: pick without replacement +[Test] +public void RandBagRemovable() +{ + RandBag testPicker = new RandBag(true, new List { 8, 5, 2, 3, 4, 1, 6, 7 }); + Mock testRand = new Mock(MockBehavior.Strict); + testRand.Setup(p => p.Next(8)).Returns(3); // Pick item at index 3 + + Assert.That(testPicker.Pick(testRand.Object), Is.EqualTo(3)); + // Item removed, next pick from remaining 7 items +} + +// Cellular automata iteration +[Test] +public void IterateAutomataIterations2() +{ + string[] inGrid = { "XXXXX", "X...X", "X...X", "X...X", "XXXXX" }; + string[] outGrid = { "XXXXX", "XXXXX", "XX.XX", "XXXXX", "XXXXX" }; + + bool[][] map = GridTest.InitBoolGrid(inGrid); + bool[][] compare = GridTest.InitBoolGrid(outGrid); + + bool[][] result = NoiseGen.IterateAutomata(map, CellRule.None, CellRule.Gte4, 2); + + Assert.That(result, Is.EqualTo(compare)); +} +``` + +## Running Tests + +```bash +dotnet test --filter "FullyQualifiedName~SpawnListTest" +dotnet test --filter "FullyQualifiedName~SpawnRangeListTest" +dotnet test --filter "FullyQualifiedName~RandRangeTest" +dotnet test --filter "FullyQualifiedName~NoiseGenTest" +``` + +## Adding Tests + +1. Use `Mock` to control random outcomes +2. Test boundary conditions (first/last item, empty lists) +3. Verify weights sum correctly after modifications +4. Use `[TestCase]` for parameterized probability tests +5. Mark incomplete tests with `[Ignore("TODO")]` diff --git a/RogueElements.sln b/RogueElements.sln index 09e0c2ac..f6f5ba9a 100644 --- a/RogueElements.sln +++ b/RogueElements.sln @@ -9,24 +9,66 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RogueElements.Tests", "Rogu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RogueElements.Examples", "RogueElements.Examples\RogueElements.Examples.csproj", "{D2A41E4D-9EA5-446A-905E-334164E291AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RogueElements.Benchmarks", "RogueElements.Benchmarks\RogueElements.Benchmarks.csproj", "{B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A777BC3D-2635-401D-96D7-162178D8DFC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A777BC3D-2635-401D-96D7-162178D8DFC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A777BC3D-2635-401D-96D7-162178D8DFC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {A777BC3D-2635-401D-96D7-162178D8DFC4}.Debug|x64.Build.0 = Debug|Any CPU + {A777BC3D-2635-401D-96D7-162178D8DFC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {A777BC3D-2635-401D-96D7-162178D8DFC4}.Debug|x86.Build.0 = Debug|Any CPU {A777BC3D-2635-401D-96D7-162178D8DFC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {A777BC3D-2635-401D-96D7-162178D8DFC4}.Release|Any CPU.Build.0 = Release|Any CPU + {A777BC3D-2635-401D-96D7-162178D8DFC4}.Release|x64.ActiveCfg = Release|Any CPU + {A777BC3D-2635-401D-96D7-162178D8DFC4}.Release|x64.Build.0 = Release|Any CPU + {A777BC3D-2635-401D-96D7-162178D8DFC4}.Release|x86.ActiveCfg = Release|Any CPU + {A777BC3D-2635-401D-96D7-162178D8DFC4}.Release|x86.Build.0 = Release|Any CPU {3EDB9E87-866C-4A51-A347-547D35C069BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3EDB9E87-866C-4A51-A347-547D35C069BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EDB9E87-866C-4A51-A347-547D35C069BF}.Debug|x64.ActiveCfg = Debug|Any CPU + {3EDB9E87-866C-4A51-A347-547D35C069BF}.Debug|x64.Build.0 = Debug|Any CPU + {3EDB9E87-866C-4A51-A347-547D35C069BF}.Debug|x86.ActiveCfg = Debug|Any CPU + {3EDB9E87-866C-4A51-A347-547D35C069BF}.Debug|x86.Build.0 = Debug|Any CPU {3EDB9E87-866C-4A51-A347-547D35C069BF}.Release|Any CPU.ActiveCfg = Release|Any CPU {3EDB9E87-866C-4A51-A347-547D35C069BF}.Release|Any CPU.Build.0 = Release|Any CPU + {3EDB9E87-866C-4A51-A347-547D35C069BF}.Release|x64.ActiveCfg = Release|Any CPU + {3EDB9E87-866C-4A51-A347-547D35C069BF}.Release|x64.Build.0 = Release|Any CPU + {3EDB9E87-866C-4A51-A347-547D35C069BF}.Release|x86.ActiveCfg = Release|Any CPU + {3EDB9E87-866C-4A51-A347-547D35C069BF}.Release|x86.Build.0 = Release|Any CPU {D2A41E4D-9EA5-446A-905E-334164E291AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D2A41E4D-9EA5-446A-905E-334164E291AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2A41E4D-9EA5-446A-905E-334164E291AE}.Debug|x64.ActiveCfg = Debug|Any CPU + {D2A41E4D-9EA5-446A-905E-334164E291AE}.Debug|x64.Build.0 = Debug|Any CPU + {D2A41E4D-9EA5-446A-905E-334164E291AE}.Debug|x86.ActiveCfg = Debug|Any CPU + {D2A41E4D-9EA5-446A-905E-334164E291AE}.Debug|x86.Build.0 = Debug|Any CPU {D2A41E4D-9EA5-446A-905E-334164E291AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2A41E4D-9EA5-446A-905E-334164E291AE}.Release|Any CPU.Build.0 = Release|Any CPU + {D2A41E4D-9EA5-446A-905E-334164E291AE}.Release|x64.ActiveCfg = Release|Any CPU + {D2A41E4D-9EA5-446A-905E-334164E291AE}.Release|x64.Build.0 = Release|Any CPU + {D2A41E4D-9EA5-446A-905E-334164E291AE}.Release|x86.ActiveCfg = Release|Any CPU + {D2A41E4D-9EA5-446A-905E-334164E291AE}.Release|x86.Build.0 = Release|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Debug|x64.Build.0 = Debug|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Debug|x86.Build.0 = Debug|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Release|Any CPU.Build.0 = Release|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Release|x64.ActiveCfg = Release|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Release|x64.Build.0 = Release|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Release|x86.ActiveCfg = Release|Any CPU + {B8E5F7A1-4C3D-4E6F-9A2B-1D8C7E5F3A0B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/RogueElements/BlobMap.cs b/RogueElements/BlobMap.cs index b056317c..9285419a 100644 --- a/RogueElements/BlobMap.cs +++ b/RogueElements/BlobMap.cs @@ -8,8 +8,16 @@ namespace RogueElements { + /// + /// Represents a 2D map that identifies connected regions (blobs) of tiles. + /// public class BlobMap { + /// + /// Initializes a new instance of the class with the specified dimensions. + /// + /// The width of the map. + /// The height of the map. public BlobMap(int width, int height) { this.Map = new int[width][]; @@ -23,15 +31,36 @@ public BlobMap(int width, int height) this.Blobs = new List(); } + /// + /// Gets the 2D array mapping each tile to its blob index (-1 if not part of a blob). + /// public int[][] Map { get; } + /// + /// Gets the list of identified blobs in the map. + /// public List Blobs { get; } + /// + /// Represents a connected region of tiles with its bounding rectangle and area. + /// public struct Blob : IEquatable { + /// + /// The bounding rectangle containing the blob. + /// public Rect Bounds; + + /// + /// The number of tiles in the blob. + /// public int Area; + /// + /// Initializes a new instance of the struct. + /// + /// The bounding rectangle. + /// The tile count. public Blob(Rect bounds, int area) { this.Bounds = bounds; @@ -48,16 +77,19 @@ public Blob(Rect bounds, int area) return !(value1 == value2); } + /// public override int GetHashCode() { return this.Bounds.GetHashCode() ^ this.Area.GetHashCode(); } + /// public override bool Equals(object obj) { return (obj is Blob) && this.Equals((Blob)obj); } + /// public bool Equals(Blob other) { return this.Area == other.Area && this.Bounds == other.Bounds; diff --git a/RogueElements/Collision.cs b/RogueElements/Collision.cs index 4f01289a..28e2ecd6 100644 --- a/RogueElements/Collision.cs +++ b/RogueElements/Collision.cs @@ -10,6 +10,9 @@ namespace RogueElements { + /// + /// Provides methods for collision detection between rectangles and bounds checking. + /// public static class Collision { /// @@ -44,11 +47,25 @@ public static bool InFront(Loc testLoc, Dir8 dir, int range) return dir.GetLoc() * foundRange == testLoc; } + /// + /// Determines if two rectangles overlap. + /// + /// The first rectangle. + /// The second rectangle. + /// true if the rectangles overlap; otherwise false. public static bool Collides(Rect bound1, Rect bound2) { return Collides(bound1.Start, bound1.Size, bound2.Start, bound2.Size); } + /// + /// Determines if two rectangular regions overlap. + /// + /// Start of the first region. + /// Size of the first region. + /// Start of the second region. + /// Size of the second region. + /// true if the regions overlap; otherwise false. public static bool Collides(Loc start1, Loc size1, Loc start2, Loc size2) { return Collides(start1.X, size1.X, start2.X, size2.X) && @@ -88,41 +105,94 @@ public static int GetIntersection(int start1, int size1, int start2, int size2) return -Math.Max(distLeft, distRight); } + /// + /// Determines if a point is within a rectangle. + /// + /// The rectangle. + /// The point to check. + /// true if the point is within the rectangle; otherwise false. public static bool InBounds(Rect rect, Loc point) { return InBounds(rect.Size.X, rect.Size.Y, point - rect.Start); } + /// + /// Determines if a point is within a rectangular region. + /// + /// Start of the region. + /// Size of the region. + /// The point to check. + /// true if the point is within the region; otherwise false. public static bool InBounds(Loc start, Loc size, Loc point) { return InBounds(size.X, size.Y, point - start); } + /// + /// Determines if a point is within a rectangular region starting at origin. + /// + /// Width of the region. + /// Height of the region. + /// The point to check. + /// true if the point is within the region; otherwise false. public static bool InBounds(int sizeX, int sizeY, Loc pt) { return InBounds(sizeX, pt.X) && InBounds(sizeY, pt.Y); } + /// + /// Determines if a value is within a 1D range. + /// + /// Start of the range. + /// Size of the range. + /// The value to check. + /// true if the value is within the range; otherwise false. public static bool InBounds(int start, int size, int pt) { return InBounds(size, pt - start); } + /// + /// Determines if a value is within a 1D range starting at zero. + /// + /// Size of the range. + /// The value to check. + /// true if the value is within [0, size); otherwise false. public static bool InBounds(int size, int pt) { return pt >= 0 && pt < size; } + /// + /// Clamps a point to be within a rectangle. + /// + /// The rectangle. + /// The point to clamp. + /// The clamped point. public static Loc ClampToBounds(Rect rect, Loc point) { return ClampToBounds(rect.Start, rect.Size, point); } + /// + /// Clamps a point to be within a rectangular region. + /// + /// Start of the region. + /// Size of the region. + /// The point to clamp. + /// The clamped point. public static Loc ClampToBounds(Loc start, Loc size, Loc point) { return ClampToBounds(size.X, size.Y, point - start) + start; } + /// + /// Clamps a point to be within a rectangular region starting at origin. + /// + /// Width of the region. + /// Height of the region. + /// The point to clamp. + /// The clamped point. public static Loc ClampToBounds(int sizeX, int sizeY, Loc pt) { return new Loc(Math.Min(Math.Max(0, pt.X), sizeX - 1), Math.Min(Math.Max(0, pt.Y), sizeY - 1)); diff --git a/RogueElements/Detection.cs b/RogueElements/Detection.cs index e424355e..efafcd91 100644 --- a/RogueElements/Detection.cs +++ b/RogueElements/Detection.cs @@ -10,10 +10,23 @@ namespace RogueElements { + /// + /// Provides methods for detecting patterns, regions, and connectivity in grids. + /// public static class Detection { + /// + /// Delegate for performing an action on a rectangle. + /// + /// The rectangle. public delegate void RectFunc(Rect rect); + /// + /// Detects and identifies connected regions (blobs) in a rectangular area. + /// + /// The rectangular area to scan. + /// Delegate determining if a tile is part of a blob. + /// A containing identified blobs. public static BlobMap DetectBlobs(Rect rect, Grid.LocTest isValid) { if (isValid == null) @@ -236,6 +249,13 @@ public static List DetectWalls(Rect rect, Grid.LocTest checkBlock, Grid return walls; } + /// + /// Gets the direction a wall faces, if it has exactly one adjacent ground tile. + /// + /// The wall location. + /// Delegate determining if a tile is blocked. + /// Delegate determining if a tile is ground. + /// A ray with the wall location and facing direction, or Dir4.None if invalid. public static LocRay4 GetWallDir(Loc loc, Grid.LocTest checkBlock, Grid.LocTest checkGround) { if (checkBlock == null) diff --git a/RogueElements/DirExt.cs b/RogueElements/DirExt.cs index e9097996..c866cf83 100644 --- a/RogueElements/DirExt.cs +++ b/RogueElements/DirExt.cs @@ -10,22 +10,52 @@ namespace RogueElements { + /// + /// Provides extension methods and utilities for direction and axis enumerations. + /// public static class DirExt { + /// Number of horizontal directions. public const int DIRH_COUNT = 2; + + /// Number of vertical directions. public const int DIRV_COUNT = 2; + + /// Number of 4-way directions. public const int DIR4_COUNT = 4; + + /// Number of 8-way directions. public const int DIR8_COUNT = 8; + + /// Number of 4-way axes. public const int AXIS4_COUNT = 2; + + /// Number of 8-way axes. public const int AXIS8_COUNT = 4; + /// All valid horizontal directions. public static readonly IEnumerable VALID_DIRH = new DirH[] { DirH.Left, DirH.Right }; + + /// All valid vertical directions. public static readonly IEnumerable VALID_DIRV = new DirV[] { DirV.Up, DirV.Down }; + + /// All valid 4-way directions. public static readonly IEnumerable VALID_DIR4 = new Dir4[] { Dir4.Down, Dir4.Left, Dir4.Up, Dir4.Right }; + + /// All valid 8-way directions. public static readonly IEnumerable VALID_DIR8 = new Dir8[] { Dir8.Down, Dir8.DownLeft, Dir8.Left, Dir8.UpLeft, Dir8.Up, Dir8.UpRight, Dir8.Right, Dir8.DownRight }; + + /// All valid 4-way axes. public static readonly IEnumerable VALID_AXIS4 = new Axis4[] { Axis4.Vert, Axis4.Horiz }; + + /// All valid 8-way axes. public static readonly IEnumerable VALID_AXIS8 = new Axis8[] { Axis8.Vert, Axis8.DiagForth, Axis8.Horiz, Axis8.DiagBack }; + /// + /// Validates that the direction is a valid value. + /// + /// The direction to validate. + /// true if valid; otherwise false. public static bool Validate(this DirV dir) { switch (dir) @@ -39,6 +69,11 @@ public static bool Validate(this DirV dir) } } + /// + /// Validates that the direction is a valid value. + /// + /// The direction to validate. + /// true if valid; otherwise false. public static bool Validate(this DirH dir) { switch (dir) @@ -52,6 +87,11 @@ public static bool Validate(this DirH dir) } } + /// + /// Validates that the direction is a valid value. + /// + /// The direction to validate. + /// true if valid; otherwise false. public static bool Validate(this Dir4 dir) { switch (dir) @@ -67,6 +107,11 @@ public static bool Validate(this Dir4 dir) } } + /// + /// Validates that the direction is a valid value. + /// + /// The direction to validate. + /// true if valid; otherwise false. public static bool Validate(this Dir8 dir) { switch (dir) @@ -86,6 +131,11 @@ public static bool Validate(this Dir8 dir) } } + /// + /// Validates that the axis is a valid value. + /// + /// The axis to validate. + /// true if valid; otherwise false. public static bool Validate(this Axis4 axis) { switch (axis) @@ -99,6 +149,11 @@ public static bool Validate(this Axis4 axis) } } + /// + /// Validates that the axis is a valid value. + /// + /// The axis to validate. + /// true if valid; otherwise false. public static bool Validate(this Axis8 axis) { switch (axis) @@ -114,6 +169,11 @@ public static bool Validate(this Axis8 axis) } } + /// + /// Converts a horizontal direction to a 4-way direction. + /// + /// The horizontal direction. + /// The corresponding 4-way direction. public static Dir4 ToDir4(this DirH dir) { switch (dir) @@ -126,6 +186,11 @@ public static Dir4 ToDir4(this DirH dir) } } + /// + /// Converts a vertical direction to a 4-way direction. + /// + /// The vertical direction. + /// The corresponding 4-way direction. public static Dir4 ToDir4(this DirV dir) { switch (dir) @@ -138,6 +203,11 @@ public static Dir4 ToDir4(this DirV dir) } } + /// + /// Converts a horizontal direction to an 8-way direction. + /// + /// The horizontal direction. + /// The corresponding 8-way direction. public static Dir8 ToDir8(this DirH dir) { switch (dir) @@ -150,6 +220,11 @@ public static Dir8 ToDir8(this DirH dir) } } + /// + /// Converts a vertical direction to an 8-way direction. + /// + /// The vertical direction. + /// The corresponding 8-way direction. public static Dir8 ToDir8(this DirV dir) { switch (dir) @@ -162,6 +237,11 @@ public static Dir8 ToDir8(this DirV dir) } } + /// + /// Converts a 4-way direction to an 8-way direction. + /// + /// The 4-way direction. + /// The corresponding 8-way direction. public static Dir8 ToDir8(this Dir4 dir) { switch (dir) @@ -176,6 +256,11 @@ public static Dir8 ToDir8(this Dir4 dir) } } + /// + /// Converts an 8-way direction to a 4-way direction. Only cardinal directions are valid. + /// + /// The 8-way direction. + /// The corresponding 4-way direction. public static Dir4 ToDir4(this Dir8 dir) { switch (dir) @@ -190,6 +275,11 @@ public static Dir4 ToDir4(this Dir8 dir) } } + /// + /// Converts a 4-way axis to an 8-way axis. + /// + /// The 4-way axis. + /// The corresponding 8-way axis. public static Axis8 ToAxis8(this Axis4 axis) { switch (axis) @@ -205,6 +295,11 @@ public static Axis8 ToAxis8(this Axis4 axis) } } + /// + /// Converts an 8-way axis to a 4-way axis. Only horizontal and vertical axes are valid. + /// + /// The 8-way axis. + /// The corresponding 4-way axis. public static Axis4 ToAxis4(this Axis8 axis) { switch (axis) @@ -220,6 +315,11 @@ public static Axis4 ToAxis4(this Axis8 axis) } } + /// + /// Gets the axis of a 4-way direction. + /// + /// The direction. + /// The axis of the direction. public static Axis4 ToAxis(this Dir4 dir) { switch (dir) @@ -257,6 +357,11 @@ public static Axis4 Orth(this Axis4 axis) } } + /// + /// Gets the axis of an 8-way direction. + /// + /// The direction. + /// The axis of the direction. public static Axis8 ToAxis(this Dir8 dir) { switch (dir) @@ -280,6 +385,11 @@ public static Axis8 ToAxis(this Dir8 dir) } } + /// + /// Determines whether the direction is diagonal. + /// + /// The direction to check. + /// true if diagonal; otherwise false. public static bool IsDiagonal(this Dir8 dir) { switch (dir) @@ -299,6 +409,11 @@ public static bool IsDiagonal(this Dir8 dir) } } + /// + /// Gets the unit vector for a horizontal direction. + /// + /// The direction. + /// The unit vector. public static Loc GetLoc(this DirH dir) { switch (dir) @@ -314,6 +429,11 @@ public static Loc GetLoc(this DirH dir) } } + /// + /// Gets the unit vector for a vertical direction. + /// + /// The direction. + /// The unit vector. public static Loc GetLoc(this DirV dir) { switch (dir) @@ -329,6 +449,11 @@ public static Loc GetLoc(this DirV dir) } } + /// + /// Gets the unit vector for a 4-way direction. + /// + /// The direction. + /// The unit vector. public static Loc GetLoc(this Dir4 dir) { switch (dir) @@ -348,6 +473,11 @@ public static Loc GetLoc(this Dir4 dir) } } + /// + /// Gets the unit vector for an 8-way direction. + /// + /// The direction. + /// The unit vector. public static Loc GetLoc(this Dir8 dir) { switch (dir) @@ -375,6 +505,11 @@ public static Loc GetLoc(this Dir8 dir) } } + /// + /// Returns the opposite horizontal direction. + /// + /// The direction. + /// The opposite direction. public static DirH Reverse(this DirH dir) { switch (dir) @@ -390,6 +525,11 @@ public static DirH Reverse(this DirH dir) } } + /// + /// Returns the opposite vertical direction. + /// + /// The direction. + /// The opposite direction. public static DirV Reverse(this DirV dir) { switch (dir) @@ -405,6 +545,11 @@ public static DirV Reverse(this DirV dir) } } + /// + /// Returns the opposite 4-way direction. + /// + /// The direction. + /// The opposite direction. public static Dir4 Reverse(this Dir4 dir) { switch (dir) @@ -424,6 +569,11 @@ public static Dir4 Reverse(this Dir4 dir) } } + /// + /// Returns the opposite 8-way direction. + /// + /// The direction. + /// The opposite direction. public static Dir8 Reverse(this Dir8 dir) { switch (dir) @@ -451,6 +601,12 @@ public static Dir8 Reverse(this Dir8 dir) } } + /// + /// Combines horizontal and vertical directions into an 8-way direction. + /// + /// The horizontal component. + /// The vertical component. + /// The combined 8-way direction. public static Dir8 Combine(DirH horiz, DirV vert) { switch (vert) @@ -499,6 +655,12 @@ public static Dir8 Combine(DirH horiz, DirV vert) } } + /// + /// Separates an 8-way direction into its horizontal and vertical components. + /// + /// The 8-way direction. + /// The horizontal component. + /// The vertical component. public static void Separate(this Dir8 dir, out DirH horiz, out DirV vert) { if (!dir.Validate()) @@ -538,11 +700,22 @@ public static void Separate(this Dir8 dir, out DirH horiz, out DirV vert) } } + /// + /// Gets the direction from one location to another. + /// + /// The starting location. + /// The ending location. + /// The direction from loc1 to loc2. public static Dir8 GetDir(Loc loc1, Loc loc2) { return GetDir(loc2 - loc1); } + /// + /// Gets the direction indicated by a location vector. + /// + /// The location vector. + /// The direction of the vector. public static Dir8 GetDir(this Loc loc) { if (loc.Y > 0) @@ -574,11 +747,24 @@ public static Dir8 GetDir(this Loc loc) } } + /// + /// Gets the direction a point is relative to a rectangular region. + /// + /// The start of the region. + /// The size of the region. + /// The point to check. + /// The direction from the region to the point. public static Dir8 GetBoundsDir(Loc start, Loc size, Loc point) { return GetBoundsDir(size, point - start); } + /// + /// Gets the direction a point is relative to a region starting at origin. + /// + /// The size of the region. + /// The point to check. + /// The direction from the region to the point. public static Dir8 GetBoundsDir(Loc size, Loc point) { if (size.X <= 0 || size.Y <= 0) @@ -751,6 +937,12 @@ public static Dir8 ApproximateDir8(this Loc loc) } } + /// + /// Rotates a 4-way direction by the specified number of 90-degree steps. + /// + /// The direction to rotate. + /// The number of 90-degree clockwise steps. + /// The rotated direction. public static Dir4 Rotate(this Dir4 dir, int n) { if (!dir.Validate()) @@ -760,6 +952,12 @@ public static Dir4 Rotate(this Dir4 dir, int n) return (Dir4)(((int)dir + n) & (DIR4_COUNT - 1)); } + /// + /// Rotates an 8-way direction by the specified number of 45-degree steps. + /// + /// The direction to rotate. + /// The number of 45-degree clockwise steps. + /// The rotated direction. public static Dir8 Rotate(this Dir8 dir, int n) { if (!dir.Validate()) @@ -769,6 +967,12 @@ public static Dir8 Rotate(this Dir8 dir, int n) return (Dir8)(((int)dir + n) & (DIR8_COUNT - 1)); } + /// + /// Adds two 4-way direction angles together. + /// + /// The first direction. + /// The second direction (treated as rotation). + /// The combined direction. public static Dir4 AddAngles(Dir4 dir1, Dir4 dir2) { // dir1 is validated by Dir4.Rotate @@ -779,6 +983,12 @@ public static Dir4 AddAngles(Dir4 dir1, Dir4 dir2) return dir1.Rotate((int)dir2); } + /// + /// Adds two 8-way direction angles together. + /// + /// The first direction. + /// The second direction (treated as rotation). + /// The combined direction. public static Dir8 AddAngles(Dir8 dir1, Dir8 dir2) { // dir1 is validated by Dir8.Rotate @@ -789,6 +999,13 @@ public static Dir8 AddAngles(Dir8 dir1, Dir8 dir2) return dir1.Rotate((int)dir2); } + /// + /// Creates a location from axis-aligned scalar components. + /// + /// The axis for the scalar value. + /// The value along the axis. + /// The value perpendicular to the axis. + /// A location with the components assigned according to the axis. public static Loc CreateLoc(this Axis4 axis, int scalar, int orth) { switch (axis) @@ -804,6 +1021,12 @@ public static Loc CreateLoc(this Axis4 axis, int scalar, int orth) } } + /// + /// Gets the direction along a 4-way axis based on a scalar sign. + /// + /// The axis. + /// The scalar value determining positive or negative direction. + /// The direction along the axis. public static Dir4 GetDir(this Axis4 axis, int scalar) { if (scalar == 0 & axis.Validate()) @@ -826,6 +1049,12 @@ public static Dir4 GetDir(this Axis4 axis, int scalar) } } + /// + /// Gets the direction along an 8-way axis based on a scalar sign. + /// + /// The axis. + /// The scalar value determining positive or negative direction. + /// The direction along the axis. public static Dir8 GetDir(this Axis8 axis, int scalar) { if (scalar == 0 & axis.Validate()) @@ -852,6 +1081,12 @@ public static Dir8 GetDir(this Axis8 axis, int scalar) } } + /// + /// Gets the scalar component of a location along a 4-way axis. + /// + /// The location. + /// The axis. + /// The component value along the axis. public static int GetScalar(this Loc loc, Axis4 axis) { switch (axis) @@ -867,6 +1102,12 @@ public static int GetScalar(this Loc loc, Axis4 axis) } } + /// + /// Sets the scalar component of a location along a 4-way axis. + /// + /// The location to modify. + /// The axis. + /// The value to set. public static void SetScalar(this ref Loc loc, Axis4 axis, int value) { switch (axis) diff --git a/RogueElements/Dirs.cs b/RogueElements/Dirs.cs index bc6ab7f0..9bd0ecbf 100644 --- a/RogueElements/Dirs.cs +++ b/RogueElements/Dirs.cs @@ -10,55 +10,123 @@ namespace RogueElements { + /// + /// Represents a vertical direction (up or down). + /// public enum DirV { + /// No direction. None = -1, + + /// Downward direction (positive Y). Down = 0, + + /// Upward direction (negative Y). Up = 1, } + /// + /// Represents a horizontal direction (left or right). + /// public enum DirH { + /// No direction. None = -1, + + /// Leftward direction (negative X). Left = 0, + + /// Rightward direction (positive X). Right = 1, } + /// + /// Represents one of four cardinal directions. + /// public enum Dir4 { + /// No direction. None = -1, + + /// Downward direction (positive Y). Down = 0, + + /// Leftward direction (negative X). Left = 1, + + /// Upward direction (negative Y). Up = 2, + + /// Rightward direction (positive X). Right = 3, } + /// + /// Represents one of eight directions including diagonals. + /// public enum Dir8 { + /// No direction. None = -1, + + /// Downward direction. Down = 0, + + /// Down-left diagonal direction. DownLeft = 1, + + /// Leftward direction. Left = 2, + + /// Up-left diagonal direction. UpLeft = 3, + + /// Upward direction. Up = 4, + + /// Up-right diagonal direction. UpRight = 5, + + /// Rightward direction. Right = 6, + + /// Down-right diagonal direction. DownRight = 7, } + /// + /// Represents one of two axes (vertical or horizontal). + /// public enum Axis4 { + /// No axis. None = -1, + + /// Vertical axis (Y). Vert = 0, + + /// Horizontal axis (X). Horiz = 1, } + /// + /// Represents one of four axes including diagonals. + /// public enum Axis8 { + /// No axis. None = -1, + + /// Vertical axis (Y). Vert = 0, + + /// Forward diagonal axis (bottom-left to top-right). DiagForth = 1, + + /// Horizontal axis (X). Horiz = 2, + + /// Backward diagonal axis (top-left to bottom-right). DiagBack = 3, } } diff --git a/RogueElements/Graph.cs b/RogueElements/Graph.cs index 1d6de86c..92f1ee22 100644 --- a/RogueElements/Graph.cs +++ b/RogueElements/Graph.cs @@ -10,10 +10,25 @@ namespace RogueElements { + /// + /// Provides utility methods for graph traversal algorithms. + /// public static class Graph { + /// + /// Delegate for retrieving adjacent nodes from a given node. + /// + /// The type of node identifier. + /// The node to get adjacents for. + /// List of adjacent node identifiers. public delegate List GetAdjacents(T nodeIndex); + /// + /// Delegate for performing an action on a node with its distance from the start. + /// + /// The type of node identifier. + /// The node identifier. + /// The distance from the starting node. public delegate void DistNodeAction(T nodeIndex, int distance); /// diff --git a/RogueElements/Grid.cs b/RogueElements/Grid.cs index c58b1d5a..1c46bc00 100644 --- a/RogueElements/Grid.cs +++ b/RogueElements/Grid.cs @@ -10,10 +10,22 @@ namespace RogueElements { + /// + /// Provides utility methods for grid-based operations including pathfinding, flood fill, and connectivity analysis. + /// public static class Grid { + /// + /// Delegate for testing a location condition. + /// + /// The location to test. + /// true if the condition is met; otherwise false. public delegate bool LocTest(Loc loc); + /// + /// Delegate for performing an action at a location. + /// + /// The location for the action. public delegate void LocAction(Loc loc); private delegate bool EvalPaths(Loc[] ends, List[] resultPaths, PathTile[] farthestTiles, PathTile currentTile); @@ -61,6 +73,16 @@ public static List FindAPath(Loc rectStart, Loc rectSize, Loc start, Loc[] throw new InvalidOperationException("Could not find a path!"); } + /// + /// Searches for paths to all endpoints. + /// + /// Top-left corner of the search area. + /// Size of the search area. + /// Starting location. + /// Array of destination locations. + /// Delegate to determine if a tile is blocked. + /// Delegate to determine if diagonal movement is blocked. + /// An array of paths, one for each endpoint. public static List[] FindAllPaths(Loc rectStart, Loc rectSize, Loc start, Loc[] ends, LocTest checkBlock, LocTest checkDiagBlock) { return FindMultiPaths(rectStart, rectSize, start, ends, checkBlock, checkDiagBlock, EvalPathAll, EvalFallbackAll); @@ -292,6 +314,16 @@ public static void FloodFill(Rect rect, LocTest checkBlock, LocTest checkDiagBlo } } + /// + /// Finds all tiles connected to a starting location that pass a specified check. + /// + /// Top-left corner of the search area. + /// Size of the search area. + /// Delegate to determine if a tile should be included in results. + /// Delegate to determine if a tile is blocked. + /// Delegate to determine if diagonal movement is blocked. + /// Starting location for the search. + /// List of connected tiles that pass the check. public static List FindConnectedTiles(Loc rectStart, Loc rectSize, LocTest checkOp, LocTest checkBlock, LocTest checkDiagBlock, Loc loc) { if (checkOp == null) @@ -311,6 +343,15 @@ void Action(Loc actLoc) return locList; } + /// + /// Performs an action on all tiles connected to a starting location. + /// + /// Top-left corner of the search area. + /// Size of the search area. + /// Action to perform on each connected tile. + /// Delegate to determine if a tile is blocked. + /// Delegate to determine if diagonal movement is blocked. + /// Starting location for the search. public static void AffectConnectedTiles(Loc rectStart, Loc rectSize, LocAction action, LocTest checkBlock, LocTest checkDiagBlock, Loc loc) { if (action == null) @@ -406,6 +447,13 @@ public static IEnumerable FindClosestConnectedTiles(Loc rectStart, Loc rect } } + /// + /// Finds all tiles within a rectangular area that pass a specified check. + /// + /// Top-left corner of the search area. + /// Size of the search area. + /// Delegate to determine if a tile should be included. + /// List of tiles that pass the check. public static List FindTilesInBox(Loc rectStart, Loc rectSize, LocTest checkOp) { if (checkOp == null) @@ -488,6 +536,13 @@ void Fill(Loc loc) return forkList.Count > 0; } + /// + /// Gets all unblocked directions from a point where adjacent directions are blocked. + /// + /// The location to check. + /// Delegate to determine if a tile is blocked. + /// Delegate to determine if diagonal movement is blocked. + /// List of directions representing forks in connectivity. public static List GetForkDirs(Loc point, LocTest checkBlock, LocTest checkDiagBlock) { List forks = new List(); @@ -510,11 +565,28 @@ public static List GetForkDirs(Loc point, LocTest checkBlock, LocTest chec return forks; } + /// + /// Determines if movement in a direction is blocked. + /// + /// The starting location. + /// The direction to check. + /// Delegate to determine if a tile is blocked. + /// Delegate to determine if diagonal movement is blocked. + /// true if blocked; otherwise false. public static bool IsDirBlocked(Loc loc, Dir8 dir, LocTest checkBlock, LocTest checkDiagBlock) { return IsDirBlocked(loc, dir, checkBlock, checkDiagBlock, 1); } + /// + /// Determines if movement in a direction is blocked over a specified distance. + /// + /// The starting location. + /// The direction to check. + /// Delegate to determine if a tile is blocked. + /// Delegate to determine if diagonal movement is blocked. + /// The distance to check. + /// true if blocked; otherwise false. public static bool IsDirBlocked(Loc loc, Dir8 dir, LocTest checkBlock, LocTest checkDiagBlock, int distance) { if (checkBlock == null) diff --git a/RogueElements/ITypeDict.cs b/RogueElements/ITypeDict.cs index 5fae1454..a890a080 100644 --- a/RogueElements/ITypeDict.cs +++ b/RogueElements/ITypeDict.cs @@ -9,38 +9,104 @@ namespace RogueElements { + /// + /// Represents a generic type-keyed dictionary interface. + /// + /// The base type of items. public interface ITypeDict : IEnumerable, ICollection { + /// + /// Determines whether an item of the specified type exists. + /// + /// The type to check. + /// true if an item of that type exists; otherwise false. bool Contains(Type type); + /// + /// Determines whether an item of the specified type exists. + /// + /// The type to check. + /// true if an item of that type exists; otherwise false. bool Contains() where TK : T; + /// + /// Gets the item of the specified type. + /// + /// The type of item to get. + /// The item of that type. TK Get() where TK : T; + /// + /// Removes the item of the specified type. + /// + /// The type to remove. + /// true if removed; otherwise false. bool Remove() where TK : T; + /// + /// Gets the item of the specified type. + /// + /// The type of item to get. + /// The item of that type. T Get(Type type); + /// + /// Sets an item in the dictionary, keyed by its runtime type. + /// + /// The item to set. void Set(T item); + /// + /// Removes the item of the specified type. + /// + /// The type to remove. + /// true if removed; otherwise false. bool Remove(Type type); } + /// + /// Represents a non-generic type-keyed dictionary interface. + /// public interface ITypeDict : IEnumerable { + /// + /// Gets the number of items in the dictionary. + /// int Count { get; } + /// + /// Removes all items from the dictionary. + /// void Clear(); + /// + /// Determines whether an item of the specified type exists. + /// + /// The type to check. + /// true if an item of that type exists; otherwise false. bool Contains(Type type); + /// + /// Gets the item of the specified type. + /// + /// The type of item to get. + /// The item of that type. object Get(Type type); + /// + /// Sets an item in the dictionary, keyed by its runtime type. + /// + /// The item to set. void Set(object item); + /// + /// Removes the item of the specified type. + /// + /// The type to remove. + /// true if removed; otherwise false. bool Remove(Type type); } } diff --git a/RogueElements/IntRange.cs b/RogueElements/IntRange.cs index 79f298c1..f7e136dc 100644 --- a/RogueElements/IntRange.cs +++ b/RogueElements/IntRange.cs @@ -10,37 +10,56 @@ namespace RogueElements { + /// + /// Represents a range of integers with inclusive minimum and exclusive maximum. + /// [Serializable] public struct IntRange : IEquatable { /// - /// Start of the range (inclusive) + /// Start of the range (inclusive). /// public int Min; /// - /// End of the range (exclusive) + /// End of the range (exclusive). /// public int Max; + /// + /// Initializes a new instance of the struct containing a single value. + /// + /// The single value in the range. public IntRange(int num) { this.Min = num; this.Max = num + 1; } + /// + /// Initializes a new instance of the struct with specified bounds. + /// + /// The inclusive minimum. + /// The exclusive maximum. public IntRange(int min, int max) { this.Min = min; this.Max = max; } + /// + /// Initializes a new instance of the struct by copying another range. + /// + /// The range to copy. public IntRange(IntRange other) { this.Min = other.Min; this.Max = other.Max; } + /// + /// Gets the length of the range (Max - Min). + /// public int Length => this.Max - this.Min; public static bool operator ==(IntRange lhs, IntRange rhs) => lhs.Equals(rhs); @@ -49,11 +68,23 @@ public IntRange(IntRange other) public static IntRange operator +(IntRange lhs, int rhs) => lhs.Add(rhs); + /// + /// Returns the intersection of two ranges. + /// + /// The first range. + /// The second range. + /// A range representing the intersection. public static IntRange Intersect(IntRange range1, IntRange range2) { return new IntRange(Math.Max(range1.Min, range2.Min), Math.Min(range1.Max, range2.Max)); } + /// + /// Returns a range that spans both input ranges. + /// + /// The first range. + /// The second range. + /// A range that includes both input ranges. public static IntRange IncludeRange(IntRange range1, IntRange range2) { int min = Math.Min(range1.Min, range2.Min); @@ -61,16 +92,27 @@ public static IntRange IncludeRange(IntRange range1, IntRange range2) return new IntRange(min, max); } + /// + /// Determines whether a value is within this range. + /// + /// The value to check. + /// true if the value is within the range; otherwise false. public bool Contains(int mid) { return this.Min <= mid && mid < this.Max; } + /// + /// Determines whether another range is entirely contained within this range. + /// + /// The range to check. + /// true if the range is entirely contained; otherwise false. public bool Contains(IntRange value) { return (this.Min <= value.Min) && (value.Max <= this.Max); } + /// public override string ToString() { if (this.Min + 1 == this.Max) @@ -79,21 +121,29 @@ public override string ToString() return $"[{this.Min}, {this.Max})"; } + /// public override bool Equals(object obj) { return (obj is IntRange) && this.Equals((IntRange)obj); } + /// public bool Equals(IntRange other) { return this.Min == other.Min && this.Max == other.Max; } + /// public override int GetHashCode() { return this.Min.GetHashCode() ^ this.Max.GetHashCode(); } + /// + /// Returns a new range offset by the specified value. + /// + /// The offset to apply to both Min and Max. + /// A new offset range. public IntRange Add(int value) { return new IntRange(this.Min + value, this.Max + value); diff --git a/RogueElements/Loc.cs b/RogueElements/Loc.cs index c3b273f1..21631768 100644 --- a/RogueElements/Loc.cs +++ b/RogueElements/Loc.cs @@ -7,29 +7,67 @@ namespace RogueElements { + /// + /// Represents a 2D integer coordinate or vector. + /// [Serializable] public struct Loc : IEquatable { + /// + /// A location with both X and Y set to 0. + /// public static readonly Loc Zero = new Loc(0, 0); + + /// + /// A location with both X and Y set to 1. + /// public static readonly Loc One = new Loc(1, 1); + + /// + /// A unit vector pointing in the positive X direction (1, 0). + /// public static readonly Loc UnitX = new Loc(1, 0); + + /// + /// A unit vector pointing in the positive Y direction (0, 1). + /// public static readonly Loc UnitY = new Loc(0, 1); + /// + /// The X component of this location. + /// public int X; + + /// + /// The Y component of this location. + /// public int Y; + /// + /// Initializes a new instance of the struct with both X and Y set to the same value. + /// + /// The value for both X and Y. public Loc(int n) { this.X = n; this.Y = n; } + /// + /// Initializes a new instance of the struct with specified X and Y values. + /// + /// The X component. + /// The Y component. public Loc(int x, int y) { this.X = x; this.Y = y; } + /// + /// Initializes a new instance of the struct by copying another location. + /// + /// The location to copy. public Loc(Loc loc) { this.X = loc.X; @@ -116,11 +154,23 @@ public Loc(Loc loc) return value1; } + /// + /// Computes the dot product of two locations. + /// + /// The first location. + /// The second location. + /// The dot product of the two locations. public static int Dot(Loc value1, Loc value2) { return (value1.X * value2.X) + (value1.Y * value2.Y); } + /// + /// Returns a location with the minimum X and Y components from two locations. + /// + /// The first location. + /// The second location. + /// A location with the component-wise minimum values. public static Loc Min(Loc value1, Loc value2) { return new Loc( @@ -128,6 +178,12 @@ public static Loc Min(Loc value1, Loc value2) value1.Y < value2.Y ? value1.Y : value2.Y); } + /// + /// Returns a location with the maximum X and Y components from two locations. + /// + /// The first location. + /// The second location. + /// A location with the component-wise maximum values. public static Loc Max(Loc value1, Loc value2) { return new Loc( @@ -135,6 +191,12 @@ public static Loc Max(Loc value1, Loc value2) value1.Y > value2.Y ? value1.Y : value2.Y); } + /// + /// Wraps a location within the specified size boundaries. + /// + /// The location to wrap. + /// The size boundaries for wrapping. + /// The wrapped location. public static Loc Wrap(Loc value, Loc size) { return ((value % size) + size) % size; @@ -185,42 +247,93 @@ public Loc Transpose() return new Loc(this.Y, this.X); } + /// + /// Returns a string representation of this location. + /// + /// A string in the format "X:{X} Y:{Y}". public override string ToString() { return $"X:{this.X} Y:{this.Y}"; } + /// public override bool Equals(object obj) { return (obj is Loc) && this.Equals((Loc)obj); } + /// public bool Equals(Loc other) { return this.X == other.X && this.Y == other.Y; } + /// public override int GetHashCode() { return this.X.GetHashCode() ^ this.Y.GetHashCode(); } + /// + /// Returns the negation of this location. + /// + /// A location with negated X and Y components. public Loc Negate() => -this; + /// + /// Adds another location to this one. + /// + /// The location to add. + /// The sum of the two locations. public Loc Add(Loc other) => this + other; + /// + /// Subtracts another location from this one. + /// + /// The location to subtract. + /// The difference of the two locations. public Loc Subtract(Loc other) => this - other; + /// + /// Multiplies this location by another location component-wise. + /// + /// The location to multiply by. + /// The component-wise product. public Loc Multiply(Loc other) => this * other; + /// + /// Multiplies this location by a scalar. + /// + /// The scale factor. + /// The scaled location. public Loc Multiply(int scaleFactor) => this * scaleFactor; + /// + /// Divides this location by another location component-wise. + /// + /// The location to divide by. + /// The component-wise quotient. public Loc Divide(Loc other) => this / other; + /// + /// Divides this location by a scalar. + /// + /// The scale factor. + /// The scaled location. public Loc Divide(int scaleFactor) => this / scaleFactor; + /// + /// Computes the component-wise modulus with another location. + /// + /// The location to mod by. + /// The component-wise modulus. public Loc Mod(Loc other) => this % other; + /// + /// Computes the modulus of both components with a scalar. + /// + /// The scale factor. + /// The modulus result. public Loc Mod(int scaleFactor) => this % scaleFactor; } } diff --git a/RogueElements/LocRay.cs b/RogueElements/LocRay.cs index 2b568e25..56dbafc3 100644 --- a/RogueElements/LocRay.cs +++ b/RogueElements/LocRay.cs @@ -9,6 +9,9 @@ namespace RogueElements { + /// + /// Represents a ray originating from a location in one of eight directions. + /// [SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1649:FileHeaderFileNameDocumentationMustMatchTypeName", @@ -18,27 +21,53 @@ namespace RogueElements [Serializable] public struct LocRay8 : IEquatable { + /// + /// The origin location of the ray. + /// public Loc Loc; + + /// + /// The direction of the ray. + /// public Dir8 Dir; + /// + /// Initializes a new instance of the struct with a location and no direction. + /// + /// The origin location. public LocRay8(Loc loc) { this.Loc = loc; this.Dir = Dir8.None; } + /// + /// Initializes a new instance of the struct with a direction and origin at zero. + /// + /// The direction of the ray. public LocRay8(Dir8 dir) { this.Loc = Loc.Zero; this.Dir = dir; } + /// + /// Initializes a new instance of the struct with specified location and direction. + /// + /// The origin location. + /// The direction of the ray. public LocRay8(Loc loc, Dir8 dir) { this.Loc = loc; this.Dir = dir; } + /// + /// Initializes a new instance of the struct with specified coordinates and direction. + /// + /// The X coordinate of the origin. + /// The Y coordinate of the origin. + /// The direction of the ray. public LocRay8(int x, int y, Dir8 dir) { this.Loc = new Loc(x, y); @@ -49,53 +78,95 @@ public LocRay8(int x, int y, Dir8 dir) public static bool operator !=(LocRay8 lhs, LocRay8 rhs) => !lhs.Equals(rhs); + /// + /// Computes the location after traversing a specified distance along the ray. + /// + /// The distance to traverse. + /// The resulting location. public Loc Traverse(int dist) { return this.Loc + (this.Dir.GetLoc() * dist); } + /// public bool Equals(LocRay8 other) => this.Loc == other.Loc && this.Dir == other.Dir; + /// public override bool Equals(object obj) => (obj is LocRay8 ray) && this.Equals(ray); + /// public override int GetHashCode() => unchecked(971 + (this.Loc.GetHashCode() * 619) ^ (this.Dir.GetHashCode() * 491)); + /// public override string ToString() { return string.Format("{0}, {1}", this.Loc, this.Dir); } } + /// + /// Represents a ray originating from a location in one of four cardinal directions. + /// [Serializable] public struct LocRay4 : IEquatable { + /// + /// The origin location of the ray. + /// public Loc Loc; + + /// + /// The direction of the ray. + /// public Dir4 Dir; + /// + /// Initializes a new instance of the struct by copying another. + /// + /// The ray to copy. public LocRay4(LocRay4 locRay4) { this.Loc = locRay4.Loc; this.Dir = locRay4.Dir; } + /// + /// Initializes a new instance of the struct with a location and no direction. + /// + /// The origin location. public LocRay4(Loc loc) { this.Loc = loc; this.Dir = Dir4.None; } + /// + /// Initializes a new instance of the struct with a direction and origin at zero. + /// + /// The direction of the ray. public LocRay4(Dir4 dir) { this.Loc = Loc.Zero; this.Dir = dir; } + /// + /// Initializes a new instance of the struct with specified location and direction. + /// + /// The origin location. + /// The direction of the ray. public LocRay4(Loc loc, Dir4 dir) { this.Loc = loc; this.Dir = dir; } + /// + /// Initializes a new instance of the struct with specified coordinates and direction. + /// + /// The X coordinate of the origin. + /// The Y coordinate of the origin. + /// The direction of the ray. public LocRay4(int x, int y, Dir4 dir) { this.Loc = new Loc(x, y); @@ -106,17 +177,26 @@ public LocRay4(int x, int y, Dir4 dir) public static bool operator !=(LocRay4 lhs, LocRay4 rhs) => !lhs.Equals(rhs); + /// + /// Computes the location after traversing a specified distance along the ray. + /// + /// The distance to traverse. + /// The resulting location. public Loc Traverse(int dist) { return this.Loc + (this.Dir.GetLoc() * dist); } + /// public bool Equals(LocRay4 other) => this.Loc == other.Loc && this.Dir == other.Dir; + /// public override bool Equals(object obj) => (obj is LocRay4) && this.Equals((LocRay4)obj); + /// public override int GetHashCode() => unchecked(571 + (this.Loc.GetHashCode() * 293) ^ (this.Dir.GetHashCode() * 827)); + /// public override string ToString() { return string.Format("{0}, {1}", this.Loc, this.Dir); diff --git a/RogueElements/MapGen/FloorPlan/AddConnectedRoomsRandStep.cs b/RogueElements/MapGen/FloorPlan/AddConnectedRoomsRandStep.cs index 0618a229..8a811ca1 100644 --- a/RogueElements/MapGen/FloorPlan/AddConnectedRoomsRandStep.cs +++ b/RogueElements/MapGen/FloorPlan/AddConnectedRoomsRandStep.cs @@ -10,25 +10,49 @@ namespace RogueElements { /// - /// Takes the current floor plan and adds new rooms that are connected to existing rooms. - /// Each addition attempt has it choose randomly from existing rooms to extend from. - /// It will try a finite number of times before it gives up. + /// Adds new rooms connected to existing rooms using random sampling with limited retries. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step extends the floor layout by adding rooms adjacent to existing rooms or halls. + /// Unlike , this version randomly samples expansion + /// points with a limited number of retries, providing better performance at the cost of + /// potentially missing valid placements. + /// + /// + /// Rooms can optionally be connected via an intermediate hallway, controlled by . + /// + /// + /// [Serializable] public class AddConnectedRoomsRandStep : AddConnectedRoomsBaseStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public AddConnectedRoomsRandStep() : base() { } + /// + /// Initializes a new instance of the class with specified room and hall generators. + /// + /// The picker for room generators. + /// The picker for hall generators. public AddConnectedRoomsRandStep(IRandPicker> genericRooms, IRandPicker> genericHalls) : base(genericRooms, genericHalls) { } + /// + /// Chooses a room expansion by randomly sampling possible placements. + /// + /// The random number generator. + /// The floor plan to expand. + /// The chosen expansion details, or null if no valid expansion is found within retry limit. public override FloorPathBranch.ListPathBranchExpansion? ChooseRoomExpansion(IRandom rand, FloorPlan floorPlan) { // TODO: don't go through all rooms, just pick randomly diff --git a/RogueElements/MapGen/FloorPlan/AddConnectedRoomsStep.cs b/RogueElements/MapGen/FloorPlan/AddConnectedRoomsStep.cs index a95e4ca2..6d91c26d 100644 --- a/RogueElements/MapGen/FloorPlan/AddConnectedRoomsStep.cs +++ b/RogueElements/MapGen/FloorPlan/AddConnectedRoomsStep.cs @@ -10,24 +10,48 @@ namespace RogueElements { /// - /// Takes the current floor plan and adds new rooms that are connected to existing rooms. - /// It gathers all possible places to add a room, and picks one. + /// Adds new rooms connected to existing rooms by exhaustively evaluating all possible placements. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step extends the floor layout by adding rooms adjacent to existing rooms or halls. + /// Unlike , this version evaluates all possible + /// expansion points before selecting one, guaranteeing placement if any valid location exists. + /// + /// + /// Rooms can optionally be connected via an intermediate hallway, controlled by . + /// + /// + /// [Serializable] public class AddConnectedRoomsStep : AddConnectedRoomsBaseStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public AddConnectedRoomsStep() : base() { } + /// + /// Initializes a new instance of the class with specified room and hall generators. + /// + /// The picker for room generators. + /// The picker for hall generators. public AddConnectedRoomsStep(IRandPicker> genericRooms, IRandPicker> genericHalls) : base(genericRooms, genericHalls) { } + /// + /// Chooses a room expansion by evaluating all possible placements. + /// + /// The random number generator. + /// The floor plan to expand. + /// The chosen expansion details, or null if no valid expansion exists. public override FloorPathBranch.ListPathBranchExpansion? ChooseRoomExpansion(IRandom rand, FloorPlan floorPlan) { List availableExpansions = new List(); diff --git a/RogueElements/MapGen/FloorPlan/AddDisconnectedRoomsRandStep.cs b/RogueElements/MapGen/FloorPlan/AddDisconnectedRoomsRandStep.cs index 737b5b78..d6b04b0a 100644 --- a/RogueElements/MapGen/FloorPlan/AddDisconnectedRoomsRandStep.cs +++ b/RogueElements/MapGen/FloorPlan/AddDisconnectedRoomsRandStep.cs @@ -10,24 +10,49 @@ namespace RogueElements { /// - /// Takes the current floor plan and adds new rooms that are disconnected from existing rooms. - /// Randomly picks a location to spawn a new room in a finite number of times before giving up. + /// Adds new rooms to the floor plan without connecting them, using random sampling with limited retries. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step places rooms that are isolated from the existing layout, useful for creating + /// secret rooms, bonus areas, or areas that will be connected later by other steps. + /// + /// + /// Unlike , this version randomly samples positions + /// with a limited number of retries (30 attempts), providing better performance but potentially + /// missing valid placements in crowded floor plans. + /// + /// + /// [Serializable] public class AddDisconnectedRoomsRandStep : AddDisconnectedRoomsBaseStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public AddDisconnectedRoomsRandStep() : base() { } + /// + /// Initializes a new instance of the class with specified room generators. + /// + /// The picker for room generators. public AddDisconnectedRoomsRandStep(IRandPicker> genericRooms) : base(genericRooms) { } + /// + /// Finds a viable location by randomly sampling positions up to 30 times. + /// + /// The random number generator. + /// The floor plan to search within. + /// The size of the room to place. + /// A valid location if found within the retry limit; otherwise, null. protected override Loc? ChooseViableLoc(IRandom rand, FloorPlan floorPlan, Loc roomSize) { Rect allowedRange = Rect.FromPoints(floorPlan.DrawRect.Start, floorPlan.DrawRect.End - roomSize + new Loc(1)); diff --git a/RogueElements/MapGen/FloorPlan/AddDisconnectedRoomsStep.cs b/RogueElements/MapGen/FloorPlan/AddDisconnectedRoomsStep.cs index 1090b827..6d64a3a3 100644 --- a/RogueElements/MapGen/FloorPlan/AddDisconnectedRoomsStep.cs +++ b/RogueElements/MapGen/FloorPlan/AddDisconnectedRoomsStep.cs @@ -10,25 +10,49 @@ namespace RogueElements { /// - /// Takes the current floor plan and adds new rooms that are disconnected from existing rooms. - /// Sweeps through the entire floor to fit in the new rooms. - /// Guaranteed to spawn the room, but can cause performance problems for larger floors. + /// Adds new rooms to the floor plan without connecting them to existing rooms. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step places rooms that are isolated from the existing layout, useful for creating + /// secret rooms, bonus areas, or areas that will be connected later by other steps. + /// + /// + /// Unlike , this version exhaustively searches + /// all possible positions, guaranteeing placement if any valid location exists but potentially + /// causing performance issues on larger floors. + /// + /// + /// [Serializable] public class AddDisconnectedRoomsStep : AddDisconnectedRoomsBaseStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public AddDisconnectedRoomsStep() : base() { } + /// + /// Initializes a new instance of the class with specified room generators. + /// + /// The picker for room generators. public AddDisconnectedRoomsStep(IRandPicker> genericRooms) : base(genericRooms) { } + /// + /// Finds a viable location by exhaustively searching all possible positions. + /// + /// The random number generator. + /// The floor plan to search within. + /// The size of the room to place. + /// A valid location if found; otherwise, null. protected override Loc? ChooseViableLoc(IRandom rand, FloorPlan floorPlan, Loc roomSize) { Rect allowedRange = Rect.FromPoints(floorPlan.DrawRect.Start, floorPlan.DrawRect.End - roomSize + new Loc(1)); diff --git a/RogueElements/MapGen/FloorPlan/BaseFloorStairsStep.cs b/RogueElements/MapGen/FloorPlan/BaseFloorStairsStep.cs index c9dd6b7a..1e508390 100644 --- a/RogueElements/MapGen/FloorPlan/BaseFloorStairsStep.cs +++ b/RogueElements/MapGen/FloorPlan/BaseFloorStairsStep.cs @@ -9,17 +9,26 @@ namespace RogueElements { /// - /// Adds the entrance and exit to the floor. Is room-conscious. + /// Base class for placing entrances and exits in a floor plan-aware manner. /// - /// - /// - /// + /// The generation context type. + /// The entrance type implementing . + /// The exit type implementing . + /// + /// This abstract class provides common functionality for placing stairs (or other entrance/exit + /// objects) while respecting the floor plan's room structure. It ensures entrances and exits + /// are placed in appropriate rooms and can filter out unsuitable rooms like boss rooms. + /// + /// [Serializable] public abstract class BaseFloorStairsStep : GenStep where TGenContext : class, IFloorPlanGenContext, IPlaceableGenContext, IPlaceableGenContext where TEntrance : IEntrance where TExit : IExit { + /// + /// Initializes a new instance of the class. + /// protected BaseFloorStairsStep() { this.Entrances = new List(); @@ -27,6 +36,11 @@ protected BaseFloorStairsStep() this.Filters = new List(); } + /// + /// Initializes a new instance of the class with a single entrance and exit. + /// + /// The entrance object to place. + /// The exit object to place. protected BaseFloorStairsStep(TEntrance entrance, TExit exit) { this.Entrances = new List { entrance }; @@ -34,6 +48,11 @@ protected BaseFloorStairsStep(TEntrance entrance, TExit exit) this.Filters = new List(); } + /// + /// Initializes a new instance of the class with multiple entrances and exits. + /// + /// The list of entrance objects to place. + /// The list of exit objects to place. protected BaseFloorStairsStep(List entrances, List exits) { this.Entrances = entrances; @@ -42,20 +61,24 @@ protected BaseFloorStairsStep(List entrances, List exits) } /// - /// List of entrance objects to spawn. + /// Gets the list of entrance objects to spawn. /// public List Entrances { get; } /// - /// List of exit objects to spawn. + /// Gets the list of exit objects to spawn. /// public List Exits { get; } /// - /// Used to filter out rooms that do not make suitable entrances/exits, such as boss rooms. + /// Gets or sets filters to exclude unsuitable rooms from entrance/exit placement. /// public List Filters { get; set; } + /// + /// Applies this step to place entrances and exits on the map. + /// + /// The generation context. public override void Apply(TGenContext map) { List free_indices = new List(); @@ -97,19 +120,23 @@ public override void Apply(TGenContext map) } } + /// + /// Returns a string representation of this step. + /// + /// A string describing this step's configuration. public override string ToString() { return string.Format("{0}: Start: {1} End: {2}", this.GetType().GetFormattedTypeName(), this.Entrances.Count, this.Exits.Count); } /// - /// Attempt to choose an outlet in a room with no entrance/exit, and updates their availability. If none exists, default to null. + /// Attempts to choose a location for an entrance or exit, preferring unused rooms. /// - /// - /// - /// - /// - /// + /// The spawnable type being placed. + /// The generation context. + /// List of room indices not yet used for any entrance or exit. + /// List of room indices already used. Can be null if not tracking usage. + /// A valid location if found; otherwise, null. protected abstract Loc? GetOutlet(TGenContext map, List free_indices, List used_indices) where T : ISpawnable; } diff --git a/RogueElements/MapGen/FloorPlan/ClampFloorStep.cs b/RogueElements/MapGen/FloorPlan/ClampFloorStep.cs index d2023792..b485c3f7 100644 --- a/RogueElements/MapGen/FloorPlan/ClampFloorStep.cs +++ b/RogueElements/MapGen/FloorPlan/ClampFloorStep.cs @@ -8,29 +8,56 @@ namespace RogueElements { /// - /// Clamps the floor plan to at least a minimum size, at most a maximum size. - /// If the bounds of the current roomplan maximum, the size will increase to include them. - /// Always shrinks in the BottomRight direction, which results in the TopLeft corner remaining constant. + /// Constrains the floor plan dimensions to specified minimum and maximum bounds. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step adjusts the floor plan size to fit within the specified bounds while ensuring + /// all existing rooms remain contained. If rooms extend beyond the maximum size, the floor + /// plan expands to include them. + /// + /// + /// The clamping operation anchors at the top-left corner, meaning shrinkage occurs from + /// the bottom-right direction. This step has no effect on wrapped floor plans. + /// + /// [Serializable] public class ClampFloorStep : GenStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class with default bounds. + /// public ClampFloorStep() { } + /// + /// Initializes a new instance of the class with specified bounds. + /// + /// The minimum allowed size for the floor plan. + /// The maximum allowed size for the floor plan. public ClampFloorStep(Loc minSize, Loc maxSize) { this.MinSize = minSize; this.MaxSize = maxSize; } + /// + /// Gets or sets the minimum allowed size for the floor plan. + /// public Loc MinSize { get; set; } + /// + /// Gets or sets the maximum allowed size for the floor plan. + /// public Loc MaxSize { get; set; } + /// + /// Applies this step by clamping the floor plan to the specified bounds. + /// + /// The generation context containing the floor plan. public override void Apply(T map) { if (map.RoomPlan.Wrap) diff --git a/RogueElements/MapGen/FloorPlan/ConnectBranchStep.cs b/RogueElements/MapGen/FloorPlan/ConnectBranchStep.cs index 4273325e..8f23d8d5 100644 --- a/RogueElements/MapGen/FloorPlan/ConnectBranchStep.cs +++ b/RogueElements/MapGen/FloorPlan/ConnectBranchStep.cs @@ -9,30 +9,52 @@ namespace RogueElements { /// - /// Takes the current floor plan and connects the ends of its branches to other rooms. - /// A room is considered the end of a branch when it is connected to only one other room. - /// ie, a dead end. + /// Connects dead-end rooms (rooms with only one connection) to other rooms. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step identifies rooms that are at the end of branches (dead ends) and attempts + /// to connect them to other rooms, creating shortcuts and loops in the layout. + /// + /// + /// A room is considered a branch end when it has exactly one adjacent room or hall. + /// The property controls what fraction of eligible branches + /// are connected. + /// + /// + /// [Serializable] public class ConnectBranchStep : ConnectStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public ConnectBranchStep() : base() { } + /// + /// Initializes a new instance of the class with specified hall generators. + /// + /// The picker for hall generators. public ConnectBranchStep(IRandPicker> genericHalls) : base(genericHalls) { } /// - /// The percentage of eligible branches to connect. + /// Gets or sets the percentage of eligible branches to connect (0-100). /// public int ConnectPercent { get; set; } + /// + /// Applies this step to connect branch ends in the floor plan. + /// + /// The random number generator. + /// The floor plan to modify. public override void ApplyToPath(IRandom rand, FloorPlan floorPlan) { List> candBranchPoints = GetBranchArms(floorPlan); @@ -93,11 +115,20 @@ public override void ApplyToPath(IRandom rand, FloorPlan floorPlan) } } + /// + /// Returns a string representation of this step. + /// + /// A string describing this step's configuration. public override string ToString() { return string.Format("{0}: {1}%", this.GetType().GetFormattedTypeName(), this.ConnectPercent); } + /// + /// Gets all branch arms in the floor plan, where each arm is a sequence of rooms forming a branch. + /// + /// The floor plan to analyze. + /// A list of branch arms, where each arm is a list of connected room indices. private protected static List> GetBranchArms(FloorPlan floorPlan) { List endBranches = new List(); diff --git a/RogueElements/MapGen/FloorPlan/ConnectStep.cs b/RogueElements/MapGen/FloorPlan/ConnectStep.cs index 3f548300..760b2693 100644 --- a/RogueElements/MapGen/FloorPlan/ConnectStep.cs +++ b/RogueElements/MapGen/FloorPlan/ConnectStep.cs @@ -9,19 +9,39 @@ namespace RogueElements { /// - /// Finds rooms in the floor plan that can be connected and connects them. + /// Base class for steps that connect rooms in a floor plan with hallways. /// - /// + /// The generation context type, which must implement . + /// + /// + /// Connect steps find pairs of rooms that can be connected by straight hallways and create + /// those connections. This is useful for creating more interconnected floor layouts after + /// the initial path generation. + /// + /// + /// The connection algorithm extends a rectangle from each eligible room toward other rooms, + /// finding the closest room that can be reached in each direction. + /// + /// + /// + /// [Serializable] public abstract class ConnectStep : FloorPlanStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// protected ConnectStep() { this.Components = new ComponentCollection(); this.Filters = new List(); } + /// + /// Initializes a new instance of the class with specified hall generators. + /// + /// The picker for hall generators. protected ConnectStep(IRandPicker> genericHalls) { this.GenericHalls = genericHalls; @@ -44,6 +64,13 @@ protected ConnectStep(IRandPicker> genericHalls) /// public ComponentCollection Components { get; set; } + /// + /// Checks if a room has any border opening toward another rectangle. + /// + /// The room to check for openings. + /// The target rectangle. + /// The direction to check. + /// True if any border tile can create an opening in the specified direction. protected static bool HasBorderOpening(IRoomGen roomFrom, Rect rectTo, Dir4 expandTo) { Loc diff = roomFrom.Draw.Start - rectTo.Start; // how far ahead the start of source is to dest @@ -62,6 +89,13 @@ protected static bool HasBorderOpening(IRoomGen roomFrom, Rect rectTo, Dir4 expa return false; } + /// + /// Finds a room that can be connected from the specified room in the given direction. + /// + /// The floor plan to search. + /// The room to connect from. + /// The direction to search for connections. + /// The connection details if a valid target is found; otherwise, null. protected static ListPathTraversalNode? GetRoomToConnect(FloorPlan floorPlan, RoomHallIndex chosenFrom, Dir4 dir) { // extend a rectangle to the border of the floor in the chosen direction @@ -227,6 +261,13 @@ protected static SpawnList GetPossibleExpansions(FloorPla return expansions; } + /// + /// Chooses a connection from a list of candidate rooms. + /// + /// The random number generator. + /// The floor plan to search. + /// The list of candidate rooms to connect from. + /// The chosen connection details if one is found; otherwise, null. protected static ListPathTraversalNode? ChooseConnection(IRandom rand, FloorPlan floorPlan, List candList) { SpawnList expansions = GetPossibleExpansions(floorPlan, candList); @@ -237,12 +278,31 @@ protected static SpawnList GetPossibleExpansions(FloorPla return null; } + /// + /// Represents the details of a potential connection between two rooms. + /// public struct ListPathTraversalNode { + /// + /// The room or hall to connect from. + /// public RoomHallIndex From; + + /// + /// The room or hall to connect to. + /// public RoomHallIndex To; + + /// + /// The bounding rectangle for the connecting hall. + /// public Rect Connector; + /// + /// Initializes a new instance of the struct without a connector. + /// + /// The room to connect from. + /// The room to connect to. public ListPathTraversalNode(RoomHallIndex from, RoomHallIndex to) { this.From = from; @@ -250,6 +310,12 @@ public ListPathTraversalNode(RoomHallIndex from, RoomHallIndex to) this.Connector = Rect.Empty; } + /// + /// Initializes a new instance of the struct with a connector. + /// + /// The room to connect from. + /// The room to connect to. + /// The bounding rectangle for the hall. public ListPathTraversalNode(RoomHallIndex from, RoomHallIndex to, Rect connector) { this.From = from; diff --git a/RogueElements/MapGen/FloorPlan/DrawFloorToTileStep.cs b/RogueElements/MapGen/FloorPlan/DrawFloorToTileStep.cs index a056c6fa..0e917eeb 100644 --- a/RogueElements/MapGen/FloorPlan/DrawFloorToTileStep.cs +++ b/RogueElements/MapGen/FloorPlan/DrawFloorToTileStep.cs @@ -9,28 +9,50 @@ namespace RogueElements { /// - /// Takes the floor plan of the map and draws all tiles in all rooms to the actual map. - /// This is typically done once per floor generation. It must only be done after the floor plan itself is complete. + /// Renders the floor plan to the tile map, converting abstract room layouts into actual tiles. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step is the final phase of floor plan-based generation. It creates the tile map, + /// fills it with wall terrain, then draws each room and hall according to their generators. + /// + /// + /// This step should only be executed after the floor plan layout is complete - after all rooms + /// have been added and connected. Subsequent steps can then operate on the tile-based map. + /// + /// + /// + /// [Serializable] public class DrawFloorToTileStep : GenStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class with no padding. + /// public DrawFloorToTileStep() { } + /// + /// Initializes a new instance of the class with specified padding. + /// + /// The number of wall tiles to add around the map border. public DrawFloorToTileStep(int padding = 0) { this.Padding = padding; } /// - /// Adds the specified number of tiles to the border of the map as wall terrain. + /// Gets or sets the number of tiles to add as a wall border around the map. /// public int Padding { get; set; } + /// + /// Applies this step by rendering the floor plan to tiles. + /// + /// The generation context containing the floor plan. public override void Apply(T map) { // draw on map @@ -53,6 +75,10 @@ public override void Apply(T map) map.RoomPlan.DrawOnMap(map); } + /// + /// Returns a string representation of this step. + /// + /// A string describing this step's configuration. public override string ToString() { return string.Format("{0}: Padding:{1}", this.GetType().GetFormattedTypeName(), this.Padding); diff --git a/RogueElements/MapGen/FloorPlan/FloorHallPlan.cs b/RogueElements/MapGen/FloorPlan/FloorHallPlan.cs index b0cd347a..61710190 100644 --- a/RogueElements/MapGen/FloorPlan/FloorHallPlan.cs +++ b/RogueElements/MapGen/FloorPlan/FloorHallPlan.cs @@ -8,8 +8,22 @@ namespace RogueElements { + /// + /// Represents a hall in a , storing its generator and connectivity information. + /// + /// + /// Halls are similar to rooms but use which allows more flexible + /// placement and connection. Halls typically serve as connectors between rooms. + /// + /// + /// public class FloorHallPlan : IFloorRoomPlan { + /// + /// Initializes a new instance of the class. + /// + /// The permissive room generator for this hall. + /// The component collection for filtering and identification. public FloorHallPlan(IPermissiveRoomGen roomGen, ComponentCollection components) { this.RoomGen = roomGen; @@ -17,12 +31,24 @@ public FloorHallPlan(IPermissiveRoomGen roomGen, ComponentCollection components) this.Adjacents = new List(); } + /// + /// Gets or sets the permissive room generator that defines this hall's shape and rendering. + /// public IPermissiveRoomGen RoomGen { get; set; } + /// + /// Gets the room generator as the base interface. + /// IRoomGen IRoomPlan.RoomGen => this.RoomGen; + /// + /// Gets the component collection for this hall, used for filtering and metadata. + /// public ComponentCollection Components { get; } + /// + /// Gets the list of adjacent rooms and halls connected to this hall. + /// public List Adjacents { get; } } } diff --git a/RogueElements/MapGen/FloorPlan/FloorPlan.cs b/RogueElements/MapGen/FloorPlan/FloorPlan.cs index 8cb4c5a6..29650fed 100644 --- a/RogueElements/MapGen/FloorPlan/FloorPlan.cs +++ b/RogueElements/MapGen/FloorPlan/FloorPlan.cs @@ -8,39 +8,103 @@ namespace RogueElements { + /// + /// Represents the high-level layout structure of a dungeon floor, managing rooms and halls + /// with their spatial relationships and connectivity. + /// + /// + /// + /// FloorPlan is the core data structure for freeform room-based map generation. Unlike grid-based + /// layouts (), FloorPlan allows rooms to be placed at arbitrary positions + /// and sizes, with halls connecting them in a graph structure. + /// + /// + /// The typical workflow is: + /// + /// Initialize with or + /// Add rooms using and halls using + /// Connect rooms by specifying adjacency relationships + /// Draw the final tiles using + /// + /// + /// + /// The floor plan supports optional wrapping for toroidal maps where edges connect to opposite sides. + /// + /// + /// + /// + /// public class FloorPlan { + /// + /// Initializes a new instance of the class. + /// public FloorPlan() { } + /// + /// Gets the total size of the floor plan in tiles. + /// public Loc Size { get; private set; } /// - /// Start loc of floor space. Room coordinates are currently NOT relative to this value and their draw locs are universal. + /// Gets the starting location of the floor space. /// + /// + /// Room coordinates are currently NOT relative to this value and their draw locs are universal. + /// This value is used for computing the drawable area and for padding when rendering to tiles. + /// public Loc Start { get; private set; } + /// + /// Gets a value indicating whether the floor plan wraps around its edges (toroidal topology). + /// + /// + /// When true, rooms near edges can connect to rooms on the opposite side, and collision + /// detection accounts for wraparound. This enables creation of maps where walking off + /// one edge brings the player to the opposite side. + /// public bool Wrap { get; private set; } + /// + /// Gets the bounding rectangle of the floor plan, combining and . + /// public Rect DrawRect => new Rect(this.Start, this.Size); + /// + /// Gets the total number of rooms in the floor plan. + /// public virtual int RoomCount => this.Rooms.Count; + /// + /// Gets the total number of halls in the floor plan. + /// public virtual int HallCount => this.Halls.Count; + /// + /// Gets the internal list of room plans. + /// protected List Rooms { get; private set; } + /// + /// Gets the internal list of hall plans. + /// protected List Halls { get; private set; } /// - /// Gets the amount of tiles that overlap when adding a new room adjacent to an existing room. + /// Calculates the number of border tiles that can connect between two adjacent rooms. /// - /// The room to have the new room added to. + /// The existing room to expand from. /// The new room to add. Its current position is not final. - /// The proposed location of the new room. Assumes this loc is indeed adjacent to the roomFrom, even in wrapped scenarios. - /// The direction to expand from the old room to new room. - /// + /// The proposed location of the new room. Assumes this location is adjacent to roomFrom. + /// The direction from the existing room toward the new room. + /// The count of border tiles where both rooms can create openings (fulfillable borders). + /// + /// This method is used to determine how well two rooms can connect at a given placement. + /// A higher return value indicates more potential connection points, which is used + /// for weighted random selection when placing rooms. + /// public static int GetBorderMatch(IRoomGen roomFrom, IRoomGen room, Loc candLoc, Dir4 expandTo) { Loc diff = roomFrom.Draw.Start - candLoc; // how far ahead the start of source is to dest @@ -63,13 +127,21 @@ public static int GetBorderMatch(IRoomGen roomFrom, IRoomGen room, Loc candLoc, } /// - /// Given two rectangles that are meant to be adjacent to each other, with a valid direction of adjacency, - /// Gets the unwrapped version of the second rectangle that is adjacent to the first. + /// Gets the unwrapped version of a rectangle that is adjacent to another in the specified direction. /// - /// - /// - /// - /// + /// The reference rectangle. + /// The rectangle to check for adjacency and potentially unwrap. + /// The direction of expected adjacency from rectFrom to rectTo. + /// + /// The unwrapped rectangle if the two rectangles are adjacent in the specified direction; + /// otherwise, null. + /// + /// + /// In wrapped floor plans, a rectangle near one edge may be adjacent to a rectangle on the + /// opposite edge. This method handles the coordinate transformation needed to compute + /// the effective position for border calculations. + /// + /// Thrown when is . public Rect? GetAdjacentRect(Rect rectFrom, Rect rectTo, Dir4 dir) { if (dir == Dir4.None) @@ -117,11 +189,14 @@ public static int GetBorderMatch(IRoomGen roomFrom, IRoomGen room, Loc candLoc, } /// - /// Gets the direction of adjacency. + /// Determines the direction in which one room is adjacent to another. /// - /// - /// - /// + /// The reference room generator. + /// The room generator to find the direction to. + /// + /// The from to + /// if they are adjacent; otherwise, . + /// public Dir4 GetDirAdjacent(IRoomGen roomGenFrom, IRoomGen roomGenTo) { foreach (Dir4 dir in DirExt.VALID_DIR4) @@ -133,11 +208,21 @@ public Dir4 GetDirAdjacent(IRoomGen roomGenFrom, IRoomGen roomGenTo) return Dir4.None; } + /// + /// Initializes the floor plan with a specified size starting at the origin. + /// + /// The size of the floor plan in tiles. + /// Whether the floor plan should wrap around edges. public void InitSize(Loc size, bool wrap = false) { this.InitRect(new Rect(Loc.Zero, size), wrap); } + /// + /// Initializes the floor plan with a specified bounding rectangle. + /// + /// The bounding rectangle defining the floor area. + /// Whether the floor plan should wrap around edges. public void InitRect(Rect rect, bool wrap) { this.Start = rect.Start; @@ -147,32 +232,60 @@ public void InitRect(Rect rect, bool wrap) this.Halls = new List(); } + /// + /// Removes all rooms and halls from the floor plan while preserving its dimensions. + /// public void Clear() { this.Rooms.Clear(); this.Halls.Clear(); } + /// + /// Gets the room plan at the specified index. + /// + /// The zero-based index of the room. + /// The at the specified index. public virtual FloorRoomPlan GetRoomPlan(int index) { return this.Rooms[index]; } + /// + /// Gets the room generator at the specified index. + /// + /// The zero-based index of the room. + /// The for the room at the specified index. public virtual IRoomGen GetRoom(int index) { return this.Rooms[index].RoomGen; } + /// + /// Gets the hall plan at the specified index. + /// + /// The zero-based index of the hall. + /// The at the specified index. public virtual FloorHallPlan GetHallPlan(int index) { return this.Halls[index]; } + /// + /// Gets the hall generator at the specified index. + /// + /// The zero-based index of the hall. + /// The for the hall at the specified index. public virtual IPermissiveRoomGen GetHall(int index) { return this.Halls[index].RoomGen; } + /// + /// Gets a room or hall plan by its combined index. + /// + /// The index identifying either a room or hall. + /// The for the specified room or hall. public virtual IFloorRoomPlan GetRoomHall(RoomHallIndex room) { if (!room.IsHall) @@ -181,6 +294,16 @@ public virtual IFloorRoomPlan GetRoomHall(RoomHallIndex room) return this.Halls[room.Index]; } + /// + /// Adds a new room to the floor plan with the specified adjacencies. + /// + /// The room generator defining the room's shape and size. + /// The component collection to attach to the room for filtering and identification. + /// The rooms and halls that this new room is adjacent to. + /// + /// Thrown when the room would overlap an existing room or hall, or when it falls outside + /// the floor plan bounds (in non-wrapped mode). + /// public void AddRoom(IRoomGen gen, ComponentCollection components, params RoomHallIndex[] attached) { // check against colliding on other rooms (and not halls) @@ -215,6 +338,16 @@ public void AddRoom(IRoomGen gen, ComponentCollection components, params RoomHal this.Rooms.Add(plan); } + /// + /// Adds a new hall to the floor plan with the specified adjacencies. + /// + /// The permissive room generator defining the hall's shape and size. + /// The component collection to attach to the hall for filtering and identification. + /// The rooms and halls that this new hall is adjacent to. + /// + /// Thrown when the hall would overlap an existing room, or when it falls outside + /// the floor plan bounds (in non-wrapped mode). + /// public void AddHall(IPermissiveRoomGen gen, ComponentCollection components, params RoomHallIndex[] attached) { // we expect that the hall has already been given a size... @@ -241,6 +374,15 @@ public void AddHall(IPermissiveRoomGen gen, ComponentCollection components, para this.Halls.Add(plan); } + /// + /// Removes a room or hall from the floor plan and updates all adjacency references. + /// + /// The index of the room or hall to remove. + /// + /// After removal, all indices greater than the removed index are decremented to maintain + /// a contiguous index space. Adjacency lists of remaining rooms and halls are updated + /// to reflect the removal and index changes. + /// public void EraseRoomHall(RoomHallIndex roomHall) { if (!roomHall.IsHall) @@ -281,6 +423,16 @@ public void EraseRoomHall(RoomHallIndex roomHall) } } + /// + /// Gets all rooms that are reachable from the specified room, traversing through halls. + /// + /// The index of the starting room. + /// A list of room indices that are adjacent to the specified room, possibly through halls. + /// + /// This method performs a breadth-first traversal starting from the given room, following + /// adjacency links through halls but stopping at rooms. It returns all rooms reachable + /// without passing through another room. + /// public virtual List GetAdjacentRooms(int roomIndex) { RoomHallIndex fullIndex = new RoomHallIndex(roomIndex, false); @@ -324,6 +476,14 @@ List GetAdjacents(RoomHallIndex nodeIndex) return returnList; } + /// + /// Calculates the shortest path distance between two rooms or halls in terms of adjacency hops. + /// + /// The starting room or hall index. + /// The destination room or hall index. + /// + /// The number of adjacency hops between the two locations, or -1 if they are not connected. + /// public int GetDistance(RoomHallIndex roomFrom, RoomHallIndex roomTo) { int returnValue = -1; @@ -338,6 +498,19 @@ void NodeAct(RoomHallIndex nodeIndex, int distance) return returnValue; } + /// + /// Determines whether removing the specified room or hall would disconnect the floor plan. + /// + /// The room or hall index to test. + /// + /// true if the room or hall is a choke point (its removal would split the graph); + /// otherwise, false. + /// + /// + /// This is useful for identifying critical paths and ensuring that floor layouts remain + /// fully connected. A room that is not a choke point can potentially be removed or + /// replaced without breaking connectivity. + /// public bool IsChokePoint(RoomHallIndex room) { int roomsHit = 0; @@ -388,6 +561,10 @@ List GetChokeAdjacents(RoomHallIndex nodeIndex) return (roomsHit != totalRooms) || (hallsHit != totalHalls); } + /// + /// Moves the floor plan's starting position and adjusts all room and hall positions accordingly. + /// + /// The new starting location for the floor plan. public void MoveStart(Loc offset) { Loc diff = offset - this.Start; @@ -400,11 +577,15 @@ public void MoveStart(Loc offset) } /// - /// Changes size without changing the start. + /// Resizes the floor plan without changing the start position. /// - /// - /// The direction to expand the floor space in. - /// The anchor point of the initial floor rect. + /// The new size for the floor plan. + /// The direction in which to expand or contract the floor space. + /// The anchor point around which existing rooms maintain their relative positions. + /// + /// This method adjusts the floor plan dimensions and repositions all rooms and halls + /// to maintain their relative positions according to the anchor direction. + /// public void Resize(Loc newSize, Dir8 dir, Dir8 anchorDir) { Loc diff = Grid.GetResizeOffset(this.Size.X, this.Size.Y, newSize.X, newSize.Y, dir); @@ -418,6 +599,22 @@ public void Resize(Loc newSize, Dir8 dir, Dir8 anchorDir) this.Halls[ii].RoomGen.SetLoc(this.Halls[ii].RoomGen.Draw.Start + anchorDiff - diff); } + /// + /// Renders all rooms and halls in the floor plan to the tile map. + /// + /// The tiled generation context to draw tiles onto. + /// + /// + /// This method is the final step in floor plan generation, converting the abstract + /// room and hall layout into actual tiles. It processes rooms first, then halls, + /// ensuring proper border negotiation between adjacent elements. + /// + /// + /// During drawing, each room queries its adjacent rooms and halls for their fulfillable + /// borders to determine where openings can be placed, then draws its tiles including + /// walls and floor terrain. + /// + /// public void DrawOnMap(ITiledGenContext map) { GenContextDebug.StepIn("Main Rooms"); @@ -484,9 +681,14 @@ public void DrawOnMap(ITiledGenContext map) } /// - /// A room's draw has been completed. It must now signal to its adjacent rooms which of its borders are open. + /// Transfers border opening information from a drawn room to its adjacent rooms and halls. /// - /// + /// The room or hall that has just been drawn. + /// + /// After a room is drawn, its adjacent rooms and halls need to know which of its border + /// tiles have openings. This method propagates that information so that adjacent elements + /// can properly connect when they are drawn. + /// public void TransferBorderToAdjacents(RoomHallIndex from) { IFloorRoomPlan basePlan = this.GetRoomHall(from); @@ -507,6 +709,12 @@ public void TransferBorderToAdjacents(RoomHallIndex from) } } + /// + /// Determines whether two rectangles collide, accounting for wrapping if enabled. + /// + /// The first rectangle. + /// The second rectangle. + /// true if the rectangles overlap; otherwise, false. public bool Collides(Rect rect1, Rect rect2) { if (this.Wrap) @@ -521,6 +729,12 @@ public bool Collides(Rect rect1, Rect rect2) } } + /// + /// Determines whether a location is within a rectangle, accounting for wrapping if enabled. + /// + /// The bounding rectangle. + /// The location to test. + /// true if the location is within the rectangle; otherwise, false. public bool InBounds(Rect rect, Loc loc) { if (this.Wrap) @@ -535,6 +749,11 @@ public bool InBounds(Rect rect, Loc loc) } } + /// + /// Finds all rooms and halls that collide with the specified rectangle. + /// + /// The rectangle to check for collisions. + /// A list of all room and hall indices that overlap with the rectangle. public List CheckCollision(Rect rect) { // gets all rooms/halls colliding with the rectangle @@ -556,6 +775,10 @@ public List CheckCollision(Rect rect) return results; } + /// + /// Enumerates all room and hall plans in the floor plan. + /// + /// An enumerable sequence of all room and hall plans. public IEnumerable GetAllPlans() { foreach (FloorRoomPlan plan in this.Rooms) @@ -565,6 +788,15 @@ public IEnumerable GetAllPlans() yield return plan; } + /// + /// Gets the list of adjacent rooms and halls for a given room or hall. + /// + /// The index of the room or hall. + /// The list of adjacent room and hall indices. + /// + /// This method is designed for use with graph traversal algorithms like + /// . + /// public virtual List GetAdjacents(RoomHallIndex nodeIndex) { return this.GetRoomHall(nodeIndex).Adjacents; diff --git a/RogueElements/MapGen/FloorPlan/FloorPlanStep.cs b/RogueElements/MapGen/FloorPlan/FloorPlanStep.cs index 55008250..c1019bf9 100644 --- a/RogueElements/MapGen/FloorPlan/FloorPlanStep.cs +++ b/RogueElements/MapGen/FloorPlan/FloorPlanStep.cs @@ -9,12 +9,30 @@ namespace RogueElements { + /// + /// Base class for generation steps that operate on a . + /// + /// The generation context type, which must implement . + /// + /// This abstract class provides a bridge between the general pattern and + /// floor plan-specific operations. Subclasses implement to modify + /// the floor plan directly. + /// [Serializable] public abstract class FloorPlanStep : GenStep where T : class, IFloorPlanGenContext { + /// + /// Applies the step's logic to the floor plan. + /// + /// The random number generator for this operation. + /// The floor plan to modify. public abstract void ApplyToPath(IRandom rand, FloorPlan floorPlan); + /// + /// Applies this generation step to the map context. + /// + /// The generation context containing the floor plan. public override void Apply(T map) { this.ApplyToPath(map.Rand, map.RoomPlan); diff --git a/RogueElements/MapGen/FloorPlan/FloorRoomPlan.cs b/RogueElements/MapGen/FloorPlan/FloorRoomPlan.cs index 9a924478..057c96e7 100644 --- a/RogueElements/MapGen/FloorPlan/FloorRoomPlan.cs +++ b/RogueElements/MapGen/FloorPlan/FloorRoomPlan.cs @@ -9,10 +9,22 @@ namespace RogueElements { /// - /// Contains data about which cells a room occupies in a FloorPlan. + /// Represents a room in a , storing its generator and connectivity information. /// + /// + /// FloorRoomPlan stores all the data needed to describe a room within a floor layout: + /// its shape generator, attached components for filtering/identification, and the list + /// of adjacent rooms and halls. + /// + /// + /// public class FloorRoomPlan : IFloorRoomPlan { + /// + /// Initializes a new instance of the class. + /// + /// The room generator for this room. + /// The component collection for filtering and identification. public FloorRoomPlan(IRoomGen roomGen, ComponentCollection components) { this.RoomGen = roomGen; @@ -20,11 +32,21 @@ public FloorRoomPlan(IRoomGen roomGen, ComponentCollection components) this.Adjacents = new List(); } + /// + /// Gets or sets the room generator that defines this room's shape and rendering. + /// public IRoomGen RoomGen { get; set; } // TODO: needs a better class. Only one RoomComponent subclass allowed per collection. Also better lookup. + + /// + /// Gets the component collection for this room, used for filtering and metadata. + /// public ComponentCollection Components { get; } + /// + /// Gets the list of adjacent rooms and halls connected to this room. + /// public List Adjacents { get; } } } diff --git a/RogueElements/MapGen/FloorPlan/FloorStairsStep.cs b/RogueElements/MapGen/FloorPlan/FloorStairsStep.cs index 16e621a4..5d8dffac 100644 --- a/RogueElements/MapGen/FloorPlan/FloorStairsStep.cs +++ b/RogueElements/MapGen/FloorPlan/FloorStairsStep.cs @@ -9,28 +9,54 @@ namespace RogueElements { /// - /// Adds the entrance and exit to the floor. Is room-conscious. - /// The algorithm will try to place them far away from each other in different rooms. + /// Places entrances and exits in the floor plan with minimum distance requirements. /// - /// - /// - /// + /// The generation context type. + /// The entrance type implementing . + /// The exit type implementing . + /// + /// + /// This step extends with + /// distance-based placement. It attempts to place entrances and exits in rooms that are + /// at least adjacencies apart, ensuring the player must traverse + /// a meaningful portion of the floor. + /// + /// + /// When a minimum distance cannot be satisfied, the step falls back to placing in any + /// available room. + /// + /// [Serializable] public class FloorStairsStep : BaseFloorStairsStep where TGenContext : class, IFloorPlanGenContext, IPlaceableGenContext, IPlaceableGenContext where TEntrance : IEntrance where TExit : IExit { + /// + /// Initializes a new instance of the class. + /// public FloorStairsStep() { } + /// + /// Initializes a new instance of the class with a single entrance and exit. + /// + /// The minimum adjacency distance between entrance and exit. + /// The entrance object to place. + /// The exit object to place. public FloorStairsStep(int minDistance, TEntrance entrance, TExit exit) : base(entrance, exit) { this.MinDistance = minDistance; } + /// + /// Initializes a new instance of the class with multiple entrances and exits. + /// + /// The minimum adjacency distance between entrances and exits. + /// The list of entrance objects to place. + /// The list of exit objects to place. public FloorStairsStep(int minDistance, List entrances, List exits) : base(entrances, exits) { @@ -38,18 +64,18 @@ public FloorStairsStep(int minDistance, List entrances, List e } /// - /// The minimum distance in room adjacencies that starts and ends should be placed from each other. + /// Gets or sets the minimum distance in room adjacencies between entrances and exits. /// public int MinDistance { get; set; } /// - /// Attempt to choose an outlet in a room with no entrance/exit, and updates their availability. If none exists, default to null. + /// Attempts to choose a location for an entrance or exit while maintaining minimum distance. /// - /// - /// - /// - /// - /// + /// The spawnable type being placed. + /// The generation context. + /// List of room indices not yet used for any entrance or exit. + /// List of room indices already used. Can be null if not tracking usage. + /// A valid location if found; otherwise, null. protected override Loc? GetOutlet(TGenContext map, List free_indices, List used_indices) { while (free_indices.Count > 0) diff --git a/RogueElements/MapGen/FloorPlan/IAddConnectedRoomsStep.cs b/RogueElements/MapGen/FloorPlan/IAddConnectedRoomsStep.cs index 2c7a8cf5..55c9f182 100644 --- a/RogueElements/MapGen/FloorPlan/IAddConnectedRoomsStep.cs +++ b/RogueElements/MapGen/FloorPlan/IAddConnectedRoomsStep.cs @@ -9,21 +9,40 @@ namespace RogueElements { + /// + /// Defines the interface for steps that add connected rooms to a floor plan. + /// public interface IAddConnectedRoomsStep { + /// + /// Gets or sets the percentage chance that a hall is added between rooms. + /// int HallPercent { get; set; } + /// + /// Gets or sets the number of rooms to add. + /// RandRange Amount { get; set; } } /// - /// Takes the current floor plan and adds new rooms that are connected to existing rooms. + /// Base class for steps that add rooms connected to existing rooms in a floor plan. /// - /// + /// The generation context type, which must implement . + /// + /// This abstract class provides common functionality for adding rooms that connect to existing + /// rooms, including support for optional hallways and room filtering. Subclasses determine + /// the specific algorithm for choosing expansion points. + /// + /// + /// [Serializable] public abstract class AddConnectedRoomsBaseStep : FloorPlanStep, IAddConnectedRoomsStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// protected AddConnectedRoomsBaseStep() : base() { @@ -32,6 +51,11 @@ protected AddConnectedRoomsBaseStep() this.Filters = new List(); } + /// + /// Initializes a new instance of the class with specified generators. + /// + /// The picker for room generators. + /// The picker for hall generators. protected AddConnectedRoomsBaseStep(IRandPicker> genericRooms, IRandPicker> genericHalls) : base() { @@ -77,6 +101,11 @@ protected AddConnectedRoomsBaseStep(IRandPicker> genericRooms, IRandP /// public ComponentCollection HallComponents { get; set; } + /// + /// Applies this step to add connected rooms to the floor plan. + /// + /// The random number generator. + /// The floor plan to modify. public override void ApplyToPath(IRandom rand, FloorPlan floorPlan) { int amount = this.Amount.Pick(rand); @@ -103,6 +132,12 @@ public override void ApplyToPath(IRandom rand, FloorPlan floorPlan) } } + /// + /// Chooses the expansion point and room configuration for adding a new room. + /// + /// The random number generator. + /// The floor plan to evaluate. + /// The expansion details if a valid placement is found; otherwise, null. public abstract FloorPathBranch.ListPathBranchExpansion? ChooseRoomExpansion(IRandom rand, FloorPlan floorPlan); /// @@ -130,6 +165,10 @@ public virtual RoomGen PrepareRoom(IRandom rand, FloorPlan floorPlan, bool is return room; } + /// + /// Returns a string representation of this step. + /// + /// A string describing this step's configuration. public override string ToString() { return string.Format("{0}: Add:{1} Hall:{2}%", this.GetType().GetFormattedTypeName(), this.Amount, this.HallPercent); diff --git a/RogueElements/MapGen/FloorPlan/IAddDisconnectedRoomsStep.cs b/RogueElements/MapGen/FloorPlan/IAddDisconnectedRoomsStep.cs index 6fb2b161..54ccd887 100644 --- a/RogueElements/MapGen/FloorPlan/IAddDisconnectedRoomsStep.cs +++ b/RogueElements/MapGen/FloorPlan/IAddDisconnectedRoomsStep.cs @@ -9,25 +9,45 @@ namespace RogueElements { + /// + /// Defines the interface for steps that add disconnected rooms to a floor plan. + /// public interface IAddDisconnectedRoomsStep { + /// + /// Gets or sets the number of rooms to add. + /// RandRange Amount { get; set; } } /// - /// Takes the current floor plan and adds new rooms that are disconnected from existing rooms. + /// Base class for steps that add rooms not connected to existing rooms in a floor plan. /// - /// + /// The generation context type, which must implement . + /// + /// This abstract class provides common functionality for adding isolated rooms that do not + /// connect to any existing rooms. These rooms can later be connected using connect steps. + /// Subclasses determine the specific algorithm for choosing placement locations. + /// + /// + /// [Serializable] public abstract class AddDisconnectedRoomsBaseStep : FloorPlanStep, IAddDisconnectedRoomsStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// protected AddDisconnectedRoomsBaseStep() : base() { this.Components = new ComponentCollection(); } + /// + /// Initializes a new instance of the class with specified room generators. + /// + /// The picker for room generators. protected AddDisconnectedRoomsBaseStep(IRandPicker> genericRooms) : base() { @@ -50,6 +70,11 @@ protected AddDisconnectedRoomsBaseStep(IRandPicker> genericRooms) /// public ComponentCollection Components { get; set; } + /// + /// Applies this step to add disconnected rooms to the floor plan. + /// + /// The random number generator. + /// The floor plan to modify. public override void ApplyToPath(IRandom rand, FloorPlan floorPlan) { int amount = this.Amount.Pick(rand); @@ -78,11 +103,22 @@ public override void ApplyToPath(IRandom rand, FloorPlan floorPlan) } } + /// + /// Returns a string representation of this step. + /// + /// A string describing this step's configuration. public override string ToString() { return string.Format("{0}: Add:{1}", this.GetType().GetFormattedTypeName(), this.Amount); } + /// + /// Finds a viable location for placing a new disconnected room. + /// + /// The random number generator. + /// The floor plan to search within. + /// The size of the room to place. + /// A valid location if found; otherwise, null. protected abstract Loc? ChooseViableLoc(IRandom rand, FloorPlan floorPlan, Loc roomSize); } } diff --git a/RogueElements/MapGen/FloorPlan/IConnectRoomStep.cs b/RogueElements/MapGen/FloorPlan/IConnectRoomStep.cs index bf4a3ecd..4550baaf 100644 --- a/RogueElements/MapGen/FloorPlan/IConnectRoomStep.cs +++ b/RogueElements/MapGen/FloorPlan/IConnectRoomStep.cs @@ -8,38 +8,72 @@ namespace RogueElements { + /// + /// Defines the interface for steps that connect rooms in a floor plan. + /// public interface IConnectRoomStep { + /// + /// Gets or sets the connection factor determining how many connections to make. + /// RandRange ConnectFactor { get; set; } } /// - /// Takes the current floor plan and connects its rooms with other rooms. + /// Connects rooms in the floor plan based on a connection factor. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step creates additional connections between rooms in the floor plan. Unlike + /// which focuses on dead ends, this step can connect + /// any eligible rooms based on the . + /// + /// + /// The connection factor is scaled: 100 means each room is connected once, 200 means + /// each room is connected twice on average. + /// + /// + /// [Serializable] public class ConnectRoomStep : ConnectStep, IConnectRoomStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public ConnectRoomStep() : base() { } + /// + /// Initializes a new instance of the class with specified hall generators. + /// + /// The picker for hall generators. public ConnectRoomStep(IRandPicker> genericHalls) : base(genericHalls) { } /// - /// Determines the number of connections to make. - /// 0 = No Connections - /// 50 = Half of all rooms connected - /// 100 = All rooms connected - /// 200 = All rooms connected twice over + /// Gets or sets the connection factor determining the number of connections to make. /// + /// + /// + /// 0 = No connections + /// 50 = Half of all rooms connected + /// 100 = All rooms connected once + /// 200 = All rooms connected twice on average + /// + /// public RandRange ConnectFactor { get; set; } + /// + /// Applies this step to connect rooms in the floor plan. + /// + /// The random number generator. + /// The floor plan to modify. public override void ApplyToPath(IRandom rand, FloorPlan floorPlan) { List candBranchPoints = new List(); @@ -91,6 +125,10 @@ public override void ApplyToPath(IRandom rand, FloorPlan floorPlan) } } + /// + /// Returns a string representation of this step. + /// + /// A string describing this step's configuration. public override string ToString() { return string.Format("{0}: {1}%", this.GetType().GetFormattedTypeName(), this.ConnectFactor); diff --git a/RogueElements/MapGen/FloorPlan/IFloorPlanGenContext.cs b/RogueElements/MapGen/FloorPlan/IFloorPlanGenContext.cs index 5e7bf2a2..a175fd85 100644 --- a/RogueElements/MapGen/FloorPlan/IFloorPlanGenContext.cs +++ b/RogueElements/MapGen/FloorPlan/IFloorPlanGenContext.cs @@ -8,10 +8,27 @@ namespace RogueElements { + /// + /// Defines the interface for generation contexts that support freeform floor plan-based room layouts. + /// + /// + /// Implement this interface to enable the use of and related steps + /// that manipulate rooms and halls in a non-grid layout. This extends + /// to add floor plan management capabilities. + /// + /// + /// public interface IFloorPlanGenContext : ITiledGenContext { + /// + /// Gets the floor plan associated with this generation context. + /// FloorPlan RoomPlan { get; } + /// + /// Initializes this context with the specified floor plan. + /// + /// The floor plan to associate with this context. void InitPlan(FloorPlan plan); } } diff --git a/RogueElements/MapGen/FloorPlan/IFloorRoomPlan.cs b/RogueElements/MapGen/FloorPlan/IFloorRoomPlan.cs index 8ebf6a0e..1bc2feee 100644 --- a/RogueElements/MapGen/FloorPlan/IFloorRoomPlan.cs +++ b/RogueElements/MapGen/FloorPlan/IFloorRoomPlan.cs @@ -7,8 +7,19 @@ namespace RogueElements { + /// + /// Defines the interface for room and hall plans within a . + /// + /// + /// This interface extends to add adjacency tracking, which is essential + /// for floor plan connectivity. Both and + /// implement this interface. + /// public interface IFloorRoomPlan : IRoomPlan { + /// + /// Gets the list of rooms and halls that are adjacent to this element. + /// List Adjacents { get; } } } diff --git a/RogueElements/MapGen/FloorPlan/InitFloorPlanStep.cs b/RogueElements/MapGen/FloorPlan/InitFloorPlanStep.cs index a8b4e30c..66ba42b4 100644 --- a/RogueElements/MapGen/FloorPlan/InitFloorPlanStep.cs +++ b/RogueElements/MapGen/FloorPlan/InitFloorPlanStep.cs @@ -9,34 +9,62 @@ namespace RogueElements { /// - /// Initializes an empty floor plan, which is a list of rooms that keep track of their size, position, and connectivity with each other. - /// Gen Steps that operate on the floor plan can add rooms, delete them, or change the rooms in some way. - /// Once finished, apply DrawFloorToTileStep to draw the actual tiles of the rooms. + /// Initializes an empty floor plan for freeform room-based map generation. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step creates a new with the specified dimensions and associates it + /// with the generation context. Subsequent steps can add rooms, delete them, or modify connections. + /// + /// + /// Once floor plan manipulation is complete, use to render + /// the rooms and halls to actual tiles. + /// + /// + /// + /// [Serializable] public class InitFloorPlanStep : GenStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class with default dimensions. + /// public InitFloorPlanStep() { } + /// + /// Initializes a new instance of the class with specified dimensions. + /// + /// The width of the floor plan in tiles. + /// The height of the floor plan in tiles. public InitFloorPlanStep(int width, int height) { this.Width = width; this.Height = height; } + /// + /// Gets or sets the width of the floor plan in tiles. + /// public int Width { get; set; } + /// + /// Gets or sets the height of the floor plan in tiles. + /// public int Height { get; set; } /// - /// Determines if the map is wrapped around. + /// Gets or sets a value indicating whether the map wraps around its edges (toroidal topology). /// public bool Wrap { get; set; } + /// + /// Applies this step by creating and initializing a new floor plan. + /// + /// The generation context to initialize. public override void Apply(T map) { var floorPlan = new FloorPlan(); @@ -45,6 +73,10 @@ public override void Apply(T map) map.InitPlan(floorPlan); } + /// + /// Returns a string representation of this step. + /// + /// A string describing this step's configuration. public override string ToString() { return string.Format("{0}: Size:{1}x{2}", this.GetType().GetFormattedTypeName(), this.Width, this.Height); diff --git a/RogueElements/MapGen/FloorPlan/Paths/FloorPathStartStep.cs b/RogueElements/MapGen/FloorPlan/Paths/FloorPathStartStep.cs index 71d80c3b..0a91d074 100644 --- a/RogueElements/MapGen/FloorPlan/Paths/FloorPathStartStep.cs +++ b/RogueElements/MapGen/FloorPlan/Paths/FloorPathStartStep.cs @@ -9,10 +9,30 @@ namespace RogueElements { + /// + /// Base class for floor path steps that initialize a floor plan's room layout. + /// + /// The generation context type, which must implement . + /// + /// Path start steps are responsible for creating the initial room layout structure. + /// They typically populate an empty floor plan with connected rooms following some + /// algorithmic pattern (branching, grid-based, etc.). + /// + /// + /// [Serializable] public abstract class FloorPathStartStep : FloorPlanStep where T : class, IFloorPlanGenContext { + /// + /// Creates a minimal error path when normal path generation fails. + /// + /// The random number generator. + /// The floor plan to populate with the error path. + /// + /// This method clears the floor plan and adds a single minimal room, ensuring + /// that generation can continue even if the main algorithm fails. + /// public void CreateErrorPath(IRandom rand, FloorPlan floorPlan) { floorPlan.Clear(); @@ -22,6 +42,10 @@ public void CreateErrorPath(IRandom rand, FloorPlan floorPlan) floorPlan.AddRoom(room, new ComponentCollection()); } + /// + /// Gets the default room generator used for error paths. + /// + /// A default room generator instance. public virtual RoomGen GetDefaultGen() { return new RoomGenDefault(); diff --git a/RogueElements/MapGen/FloorPlan/Paths/FloorPathStartStepGeneric.cs b/RogueElements/MapGen/FloorPlan/Paths/FloorPathStartStepGeneric.cs index 293beeea..a8820be5 100644 --- a/RogueElements/MapGen/FloorPlan/Paths/FloorPathStartStepGeneric.cs +++ b/RogueElements/MapGen/FloorPlan/Paths/FloorPathStartStepGeneric.cs @@ -9,16 +9,34 @@ namespace RogueElements { + /// + /// Base class for floor path steps that use configurable room and hall generators. + /// + /// The generation context type, which must implement . + /// + /// This class extends with support for generic room and + /// hall pickers, allowing flexible configuration of what room and hall types are generated. + /// It also supports component labeling for the created rooms and halls. + /// + /// [Serializable] public abstract class FloorPathStartStepGeneric : FloorPathStartStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// protected FloorPathStartStepGeneric() { this.RoomComponents = new ComponentCollection(); this.HallComponents = new ComponentCollection(); } + /// + /// Initializes a new instance of the class with specified generators. + /// + /// The picker for room generators. + /// The picker for hall generators. protected FloorPathStartStepGeneric(IRandPicker> genericRooms, IRandPicker> genericHalls) { this.GenericRooms = genericRooms; @@ -28,25 +46,30 @@ protected FloorPathStartStepGeneric(IRandPicker> genericRooms, IRandP } /// - /// The room types that can be used for the rooms of the layout. + /// Gets or sets the picker for room generators used in the layout. /// public IRandPicker> GenericRooms { get; set; } /// - /// Components that the newly added rooms will be labeled with. + /// Gets or sets components to add to newly created rooms. /// public ComponentCollection RoomComponents { get; set; } /// - /// The room types that can be used for the halls of the layout. + /// Gets or sets the picker for hall generators used in the layout. /// public IRandPicker> GenericHalls { get; set; } /// - /// Components that the newly added halls will be labeled with. + /// Gets or sets components to add to newly created halls. /// public ComponentCollection HallComponents { get; set; } + /// + /// Applies this step to the map, validating that rooms and halls are configured. + /// + /// The generation context. + /// Thrown when rooms or halls cannot be picked. public override void Apply(T map) { if (!this.GenericRooms.CanPick || !this.GenericHalls.CanPick) diff --git a/RogueElements/MapGen/FloorPlan/Paths/IFloorPathBranch.cs b/RogueElements/MapGen/FloorPlan/Paths/IFloorPathBranch.cs index 041cd449..49409885 100644 --- a/RogueElements/MapGen/FloorPlan/Paths/IFloorPathBranch.cs +++ b/RogueElements/MapGen/FloorPlan/Paths/IFloorPathBranch.cs @@ -8,56 +8,98 @@ namespace RogueElements { + /// + /// Defines the interface for floor path generation with branching layouts. + /// public interface IFloorPathBranch { + /// + /// Gets or sets the target fill percentage for room coverage. + /// RandRange FillPercent { get; set; } + /// + /// Gets or sets the percentage chance of adding halls between rooms. + /// int HallPercent { get; set; } + /// + /// Gets or sets the branching ratio for the layout. + /// RandRange BranchRatio { get; set; } } /// - /// Populates the empty floor plan of a map by creating a minimum spanning tree of connected rooms and halls. + /// Creates a branching tree layout of rooms connected by halls. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step generates a floor layout by starting with a single room and repeatedly adding + /// new rooms adjacent to existing ones. The controls how often + /// the algorithm branches from non-terminal rooms versus extending existing branches. + /// + /// + /// The resulting layout resembles a tree or organic growth pattern, with dead ends that + /// can optionally be connected using . + /// + /// [Serializable] public class FloorPathBranch : FloorPathStartStepGeneric, IFloorPathBranch where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public FloorPathBranch() : base() { } + /// + /// Initializes a new instance of the class with specified generators. + /// + /// The picker for room generators. + /// The picker for hall generators. public FloorPathBranch(IRandPicker> genericRooms, IRandPicker> genericHalls) : base(genericRooms, genericHalls) { } + /// + /// Delegate for preparing room generators. + /// + /// The random number generator. + /// The floor plan being built. + /// True if generating a hall; false for a room. + /// A room generator configured for the floor plan. public delegate RoomGen RoomPrep(IRandom rand, FloorPlan floorPlan, bool isHall); /// - /// The percentage of total space in the floor plan that the step aims to fill with rooms. + /// Gets or sets the target percentage of floor space to fill with rooms. /// public RandRange FillPercent { get; set; } /// - /// The chance that rooms are attached to each other using an intermediate hallway. + /// Gets or sets the percentage chance of adding an intermediate hall between rooms. /// public int HallPercent { get; set; } /// - /// The percent amount of branching paths the layout will have in relation to its straight paths. - /// 0 = A layout without branches. (Worm) - /// 50 = A layout that branches once for every two extensions. (Tree) - /// 100 = A layout that branches once for every extension. (Branchier Tree) - /// 200 = A layout that branches twice for every extension. (Fuzzy Worm) + /// Gets or sets the branching ratio controlling layout shape. /// + /// + /// + /// 0 = Linear layout without branches (worm-like) + /// 50 = Branches once per two extensions (tree-like) + /// 100 = Branches once per extension (dense tree) + /// 200 = Branches twice per extension (fuzzy/organic) + /// + /// public RandRange BranchRatio { get; set; } /// - /// Prevents the step from making branches in the path, even if it would fail the space-fill quota. + /// Gets or sets a value indicating whether to prevent forced branching when fill quota cannot be met. /// public bool NoForcedBranches { get; set; } @@ -420,12 +462,33 @@ public override string ToString() return (tilesCovered, roomsAdded); } + /// + /// Represents an expansion operation for a branching floor path, containing the source room/hall, + /// the new room to add, and an optional connecting hall. + /// public struct ListPathBranchExpansion { + /// + /// The index of the room or hall to expand from. + /// public RoomHallIndex From; + + /// + /// The optional intermediate hall connecting the source to the new room. May be null. + /// public IPermissiveRoomGen Hall; + + /// + /// The new room to add to the floor plan. + /// public IRoomGen Room; + /// + /// Initializes a new instance of the struct. + /// + /// The index of the room or hall to expand from. + /// The new room to add. + /// The optional intermediate hall, or null for direct connection. public ListPathBranchExpansion(RoomHallIndex from, IRoomGen room, IPermissiveRoomGen hall) { this.From = from; diff --git a/RogueElements/MapGen/FloorPlan/Paths/README.md b/RogueElements/MapGen/FloorPlan/Paths/README.md new file mode 100644 index 00000000..0d2aad6d --- /dev/null +++ b/RogueElements/MapGen/FloorPlan/Paths/README.md @@ -0,0 +1,261 @@ +# FloorPlan Paths + +[![RogueElements](https://img.shields.io/nuget/v/RogueElements?label=RogueElements)](https://www.nuget.org/packages/RogueElements) + +Path generation algorithms for freeform floor plan layouts. This module provides steps that populate `FloorPlan` objects with rooms and halls in various configurations. + +## Purpose + +FloorPlan Paths generate the room/hall connectivity graph for freeform (non-grid) map layouts. Unlike grid paths which place rooms in fixed cells, floor plan paths allow rooms to be positioned freely, creating more organic layouts. + +## Core Classes + +### FloorPathStartStep + +Abstract base class for all floor plan path generators: + +```csharp +public abstract class FloorPathStartStep : FloorPlanStep + where T : class, IFloorPlanGenContext +{ + public void CreateErrorPath(IRandom rand, FloorPlan floorPlan); + public virtual RoomGen GetDefaultGen(); +} +``` + +### FloorPathStartStepGeneric + +Extends `FloorPathStartStep` with room/hall spawning support: + +```csharp +public abstract class FloorPathStartStepGeneric : FloorPathStartStep +{ + // The room types that can be used for rooms + public IRandPicker> GenericRooms { get; set; } + + // Components to label rooms with + public ComponentCollection RoomComponents { get; set; } + + // The room types that can be used for halls + public IRandPicker> GenericHalls { get; set; } + + // Components to label halls with + public ComponentCollection HallComponents { get; set; } +} +``` + +## Path Algorithm Classes + +### FloorPathBranch + +The primary floor plan path generator. Creates a minimum spanning tree of connected rooms and halls using a branching algorithm. + +```csharp +var genericRooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, +}; + +var genericHalls = new SpawnList> +{ + { new RoomGenAngledHall(0, new RandRange(3, 7), new RandRange(3, 7)), 10 }, + { new RoomGenSquare(new RandRange(1), new RandRange(1)), 20 }, +}; + +var path = new FloorPathBranch(genericRooms, genericHalls) +{ + FillPercent = new RandRange(45), // Target 45% floor coverage + HallPercent = 50, // 50% chance of hall between rooms + BranchRatio = new RandRange(0, 25), // Branching rate +}; + +layout.GenSteps.Add(-1, path); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `FillPercent` | `RandRange` | Percentage of floor plan area to fill with rooms | +| `HallPercent` | `int` | Chance (0-100) that rooms connect via an intermediate hall | +| `BranchRatio` | `RandRange` | Branching rate (0 = worm, 50 = tree, 100+ = fuzzy worm) | +| `NoForcedBranches` | `bool` | Prevent forced branches even if quota not met | + +#### BranchRatio Guide + +- **0**: Linear layout (worm shape) +- **50**: Moderate branching (tree shape) +- **100**: Branch on every extension +- **200**: Multiple branches per extension (fuzzy worm) + +### IFloorPathBranch + +Interface for branching path algorithms: + +```csharp +public interface IFloorPathBranch +{ + RandRange FillPercent { get; set; } + int HallPercent { get; set; } + RandRange BranchRatio { get; set; } +} +``` + +## Usage Example + +From `Ex2_Rooms`: + +```csharp +var layout = new MapGen(); + +// Initialize a 54x40 floorplan +InitFloorPlanStep startGen = new InitFloorPlanStep(54, 40); +layout.GenSteps.Add(-2, startGen); + +// Create room types to place +var genericRooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, +}; + +// Create hall types to place +var genericHalls = new SpawnList> +{ + { new RoomGenAngledHall(0, new RandRange(3, 7), new RandRange(3, 7)), 10 }, + { new RoomGenSquare(new RandRange(1), new RandRange(1)), 20 }, +}; + +// Create branching path +FloorPathBranch path = new FloorPathBranch(genericRooms, genericHalls) +{ + HallPercent = 50, + FillPercent = new RandRange(45), + BranchRatio = new RandRange(0, 25), +}; + +layout.GenSteps.Add(-1, path); + +// Draw the floor plan to tiles +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + +MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); +``` + +## Algorithm Details + +### Room Expansion Process + +1. **Start Room**: Place initial room at random valid location +2. **Terminal Extension**: Expand from end nodes (rooms with one connection) +3. **Branching**: Create branches from multi-connected rooms +4. **Collision Check**: Validate new room positions don't overlap +5. **Border Matching**: Ensure rooms can connect at shared borders + +### Key Static Methods + +```csharp +// Get all possible expansion points +public static List GetPossibleExpansions( + FloorPlan floorPlan, + bool branch // True for branches, false for extensions +); + +// Add valid placement locations for a room +public static void AddLegalPlacements( + SpawnList possiblePlacements, + FloorPlan floorPlan, + RoomHallIndex indexFrom, + IRoomGen roomFrom, + IRoomGen room, + Dir4 expandTo +); + +// Choose a random room expansion +public static ListPathBranchExpansion? ChooseRandRoomExpansion( + IRoomGen room, + IRoomGen hall, + IRandom rand, + FloorPlan floorPlan, + List availableExpansions +); +``` + +## Creating Custom Path Algorithms + +1. Inherit from `FloorPathStartStepGeneric` +2. Override `ApplyToPath()` to implement your algorithm + +```csharp +[Serializable] +public class FloorPathRing : FloorPathStartStepGeneric + where T : class, IFloorPlanGenContext +{ + public int RoomCount { get; set; } = 8; + + public override void ApplyToPath(IRandom rand, FloorPlan floorPlan) + { + // Place rooms in a ring pattern + int centerX = floorPlan.DrawRect.Width / 2; + int centerY = floorPlan.DrawRect.Height / 2; + int radius = Math.Min(centerX, centerY) - 5; + + RoomHallIndex? prevRoom = null; + + for (int i = 0; i < RoomCount; i++) + { + double angle = (2 * Math.PI * i) / RoomCount; + int x = centerX + (int)(radius * Math.Cos(angle)); + int y = centerY + (int)(radius * Math.Sin(angle)); + + // Create and position room + var room = GenericRooms.Pick(rand).Copy(); + var size = room.ProposeSize(rand); + room.PrepareSize(rand, size); + room.SetLoc(new Loc(x - size.X / 2, y - size.Y / 2)); + + // Add to floor plan + if (prevRoom.HasValue) + { + // Connect with hall + var hall = GenericHalls.Pick(rand).Copy(); + floorPlan.AddRoom(room, RoomComponents.Clone(), prevRoom.Value); + } + else + { + floorPlan.AddRoom(room, RoomComponents.Clone()); + } + + prevRoom = new RoomHallIndex(floorPlan.RoomCount - 1, false); + } + + // Close the ring by connecting last to first + // ... additional connection logic + } +} +``` + +## Expansion Data Structure + +```csharp +public struct ListPathBranchExpansion +{ + public RoomHallIndex From; // Source room/hall + public IPermissiveRoomGen Hall; // Connecting hall (may be null) + public IRoomGen Room; // New room to add +} +``` + +## Related Modules + +- **[../](../)** - Parent FloorPlan module +- **[Grid/Paths/](../../Grid/Paths/)** - Grid-based path algorithms +- **[Rooms/](../../Rooms/)** - Room generators used by paths +- **[Rooms/Halls/](../../Rooms/Halls/)** - Hall generators + +## See Also + +- `Ex2_Rooms` - Freeform floor plan generation example +- `FloorPlan` - The data structure populated by path steps +- `DrawFloorToTileStep` - Converts floor plan to tiles diff --git a/RogueElements/MapGen/FloorPlan/README.md b/RogueElements/MapGen/FloorPlan/README.md new file mode 100644 index 00000000..83a4481b --- /dev/null +++ b/RogueElements/MapGen/FloorPlan/README.md @@ -0,0 +1,412 @@ +# FloorPlan - Freeform Room Generation + +[![Build](https://img.shields.io/github/actions/workflow/status/audinowho/RogueElements/build.yml?branch=master)](https://github.com/audinowho/RogueElements/actions) +[![NuGet](https://img.shields.io/nuget/v/RogueElements)](https://www.nuget.org/packages/RogueElements/) + +The FloorPlan system provides **freeform room placement** where rooms and halls can be positioned anywhere within the map bounds, without being constrained to a grid. This gives maximum flexibility for organic, irregular dungeon layouts. + +## What is a FloorPlan? + +A `FloorPlan` is an abstract representation of a dungeon layout consisting of: +- **Rooms** - Spaces where gameplay occurs (combat, exploration, treasure) +- **Halls** - Corridors connecting rooms +- **Adjacency Graph** - Tracks which rooms/halls are connected + +Unlike `GridPlan`, rooms in a `FloorPlan` can be placed at any position and have any size, allowing for natural, non-uniform layouts. + +## Class Diagram + +```mermaid +classDiagram + class FloorPlan { + +Loc Size + +Loc Start + +bool Wrap + +int RoomCount + +int HallCount + +AddRoom(IRoomGen, ComponentCollection, RoomHallIndex[]) + +AddHall(IPermissiveRoomGen, ComponentCollection, RoomHallIndex[]) + +GetRoomPlan(int index) FloorRoomPlan + +GetHallPlan(int index) FloorHallPlan + +GetAdjacentRooms(int roomIndex) List~int~ + +DrawOnMap(ITiledGenContext map) + } + + class FloorRoomPlan { + +IRoomGen RoomGen + +ComponentCollection Components + +List~RoomHallIndex~ Adjacents + } + + class FloorHallPlan { + +IPermissiveRoomGen RoomGen + +ComponentCollection Components + +List~RoomHallIndex~ Adjacents + } + + class RoomHallIndex { + +int Index + +bool IsHall + } + + class IFloorRoomPlan { + <> + +List~RoomHallIndex~ Adjacents + } + + class IRoomPlan { + <> + +IRoomGen RoomGen + +ComponentCollection Components + } + + FloorPlan "1" *-- "*" FloorRoomPlan : Rooms + FloorPlan "1" *-- "*" FloorHallPlan : Halls + FloorRoomPlan ..|> IFloorRoomPlan + FloorHallPlan ..|> IFloorRoomPlan + IFloorRoomPlan --|> IRoomPlan + FloorRoomPlan --> RoomHallIndex : references + FloorHallPlan --> RoomHallIndex : references +``` + +## Key Classes + +### `FloorPlan` + +The main data structure holding all rooms and halls. + +```csharp +// From FloorPlan.cs +public class FloorPlan +{ + public Loc Size { get; private set; } + public Loc Start { get; private set; } + public bool Wrap { get; private set; } + + public virtual int RoomCount => this.Rooms.Count; + public virtual int HallCount => this.Halls.Count; + + // Initialize the floor plan bounds + public void InitSize(Loc size, bool wrap = false); + + // Add a room connected to existing rooms/halls + public void AddRoom(IRoomGen gen, ComponentCollection components, params RoomHallIndex[] attached); + + // Add a hall (corridor) connecting rooms + public void AddHall(IPermissiveRoomGen gen, ComponentCollection components, params RoomHallIndex[] attached); + + // Get adjacent room indices (traverses through halls) + public virtual List GetAdjacentRooms(int roomIndex); + + // Render all rooms and halls to the tile map + public void DrawOnMap(ITiledGenContext map); +} +``` + +### `FloorRoomPlan` + +Wraps an `IRoomGen` with metadata and connectivity information. + +```csharp +// From FloorRoomPlan.cs +public class FloorRoomPlan : IFloorRoomPlan +{ + public IRoomGen RoomGen { get; set; } // The room generator + public ComponentCollection Components { get; } // Tags/metadata + public List Adjacents { get; } // Connected rooms/halls +} +``` + +### `FloorHallPlan` + +Similar to `FloorRoomPlan` but uses `IPermissiveRoomGen` (can connect from any side). + +```csharp +// From FloorHallPlan.cs +public class FloorHallPlan : IFloorRoomPlan +{ + public IPermissiveRoomGen RoomGen { get; set; } + public ComponentCollection Components { get; } + public List Adjacents { get; } +} +``` + +### `RoomHallIndex` + +A reference to either a room or hall by index. + +```csharp +// From RoomHallIndex.cs +public struct RoomHallIndex +{ + public bool IsHall; // true = hall, false = room + public int Index; // Index in the respective list +} +``` + +## Common Steps + +### 1. `InitFloorPlanStep` - Initialize the Plan + +Creates an empty floor plan with specified dimensions. + +```csharp +// From InitFloorPlanStep.cs +[Serializable] +public class InitFloorPlanStep : GenStep + where T : class, IFloorPlanGenContext +{ + public int Width { get; set; } + public int Height { get; set; } + public bool Wrap { get; set; } // Wrapping (toroidal) map + + public override void Apply(T map) + { + var floorPlan = new FloorPlan(); + floorPlan.InitSize(new Loc(this.Width, this.Height), this.Wrap); + map.InitPlan(floorPlan); + } +} +``` + +**Usage:** +```csharp +// Initialize a 54x40 floor plan +layout.GenSteps.Add(-2, new InitFloorPlanStep(54, 40)); +``` + +### 2. `FloorPathBranch` - Generate Branching Paths + +Creates a minimum spanning tree of connected rooms and halls. + +```csharp +// From IFloorPathBranch.cs +[Serializable] +public class FloorPathBranch : FloorPathStartStepGeneric + where T : class, IFloorPlanGenContext +{ + // Percentage of floor area to fill with rooms + public RandRange FillPercent { get; set; } + + // Chance (0-100) to use a hall between rooms + public int HallPercent { get; set; } + + // How much the path branches (0=linear, 50=tree, 100+=bushy) + public RandRange BranchRatio { get; set; } +} +``` + +**Usage:** +```csharp +var path = new FloorPathBranch(genericRooms, genericHalls) +{ + HallPercent = 50, + FillPercent = new RandRange(45), + BranchRatio = new RandRange(0, 25), +}; +layout.GenSteps.Add(-1, path); +``` + +### 3. `ConnectStep` - Add Extra Connections + +Finds disconnected or distant rooms and connects them with halls. + +```csharp +// From ConnectStep.cs +[Serializable] +public abstract class ConnectStep : FloorPlanStep + where T : class, IFloorPlanGenContext +{ + // Filters to determine which rooms can be connected + public List Filters { get; set; } + + // Hall types to use for connections + public IRandPicker> GenericHalls { get; set; } + + // Components to add to newly created halls + public ComponentCollection Components { get; set; } +} +``` + +### 4. `DrawFloorToTileStep` - Render to Tiles + +Converts the abstract floor plan into actual tiles on the map. + +```csharp +// From DrawFloorToTileStep.cs +[Serializable] +public class DrawFloorToTileStep : GenStep + where T : class, IFloorPlanGenContext +{ + // Tiles to pad around the border as wall terrain + public int Padding { get; set; } + + public override void Apply(T map) + { + // Create the tile array + map.CreateNew( + map.RoomPlan.DrawRect.Width + (2 * this.Padding), + map.RoomPlan.DrawRect.Height + (2 * this.Padding), + map.RoomPlan.Wrap); + + // Fill with walls + for (int ii = 0; ii < map.Width; ii++) + for (int jj = 0; jj < map.Height; jj++) + map.SetTile(new Loc(ii, jj), map.WallTerrain.Copy()); + + // Draw all rooms and halls + map.RoomPlan.DrawOnMap(map); + } +} +``` + +**Usage:** +```csharp +// Draw with 1-tile wall padding around the border +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); +``` + +## Generation Pipeline + +```mermaid +flowchart TD + subgraph Init["Initialization"] + IF[InitFloorPlanStep] + end + + subgraph Path["Path Generation"] + FP[FloorPathBranch
or other path step] + AR[AddConnectedRoomsStep] + DR[AddDisconnectedRoomsStep] + end + + subgraph Connect["Additional Connections"] + CS[ConnectStep] + CB[ConnectBranchStep] + end + + subgraph Render["Tile Rendering"] + DT[DrawFloorToTileStep] + end + + IF --> FP + FP --> AR + AR --> DR + DR --> CS + CS --> CB + CB --> DT + + style Init fill:#e1f5fe + style Path fill:#fff3e0 + style Connect fill:#e8f5e9 + style Render fill:#fce4ec +``` + +## When to Use FloorPlan vs Grid + +| Feature | FloorPlan | GridPlan | +|---------|-----------|----------| +| Room placement | Anywhere | Fixed grid cells | +| Room sizes | Any size | Constrained by cell size | +| Layout style | Organic, irregular | Structured, uniform | +| Use case | Natural caves, varied dungeons | Classic roguelike grids | +| Complexity | More complex collision handling | Simpler cell-based logic | +| Performance | Slower (collision checks) | Faster (index-based) | + +**Choose FloorPlan when:** +- You want organic, natural-looking layouts +- Rooms should vary significantly in size +- You don't need rigid structure + +**Choose GridPlan when:** +- You want predictable, structured layouts +- Classic roguelike grid aesthetics +- Performance is critical +- You need to convert to FloorPlan later anyway + +## Complete Example + +From `Ex2_Rooms/Example2.cs`: + +```csharp +public static void Run() +{ + var layout = new MapGen(); + + // Step 1: Initialize a 54x40 floor plan + InitFloorPlanStep startGen = new InitFloorPlanStep(54, 40); + layout.GenSteps.Add(-2, startGen); + + // Step 2: Define room types + var genericRooms = new SpawnList> + { + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, + }; + + // Step 3: Define hall types + var genericHalls = new SpawnList> + { + { new RoomGenAngledHall(0, new RandRange(3, 7), new RandRange(3, 7)), 10 }, + { new RoomGenSquare(new RandRange(1), new RandRange(1)), 20 }, + }; + + // Step 4: Generate branching path + FloorPathBranch path = new FloorPathBranch(genericRooms, genericHalls) + { + HallPercent = 50, + FillPercent = new RandRange(45), + BranchRatio = new RandRange(0, 25), + }; + layout.GenSteps.Add(-1, path); + + // Step 5: Render to tiles with 1-tile border + layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + + // Generate! + MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); +} +``` + +## Interface Requirements + +To use FloorPlan steps, your context must implement `IFloorPlanGenContext`: + +```csharp +// From IFloorPlanGenContext.cs +public interface IFloorPlanGenContext : ITiledGenContext +{ + FloorPlan RoomPlan { get; } + void InitPlan(FloorPlan plan); +} +``` + +## Paths/ Subdirectory + +The `Paths/` subfolder contains path generation algorithms: + +| Class | Description | +|-------|-------------| +| `FloorPathStartStep` | Base class for path generators | +| `FloorPathStartStepGeneric` | Generic version with room/hall pickers | +| `FloorPathBranch` | Branching tree path generator | +| `IFloorPathBranch` | Interface for branch-style paths | + +## Other Steps in This Folder + +| Step | Purpose | +|------|---------| +| `AddConnectedRoomsStep` | Add rooms connected to existing ones | +| `AddDisconnectedRoomsStep` | Add rooms anywhere (not connected) | +| `ConnectBranchStep` | Connect branch endpoints | +| `ClampFloorStep` | Shrink floor bounds to content | +| `ResizeFloorStep` | Resize the floor plan | +| `SetFloorPlanComponentStep` | Add components to rooms matching criteria | +| `SetSpecialRoomStep` | Mark certain rooms as special | +| `FloorStairsStep` | Place stairs in the floor plan | + +## See Also + +- [MapGen README](../README.md) - Core pipeline documentation +- [Grid README](../Grid/README.md) - Grid-based alternative +- [Ex2_Rooms](../../../RogueElements.Examples/Ex2_Rooms/) - FloorPlan example diff --git a/RogueElements/MapGen/FloorPlan/ResizeFloorStep.cs b/RogueElements/MapGen/FloorPlan/ResizeFloorStep.cs index 652a6b22..801d47c3 100644 --- a/RogueElements/MapGen/FloorPlan/ResizeFloorStep.cs +++ b/RogueElements/MapGen/FloorPlan/ResizeFloorStep.cs @@ -8,17 +8,31 @@ namespace RogueElements { /// - /// Resizes the floor plan. + /// Expands the floor plan by a specified amount in a given direction. /// - /// + /// The generation context type, which must implement . + /// + /// This step increases the floor plan dimensions, allowing additional rooms to be placed + /// in the newly created space. The expansion direction controls where the new space appears + /// relative to existing rooms. This step has no effect on wrapped floor plans. + /// [Serializable] public class ResizeFloorStep : GenStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class with default values. + /// public ResizeFloorStep() { } + /// + /// Initializes a new instance of the class with expansion in a specific direction. + /// + /// The number of tiles to add to each dimension. + /// The direction in which to add new space relative to existing rooms. + /// The direction in which to expand the draw rectangle. public ResizeFloorStep(Loc addedSize, Dir8 expandDir, Dir8 spaceExpandDir) { this.AddedSize = addedSize; @@ -26,26 +40,35 @@ public ResizeFloorStep(Loc addedSize, Dir8 expandDir, Dir8 spaceExpandDir) this.SpaceExpandDir = spaceExpandDir; } + /// + /// Initializes a new instance of the class with default space expansion. + /// + /// The number of tiles to add to each dimension. + /// The direction in which to add new space relative to existing rooms. public ResizeFloorStep(Loc addedSize, Dir8 expandDir) : this(addedSize, expandDir, Dir8.DownRight) { } /// - /// The number of tiles to add to each dimension. + /// Gets or sets the number of tiles to add to each dimension. /// public Loc AddedSize { get; set; } /// - /// The direction in which to expand the floor space relative to existing rooms. + /// Gets or sets the direction in which to expand the floor space relative to existing rooms. /// public Dir8 ExpandDir { get; set; } /// - /// The direction in which to expand the floor's draw rectangle. + /// Gets or sets the direction in which to expand the floor's draw rectangle. /// public Dir8 SpaceExpandDir { get; set; } + /// + /// Applies this step by resizing the floor plan. + /// + /// The generation context containing the floor plan. public override void Apply(T map) { if (map.RoomPlan.Wrap) diff --git a/RogueElements/MapGen/FloorPlan/RoomHallIndex.cs b/RogueElements/MapGen/FloorPlan/RoomHallIndex.cs index b4e38461..f732e951 100644 --- a/RogueElements/MapGen/FloorPlan/RoomHallIndex.cs +++ b/RogueElements/MapGen/FloorPlan/RoomHallIndex.cs @@ -8,37 +8,84 @@ namespace RogueElements { + /// + /// Represents a unique identifier for a room or hall within a . + /// + /// + /// Since rooms and halls are stored in separate lists within a FloorPlan, this struct + /// combines the list index with a flag indicating whether the element is a hall. + /// This allows unified referencing of both rooms and halls in adjacency lists and + /// graph traversal operations. + /// public struct RoomHallIndex : IEquatable { + /// + /// Indicates whether this index refers to a hall (true) or a room (false). + /// public bool IsHall; + + /// + /// The zero-based index within the rooms or halls list. + /// public int Index; + /// + /// Initializes a new instance of the struct. + /// + /// The zero-based index within the appropriate list. + /// True if referring to a hall; false for a room. public RoomHallIndex(int index, bool isHall) { this.Index = index; this.IsHall = isHall; } + /// + /// Determines whether two values are equal. + /// + /// The first value. + /// The second value. + /// True if both values are equal; otherwise, false. public static bool operator ==(RoomHallIndex value1, RoomHallIndex value2) { return value1.Equals(value2); } + /// + /// Determines whether two values are not equal. + /// + /// The first value. + /// The second value. + /// True if the values are not equal; otherwise, false. public static bool operator !=(RoomHallIndex value1, RoomHallIndex value2) { return !(value1 == value2); } + /// + /// Determines whether this instance equals another object. + /// + /// The object to compare. + /// True if the object is an equal ; otherwise, false. public override bool Equals(object obj) { return (obj is RoomHallIndex) && this.Equals((RoomHallIndex)obj); } + /// + /// Determines whether this instance equals another . + /// + /// The other value to compare. + /// True if both the index and hall flag match; otherwise, false. public bool Equals(RoomHallIndex other) { return this.IsHall == other.IsHall && this.Index == other.Index; } + /// + /// Returns a hash code for this instance. + /// + /// A hash code combining the index and hall flag. public override int GetHashCode() { return this.IsHall.GetHashCode() ^ this.Index.GetHashCode(); diff --git a/RogueElements/MapGen/FloorPlan/SetFloorPlanComponentStep.cs b/RogueElements/MapGen/FloorPlan/SetFloorPlanComponentStep.cs index fb0d441f..e5636b5d 100644 --- a/RogueElements/MapGen/FloorPlan/SetFloorPlanComponentStep.cs +++ b/RogueElements/MapGen/FloorPlan/SetFloorPlanComponentStep.cs @@ -9,27 +9,41 @@ namespace RogueElements { /// - /// Takes all rooms in the map's floor plan and gives them a specified component. - /// These components can be used to identify the room in some way for future filtering. + /// Adds components to rooms in the floor plan for tagging and filtering purposes. /// - /// + /// The generation context type, which must implement . + /// + /// Components are used to tag rooms with metadata that can be used by subsequent generation + /// steps for filtering. For example, rooms can be marked as "critical path" or "secret" + /// so that later steps can treat them differently. + /// [Serializable] public class SetFloorPlanComponentStep : GenStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public SetFloorPlanComponentStep() { this.Components = new ComponentCollection(); this.Filters = new List(); } + /// + /// Gets or sets filters to select which rooms receive the components. + /// public List Filters { get; set; } /// - /// Components to add. + /// Gets or sets the components to add to matching rooms. /// public ComponentCollection Components { get; set; } + /// + /// Applies this step by adding components to filtered rooms. + /// + /// The generation context. public override void Apply(T map) { foreach (IRoomPlan plan in map.RoomPlan.GetAllPlans()) @@ -44,6 +58,10 @@ public override void Apply(T map) } } + /// + /// Returns a string representation of this step. + /// + /// A string describing this step's configuration. public override string ToString() { return string.Format("{0}[{1}]", this.GetType().GetFormattedTypeName(), this.Components.Count); diff --git a/RogueElements/MapGen/FloorPlan/SetSpecialRoomStep.cs b/RogueElements/MapGen/FloorPlan/SetSpecialRoomStep.cs index 751b6a26..2004cff2 100644 --- a/RogueElements/MapGen/FloorPlan/SetSpecialRoomStep.cs +++ b/RogueElements/MapGen/FloorPlan/SetSpecialRoomStep.cs @@ -9,14 +9,27 @@ namespace RogueElements { /// - /// Takes an existing floor plan and changes one of the rooms into the specified room type. - /// The size of the room may change because of this, and thus may also require the addition of a supporting hallway. + /// Replaces an existing room in the floor plan with a special room type. /// - /// + /// The generation context type, which must implement . + /// + /// + /// This step is used to designate certain rooms as special areas like boss rooms, treasure + /// rooms, or shops. It finds an eligible room and replaces it with a different room generator, + /// potentially requiring support hallways if the new room is smaller than the original. + /// + /// + /// The replacement preserves connectivity by maintaining adjacencies through the new room + /// or through automatically generated support halls. + /// + /// [Serializable] public class SetSpecialRoomStep : FloorPlanStep where T : class, IFloorPlanGenContext { + /// + /// Initializes a new instance of the class. + /// public SetSpecialRoomStep() { this.Rooms = null; @@ -26,6 +39,11 @@ public SetSpecialRoomStep() this.Filters = new List(); } + /// + /// Initializes a new instance of the class with specified generators. + /// + /// The picker for room generators. + /// The picker for support hall generators. public SetSpecialRoomStep(IRandPicker> rooms, IRandPicker> halls) { this.Rooms = rooms; @@ -36,31 +54,43 @@ public SetSpecialRoomStep(IRandPicker> rooms, IRandPicker - /// The room to place. It can be chosen out of several possibilities, but only one room will be placed. + /// Gets or sets the picker for room generators. One room will be chosen and placed. /// public IRandPicker> Rooms { get; set; } /// - /// Determines which rooms are eligible to be turned into the new room type. + /// Gets or sets filters that determine which existing rooms are eligible for replacement. /// public List Filters { get; set; } /// - /// Components that the newly added room will be labeled with. + /// Gets or sets components to add to the newly placed room. /// public ComponentCollection RoomComponents { get; set; } /// - /// When changing a room to a new type, the new type may be smaller and require a supporting hallway. - /// This variable determines the room types that can be used as the intermediate hall. + /// Gets or sets the picker for support hallway generators. /// + /// + /// When the new room is smaller than the original, support halls are added to maintain + /// connectivity with rooms that were adjacent to the original. + /// public IRandPicker> Halls { get; set; } /// - /// Components that the newly added halls will be labeled with. + /// Gets or sets components to add to any support halls created. /// public ComponentCollection HallComponents { get; set; } + /// + /// Calculates the support rectangle needed to connect a new room to adjacent rooms in a given direction. + /// + /// The floor plan. + /// The original room generator being replaced. + /// The new room generator. + /// The direction to check for adjacents. + /// The adjacent rooms in the specified direction. + /// The rectangle defining the support hall's bounds. public static Rect GetSupportRect(FloorPlan floorPlan, IRoomGen oldGen, IRoomGen newGen, Dir4 dir, List adjacentsInDir) { bool vertical = dir.ToAxis() == Axis4.Vert; diff --git a/RogueElements/MapGen/GenContextDebug.cs b/RogueElements/MapGen/GenContextDebug.cs index 705bb2a7..2f203515 100644 --- a/RogueElements/MapGen/GenContextDebug.cs +++ b/RogueElements/MapGen/GenContextDebug.cs @@ -10,26 +10,158 @@ namespace RogueElements { + /// + /// Provides debug events and hooks for monitoring the map generation process. + /// + /// + /// + /// is a static utility class that exposes events fired during + /// execution. These events enable visualization, logging, + /// debugging, and step-by-step analysis of the generation process. + /// + /// + /// Events are raised at key points in the generation lifecycle: + /// + /// - After map context initialization, before any steps run + /// - Before each generation step executes + /// - After each generation step completes + /// - For progress updates during step execution + /// - When a generation step throws an exception + /// + /// + /// + /// These events are particularly useful for: + /// + /// Building map generation visualizers that show step-by-step progress + /// Debugging problematic generation steps + /// Logging generation metrics and timing + /// Implementing breakpoints or step-through debugging + /// + /// + /// + /// + /// + /// // Subscribe to generation events for debugging + /// GenContextDebug.OnInit += (map) => + /// Console.WriteLine($"Generation started with seed: {map.Rand}"); + /// + /// GenContextDebug.OnStepIn += (stepName) => + /// Console.WriteLine($"Starting step: {stepName}"); + /// + /// GenContextDebug.OnStepOut += () => + /// Console.WriteLine("Step completed"); + /// + /// GenContextDebug.OnError += (ex) => + /// Console.WriteLine($"Step failed: {ex.Message}"); + /// + /// // Now generate a map - events will fire during generation + /// var map = layout.GenMap(seed); + /// + /// + /// public static class GenContextDebug { + /// + /// Occurs after the map context is initialized but before any generation steps execute. + /// + /// + /// The event handler receives the newly created instance + /// after has been called. This is useful for + /// capturing the initial map state or setting up per-generation debug state. + /// public static event Action OnInit; + /// + /// Occurs when a generation step reports progress during its execution. + /// + /// + /// This event is triggered by and can be called by + /// implementations to report intermediate progress for + /// long-running steps. The string parameter contains a progress message. + /// public static event Action OnStep; + /// + /// Occurs immediately before a generation step begins execution. + /// + /// + /// The string parameter contains the step's name (from ). + /// This event pairs with to bracket step execution, + /// enabling timing measurements and hierarchical visualization. + /// public static event Action OnStepIn; + /// + /// Occurs immediately after a generation step completes execution. + /// + /// + /// This event fires after each step completes, regardless of whether the step + /// succeeded or threw an exception. It pairs with to + /// bracket step execution. + /// public static event Action OnStepOut; + /// + /// Occurs when a generation step throws an exception during execution. + /// + /// + /// + /// Exceptions are caught by and reported via this event, + /// allowing generation to continue with subsequent steps. The exception is passed + /// to handlers for logging or debugging purposes. + /// + /// + /// Note that generation continues after an error, which may result in incomplete + /// or malformed maps depending on which step failed. + /// + /// public static event Action OnError; + /// + /// Raises the event to signal that a generation step is starting. + /// + /// The name or description of the step about to execute. + /// + /// This method is called internally by before each step executes. + /// public static void StepIn(string msg) => OnStepIn?.Invoke(msg); + /// + /// Raises the event to signal that a generation step has completed. + /// + /// + /// This method is called internally by after each step completes. + /// public static void StepOut() => OnStepOut?.Invoke(); + /// + /// Raises the event to signal that generation has been initialized. + /// + /// The newly initialized map context. + /// + /// This method is called internally by after calling + /// but before any generation steps execute. + /// public static void DebugInit(IGenContext map) => OnInit?.Invoke(map); + /// + /// Raises the event to report generation progress. + /// + /// A message describing the current progress. + /// + /// implementations can call this method to report + /// intermediate progress for long-running operations. + /// public static void DebugProgress(string msg) => OnStep?.Invoke(msg); + /// + /// Raises the event to report an exception during generation. + /// + /// The exception that was thrown. + /// + /// This method is called internally by when a step throws + /// an exception. The exception is reported to handlers but generation continues. + /// public static void DebugError(Exception ex) => OnError?.Invoke(ex); } } diff --git a/RogueElements/MapGen/GenStep.cs b/RogueElements/MapGen/GenStep.cs index da1bb888..47167596 100644 --- a/RogueElements/MapGen/GenStep.cs +++ b/RogueElements/MapGen/GenStep.cs @@ -8,18 +8,118 @@ namespace RogueElements { + /// + /// Base class for all map generation steps that modify a specific type of map context. + /// + /// + /// The type of map context this step operates on. Must implement . + /// Constraining to more specific interfaces (like or + /// ) enables type-safe access to specialized map features. + /// + /// + /// + /// is the foundation of the RogueElements pipeline architecture. + /// Each step represents a discrete transformation applied to the map during generation, + /// such as placing rooms, adding terrain features, or spawning entities. + /// + /// + /// To create a custom generation step: + /// + /// Inherit from with appropriate type constraints + /// Override to implement the generation logic + /// Add the step to a via its collection + /// + /// + /// + /// Steps should use for all random decisions to ensure + /// reproducible map generation when using the same seed. + /// + /// + /// All subclasses should be marked with + /// to support save/load functionality and map layout serialization. + /// + /// + /// + /// + /// // A simple step that fills the map with floor tiles + /// [Serializable] + /// public class FillFloorStep : GenStep<ITiledGenContext> + /// { + /// public override void Apply(ITiledGenContext map) + /// { + /// for (int x = 0; x < map.Width; x++) + /// { + /// for (int y = 0; y < map.Height; y++) + /// { + /// map.SetTile(new Loc(x, y), map.RoomTerrain); + /// } + /// } + /// } + /// } + /// + /// // Add to a layout + /// layout.GenSteps.Add(new Priority(1), new FillFloorStep()); + /// + /// [Serializable] public abstract class GenStep : IGenStep where T : class, IGenContext { - // change activemap into an interface that supports tile, mob, and item modification + /// + /// Applies this generation step to the specified map context. + /// + /// + /// The map context to modify. Provides access to map data, the random number generator, + /// and any features exposed by the interface. + /// + /// + /// + /// Implementations should use for all random decisions + /// to maintain reproducibility when generating maps with the same seed. + /// + /// + /// This method is called by for each step in priority order. + /// Steps can assume that all lower-priority steps have already been applied. + /// + /// public abstract void Apply(T map); + /// + /// Determines whether this step can be applied to the specified context. + /// + /// The generation context to check for compatibility. + /// + /// if the is assignable to type ; + /// otherwise, . + /// + /// + /// This method enables runtime type checking when working with non-generic step collections, + /// allowing the generation system to verify compatibility before applying a step. + /// public bool CanApply(IGenContext context) { return context is T; } + /// + /// Applies this generation step to the specified context after performing type validation. + /// + /// The generation context to modify. + /// + /// Thrown when is not assignable to type . + /// The exception message includes both the actual context type and the expected step type. + /// + /// + /// + /// This method implements and provides the non-generic interface + /// used by during generation. It performs a type cast and delegates + /// to the strongly-typed method. + /// + /// + /// Use to check compatibility before calling this method if you need + /// to avoid exceptions. + /// + /// public void Apply(IGenContext context) { if (context is T map) diff --git a/RogueElements/MapGen/Grid/ConnectGridBranchStep.cs b/RogueElements/MapGen/Grid/ConnectGridBranchStep.cs index 6ffabbf5..0cdf4a94 100644 --- a/RogueElements/MapGen/Grid/ConnectGridBranchStep.cs +++ b/RogueElements/MapGen/Grid/ConnectGridBranchStep.cs @@ -9,15 +9,28 @@ namespace RogueElements { /// - /// Takes the current grid plan and connects the ends of its branches to other rooms. - /// A room is considered the end of a branch when it is connected to only one other room. - /// ie, a dead end. + /// Connects dead-end rooms in the grid plan to create additional paths. /// - /// + /// The map context type, which must implement . + /// + /// + /// This step identifies rooms that are branch ends (connected to only one other room) + /// and creates additional hall connections to nearby rooms. This reduces dead ends + /// and creates more interconnected layouts. + /// + /// + /// The algorithm traces back from each dead end until it finds a room with multiple + /// possible connections, then randomly selects one to connect. + /// + /// + /// [Serializable] public class ConnectGridBranchStep : GridPlanStep where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public ConnectGridBranchStep() { this.GenericHalls = new SpawnList>(); @@ -25,6 +38,11 @@ public ConnectGridBranchStep() this.Filters = new List(); } + /// + /// Initializes a new instance of the class + /// with the specified connection percentage. + /// + /// The percentage of eligible branches to connect. public ConnectGridBranchStep(int connectPercent) : this() { @@ -32,25 +50,30 @@ public ConnectGridBranchStep(int connectPercent) } /// - /// The percentage of eligible branches to connect. + /// Gets or sets the percentage of eligible branches to connect. /// public int ConnectPercent { get; set; } /// - /// Determines which rooms are eligible to be connected. + /// Gets or sets the filters that determine which rooms are eligible to be connected. /// public List Filters { get; set; } /// - /// The room types that can be used as the hall connecting the two base rooms. + /// Gets or sets the hall generators that can be used for connecting halls. /// public IRandPicker> GenericHalls { get; set; } /// - /// Components that the newly added halls will be labeled with. + /// Gets or sets the components to attach to newly created halls. /// public ComponentCollection HallComponents { get; set; } + /// + /// Finds branch ends and connects them to adjacent rooms. + /// + /// The random number generator. + /// The grid plan to modify. public override void ApplyToPath(IRandom rand, GridPlan floorPlan) { List endBranches = new List(); diff --git a/RogueElements/MapGen/Grid/DrawGridToFloorStep.cs b/RogueElements/MapGen/Grid/DrawGridToFloorStep.cs index 05f96b54..1a8683a3 100644 --- a/RogueElements/MapGen/Grid/DrawGridToFloorStep.cs +++ b/RogueElements/MapGen/Grid/DrawGridToFloorStep.cs @@ -9,18 +9,39 @@ namespace RogueElements { /// - /// Takes the grid plan of the map and draws all cells and halls into rooms of a floor plan. - /// This is typically done once per floor generation. It must only be done after the grid plan itself is complete. + /// Converts the grid plan into a floor plan by calculating room and hall bounds. /// - /// + /// The map context type, which must implement . + /// + /// + /// This step bridges the gap between grid-based layout and tile-based generation. + /// It creates a new and populates it with rooms and halls + /// from the . + /// + /// + /// This step should be executed once after the grid plan is fully populated with + /// rooms and halls. After this step, floor plan-based generation steps can be used + /// for additional modifications before tile drawing occurs. + /// + /// + /// + /// + /// [Serializable] public class DrawGridToFloorStep : GenStep where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public DrawGridToFloorStep() { } + /// + /// Creates a floor plan from the grid plan and places all rooms and halls. + /// + /// The map context containing the grid plan to convert. public override void Apply(T map) { var floorPlan = new FloorPlan(); diff --git a/RogueElements/MapGen/Grid/GridHallGroup.cs b/RogueElements/MapGen/Grid/GridHallGroup.cs index 3b12f240..e24e1dd6 100644 --- a/RogueElements/MapGen/Grid/GridHallGroup.cs +++ b/RogueElements/MapGen/Grid/GridHallGroup.cs @@ -9,19 +9,45 @@ namespace RogueElements { /// - /// Contains data about which cells a room occupies in a GridFloorPlan. + /// Represents a logical hall connection between two adjacent cells in a . /// + /// + /// + /// A hall group contains one or more segments. In simple cases, + /// there is only one segment. However, when the rooms on either side of the hall are offset + /// from each other, the hall may need to be split into multiple segments to properly connect them. + /// + /// + /// The property provides access to the primary hall segment, + /// which is used for initial hall setup before bounds calculation. + /// + /// + /// + /// public class GridHallGroup { + /// + /// Initializes a new instance of the class. + /// public GridHallGroup() { this.HallParts = new List(); } + /// + /// Gets the primary hall segment, or null if no hall exists. + /// public GridHallPlan MainHall => this.HallParts.Count > 0 ? this.HallParts[0] : null; + /// + /// Gets the list of hall segments that make up this logical hall connection. + /// public List HallParts { get; } + /// + /// Sets or clears the hall for this connection. + /// + /// The hall plan to set, or null to clear the hall. public void SetHall(GridHallPlan plan) { this.HallParts.Clear(); diff --git a/RogueElements/MapGen/Grid/GridHallPlan.cs b/RogueElements/MapGen/Grid/GridHallPlan.cs index 3a9ef3fa..9a3b9339 100644 --- a/RogueElements/MapGen/Grid/GridHallPlan.cs +++ b/RogueElements/MapGen/Grid/GridHallPlan.cs @@ -9,22 +9,49 @@ namespace RogueElements { /// - /// Contains data about which cells a room occupies in a GridFloorPlan. + /// Represents a single hall segment in a . /// + /// + /// + /// A hall connects two adjacent cells in the grid. When rooms are offset from each other, + /// a single logical hall may be split into multiple segments + /// during the process. + /// + /// + /// Hall plans use generators because halls must be able + /// to connect rooms at various positions and sizes. + /// + /// + /// + /// public class GridHallPlan : IRoomPlan { + /// + /// Initializes a new instance of the class. + /// + /// The permissive room generator for this hall. + /// The components to attach to this hall. public GridHallPlan(IPermissiveRoomGen roomGen, ComponentCollection components) { this.RoomGen = roomGen; this.Components = components; } + /// + /// Gets the permissive room generator for this hall. + /// public IPermissiveRoomGen RoomGen { get; } + /// IRoomGen IRoomPlan.RoomGen => this.RoomGen; - // This member will be assigned by reference to the Components of FloorHallPlan, - // as well as to the components of any halls it is split into during bounds calculation + /// + /// Gets the components attached to this hall. + /// + /// + /// This collection is shared by reference with the corresponding + /// and any hall segments created when the hall is split during bounds calculation. + /// public ComponentCollection Components { get; } } } diff --git a/RogueElements/MapGen/Grid/GridPlan.cs b/RogueElements/MapGen/Grid/GridPlan.cs index 4ac48648..5e2bd7ad 100644 --- a/RogueElements/MapGen/Grid/GridPlan.cs +++ b/RogueElements/MapGen/Grid/GridPlan.cs @@ -9,24 +9,73 @@ namespace RogueElements { /// - /// A dungeon layout that uses a rectangular array of rooms, connected to each other in cardinal directions. + /// Represents a dungeon layout using a rectangular grid of cells, where each cell can contain a room + /// and cells are connected to adjacent cells via hallways in cardinal directions. /// + /// + /// + /// The grid plan provides a structured approach to dungeon generation where rooms are placed + /// within discrete cells of a grid. Each cell has uniform dimensions, and hallways connect + /// adjacent cells horizontally or vertically. + /// + /// + /// The layout supports multi-cell rooms (rooms that span multiple grid cells), wrapped maps + /// (where edges connect to opposite edges), and configurable cell dimensions and wall thicknesses. + /// + /// + /// After populating the grid with rooms and halls using subclasses, + /// call to convert the grid plan into a + /// for actual tile-based generation. + /// + /// + /// + /// + /// public class GridPlan { + /// + /// Initializes a new instance of the class. + /// public GridPlan() { } + /// + /// Gets or sets the thickness of dividers between cells, in tiles. + /// + /// + /// This value determines the space between cells where hallways are placed. + /// Must be at least 1. + /// public int CellWall { get; set; } + /// + /// Gets or sets the width of each cell in the grid, in tiles. + /// public int WidthPerCell { get; set; } + /// + /// Gets or sets the height of each cell in the grid, in tiles. + /// public int HeightPerCell { get; set; } + /// + /// Gets the number of columns in the grid. + /// public int GridWidth => this.Rooms.Length; + /// + /// Gets the number of rows in the grid. + /// public int GridHeight => this.Rooms[0].Length; + /// + /// Gets the total size of the map in tiles. + /// + /// + /// The size is calculated based on the grid dimensions, cell sizes, and wall thickness. + /// For wrapped maps, the outer wall is included; for non-wrapped maps, it is excluded. + /// public Loc Size { get @@ -37,20 +86,52 @@ public Loc Size } } + /// + /// Gets or sets a value indicating whether the map wraps around at the edges. + /// + /// + /// When enabled, the left edge connects to the right edge, and the top edge connects + /// to the bottom edge, creating a toroidal topology. + /// public bool Wrap { get; set; } + /// + /// Gets the total number of rooms in the grid plan. + /// public int RoomCount => this.ArrayRooms.Count; + /// + /// Gets or sets the 2D array mapping grid coordinates to room indices. + /// A value of -1 indicates an empty cell. + /// protected int[][] Rooms { get; set; } + /// + /// Gets or sets the 2D array of vertical hall groups connecting cells vertically. + /// protected GridHallGroup[][] VHalls { get; set; } + /// + /// Gets or sets the 2D array of horizontal hall groups connecting cells horizontally. + /// protected GridHallGroup[][] HHalls { get; set; } - // list of all rooms on the entire floor - // each entry is a different room, guaranteed + /// + /// Gets or sets the list of all rooms in the grid plan. + /// Each entry represents a unique room that may span one or more cells. + /// protected List ArrayRooms { get; set; } + /// + /// Initializes the grid plan with the specified dimensions and cell properties. + /// + /// The number of columns in the grid. + /// The number of rows in the grid. + /// The width of each cell in tiles. + /// The height of each cell in tiles. + /// The thickness of dividers between cells, in tiles. Must be at least 1. + /// Whether the map wraps around at the edges. + /// Thrown when is less than 1. public void InitSize(int width, int height, int widthPerCell, int heightPerCell, int cellWall = 1, bool wrap = false) { this.Rooms = new int[width][]; @@ -79,6 +160,9 @@ public void InitSize(int width, int height, int widthPerCell, int heightPerCell, this.Wrap = wrap; } + /// + /// Clears all rooms and halls from the grid while preserving the grid dimensions. + /// public void Clear() { int width = this.GridWidth; @@ -104,9 +188,21 @@ public void Clear() } /// - /// Generates the position and size of each room and hall, and places it in the specified IFloorPlanGenContext. + /// Generates the position and size of each room and hall, and places them into the floor plan. /// - /// + /// The floor plan generation context to populate with rooms and halls. + /// + /// + /// This method converts the abstract grid plan into concrete room and hall placements. + /// It performs the following steps: + /// + /// + /// Determines the bounds for each room within its cell(s). + /// Calculates hall bounds and handles cases where halls need to be split. + /// Adds all rooms to the floor plan, respecting the setting. + /// Connects rooms with the appropriate hallways. + /// + /// public void PlaceRoomsOnFloor(IFloorPlanGenContext map) { // decide on room sizes @@ -233,15 +329,19 @@ public void PlaceRoomsOnFloor(IFloorPlanGenContext map) } /// - /// Returns the RoomGen found in the specified hall. + /// Gets the hall plan at the specified location and direction. /// - /// The location of the room + the direction of the connecting hall relative to the room. - /// + /// The grid location and direction of the hall relative to a room. + /// The at the specified location, or null if no hall exists. public GridHallPlan GetHall(LocRay4 locRay) { return this.GetHallGroup(locRay)?.MainHall; } + /// + /// Enumerates all room and hall plans in the grid. + /// + /// An enumerable of all instances, including both rooms and halls. public IEnumerable GetAllPlans() { foreach (GridRoomPlan plan in this.ArrayRooms) @@ -266,11 +366,22 @@ public IEnumerable GetAllPlans() } } + /// + /// Wraps a grid location to valid coordinates for wrapped maps. + /// + /// The grid location to wrap. + /// The wrapped location within grid bounds. public Loc WrapRoom(Loc loc) { return Loc.Wrap(loc, new Loc(this.GridWidth, this.GridHeight)); } + /// + /// Checks if two rectangles collide, accounting for map wrapping if enabled. + /// + /// The first rectangle. + /// The second rectangle. + /// True if the rectangles collide; otherwise, false. public bool Collides(Rect rect1, Rect rect2) { if (this.Wrap) @@ -279,6 +390,12 @@ public bool Collides(Rect rect1, Rect rect2) return Collision.Collides(rect1, rect2); } + /// + /// Checks if a location is within a rectangle, accounting for map wrapping if enabled. + /// + /// The bounding rectangle. + /// The location to check. + /// True if the location is within the rectangle; otherwise, false. public bool InBounds(Rect rect, Loc loc) { if (this.Wrap) @@ -287,16 +404,31 @@ public bool InBounds(Rect rect, Loc loc) return Collision.InBounds(rect, loc); } + /// + /// Gets the room generator at the specified index. + /// + /// The index of the room in the room list. + /// The for the room. public IRoomGen GetRoom(int index) { return this.ArrayRooms[index].RoomGen; } + /// + /// Gets the room plan at the specified index. + /// + /// The index of the room in the room list. + /// The for the room. public GridRoomPlan GetRoomPlan(int index) { return this.ArrayRooms[index]; } + /// + /// Gets the room plan at the specified grid location. + /// + /// The grid coordinates of the cell. + /// The at that location, or null if the cell is empty. public GridRoomPlan GetRoomPlan(Loc loc) { int index = this.GetRoomIndex(loc); @@ -305,6 +437,11 @@ public GridRoomPlan GetRoomPlan(Loc loc) return null; } + /// + /// Gets the index of the room at the specified grid location. + /// + /// The grid coordinates of the cell. + /// The room index, or -1 if the cell is empty or out of bounds. public int GetRoomIndex(Loc loc) { if (this.Wrap) @@ -315,6 +452,10 @@ public int GetRoomIndex(Loc loc) return this.Rooms[loc.X][loc.Y]; } + /// + /// Removes the room at the specified grid location and updates all room indices. + /// + /// The grid coordinates of the room to erase. public void EraseRoom(Loc loc) { if (this.Wrap) @@ -343,21 +484,49 @@ public void EraseRoom(Loc loc) } } + /// + /// Adds a room to a single cell in the grid. + /// + /// The grid coordinates of the cell. + /// The room generator to use. + /// The components to attach to the room. public void AddRoom(Loc loc, IRoomGen gen, ComponentCollection components) { this.AddRoom(new Rect(loc, new Loc(1)), gen, components, false); } + /// + /// Adds a room to a single cell in the grid with hall preference. + /// + /// The grid coordinates of the cell. + /// The room generator to use. + /// The components to attach to the room. + /// Whether the room should be treated as a hall when added to the floor plan. public void AddRoom(Loc loc, IRoomGen gen, ComponentCollection components, bool preferHall) { this.AddRoom(new Rect(loc, new Loc(1)), gen, components, preferHall); } + /// + /// Adds a multi-cell room to the grid. + /// + /// The grid rectangle defining which cells the room occupies. + /// The room generator to use. + /// The components to attach to the room. public void AddRoom(Rect rect, IRoomGen gen, ComponentCollection components) { this.AddRoom(rect, gen, components, false); } + /// + /// Adds a multi-cell room to the grid with hall preference. + /// + /// The grid rectangle defining which cells the room occupies. + /// The room generator to use. + /// The components to attach to the room. + /// Whether the room should be treated as a hall when added to the floor plan. + /// Thrown when the room is out of bounds or larger than the grid. + /// Thrown when adding on top of an existing room or hall, or when preferHall is true but gen is not permissive. public void AddRoom(Rect rect, IRoomGen gen, ComponentCollection components, bool preferHall) { Rect floorRect = new Rect(0, 0, this.GridWidth, this.GridHeight); @@ -414,6 +583,11 @@ public void AddRoom(Rect rect, IRoomGen gen, ComponentCollection components, boo } } + /// + /// Checks if a room can be added at the specified grid rectangle. + /// + /// The grid rectangle to check. + /// True if a room can be added at the specified location; otherwise, false. public bool CanAddRoom(Rect rect) { Rect floorRect = new Rect(0, 0, this.GridWidth, this.GridHeight); @@ -447,11 +621,12 @@ public bool CanAddRoom(Rect rect) } /// - /// Sets the RoomGen found in the specified hall. + /// Sets or clears the hall at the specified location and direction. /// - /// The location of the room + the direction of the connecting hall relative to the room. - /// - /// components to include in the hall + /// The grid location and direction of the hall relative to a room. + /// The hall generator to use, or null to clear the hall. + /// The components to attach to the hall. + /// Thrown when the hall position is invalid. public void SetHall(LocRay4 locRay, IPermissiveRoomGen hallGen, ComponentCollection components) { GridHallPlan plan = null; @@ -466,10 +641,15 @@ public void SetHall(LocRay4 locRay, IPermissiveRoomGen hallGen, ComponentCollect } /// - /// Decides on the room bounds for each room. Results will be out of bounds and unwrapped in wrapped floor scenarios. + /// Determines the tile-space bounds for a room within its grid cell(s). /// - /// - /// + /// The random number generator. + /// The index of the room to process. + /// + /// The room generator proposes a size, which is capped to fit within the cell bounds. + /// The room is then randomly positioned within the available cell space. + /// For wrapped maps, results may be unwrapped (extending beyond normal bounds). + /// public void ChooseRoomBounds(IRandom rand, int roomIndex) { GridRoomPlan roomPair = this.ArrayRooms[roomIndex]; @@ -485,12 +665,23 @@ public void ChooseRoomBounds(IRandom rand, int roomIndex) } /// - /// Decides on the bounds for each hall. Also writes to the adjacent rooms' SideReqs and tile permissions + /// Determines the tile-space bounds for a hall connecting two adjacent cells. /// - /// - /// - /// - /// todo: describe rand parameter on ChooseHallBounds + /// The random number generator. + /// The X coordinate of the hall in the grid. + /// The Y coordinate of the hall in the grid. + /// True for vertical halls, false for horizontal halls. + /// + /// + /// This method calculates the hall dimensions based on the rooms it connects. + /// It handles complex cases where rooms may be offset from each other, potentially + /// splitting the hall into multiple segments if needed. + /// + /// + /// The algorithm considers the fulfillable borders of each room to ensure the hall + /// connects to valid entry points. + /// + /// public void ChooseHallBounds(IRandom rand, int x, int y, bool vertical) { GridHallGroup hallGroup = vertical ? this.VHalls[x][y] : this.HHalls[x][y]; @@ -638,6 +829,11 @@ public void ChooseHallBounds(IRandom rand, int x, int y, bool vertical) } } + /// + /// Gets the indices of all rooms connected to the specified room via halls. + /// + /// The index of the room to query. + /// A list of room indices that are adjacent (connected by halls) to the specified room. public List GetAdjacentRooms(int roomIndex) { List returnList = new List(); @@ -671,6 +867,11 @@ public List GetAdjacentRooms(int roomIndex) return returnList; } + /// + /// Gets the room index connected via a hall in the specified direction. + /// + /// The location and direction to check. + /// The index of the connected room, or -1 if no hall exists in that direction. public int GetRoomIndex(LocRay4 locRay) { GridHallPlan hall = this.GetHall(locRay); @@ -683,6 +884,11 @@ public int GetRoomIndex(LocRay4 locRay) return -1; } + /// + /// Converts grid cell bounds to tile-space bounds. + /// + /// The grid cell rectangle. + /// The corresponding tile-space rectangle. public virtual Rect GetCellBounds(Rect bounds) { return new Rect( @@ -693,14 +899,18 @@ public virtual Rect GetCellBounds(Rect bounds) } /// - /// Gets the minimum range along the side of a room that includes all of its fulfillable borders. - /// Special cases arise if the room is multi-cell. + /// Calculates the range along a room's side where a hall can connect. /// - /// todo: describe rect parameter on GetHallTouchRange - /// todo: describe borderQuery parameter on GetHallTouchRange - /// Direction from room to hall. - /// - /// + /// The tile-space rectangle of the room. + /// A function that returns true if a border tile at the given position is fulfillable. + /// The direction from the room toward the hall. + /// The grid coordinate perpendicular to the hall direction. + /// The range of valid tile positions where the hall can connect. + /// + /// This method handles the complex case of multi-cell rooms where the hall may need + /// to extend beyond the current cell to reach a fulfillable border tile. + /// + /// Thrown when no fulfillable border tile exists. public virtual IntRange GetHallTouchRange(Rect rect, Func borderQuery, Dir4 dir, int tier) { bool vertical = dir.ToAxis() == Axis4.Vert; @@ -776,6 +986,13 @@ public virtual IntRange GetHallTouchRange(Rect rect, Func borde return endRange; } + /// + /// Gets the hall group at the specified location and direction. + /// + /// The grid location and direction of the hall. + /// The at the specified location, or null if out of bounds. + /// Thrown when the direction is invalid. + /// Thrown when the direction is None. private GridHallGroup GetHallGroup(LocRay4 locRay) { if (!locRay.Dir.Validate()) diff --git a/RogueElements/MapGen/Grid/GridPlanStep.cs b/RogueElements/MapGen/Grid/GridPlanStep.cs index 51c03357..91b53ca0 100644 --- a/RogueElements/MapGen/Grid/GridPlanStep.cs +++ b/RogueElements/MapGen/Grid/GridPlanStep.cs @@ -9,16 +9,44 @@ namespace RogueElements { + /// + /// Base class for generation steps that operate on a . + /// + /// The map context type, which must implement . + /// + /// + /// Grid plan steps modify the grid layout by adding, removing, or modifying rooms and halls. + /// They operate on the abstract grid structure rather than the actual tile map. + /// + /// + /// Subclasses must implement to define their grid modification logic. + /// The base method automatically extracts the grid plan from the map context. + /// + /// + /// + /// [Serializable] public abstract class GridPlanStep : GenStep where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// protected GridPlanStep() { } + /// + /// Applies the generation logic to the grid plan. + /// + /// The random number generator. + /// The grid plan to modify. public abstract void ApplyToPath(IRandom rand, GridPlan floorPlan); + /// + /// Applies this generation step to the map by delegating to . + /// + /// The map context containing the grid plan. public override void Apply(T map) { // actual map creation step diff --git a/RogueElements/MapGen/Grid/GridRoomPlan.cs b/RogueElements/MapGen/Grid/GridRoomPlan.cs index bb3f2229..49bd80e1 100644 --- a/RogueElements/MapGen/Grid/GridRoomPlan.cs +++ b/RogueElements/MapGen/Grid/GridRoomPlan.cs @@ -9,11 +9,30 @@ namespace RogueElements { /// - /// Contains data about which cells a room occupies in a GridFloorPlan. + /// Represents a room in a , tracking its cell bounds and generation settings. /// + /// + /// + /// A grid room plan stores the grid cell bounds (which cells the room occupies), + /// the room generator, and any attached components. Rooms can span multiple cells + /// for larger room types. + /// + /// + /// When converted to a via , + /// rooms with set to true will be added as halls rather than rooms. + /// + /// + /// + /// [Serializable] public class GridRoomPlan : IRoomPlan { + /// + /// Initializes a new instance of the class. + /// + /// The grid cell rectangle that this room occupies. + /// The room generator for this room. + /// The components to attach to this room. public GridRoomPlan(Rect bounds, IRoomGen roomGen, ComponentCollection components) { this.Bounds = bounds; @@ -21,17 +40,33 @@ public GridRoomPlan(Rect bounds, IRoomGen roomGen, ComponentCollection component this.Components = components; } + /// + /// Gets or sets the grid cell rectangle that this room occupies. + /// public Rect Bounds { get; set; } /// - /// Prefers to be counted as a hall when translated into floorplan rooms + /// Gets or sets a value indicating whether this room should be counted as a hall + /// when translated into floor plan rooms. /// + /// + /// Hall-preferred rooms are typically single-tile connector rooms or corridors + /// that serve as passageways rather than destination rooms. + /// public bool PreferHall { get; set; } + /// + /// Gets or sets the room generator for this room. + /// public IRoomGen RoomGen { get; set; } - // TODO: needs a better class. Only one RoomComponent subclass allowed per collection. Also better lookup. - // This member will be assigned by reference to the Components of FloorRoomPlan + /// + /// Gets or sets the components attached to this room. + /// + /// + /// This collection is shared by reference with the corresponding + /// when the grid is converted to a floor plan. + /// public ComponentCollection Components { get; set; } } } diff --git a/RogueElements/MapGen/Grid/IRoomGridGenContext.cs b/RogueElements/MapGen/Grid/IRoomGridGenContext.cs index 1ff4597b..3d7a2ea4 100644 --- a/RogueElements/MapGen/Grid/IRoomGridGenContext.cs +++ b/RogueElements/MapGen/Grid/IRoomGridGenContext.cs @@ -8,10 +8,33 @@ namespace RogueElements { + /// + /// Defines a map generation context that supports grid-based room layouts. + /// + /// + /// + /// This interface extends to add support for + /// grid-based dungeon generation using a . + /// + /// + /// Implementations must provide access to the current grid plan and a method + /// to initialize it. The grid plan is typically converted to a floor plan + /// using before tile-level generation occurs. + /// + /// + /// + /// public interface IRoomGridGenContext : IFloorPlanGenContext { + /// + /// Gets the current grid plan for this map context. + /// GridPlan GridPlan { get; } + /// + /// Initializes the map context with a grid plan. + /// + /// The grid plan to use for this map. void InitGrid(GridPlan plan); } } diff --git a/RogueElements/MapGen/Grid/InitGridPlanStep.cs b/RogueElements/MapGen/Grid/InitGridPlanStep.cs index a994b356..8369f660 100644 --- a/RogueElements/MapGen/Grid/InitGridPlanStep.cs +++ b/RogueElements/MapGen/Grid/InitGridPlanStep.cs @@ -9,55 +9,81 @@ namespace RogueElements { /// - /// Initializes an empty grid plan, which is a grid of rooms and connecting hallways. - /// Unlike a floor plan, the rooms in a grid plan are rigidly defined to be cells in a grid, instead of being placed in freestyle. - /// Gen Steps that operate on the grid plan can add, erase, or change rooms/hallways. - /// Once finished, apply DrawGridToFloorStep to take the grid and create a floor plan. + /// Initializes an empty grid plan with the specified dimensions and cell properties. /// - /// + /// The map context type, which must implement . + /// + /// + /// This step creates a new with empty cells. Unlike a floor plan where + /// rooms can be placed freely, a grid plan uses a rigid cell-based structure where rooms + /// occupy one or more cells and halls connect adjacent cells. + /// + /// + /// After initialization, use subclasses to populate the grid + /// with rooms and halls. Once the grid is complete, use + /// to convert it to a floor plan for tile-level generation. + /// + /// + /// + /// [Serializable] public class InitGridPlanStep : GenStep where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public InitGridPlanStep() { } + /// + /// Initializes a new instance of the class with the specified wall thickness. + /// + /// The thickness of dividers between cells, in tiles. public InitGridPlanStep(int cellWall) { this.CellWall = cellWall; } /// - /// The width of all cells in the grid. + /// Gets or sets the width of all cells in the grid, in tiles. /// public int CellWidth { get; set; } /// - /// The height of all cells in the grid. + /// Gets or sets the height of all cells in the grid, in tiles. /// public int CellHeight { get; set; } /// - /// The number of columns in the grid. + /// Gets or sets the number of columns in the grid. /// public int CellX { get; set; } /// - /// The number of rows in the grid. + /// Gets or sets the number of rows in the grid. /// public int CellY { get; set; } /// - /// The thickness of the dividers between cells in the grid, in tiles. + /// Gets or sets the thickness of the dividers between cells in the grid, in tiles. /// public int CellWall { get; set; } /// - /// Determines if the map is wrapped around. + /// Gets or sets a value indicating whether the map wraps around at the edges. /// + /// + /// When enabled, the left edge connects to the right edge, and the top edge connects + /// to the bottom edge, creating a toroidal topology. + /// public bool Wrap { get; set; } + /// + /// Creates and initializes a new grid plan with the configured dimensions. + /// + /// The map context to initialize. public override void Apply(T map) { // initialize grid diff --git a/RogueElements/MapGen/Grid/Paths/GridPathCross.cs b/RogueElements/MapGen/Grid/Paths/GridPathCross.cs index 50c96318..4f894dbc 100644 --- a/RogueElements/MapGen/Grid/Paths/GridPathCross.cs +++ b/RogueElements/MapGen/Grid/Paths/GridPathCross.cs @@ -8,19 +8,38 @@ namespace RogueElements { /// - /// Creates a grid plan made up of a center room and halls and rooms extending off in the four cardinal directions. - /// For best results, it is recommended to make grid height and width odd numbers. + /// Creates a cross-shaped layout with rooms extending in four cardinal directions from the center. /// - /// + /// The map context type, which must implement . + /// + /// + /// This path generator creates a layout shaped like a plus sign (+), with a central room + /// and connected rooms extending horizontally and vertically. All rooms are connected + /// via hallways. + /// + /// + /// For symmetrical results, it is recommended to use odd numbers for grid width and height, + /// which places the central room exactly in the middle. + /// + /// + /// [Serializable] public class GridPathCross : GridPathStartStepGeneric where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public GridPathCross() : base() { } + /// + /// Creates a cross-shaped layout of rooms and halls. + /// + /// The random number generator. + /// The grid plan to populate. public override void ApplyToPath(IRandom rand, GridPlan floorPlan) { // always clear before trying diff --git a/RogueElements/MapGen/Grid/Paths/GridPathSpecific.cs b/RogueElements/MapGen/Grid/Paths/GridPathSpecific.cs index e44fceb6..50ca8e3c 100644 --- a/RogueElements/MapGen/Grid/Paths/GridPathSpecific.cs +++ b/RogueElements/MapGen/Grid/Paths/GridPathSpecific.cs @@ -9,14 +9,29 @@ namespace RogueElements { /// - /// Populates an empty grid plan of a map by creating a specific path of rooms and hallways. - /// VERY EDITOR UNFRIENDLY + /// Creates a grid layout with explicitly specified room and hall positions. /// - /// + /// The map context type, which must implement . + /// + /// + /// This path generator creates a layout from explicit room and hall specifications + /// rather than using procedural algorithms. Every room and hall must be manually defined. + /// + /// + /// Warning: This class is difficult to use in visual editors because + /// the hall arrays must exactly match the grid dimensions. It is primarily useful for + /// creating specific, handcrafted layouts or for testing purposes. + /// + /// + /// + /// [Serializable] public class GridPathSpecific : GridPathStartStep where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public GridPathSpecific() : base() { @@ -25,25 +40,39 @@ public GridPathSpecific() } /// - /// The rooms to place, and where they go. + /// Gets or sets the list of rooms to place with their specific positions. /// public List> SpecificRooms { get; set; } /// - /// The full array of vertical halls. + /// Gets or sets the 2D array of vertical hall generators. /// + /// + /// The array dimensions must be [GridWidth][GridHeight-1]. Null entries indicate no hall. + /// public PermissiveRoomGen[][] SpecificVHalls { get; set; } /// - /// The full array of horizontal halls. + /// Gets or sets the 2D array of horizontal hall generators. /// + /// + /// The array dimensions must be [GridWidth-1][GridHeight]. Null entries indicate no hall. + /// public PermissiveRoomGen[][] SpecificHHalls { get; set; } /// - /// Components that the halls will be labeled with. + /// Gets or sets the components to attach to all halls. /// public ComponentCollection HallComponents { get; set; } + /// + /// Adds a hall without checking if connecting rooms exist. + /// + /// The location and direction of the hall. + /// The grid plan to modify. + /// The hall generator to use. + /// The components to attach to the hall. + /// Thrown when the hall would not connect two rooms. public static void UnsafeAddHall(LocRay4 locRay, GridPlan floorPlan, IPermissiveRoomGen hallGen, ComponentCollection components) { floorPlan.SetHall(locRay, hallGen, components.Clone()); @@ -55,6 +84,12 @@ public static void UnsafeAddHall(LocRay4 locRay, GridPlan floorPlan, IPermissive } } + /// + /// Places the specified rooms and halls into the grid plan. + /// + /// The random number generator. + /// The grid plan to populate. + /// Thrown when hall array dimensions do not match the grid size. public override void ApplyToPath(IRandom rand, GridPlan floorPlan) { if (floorPlan.GridWidth != this.SpecificVHalls.Length || diff --git a/RogueElements/MapGen/Grid/Paths/GridPathStartStep.cs b/RogueElements/MapGen/Grid/Paths/GridPathStartStep.cs index e1772ab9..da40a715 100644 --- a/RogueElements/MapGen/Grid/Paths/GridPathStartStep.cs +++ b/RogueElements/MapGen/Grid/Paths/GridPathStartStep.cs @@ -8,10 +8,39 @@ namespace RogueElements { + /// + /// Base class for grid path generators that create initial room and hall layouts. + /// + /// The map context type, which must implement . + /// + /// + /// Grid path start steps are responsible for populating an empty grid plan with + /// rooms and connecting halls. They define the overall structure and connectivity + /// of the dungeon layout. + /// + /// + /// Subclasses implement specific layout patterns such as branching paths, circles, + /// grids, or crosses. Helper methods are provided for common operations like + /// adding halls with their connected rooms. + /// + /// + /// + /// [Serializable] public abstract class GridPathStartStep : GridPlanStep where T : class, IRoomGridGenContext { + /// + /// Randomly determines whether to perform an action based on a ratio and maximum count. + /// + /// The random number generator. + /// The number of actions remaining to perform. Decremented if successful. + /// The maximum number of opportunities remaining. Always decremented. + /// True if the action should be performed; otherwise, false. + /// + /// This method is used for distributing a fixed number of actions across a set of opportunities, + /// ensuring the target ratio is achieved on average. + /// public static bool RollRatio(IRandom rand, ref int ratio, ref int max) { bool roll = false; @@ -25,6 +54,16 @@ public static bool RollRatio(IRandom rand, ref int ratio, ref int max) return roll; } + /// + /// Adds a hall and ensures rooms exist on both ends. + /// + /// The location and direction of the hall. + /// The grid plan to modify. + /// The hall generator to use. + /// The room generator to use for missing rooms. + /// The components to attach to new rooms. + /// The components to attach to the hall. + /// Whether new rooms should be treated as halls. public static void SafeAddHall(LocRay4 locRay, GridPlan floorPlan, IPermissiveRoomGen hallGen, IRoomGen roomGen, ComponentCollection roomComponents, ComponentCollection hallComponents, bool preferHall = false) { floorPlan.SetHall(locRay, hallGen, hallComponents.Clone()); @@ -36,12 +75,21 @@ public static void SafeAddHall(LocRay4 locRay, GridPlan floorPlan, IPermissiveRo floorPlan.AddRoom(dest, roomGen, collection.Clone(), preferHall); } + /// + /// Creates a minimal fallback path when the main algorithm fails. + /// + /// The random number generator. + /// The grid plan to populate. public virtual void CreateErrorPath(IRandom rand, GridPlan floorPlan) { floorPlan.Clear(); floorPlan.AddRoom(new Loc(0, 0), this.GetDefaultGen(), new ComponentCollection()); } + /// + /// Gets the default room generator used for placeholder rooms. + /// + /// A minimal single-tile room generator. public virtual RoomGen GetDefaultGen() { return new RoomGenDefault(); diff --git a/RogueElements/MapGen/Grid/Paths/GridPathStartStepGeneric.cs b/RogueElements/MapGen/Grid/Paths/GridPathStartStepGeneric.cs index b12d3c7e..aeba7b93 100644 --- a/RogueElements/MapGen/Grid/Paths/GridPathStartStepGeneric.cs +++ b/RogueElements/MapGen/Grid/Paths/GridPathStartStepGeneric.cs @@ -8,10 +8,29 @@ namespace RogueElements { + /// + /// Base class for grid path generators that use configurable room and hall generators. + /// + /// The map context type, which must implement . + /// + /// + /// This class extends by adding properties for + /// room and hall generators with their associated components. Most path generation + /// algorithms inherit from this class rather than directly from GridPathStartStep. + /// + /// + /// Subclasses must ensure that both and + /// are configured with at least one option before generation, or an exception will be thrown. + /// + /// + /// [Serializable] public abstract class GridPathStartStepGeneric : GridPathStartStep where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// protected GridPathStartStepGeneric() { this.RoomComponents = new ComponentCollection(); @@ -19,25 +38,30 @@ protected GridPathStartStepGeneric() } /// - /// The room types that can be used for the rooms of the layout. + /// Gets or sets the room generators to use for layout rooms. /// public IRandPicker> GenericRooms { get; set; } /// - /// Components that the newly added rooms will be labeled with. + /// Gets or sets the components to attach to newly created rooms. /// public ComponentCollection RoomComponents { get; set; } /// - /// The room types that can be used for the halls of the layout. + /// Gets or sets the hall generators to use for connecting halls. /// public IRandPicker> GenericHalls { get; set; } /// - /// Components that the newly added halls will be labeled with. + /// Gets or sets the components to attach to newly created halls. /// public ComponentCollection HallComponents { get; set; } + /// + /// Validates that room and hall generators are configured before applying. + /// + /// The map context to modify. + /// Thrown when no room or hall generators are configured. public override void Apply(T map) { if (!this.GenericRooms.CanPick || !this.GenericHalls.CanPick) diff --git a/RogueElements/MapGen/Grid/Paths/GridPathTwoSides.cs b/RogueElements/MapGen/Grid/Paths/GridPathTwoSides.cs index ef87fc01..b09cf73f 100644 --- a/RogueElements/MapGen/Grid/Paths/GridPathTwoSides.cs +++ b/RogueElements/MapGen/Grid/Paths/GridPathTwoSides.cs @@ -9,23 +9,48 @@ namespace RogueElements { /// - /// Populates the empty floor plan of a map by creating a path consisting of rooms on the far left and far right, with hallways connecting the two sides. + /// Creates a layout with rooms on opposite sides of the grid connected by a corridor. /// - /// + /// The map context type, which must implement . + /// + /// + /// This path generator places rooms along two opposite edges of the grid (left/right or top/bottom) + /// and connects them with hallways spanning the gap. Additional halls may connect rooms + /// on the same side. + /// + /// + /// The layout creates a clear division between two "sides" of the dungeon, useful for + /// scenarios where players need to cross from one area to another. + /// + /// + /// [Serializable] public class GridPathTwoSides : GridPathStartStepGeneric where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public GridPathTwoSides() : base() { } /// - /// Choose a horizontal or vertical orientation. + /// Gets or sets the axis along which the gap between sides runs. /// + /// + /// places rooms on left and right with a horizontal gap. + /// places rooms on top and bottom with a vertical gap. + /// public Axis4 GapAxis { get; set; } + /// + /// Creates the two-sided layout with connecting hallways. + /// + /// The random number generator. + /// The grid plan to populate. + /// Thrown when the grid is too small for this layout. public override void ApplyToPath(IRandom rand, GridPlan floorPlan) { // open rooms on both sides @@ -117,12 +142,22 @@ public override void ApplyToPath(IRandom rand, GridPlan floorPlan) GenContextDebug.StepOut(); } + /// + /// Places a hall at the specified position in a direction determined by the axis. + /// + /// The axis along which to place the hall. + /// The position along the gap axis. + /// The position along the orthogonal axis. + /// The direction (+1 or -1) to extend the hall. + /// The grid plan to modify. + /// The hall generator to use. public void PlaceOrientedHall(Axis4 axis, int scalar, int orth, int scalarDiff, GridPlan floorPlan, PermissiveRoomGen hallGen) { Loc loc = this.GapAxis.CreateLoc(scalar, orth); floorPlan.SetHall(new LocRay4(loc, axis.GetDir(scalarDiff)), hallGen, this.HallComponents.Clone()); } + /// public override string ToString() { return string.Format("{0}: Axis:{1}", this.GetType().GetFormattedTypeName(), this.GapAxis); diff --git a/RogueElements/MapGen/Grid/Paths/IGridPathBranch.cs b/RogueElements/MapGen/Grid/Paths/IGridPathBranch.cs index 1d450c6f..30960004 100644 --- a/RogueElements/MapGen/Grid/Paths/IGridPathBranch.cs +++ b/RogueElements/MapGen/Grid/Paths/IGridPathBranch.cs @@ -8,45 +8,85 @@ namespace RogueElements { + /// + /// Defines the configuration interface for branching path generators. + /// public interface IGridPathBranch { + /// + /// Gets or sets the percentage of grid cells to fill with rooms. + /// RandRange RoomRatio { get; set; } + /// + /// Gets or sets the ratio of branches to straight path extensions. + /// RandRange BranchRatio { get; set; } } /// - /// Populates the empty grid plan of a map by creating a minimum spanning tree of connected rooms and halls. + /// Creates a layout using a tree-like algorithm that grows paths and branches. /// - /// + /// The map context type, which must implement . + /// + /// + /// This path generator creates layouts by starting from a random room and expanding + /// outward. The main path wanders randomly, and branches are added based on the + /// setting. + /// + /// + /// The algorithm produces tree-like structures where all rooms are connected but + /// there are no loops. The shape can range from a simple worm (0% branches) to + /// a heavily branched tree (high branch ratio). + /// + /// + /// + /// [Serializable] public class GridPathBranch : GridPathStartStepGeneric, IGridPathBranch where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public GridPathBranch() : base() { } /// - /// The percentage of total rooms in the grid plan that the step aims to fill. + /// Gets or sets the percentage of grid cells to fill with rooms. /// public RandRange RoomRatio { get; set; } /// - /// The percent amount of branching paths the layout will have in relation to its straight paths. - /// 0 = A layout without branches. (Worm) - /// 50 = A layout that branches once for every two extensions. (Tree) - /// 100 = A layout that branches once for every extension. (Branchier Tree) - /// 200 = A layout that branches twice for every extension. (Fuzzy Worm) + /// Gets or sets the ratio of branches to straight path extensions. /// + /// + /// + /// 0 = No branches, creates a worm-like path + /// 50 = One branch per two extensions, tree-like + /// 100 = One branch per extension, heavily branched + /// 200 = Two branches per extension, very dense + /// + /// public RandRange BranchRatio { get; set; } /// - /// Prevents the step from making branches in the path, even if it would fail the space-fill quota. + /// Gets or sets a value indicating whether to stop early rather than force branches. /// + /// + /// When true, the algorithm stops if it cannot extend without branching. + /// When false, branches are forced to meet the room quota. + /// public bool NoForcedBranches { get; set; } + /// + /// Gets all possible expansion directions from existing rooms. + /// + /// The grid plan to analyze. + /// If true, only returns rooms suitable for branching; if false, returns terminal rooms. + /// A list of location-direction pairs for possible expansions. public static List GetPossibleExpansions(GridPlan floorPlan, bool branch) { List availableRays = new List(); @@ -63,6 +103,11 @@ public static List GetPossibleExpansions(GridPlan floorPlan, bool branc return availableRays; } + /// + /// Creates the branching path layout. + /// + /// The random number generator. + /// The grid plan to populate. public override void ApplyToPath(IRandom rand, GridPlan floorPlan) { for (int ii = 0; ii < 10; ii++) @@ -167,9 +212,9 @@ public override string ToString() /// /// Gets the directions a room can expand in. /// - /// - /// - /// + /// The grid plan to check. + /// The location to check expansion from. + /// An enumerable of valid expansion directions. protected static IEnumerable GetRoomExpandDirs(GridPlan floorPlan, Loc loc) { foreach (Dir4 dir in DirExt.VALID_DIR4) @@ -182,11 +227,11 @@ protected static IEnumerable GetRoomExpandDirs(GridPlan floorPlan, Loc loc } /// - /// Gets a possible terminal room to expand. Equal distribution. + /// Removes and returns a random location from the list with equal distribution. /// - /// - /// - /// + /// The random number generator. + /// The list of locations to choose from. + /// The randomly selected location. protected static Loc PopRandomLocEqual(IRandom rand, List locs) { int branchIdx = rand.Next(locs.Count); @@ -195,6 +240,13 @@ protected static Loc PopRandomLocEqual(IRandom rand, List locs) return newBranch; } + /// + /// Expands the path by adding a hall and room in the specified direction. + /// + /// The random number generator. + /// The grid plan to modify. + /// The location and direction to expand. + /// True if the expansion was successful. protected bool ExpandPath(IRandom rand, GridPlan floorPlan, LocRay4 chosenRay) { floorPlan.SetHall(chosenRay, this.GenericHalls.Pick(rand), this.HallComponents.Clone()); @@ -204,11 +256,24 @@ protected bool ExpandPath(IRandom rand, GridPlan floorPlan, LocRay4 chosenRay) return true; } + /// + /// Removes and returns a random location from the list. + /// + /// The grid plan for context. + /// The random number generator. + /// The list of locations to choose from. + /// The randomly selected location. protected virtual Loc PopRandomLoc(GridPlan floorPlan, IRandom rand, List locs) { return PopRandomLocEqual(rand, locs); } + /// + /// Gets the possible expansion directions from a location with their relative weights. + /// + /// The grid plan to check. + /// The location to expand from. + /// A weighted list of possible expansion directions. protected virtual SpawnList GetExpandDirChances(GridPlan floorPlan, Loc newTerminal) { SpawnList availableRays = new SpawnList(); diff --git a/RogueElements/MapGen/Grid/Paths/IGridPathCircle.cs b/RogueElements/MapGen/Grid/Paths/IGridPathCircle.cs index 522abd70..e4f99d0d 100644 --- a/RogueElements/MapGen/Grid/Paths/IGridPathCircle.cs +++ b/RogueElements/MapGen/Grid/Paths/IGridPathCircle.cs @@ -9,36 +9,65 @@ namespace RogueElements { + /// + /// Defines the configuration interface for circular path generators. + /// public interface IGridPathCircle { + /// + /// Gets or sets the percentage of perimeter rooms that are full rooms rather than halls. + /// RandRange CircleRoomRatio { get; set; } + /// + /// Gets or sets the number of paths extending into the inner grid area. + /// RandRange Paths { get; set; } } /// - /// Populates the empty grid plan of a map by creating a ring of rooms and halls at the outer cells of the grid. + /// Creates a ring-shaped layout with rooms on the grid perimeter and optional inner paths. /// - /// + /// The map context type, which must implement . + /// + /// + /// This path generator creates a ring of connected rooms around the outer edge of the grid. + /// Additional paths can extend from the ring into the interior of the grid. + /// + /// + /// The grid must be at least 3x3 to accommodate the ring structure with an interior area. + /// + /// + /// + /// [Serializable] public class GridPathCircle : GridPathStartStepGeneric, IGridPathCircle where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public GridPathCircle() : base() { } /// - /// The percentage of rooms in the outer circle that are NOT treated as 1-tile halls. + /// Gets or sets the percentage of perimeter rooms that are full rooms rather than halls. /// public RandRange CircleRoomRatio { get; set; } /// - /// The number of paths going to the inner circle. + /// Gets or sets the number of paths extending into the inner grid area. /// public RandRange Paths { get; set; } + /// + /// Creates the circular path layout with optional inner paths. + /// + /// The random number generator. + /// The grid plan to populate. + /// Thrown when the grid is smaller than 3x3. public override void ApplyToPath(IRandom rand, GridPlan floorPlan) { if (floorPlan.GridWidth < 3 || floorPlan.GridHeight < 3) diff --git a/RogueElements/MapGen/Grid/Paths/IGridPathGrid.cs b/RogueElements/MapGen/Grid/Paths/IGridPathGrid.cs index 0efbdb89..e13ef507 100644 --- a/RogueElements/MapGen/Grid/Paths/IGridPathGrid.cs +++ b/RogueElements/MapGen/Grid/Paths/IGridPathGrid.cs @@ -9,36 +9,66 @@ namespace RogueElements { + /// + /// Defines the configuration interface for grid path generators. + /// public interface IGridPathGrid { + /// + /// Gets or sets the percentage of perimeter rooms that are full rooms. + /// int RoomRatio { get; set; } + /// + /// Gets or sets the percentage of additional halls connecting perimeter rooms. + /// int HallRatio { get; set; } } /// - /// Populates the empty floor plan of a map by creating a path consisting of rooms on the perimeter, with hallways creating a grid inwards. + /// Creates a grid-like layout with an inner corridor network and perimeter rooms. /// - /// + /// The map context type, which must implement . + /// + /// + /// This path generator fills the interior of the grid with a network of connected + /// single-tile halls, then adds rooms around the perimeter. Additional halls can + /// connect adjacent perimeter rooms. + /// + /// + /// The grid must be at least 3x3 to have both an interior network and a perimeter. + /// + /// + /// + /// [Serializable] public class GridPathGrid : GridPathStartStepGeneric, IGridPathGrid where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public GridPathGrid() : base() { } /// - /// The percentage of rooms at the perimeter that are NOT default 1-tile halls. + /// Gets or sets the percentage of perimeter rooms that are full rooms rather than halls. /// public int RoomRatio { get; set; } /// - /// The amount of additional halls added to connect adjacent rooms at the perimeter. + /// Gets or sets the percentage of additional halls connecting adjacent perimeter rooms. /// public int HallRatio { get; set; } + /// + /// Creates the grid layout with inner corridors and perimeter rooms. + /// + /// The random number generator. + /// The grid plan to populate. + /// Thrown when the grid is smaller than 3x3. public override void ApplyToPath(IRandom rand, GridPlan floorPlan) { if (floorPlan.GridWidth < 3 || floorPlan.GridHeight < 3) diff --git a/RogueElements/MapGen/Grid/Paths/README.md b/RogueElements/MapGen/Grid/Paths/README.md new file mode 100644 index 00000000..e26437ac --- /dev/null +++ b/RogueElements/MapGen/Grid/Paths/README.md @@ -0,0 +1,366 @@ +# Grid Paths + +[![RogueElements](https://img.shields.io/nuget/v/RogueElements?label=RogueElements)](https://www.nuget.org/packages/RogueElements) + +Path generation algorithms for grid-based floor plan layouts. This module provides steps that populate `GridPlan` objects with rooms and halls arranged in regular grid patterns. + +## Purpose + +Grid Paths generate room/hall connectivity for grid-based map layouts. Unlike freeform floor plans, grid paths place rooms in fixed cells of a grid, creating more structured and predictable dungeon layouts. + +## Core Classes + +### GridPathStartStep + +Abstract base class for all grid path generators: + +```csharp +public abstract class GridPathStartStep : GridPlanStep + where T : class, IRoomGridGenContext +{ + public virtual void CreateErrorPath(IRandom rand, GridPlan floorPlan); + public virtual RoomGen GetDefaultGen(); + + // Utility: Roll probability for ratio-based placement + public static bool RollRatio(IRandom rand, ref int ratio, ref int max); + + // Utility: Safely add a hall with connected rooms + public static void SafeAddHall(LocRay4 locRay, GridPlan floorPlan, + IPermissiveRoomGen hallGen, IRoomGen roomGen, + ComponentCollection roomComponents, ComponentCollection hallComponents, + bool preferHall = false); +} +``` + +### GridPathStartStepGeneric + +Extends base class with room/hall spawning support: + +```csharp +public abstract class GridPathStartStepGeneric : GridPathStartStep +{ + public IRandPicker> GenericRooms { get; set; } + public ComponentCollection RoomComponents { get; set; } + public IRandPicker> GenericHalls { get; set; } + public ComponentCollection HallComponents { get; set; } +} +``` + +## Path Algorithm Classes + +### GridPathBranch + +Creates a minimum spanning tree of connected rooms using a branching algorithm. The most versatile grid path generator. + +```csharp +var path = new GridPathBranch +{ + RoomRatio = new RandRange(70), // Fill 70% of grid cells + BranchRatio = new RandRange(0, 50), // Branching rate +}; + +var genericRooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, +}; +path.GenericRooms = genericRooms; + +var genericHalls = new SpawnList> +{ + { new RoomGenAngledHall(50), 10 } +}; +path.GenericHalls = genericHalls; + +layout.GenSteps.Add(-4, path); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `RoomRatio` | `RandRange` | Percentage of grid cells to fill with rooms | +| `BranchRatio` | `RandRange` | Branching rate (0 = worm, 50 = tree, 100+ = fuzzy) | +| `NoForcedBranches` | `bool` | Prevent forced branches if quota not met | + +### GridPathCircle + +Creates a ring of rooms around the outer edge of the grid, with optional paths going inward. + +```csharp +var path = new GridPathCircle +{ + CircleRoomRatio = new RandRange(50), // 50% are real rooms, rest are halls + Paths = new RandRange(2, 4), // 2-4 paths to inner area +}; +path.GenericRooms = genericRooms; +path.GenericHalls = genericHalls; + +layout.GenSteps.Add(-4, path); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `CircleRoomRatio` | `RandRange` | Percentage of outer rooms that are full rooms (not halls) | +| `Paths` | `RandRange` | Number of paths going to inner area | + +### GridPathCross + +Creates a cross (plus sign) pattern with a center room and rooms extending in four cardinal directions. + +```csharp +var path = new GridPathCross(); +path.GenericRooms = genericRooms; +path.GenericHalls = genericHalls; + +layout.GenSteps.Add(-4, path); +``` + +Best results with odd-numbered grid dimensions (e.g., 5x5, 7x7). + +### GridPathGrid + +Creates a grid pattern with rooms on the perimeter and hallways forming an inner grid. + +```csharp +var path = new GridPathGrid +{ + RoomRatio = 70, // 70% of perimeter cells get real rooms + HallRatio = 50, // 50% of possible extra halls are placed +}; +path.GenericRooms = genericRooms; +path.GenericHalls = genericHalls; + +layout.GenSteps.Add(-4, path); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `RoomRatio` | `int` | Percentage of perimeter rooms that are real rooms | +| `HallRatio` | `int` | Percentage of additional halls connecting perimeter rooms | + +### GridPathTwoSides + +Creates rooms on opposite sides of the grid with hallways bridging the gap. + +```csharp +var path = new GridPathTwoSides +{ + GapAxis = Axis4.Horiz, // Rooms on left/right, halls in between +}; +path.GenericRooms = genericRooms; +path.GenericHalls = genericHalls; + +layout.GenSteps.Add(-4, path); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `GapAxis` | `Axis4` | Direction of the gap (Horiz = left/right rooms, Vert = top/bottom rooms) | + +### GridPathSpecific + +Creates an exact layout from pre-specified room and hall positions. Useful for hand-crafted levels. + +```csharp +var path = new GridPathSpecific(); + +// Define specific rooms +path.SpecificRooms = new List> +{ + new SpecificGridRoomPlan(new Rect(0, 0, 1, 1), roomGen, components), + new SpecificGridRoomPlan(new Rect(1, 0, 1, 1), roomGen, components), +}; + +// Define halls (2D arrays matching grid dimensions) +path.SpecificVHalls = new PermissiveRoomGen[gridWidth][]; +path.SpecificHHalls = new PermissiveRoomGen[gridWidth - 1][]; +// ... populate arrays ... + +layout.GenSteps.Add(-4, path); +``` + +## Usage Example + +From `Ex3_Grid`: + +```csharp +var layout = new MapGen(); + +// Initialize a 6x4 grid of 10x10 cells +var startGen = new InitGridPlanStep(1) +{ + CellX = 6, + CellY = 4, + CellWidth = 9, + CellHeight = 9, +}; +layout.GenSteps.Add(-4, startGen); + +// Create branching path +var path = new GridPathBranch +{ + RoomRatio = new RandRange(70), + BranchRatio = new RandRange(0, 50), +}; + +var genericRooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, +}; +path.GenericRooms = genericRooms; + +var genericHalls = new SpawnList> +{ + { new RoomGenAngledHall(50), 10 } +}; +path.GenericHalls = genericHalls; + +layout.GenSteps.Add(-4, path); + +// Convert grid to floor plan +layout.GenSteps.Add(-2, new DrawGridToFloorStep()); + +// Draw floor plan to tiles +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + +MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); +``` + +## Algorithm Interfaces + +### IGridPathBranch + +```csharp +public interface IGridPathBranch +{ + RandRange RoomRatio { get; set; } + RandRange BranchRatio { get; set; } +} +``` + +### IGridPathCircle + +```csharp +public interface IGridPathCircle +{ + RandRange CircleRoomRatio { get; set; } + RandRange Paths { get; set; } +} +``` + +### IGridPathGrid + +```csharp +public interface IGridPathGrid +{ + int RoomRatio { get; set; } + int HallRatio { get; set; } +} +``` + +## Creating Custom Path Algorithms + +1. Inherit from `GridPathStartStepGeneric` +2. Override `ApplyToPath()` to implement your algorithm + +```csharp +[Serializable] +public class GridPathSpiral : GridPathStartStepGeneric + where T : class, IRoomGridGenContext +{ + public override void ApplyToPath(IRandom rand, GridPlan floorPlan) + { + floorPlan.Clear(); + + // Spiral from outside in + int left = 0, right = floorPlan.GridWidth - 1; + int top = 0, bottom = floorPlan.GridHeight - 1; + Loc? prevLoc = null; + + while (left <= right && top <= bottom) + { + // Top edge (left to right) + for (int x = left; x <= right; x++) + { + AddRoomWithHall(rand, floorPlan, new Loc(x, top), prevLoc); + prevLoc = new Loc(x, top); + } + top++; + + // Right edge (top to bottom) + for (int y = top; y <= bottom; y++) + { + AddRoomWithHall(rand, floorPlan, new Loc(right, y), prevLoc); + prevLoc = new Loc(right, y); + } + right--; + + // Bottom edge (right to left) + if (top <= bottom) + { + for (int x = right; x >= left; x--) + { + AddRoomWithHall(rand, floorPlan, new Loc(x, bottom), prevLoc); + prevLoc = new Loc(x, bottom); + } + bottom--; + } + + // Left edge (bottom to top) + if (left <= right) + { + for (int y = bottom; y >= top; y--) + { + AddRoomWithHall(rand, floorPlan, new Loc(left, y), prevLoc); + prevLoc = new Loc(left, y); + } + left++; + } + } + } + + private void AddRoomWithHall(IRandom rand, GridPlan floorPlan, Loc loc, Loc? prevLoc) + { + floorPlan.AddRoom(loc, GenericRooms.Pick(rand), RoomComponents.Clone()); + + if (prevLoc.HasValue) + { + Loc diff = loc - prevLoc.Value; + Dir4 dir = DirExt.GetDir(diff); + floorPlan.SetHall(new LocRay4(prevLoc.Value, dir), + GenericHalls.Pick(rand), HallComponents.Clone()); + } + } +} +``` + +## Grid Pipeline + +A typical grid-based generation pipeline: + +1. `InitGridPlanStep` - Initialize the grid structure +2. Grid Path Step (e.g., `GridPathBranch`) - Populate with rooms/halls +3. `DrawGridToFloorStep` - Convert grid to floor plan +4. `DrawFloorToTileStep` - Render floor plan to tiles +5. Additional steps (stairs, water, spawning, etc.) + +## Related Modules + +- **[../](../)** - Parent Grid module +- **[FloorPlan/Paths/](../../FloorPlan/Paths/)** - Freeform path algorithms +- **[Rooms/](../../Rooms/)** - Room generators +- **[Rooms/Halls/](../../Rooms/Halls/)** - Hall generators + +## See Also + +- `Ex3_Grid` - Grid-based generation example +- `GridPlan` - The data structure populated by path steps +- `DrawGridToFloorStep` - Converts grid plan to floor plan diff --git a/RogueElements/MapGen/Grid/Paths/SpecificGridRoomPlan.cs b/RogueElements/MapGen/Grid/Paths/SpecificGridRoomPlan.cs index 33046cc0..a623ffee 100644 --- a/RogueElements/MapGen/Grid/Paths/SpecificGridRoomPlan.cs +++ b/RogueElements/MapGen/Grid/Paths/SpecificGridRoomPlan.cs @@ -8,10 +8,27 @@ namespace RogueElements { + /// + /// Represents a room specification for use with . + /// + /// The tile context type, which must implement . + /// + /// + /// This class defines a room with explicit bounds, generator, and properties for + /// use in handcrafted grid layouts. It is used by + /// to place rooms at specific grid locations. + /// + /// + /// [Serializable] public class SpecificGridRoomPlan where T : ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// + /// The grid cell rectangle that this room occupies. + /// The room generator for this room. public SpecificGridRoomPlan(Rect bounds, RoomGen roomGen) { this.Bounds = bounds; @@ -19,12 +36,24 @@ public SpecificGridRoomPlan(Rect bounds, RoomGen roomGen) this.Components = new ComponentCollection(); } + /// + /// Gets or sets the grid cell rectangle that this room occupies. + /// public Rect Bounds { get; set; } + /// + /// Gets or sets a value indicating whether this room should be treated as a hall. + /// public bool PreferHall { get; set; } + /// + /// Gets or sets the room generator for this room. + /// public RoomGen RoomGen { get; set; } + /// + /// Gets or sets the components attached to this room. + /// public ComponentCollection Components { get; set; } } } \ No newline at end of file diff --git a/RogueElements/MapGen/Grid/README.md b/RogueElements/MapGen/Grid/README.md new file mode 100644 index 00000000..9fca99bb --- /dev/null +++ b/RogueElements/MapGen/Grid/README.md @@ -0,0 +1,477 @@ +# Grid - Grid-Based Room Layouts + +[![Build](https://img.shields.io/github/actions/workflow/status/audinowho/RogueElements/build.yml?branch=master)](https://github.com/audinowho/RogueElements/actions) +[![NuGet](https://img.shields.io/nuget/v/RogueElements)](https://www.nuget.org/packages/RogueElements/) + +The Grid system provides **structured room placement** where rooms occupy cells in a rectangular grid and halls connect adjacent cells. This creates classic roguelike dungeon layouts with predictable, uniform structure. + +## What is a GridPlan? + +A `GridPlan` is a dungeon layout where: +- The map is divided into a **regular grid of cells** +- Each cell can contain **one room** +- **Halls** connect adjacent cells in cardinal directions (up, down, left, right) +- Rooms can span **multiple cells** for larger rooms + +``` ++-------+-------+-------+ +| | | | +| Room |--Hall-| Room | +| | | | ++---+---+-------+---+---+ + | | + Hall Hall + | | ++---+---+-------+---+---+ +| | | | +| Room |--Hall-| Room | +| | | | ++-------+-------+-------+ +``` + +## Class Diagram + +```mermaid +classDiagram + class GridPlan { + +int CellWall + +int WidthPerCell + +int HeightPerCell + +int GridWidth + +int GridHeight + +Loc Size + +bool Wrap + +int RoomCount + +InitSize(int width, int height, int widthPerCell, int heightPerCell, int cellWall) + +AddRoom(Loc loc, IRoomGen gen, ComponentCollection components) + +SetHall(LocRay4 locRay, IPermissiveRoomGen hallGen, ComponentCollection components) + +GetRoomPlan(int index) GridRoomPlan + +GetHall(LocRay4 locRay) GridHallPlan + +PlaceRoomsOnFloor(IFloorPlanGenContext map) + } + + class GridRoomPlan { + +Rect Bounds + +bool PreferHall + +IRoomGen RoomGen + +ComponentCollection Components + } + + class GridHallPlan { + +IPermissiveRoomGen RoomGen + +ComponentCollection Components + } + + class GridHallGroup { + +GridHallPlan MainHall + +List~GridHallPlan~ HallParts + +SetHall(GridHallPlan plan) + } + + class IRoomPlan { + <> + +IRoomGen RoomGen + +ComponentCollection Components + } + + GridPlan "1" *-- "*" GridRoomPlan : ArrayRooms + GridPlan "1" *-- "*" GridHallGroup : VHalls, HHalls + GridHallGroup "1" *-- "*" GridHallPlan : HallParts + GridRoomPlan ..|> IRoomPlan + GridHallPlan ..|> IRoomPlan +``` + +## Key Classes + +### `GridPlan` + +The main data structure for grid-based layouts. + +```csharp +// From GridPlan.cs +public class GridPlan +{ + // Cell dimensions + public int CellWall { get; set; } // Wall thickness between cells (tiles) + public int WidthPerCell { get; set; } // Width of each cell (tiles) + public int HeightPerCell { get; set; } // Height of each cell (tiles) + + // Grid dimensions + public int GridWidth => this.Rooms.Length; // Number of columns + public int GridHeight => this.Rooms[0].Length; // Number of rows + + // Total size in tiles + public Loc Size { + get { + return new Loc( + (this.GridWidth * (this.WidthPerCell + this.CellWall)) - (this.Wrap ? 0 : this.CellWall), + (this.GridHeight * (this.HeightPerCell + this.CellWall)) - (this.Wrap ? 0 : this.CellWall)); + } + } + + // Initialize the grid + public void InitSize(int width, int height, int widthPerCell, int heightPerCell, int cellWall = 1, bool wrap = false); + + // Add a room at a grid cell + public void AddRoom(Loc loc, IRoomGen gen, ComponentCollection components); + public void AddRoom(Rect rect, IRoomGen gen, ComponentCollection components); // Multi-cell room + + // Set a hall between two adjacent cells + public void SetHall(LocRay4 locRay, IPermissiveRoomGen hallGen, ComponentCollection components); + + // Convert to FloorPlan for rendering + public void PlaceRoomsOnFloor(IFloorPlanGenContext map); +} +``` + +### `GridRoomPlan` + +Represents a room within the grid. + +```csharp +// From GridRoomPlan.cs +[Serializable] +public class GridRoomPlan : IRoomPlan +{ + public Rect Bounds { get; set; } // Grid cells occupied (can span multiple) + public bool PreferHall { get; set; } // Treat as hall when converting to FloorPlan + public IRoomGen RoomGen { get; set; } // Room generator + public ComponentCollection Components { get; set; } // Tags/metadata +} +``` + +### `GridHallPlan` + +Represents a hall connecting two cells. + +```csharp +// From GridHallPlan.cs +public class GridHallPlan : IRoomPlan +{ + public IPermissiveRoomGen RoomGen { get; } + public ComponentCollection Components { get; } +} +``` + +### `GridHallGroup` + +Manages hall segments (halls can be split into multiple parts for complex layouts). + +```csharp +// From GridHallGroup.cs +public class GridHallGroup +{ + public GridHallPlan MainHall { get; } + public List HallParts { get; } // May contain multiple segments +} +``` + +## Common Steps + +### 1. `InitGridPlanStep` - Initialize the Grid + +Creates an empty grid with specified dimensions. + +```csharp +// From InitGridPlanStep.cs +[Serializable] +public class InitGridPlanStep : GenStep + where T : class, IRoomGridGenContext +{ + public int CellWidth { get; set; } // Width of each cell in tiles + public int CellHeight { get; set; } // Height of each cell in tiles + public int CellX { get; set; } // Number of columns + public int CellY { get; set; } // Number of rows + public int CellWall { get; set; } // Wall thickness between cells + public bool Wrap { get; set; } // Toroidal wrapping + + public override void Apply(T map) + { + var floorPlan = new GridPlan(); + floorPlan.InitSize(this.CellX, this.CellY, this.CellWidth, this.CellHeight, this.CellWall, this.Wrap); + map.InitGrid(floorPlan); + } +} +``` + +**Usage:** +```csharp +// Create a 6x4 grid of 9x9 cells with 1-tile walls +var startGen = new InitGridPlanStep(1) // 1 = cellWall +{ + CellX = 6, // 6 columns + CellY = 4, // 4 rows + CellWidth = 9, // Each cell is 9 tiles wide + CellHeight = 9, // Each cell is 9 tiles tall +}; +layout.GenSteps.Add(-4, startGen); +``` + +### 2. `GridPathBranch` - Generate Branching Paths + +Creates a minimum spanning tree of rooms and halls on the grid. + +```csharp +// From IGridPathBranch.cs +[Serializable] +public class GridPathBranch : GridPathStartStepGeneric + where T : class, IRoomGridGenContext +{ + // Percentage of grid cells to fill with rooms + public RandRange RoomRatio { get; set; } + + // How much the path branches (0=linear, 50=tree, 100+=bushy) + public RandRange BranchRatio { get; set; } + + // Prevents forced branches even if quota not met + public bool NoForcedBranches { get; set; } +} +``` + +**Usage:** +```csharp +var path = new GridPathBranch +{ + RoomRatio = new RandRange(70), // Fill 70% of cells + BranchRatio = new RandRange(0, 50), // Moderate branching +}; +path.GenericRooms = genericRooms; // Room types to use +path.GenericHalls = genericHalls; // Hall types to use +layout.GenSteps.Add(-4, path); +``` + +### 3. `DrawGridToFloorStep` - Convert to FloorPlan + +Converts the grid structure into a FloorPlan for further processing or rendering. + +```csharp +// From DrawGridToFloorStep.cs +[Serializable] +public class DrawGridToFloorStep : GenStep + where T : class, IRoomGridGenContext +{ + public override void Apply(T map) + { + var floorPlan = new FloorPlan(); + floorPlan.InitSize(map.GridPlan.Size, map.GridPlan.Wrap); + map.InitPlan(floorPlan); + + // Place all rooms and halls from grid onto floor plan + map.GridPlan.PlaceRoomsOnFloor(map); + } +} +``` + +**Usage:** +```csharp +// Convert grid to floor plan +layout.GenSteps.Add(-2, new DrawGridToFloorStep()); + +// Then render floor plan to tiles +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); +``` + +## Generation Pipeline + +```mermaid +flowchart TD + subgraph Grid["Grid Phase"] + IG[InitGridPlanStep
Create empty grid] + GP[GridPathBranch
Generate room/hall tree] + GS[SetGridDefaultsStep
Fill empty cells] + GSR[SetGridSpecialRoomStep
Mark special rooms] + end + + subgraph Convert["Conversion Phase"] + DG[DrawGridToFloorStep
Grid -> FloorPlan] + end + + subgraph Floor["Floor Phase (Optional)"] + CF[ConnectStep
Add shortcuts] + SR[SetSpecialRoomStep] + end + + subgraph Tile["Tile Phase"] + DT[DrawFloorToTileStep
FloorPlan -> Tiles] + TT[Terrain Steps] + SP[Spawning Steps] + end + + IG --> GP + GP --> GS + GS --> GSR + GSR --> DG + DG --> CF + CF --> SR + SR --> DT + DT --> TT + TT --> SP + + style Grid fill:#e3f2fd + style Convert fill:#fff8e1 + style Floor fill:#e8f5e9 + style Tile fill:#fce4ec +``` + +## When to Use Grid vs FloorPlan + +| Feature | GridPlan | FloorPlan | +|---------|----------|-----------| +| Room placement | Fixed grid cells | Anywhere | +| Room sizes | Constrained by cell | Any size | +| Layout style | Structured, uniform | Organic, irregular | +| Adjacency | Cardinal directions only | Any adjacent rooms | +| Performance | Fast (index-based) | Slower (collision checks) | +| Complexity | Simpler to reason about | More flexible | + +**Choose GridPlan when:** +- You want classic roguelike grid aesthetics +- Predictable, structured layouts are desired +- Performance matters (large dungeons) +- You'll convert to FloorPlan for additional processing + +**Choose FloorPlan when:** +- Organic, natural-looking layouts are needed +- Rooms should vary significantly in size and position +- Maximum flexibility is required + +## Complete Example + +From `Ex3_Grid/Example3.cs`: + +```csharp +public static void Run() +{ + var layout = new MapGen(); + + // Step 1: Initialize a 6x4 grid of 9x9 cells + var startGen = new InitGridPlanStep(1) + { + CellX = 6, + CellY = 4, + CellWidth = 9, + CellHeight = 9, + }; + layout.GenSteps.Add(-4, startGen); + + // Step 2: Create branching path of rooms and halls + var path = new GridPathBranch + { + RoomRatio = new RandRange(70), + BranchRatio = new RandRange(0, 50), + }; + + // Define room types + var genericRooms = new SpawnList> + { + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, + }; + path.GenericRooms = genericRooms; + + // Define hall types + var genericHalls = new SpawnList> + { + { new RoomGenAngledHall(50), 10 } + }; + path.GenericHalls = genericHalls; + + layout.GenSteps.Add(-4, path); + + // Step 3: Convert grid to floor plan + layout.GenSteps.Add(-2, new DrawGridToFloorStep()); + + // Step 4: Render to tiles with 1-tile border + layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + + // Generate! + MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); +} +``` + +## Interface Requirements + +To use Grid steps, your context must implement `IRoomGridGenContext`: + +```csharp +// From IRoomGridGenContext.cs +public interface IRoomGridGenContext : IFloorPlanGenContext +{ + GridPlan GridPlan { get; } + void InitGrid(GridPlan plan); +} +``` + +Note that `IRoomGridGenContext` extends `IFloorPlanGenContext`, so Grid contexts can also use FloorPlan steps after conversion. + +## Paths/ Subdirectory + +The `Paths/` subfolder contains path generation algorithms for grids: + +| Class | Description | +|-------|-------------| +| `GridPathStartStep` | Base class for grid path generators | +| `GridPathStartStepGeneric` | Generic version with room/hall pickers | +| `GridPathBranch` | Branching tree path generator | +| `GridPathCross` | Cross-shaped path | +| `GridPathTwoSides` | Rooms on two sides | +| `GridPathSpecific` | Manually specified layout | +| `IGridPathBranch` | Interface for branch-style paths | +| `IGridPathCircle` | Interface for circular paths | +| `IGridPathGrid` | Interface for full grid paths | +| `SpecificGridRoomPlan` | For manually placed rooms | + +## Other Steps in This Folder + +| Step | Purpose | +|------|---------| +| `ConnectGridBranchStep` | Connect branch endpoints on the grid | +| `SetGridDefaultsStep` | Fill empty cells with default rooms | +| `SetGridPlanComponentStep` | Add components to matching rooms | +| `SetGridSpecialRoomStep` | Mark certain rooms as special | + +## Grid Coordinates vs Tile Coordinates + +``` +Grid (3x2 cells, 5x5 tiles per cell, 1-tile walls): + Cell(0,0) Cell(1,0) Cell(2,0) + | | | + v v v ++-----+--+-----+--+-----+ +| |##| |##| | <- Row 0 cells +| |##| |##| | +| |##| |##| | +| |##| |##| | +| |##| |##| | ++#####++##+#####++##+##### <- Wall row ++#####++##+#####++##+##### +| |##| |##| | <- Row 1 cells +| |##| |##| | +| |##| |##| | +| |##| |##| | +| |##| |##| | ++-----+--+-----+--+-----+ + +Legend: + ..... = Room space (5x5 tiles) + ## = Wall between cells (1 tile wide) +``` + +**Cell bounds calculation:** +```csharp +// From GridPlan.cs +public virtual Rect GetCellBounds(Rect bounds) +{ + return new Rect( + bounds.X * (this.WidthPerCell + this.CellWall), + bounds.Y * (this.HeightPerCell + this.CellWall), + (bounds.Size.X * (this.WidthPerCell + this.CellWall)) - this.CellWall, + (bounds.Size.Y * (this.HeightPerCell + this.CellWall)) - this.CellWall); +} +``` + +## See Also + +- [MapGen README](../README.md) - Core pipeline documentation +- [FloorPlan README](../FloorPlan/README.md) - Freeform alternative +- [Ex3_Grid](../../../RogueElements.Examples/Ex3_Grid/) - Grid example diff --git a/RogueElements/MapGen/Grid/SetGridDefaultsStep.cs b/RogueElements/MapGen/Grid/SetGridDefaultsStep.cs index 70454caa..2d622af0 100644 --- a/RogueElements/MapGen/Grid/SetGridDefaultsStep.cs +++ b/RogueElements/MapGen/Grid/SetGridDefaultsStep.cs @@ -9,19 +9,40 @@ namespace RogueElements { /// - /// Takes an existing grid plan and changes some of the rooms into the default room type. - /// The default room is a single tile in size and effectively acts as a hallway. + /// Converts some rooms to the default single-tile room type, effectively turning them into hallways. /// - /// + /// The map context type, which must implement . + /// + /// + /// This step reduces the number of full rooms in the layout by converting some of them + /// to minimal single-tile rooms. These default rooms act as connectors or hallways, + /// creating a more varied layout with larger open areas connected by narrower passages. + /// + /// + /// Only non-terminal rooms (those connected to more than one other room) are eligible + /// for conversion to prevent dead ends from becoming inaccessible. + /// + /// + /// + /// [Serializable] public class SetGridDefaultsStep : GridPlanStep where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public SetGridDefaultsStep() { this.Filters = new List(); } + /// + /// Initializes a new instance of the class + /// with the specified default ratio and filters. + /// + /// The percentage range of rooms to convert to default. + /// The filters to determine eligible rooms. public SetGridDefaultsStep(RandRange defaultRatio, List filter) { this.DefaultRatio = defaultRatio; @@ -29,15 +50,20 @@ public SetGridDefaultsStep(RandRange defaultRatio, List filter) } /// - /// Determines the percentage of eligible rooms that will be turned into default. + /// Gets or sets the percentage range of eligible rooms to convert to default. /// public RandRange DefaultRatio { get; set; } /// - /// Determines which rooms are eligible to be turned into default. + /// Gets or sets the filters that determine which rooms are eligible for conversion. /// public List Filters { get; set; } + /// + /// Converts a percentage of eligible rooms to the default room type. + /// + /// The random number generator. + /// The grid plan to modify. public override void ApplyToPath(IRandom rand, GridPlan floorPlan) { List candidates = new List(); diff --git a/RogueElements/MapGen/Grid/SetGridPlanComponentStep.cs b/RogueElements/MapGen/Grid/SetGridPlanComponentStep.cs index 4fbe0d4a..b5903664 100644 --- a/RogueElements/MapGen/Grid/SetGridPlanComponentStep.cs +++ b/RogueElements/MapGen/Grid/SetGridPlanComponentStep.cs @@ -9,24 +9,50 @@ namespace RogueElements { /// - /// Takes all rooms in the map's grid plan and gives them a specified component. - /// These components can be used to identify the room in some way for future filtering. + /// Adds specified components to rooms and halls in the grid plan for identification and filtering. /// - /// + /// The map context type, which must implement . + /// + /// + /// Components are used to tag rooms and halls with metadata that can be used by + /// subsequent generation steps for filtering or special handling. For example, + /// a component might mark rooms as eligible for special spawns or as belonging + /// to a particular zone. + /// + /// + /// This step iterates through all room and hall plans in the grid and adds + /// copies of the specified components to those that pass the configured filters. + /// + /// + /// + /// [Serializable] public class SetGridPlanComponentStep : GenStep where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public SetGridPlanComponentStep() { this.Components = new ComponentCollection(); this.Filters = new List(); } + /// + /// Gets or sets the filters that determine which rooms receive the components. + /// public List Filters { get; set; } + /// + /// Gets or sets the components to add to matching rooms. + /// public ComponentCollection Components { get; set; } + /// + /// Adds components to all rooms and halls that pass the configured filters. + /// + /// The map context containing the grid plan. public override void Apply(T map) { foreach (IRoomPlan plan in map.GridPlan.GetAllPlans()) diff --git a/RogueElements/MapGen/Grid/SetGridSpecialRoomStep.cs b/RogueElements/MapGen/Grid/SetGridSpecialRoomStep.cs index 531ac2f9..4e7654bc 100644 --- a/RogueElements/MapGen/Grid/SetGridSpecialRoomStep.cs +++ b/RogueElements/MapGen/Grid/SetGridSpecialRoomStep.cs @@ -9,13 +9,28 @@ namespace RogueElements { /// - /// Takes an existing grid plan and changes one of the rooms into the specified room type. + /// Replaces one room in the grid plan with a special room type. /// - /// + /// The map context type, which must implement . + /// + /// + /// This step is used to place unique or important rooms like boss rooms, treasure rooms, + /// or other special areas. It selects one eligible room from the grid and replaces its + /// generator with the specified special room type. + /// + /// + /// The room must be large enough to accommodate the special room's proposed size. + /// Rooms marked as are excluded from consideration. + /// + /// + /// [Serializable] public class SetGridSpecialRoomStep : GridPlanStep where T : class, IRoomGridGenContext { + /// + /// Initializes a new instance of the class. + /// public SetGridSpecialRoomStep() : base() { @@ -24,20 +39,28 @@ public SetGridSpecialRoomStep() } /// - /// The type of room to place. It can be chosen out of several possibilities, but only one room will be placed. + /// Gets or sets the room generators to choose from for the special room. /// + /// + /// One room type is randomly selected from this picker, but only one room is placed. + /// public IRandPicker> Rooms { get; set; } /// - /// Determines which rooms are eligible to be turned into the new room type. + /// Gets or sets the filters that determine which rooms are eligible for replacement. /// public List Filters { get; set; } /// - /// Components that the newly added room will be labeled with. + /// Gets or sets the components to attach to the special room. /// public ComponentCollection RoomComponents { get; set; } + /// + /// Selects an eligible room and replaces it with a special room type. + /// + /// The random number generator. + /// The grid plan to modify. public override void ApplyToPath(IRandom rand, GridPlan floorPlan) { IRoomGen newGen = this.Rooms.Pick(rand).Copy(); diff --git a/RogueElements/MapGen/IGenContext.cs b/RogueElements/MapGen/IGenContext.cs index dc5f6419..f3215b2c 100644 --- a/RogueElements/MapGen/IGenContext.cs +++ b/RogueElements/MapGen/IGenContext.cs @@ -8,12 +8,118 @@ namespace RogueElements { + /// + /// Defines the core contract for a map generation context that holds state during the generation process. + /// + /// + /// + /// is the base interface that all map contexts must implement. + /// It provides the essential infrastructure for procedural generation: a seeded random number + /// generator and lifecycle hooks for initialization and finalization. + /// + /// + /// Implementations typically extend this interface with additional capabilities: + /// + /// - Tile-based map operations (get/set tiles, wall detection) + /// - Freeform room placement via + /// - Grid-based room layouts via + /// - Entity spawning (items, stairs, mobs) + /// + /// + /// + /// The context is created by using , + /// so implementations must have a public parameterless constructor. + /// + /// + /// + /// + /// public class MyMapContext : IGenContext, ITiledGenContext + /// { + /// private ReRandom rand; + /// + /// public IRandom Rand => this.rand; + /// + /// public void InitSeed(ulong seed) + /// { + /// this.rand = new ReRandom(seed); + /// // Initialize map data structures + /// } + /// + /// public void FinishGen() + /// { + /// // Final processing, cleanup, or validation + /// } + /// + /// // ... additional ITiledGenContext members ... + /// } + /// + /// + /// + /// public interface IGenContext { + /// + /// Gets the random number generator used for all procedural decisions during map generation. + /// + /// + /// An instance that provides deterministic random values based on the + /// seed initialized via . + /// + /// + /// + /// All implementations should use this property for random decisions + /// to ensure reproducible map generation. Using the same seed with the same generation steps + /// will produce identical maps. + /// + /// + /// This property should be initialized in and remain consistent + /// throughout the generation process. + /// + /// IRandom Rand { get; } + /// + /// Initializes the generation context with a random seed. + /// + /// + /// The seed value used to initialize the random number generator. + /// The same seed produces the same sequence of random values, enabling reproducible generation. + /// + /// + /// + /// This method is called by immediately after creating + /// the context instance, before any generation steps execute. + /// + /// + /// Implementations should: + /// + /// Initialize the property with a seeded RNG + /// Allocate initial data structures (tile arrays, room collections, etc.) + /// Set default values for generation state + /// + /// + /// void InitSeed(ulong seed); + /// + /// Performs final processing after all generation steps have completed. + /// + /// + /// + /// This method is called by after the last generation step + /// has been applied. It provides an opportunity for final cleanup, validation, or + /// post-processing. + /// + /// + /// Common uses include: + /// + /// Converting intermediate data structures to final representations + /// Validating map integrity (connectivity, required features, etc.) + /// Releasing temporary resources used only during generation + /// Computing derived data (visibility grids, pathfinding graphs, etc.) + /// + /// + /// void FinishGen(); } } diff --git a/RogueElements/MapGen/IGenStep.cs b/RogueElements/MapGen/IGenStep.cs index 324e7122..ce479ac4 100644 --- a/RogueElements/MapGen/IGenStep.cs +++ b/RogueElements/MapGen/IGenStep.cs @@ -5,10 +5,53 @@ namespace RogueElements { + /// + /// Defines the non-generic contract for a generation step in the map generation pipeline. + /// + /// + /// + /// provides a type-agnostic interface for generation steps, enabling + /// to work with heterogeneous collections of steps through a common contract. + /// + /// + /// Most implementations should inherit from rather than implementing + /// this interface directly. provides automatic type checking and + /// delegation to a strongly-typed method. + /// + /// + /// + /// public interface IGenStep { + /// + /// Determines whether this step can be applied to the specified generation context. + /// + /// The generation context to check for compatibility. + /// + /// if this step is compatible with and can be applied to + /// the ; otherwise, . + /// + /// + /// For implementations, this returns + /// when the context is assignable to the step's generic type parameter T. + /// bool CanApply(IGenContext context); + /// + /// Applies this generation step to the specified context, modifying the map state. + /// + /// The generation context to modify. + /// + /// + /// This method is called by for each step in priority order. + /// Implementations should verify context compatibility, either by checking + /// first or by throwing an appropriate exception for incompatible contexts. + /// + /// + /// Steps should use for all random decisions to maintain + /// reproducibility when generating maps with the same seed. + /// + /// void Apply(IGenContext context); } } diff --git a/RogueElements/MapGen/MapGen.cs b/RogueElements/MapGen/MapGen.cs index bee8ae69..3c6ad598 100644 --- a/RogueElements/MapGen/MapGen.cs +++ b/RogueElements/MapGen/MapGen.cs @@ -9,18 +9,117 @@ namespace RogueElements { + /// + /// Orchestrates procedural map generation by executing a sequence of passes. + /// + /// + /// The type of map context to generate. Must implement and have a parameterless constructor. + /// Common implementations include for tile-based maps and + /// for room-based layouts. + /// + /// + /// + /// is the central orchestrator in the RogueElements pipeline architecture. + /// It maintains a priority-ordered collection of generation steps and executes them sequentially + /// to transform an empty context into a fully realized map. + /// + /// + /// The generation pipeline follows this flow: + /// + /// Create a new instance of via reflection + /// Initialize the random seed via + /// Execute each in priority order + /// Finalize the map via + /// + /// + /// + /// Generation steps are stored in a allowing fine-grained control + /// over execution order. Steps with lower priority values execute first. Multiple steps can share + /// the same priority and will execute in insertion order. + /// + /// + /// + /// + /// // Define a layout with generation steps + /// var layout = new MapGen<MyMapContext>(); + /// + /// // Add steps in priority order (lower executes first) + /// layout.GenSteps.Add(new Priority(1), new InitTilesStep<MyMapContext>(50, 50)); + /// layout.GenSteps.Add(new Priority(2), new DrawFloorPlanStep<MyMapContext>()); + /// layout.GenSteps.Add(new Priority(3), new PlaceStairsStep<MyMapContext>()); + /// + /// // Generate a map with a specific seed for reproducibility + /// ulong seed = 12345; + /// MyMapContext map = layout.GenMap(seed); + /// + /// [Serializable] public class MapGen where T : class, IGenContext { + /// + /// Initializes a new instance of the class with an empty step collection. + /// public MapGen() { this.GenSteps = new PriorityList>(); } + /// + /// Gets the priority-ordered collection of generation steps to execute. + /// + /// + /// A containing all instances + /// that will be executed during map generation. Steps are executed in ascending priority order. + /// + /// + /// Add steps using with a + /// value to control execution order. Lower priority values execute earlier in the pipeline. + /// public PriorityList> GenSteps { get; } - // an initial create-map method + /// + /// Generates a complete map by executing all registered generation steps in priority order. + /// + /// + /// The random seed used to initialize the map's random number generator. + /// Using the same seed with the same steps produces identical maps, enabling reproducibility. + /// + /// + /// A fully generated map context of type after all steps have been applied. + /// + /// + /// + /// This method creates a new instance of using , + /// requiring that has a public parameterless constructor. + /// + /// + /// The generation process triggers debug events at key points: + /// + /// - After map initialization + /// - Before each step executes + /// - After each step completes + /// - If a step throws an exception + /// + /// + /// + /// Exceptions thrown by individual steps are caught and forwarded to , + /// allowing generation to continue with subsequent steps. This behavior enables partial map generation + /// and debugging of problematic steps. + /// + /// + /// + /// + /// var layout = new MapGen<MyMapContext>(); + /// // ... add generation steps ... + /// + /// // Generate with a fixed seed for testing + /// var testMap = layout.GenMap(42); + /// + /// // Generate with a random seed for variety + /// var randomMap = layout.GenMap((ulong)DateTime.Now.Ticks); + /// + /// public T GenMap(ulong seed) { // may not need floor ID @@ -44,6 +143,24 @@ public T GenMap(ulong seed) return map; } + /// + /// Executes all generation steps from the priority queue on the specified map context. + /// + /// The map context to modify with generation steps. + /// + /// A priority queue containing the generation steps to execute, ordered by . + /// Steps are dequeued and applied sequentially. + /// + /// + /// + /// This method processes each step in priority order, invoking debug events before and after + /// each step via and . + /// + /// + /// Exceptions thrown during step execution are caught and reported via , + /// allowing the generation process to continue with remaining steps. + /// + /// protected static void ApplyGenSteps(T map, StablePriorityQueue queue) { while (queue.Count > 0) diff --git a/RogueElements/MapGen/README.md b/RogueElements/MapGen/README.md new file mode 100644 index 00000000..293bb55b --- /dev/null +++ b/RogueElements/MapGen/README.md @@ -0,0 +1,319 @@ +# MapGen - Core Pipeline + +[![Build](https://img.shields.io/github/actions/workflow/status/audinowho/RogueElements/build.yml?branch=master)](https://github.com/audinowho/RogueElements/actions) +[![NuGet](https://img.shields.io/nuget/v/RogueElements)](https://www.nuget.org/packages/RogueElements/) +[![License](https://img.shields.io/github/license/audinowho/RogueElements)](https://github.com/audinowho/RogueElements/blob/master/LICENSE) + +The MapGen folder contains the core procedural generation pipeline for RogueElements. This pipeline orchestrates the step-by-step construction of roguelike dungeon maps. + +## Architecture Overview + +RogueElements uses a **pipeline architecture** where `MapGen` orchestrates `GenStep` passes that sequentially modify an `IGenContext`. + +``` +MapGen.GenMap(seed) + | + v +Initialize Context (T) with seed + | + v +For each GenStep in priority order: + |-- GenStep.Apply(context) + |-- GenStep.Apply(context) + |-- ... + v +context.FinishGen() + | + v +Return completed map context +``` + +## Pipeline Flow Diagram + +```mermaid +flowchart TD + subgraph Orchestrator + MG[MapGen<T>] + end + + subgraph Context["IGenContext (Map State)"] + IC[IGenContext] + IC --> ITC[ITiledGenContext] + ITC --> IFPC[IFloorPlanGenContext] + IFPC --> IRGC[IRoomGridGenContext] + end + + subgraph Steps["GenStep Pipeline"] + GS1[GenStep 1
Priority: -4] + GS2[GenStep 2
Priority: -2] + GS3[GenStep 3
Priority: 0] + GS4[GenStep 4
Priority: 5] + end + + MG -->|"GenMap(seed)"| IC + MG -->|"Execute in order"| GS1 + GS1 -->|"Apply(context)"| GS2 + GS2 -->|"Apply(context)"| GS3 + GS3 -->|"Apply(context)"| GS4 + GS4 -->|"FinishGen()"| IC +``` + +## Core Classes and Interfaces + +### `MapGen` - The Orchestrator + +The central class that manages the generation pipeline. + +```csharp +// From MapGen.cs +public class MapGen + where T : class, IGenContext +{ + public PriorityList> GenSteps { get; } + + public T GenMap(ulong seed) + { + T map = (T)Activator.CreateInstance(typeof(T)); + map.InitSeed(seed); + + // Execute all steps in priority order + StablePriorityQueue queue = new StablePriorityQueue(); + foreach (Priority priority in this.GenSteps.GetPriorities()) + { + foreach (IGenStep genStep in this.GenSteps.GetItems(priority)) + queue.Enqueue(priority, genStep); + } + + ApplyGenSteps(map, queue); + map.FinishGen(); + + return map; + } +} +``` + +### `GenStep` - Base Step Class + +All generation steps inherit from this abstract class. + +```csharp +// From GenStep.cs +public abstract class GenStep : IGenStep + where T : class, IGenContext +{ + // Override this to implement your generation logic + public abstract void Apply(T map); + + public bool CanApply(IGenContext context) => context is T; +} +``` + +### `IGenStep` - Step Interface + +The non-generic interface that allows the pipeline to work with any step. + +```csharp +// From IGenStep.cs +public interface IGenStep +{ + bool CanApply(IGenContext context); + void Apply(IGenContext context); +} +``` + +## Context Interface Hierarchy + +The context interfaces form a hierarchy that enables increasingly specialized generation steps: + +```mermaid +classDiagram + class IGenContext { + <> + +IRandom Rand + +InitSeed(ulong seed) + +FinishGen() + } + + class ITiledGenContext { + <> + +ITile RoomTerrain + +ITile WallTerrain + +int Width + +int Height + +bool Wrap + +GetTile(Loc loc) ITile + +SetTile(Loc loc, ITile tile) + +CreateNew(int width, int height) + } + + class IFloorPlanGenContext { + <> + +FloorPlan RoomPlan + +InitPlan(FloorPlan plan) + } + + class IRoomGridGenContext { + <> + +GridPlan GridPlan + +InitGrid(GridPlan plan) + } + + IGenContext <|-- ITiledGenContext + ITiledGenContext <|-- IFloorPlanGenContext + IFloorPlanGenContext <|-- IRoomGridGenContext +``` + +| Interface | Purpose | Enables | +|-----------|---------|---------| +| `IGenContext` | Base interface with RNG and lifecycle | Basic generation steps | +| `ITiledGenContext` | Tile-based operations | Tile manipulation, terrain, walls | +| `IFloorPlanGenContext` | Freeform room placement | FloorPlan-based room/hall generation | +| `IRoomGridGenContext` | Grid-based room layouts | GridPlan-based structured layouts | + +## Creating Custom GenSteps + +### Basic Custom Step + +```csharp +[Serializable] +public class MyCustomStep : GenStep + where T : class, ITiledGenContext +{ + public int SomeParameter { get; set; } + + public override void Apply(T map) + { + // Use map.Rand for randomness (deterministic based on seed) + int randomX = map.Rand.Next(map.Width); + int randomY = map.Rand.Next(map.Height); + + // Modify the map + map.SetTile(new Loc(randomX, randomY), map.RoomTerrain); + } +} +``` + +### Adding Steps to the Pipeline + +```csharp +var layout = new MapGen(); + +// Steps are ordered by priority (lower = earlier) +layout.GenSteps.Add(-4, new InitGridPlanStep { ... }); +layout.GenSteps.Add(-2, new GridPathBranch { ... }); +layout.GenSteps.Add(-1, new DrawGridToFloorStep()); +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + +// Generate the map +MyMapGenContext result = layout.GenMap(seed); +``` + +## Priority System + +Steps execute in priority order. Common conventions: + +| Priority Range | Typical Usage | +|----------------|---------------| +| -10 to -5 | Plan initialization (InitGridPlanStep, InitFloorPlanStep) | +| -4 to -2 | Path/room generation (GridPathBranch, FloorPathBranch) | +| -1 | Plan-to-plan conversion (DrawGridToFloorStep) | +| 0 | Plan-to-tile conversion (DrawFloorToTileStep) | +| 1 to 10 | Post-processing (terrain, spawning, etc.) | + +## Debug Support + +The `GenContextDebug` class provides hooks for debugging generation: + +```csharp +// From GenContextDebug.cs +public static class GenContextDebug +{ + public static event Action OnInit; // Map initialization + public static event Action OnStep; // Progress updates + public static event Action OnStepIn; // Step entry + public static event Action OnStepOut; // Step exit + public static event Action OnError; // Error handling +} + +// Usage +GenContextDebug.OnStepIn += (stepName) => Console.WriteLine($"Starting: {stepName}"); +GenContextDebug.OnStep += (msg) => Console.WriteLine($"Progress: {msg}"); +``` + +## Subdirectories + +| Directory | Purpose | Documentation | +|-----------|---------|---------------| +| [`FloorPlan/`](./FloorPlan/README.md) | Freeform room-based generation | Rooms placed without grid constraint | +| [`Grid/`](./Grid/README.md) | Grid-based room layouts | Rooms arranged on a regular grid | +| `Rooms/` | Room shape generators | RoomGenSquare, RoomGenCave, etc. | +| `Spawning/` | Entity placement | Items, stairs, mobs | +| `Tiles/` | Tile manipulation | Terrain, water, post-processing | + +## Typical Generation Pipeline + +```mermaid +flowchart LR + subgraph Grid["Grid Phase (Optional)"] + IG[InitGridPlanStep] --> GP[GridPathBranch] + GP --> DG[DrawGridToFloorStep] + end + + subgraph Floor["Floor Phase"] + IF[InitFloorPlanStep] --> FP[FloorPathBranch] + FP --> CF[ConnectStep] + end + + subgraph Tile["Tile Phase"] + DT[DrawFloorToTileStep] --> TT[Terrain Steps] + TT --> SP[Spawning Steps] + end + + DG --> DT + CF --> DT +``` + +## Example: Minimal Pipeline + +From `Ex2_Rooms/Example2.cs`: + +```csharp +var layout = new MapGen(); + +// 1. Initialize a 54x40 floor plan +InitFloorPlanStep startGen = new InitFloorPlanStep(54, 40); +layout.GenSteps.Add(-2, startGen); + +// 2. Create room and hall types +var genericRooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, +}; + +var genericHalls = new SpawnList> +{ + { new RoomGenAngledHall(0, new RandRange(3, 7), new RandRange(3, 7)), 10 }, +}; + +// 3. Generate branching path of rooms and halls +FloorPathBranch path = new FloorPathBranch(genericRooms, genericHalls) +{ + HallPercent = 50, + FillPercent = new RandRange(45), + BranchRatio = new RandRange(0, 25), +}; +layout.GenSteps.Add(-1, path); + +// 4. Draw rooms to tiles with 1-tile padding +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + +// 5. Generate! +MapGenContext context = layout.GenMap(seed); +``` + +## See Also + +- [RogueElements Examples](../../RogueElements.Examples/) - Progressive examples (Ex1-Ex8) +- [FloorPlan README](./FloorPlan/README.md) - Freeform room generation +- [Grid README](./Grid/README.md) - Grid-based layouts diff --git a/RogueElements/MapGen/RandGenStep.cs b/RogueElements/MapGen/RandGenStep.cs index 0f5759a5..2cd645d9 100644 --- a/RogueElements/MapGen/RandGenStep.cs +++ b/RogueElements/MapGen/RandGenStep.cs @@ -9,19 +9,88 @@ namespace RogueElements { /// - /// Randomly chooses one of several gensteps to perform. + /// A generation step that randomly selects and executes one or more child steps from a weighted collection. /// - /// + /// + /// The type of map context this step operates on. Must implement . + /// + /// + /// + /// enables probabilistic generation by choosing which steps to execute + /// based on random selection from a weighted pool. This is useful for introducing variety into + /// map generation, such as randomly selecting room types, enemy spawns, or terrain features. + /// + /// + /// The property accepts any implementation, + /// which determines how many steps are selected and with what probability. Common implementations include: + /// + /// - Weighted random selection of a single item + /// - Selection from a bag with possible repeats + /// + /// + /// + /// When is called, the method determines + /// which child steps to execute, then each selected step is applied in sequence. + /// + /// + /// + /// + /// // Create a step that randomly chooses between different room types + /// var randStep = new RandGenStep<ITiledGenContext>(); + /// + /// var spawns = new SpawnList<GenStep<ITiledGenContext>>(); + /// spawns.Add(new RoomGenSquare(), 50); // 50% weight for square rooms + /// spawns.Add(new RoomGenRound(), 30); // 30% weight for round rooms + /// spawns.Add(new RoomGenCave(), 20); // 20% weight for cave rooms + /// + /// randStep.Spawns = spawns; + /// layout.GenSteps.Add(new Priority(5), randStep); + /// + /// + /// + /// [Serializable] public class RandGenStep : GenStep where T : class, IGenContext { + /// + /// Initializes a new instance of the class with no spawns configured. + /// + /// + /// The property must be set before this step is applied, + /// otherwise an exception will be thrown during generation. + /// public RandGenStep() { } + /// + /// Gets or sets the random picker that determines which child steps to execute. + /// + /// + /// An that returns a list of + /// instances to execute when is called. + /// + /// + /// + /// The picker is rolled using from the map context, + /// ensuring reproducible results when using the same generation seed. + /// + /// + /// This property must be set before is called. + /// + /// public IMultiRandPicker> Spawns { get; set; } + /// + /// Randomly selects and executes child generation steps from the collection. + /// + /// The map context to modify. + /// + /// This method rolls the picker to determine which steps to execute, + /// then applies each selected step to the map in sequence. The number of steps executed + /// depends on the picker implementation. + /// public override void Apply(T map) { List> steps = this.Spawns.Roll(map.Rand); @@ -29,6 +98,13 @@ public override void Apply(T map) step.Apply(map); } + /// + /// Returns a string representation of this step, including its spawns information. + /// + /// + /// A formatted string containing the type name and, if is set, + /// the spawns description in brackets. + /// public override string ToString() { if (this.Spawns == null) diff --git a/RogueElements/MapGen/Rooms/BaseRoomFilter.cs b/RogueElements/MapGen/Rooms/BaseRoomFilter.cs index 686a08b7..fe2f0a12 100644 --- a/RogueElements/MapGen/Rooms/BaseRoomFilter.cs +++ b/RogueElements/MapGen/Rooms/BaseRoomFilter.cs @@ -9,9 +9,19 @@ namespace RogueElements { + /// + /// Base class for filters that determine whether a room plan meets certain criteria. + /// Used to selectively apply operations to specific rooms in a floor layout. + /// [Serializable] public abstract class BaseRoomFilter { + /// + /// Checks whether a room plan passes all filters in a list. + /// + /// The room plan to check. + /// The list of filters to apply. + /// True if the room passes all filters; otherwise, false. public static bool PassesAllFilters(IRoomPlan plan, List filters) { foreach (BaseRoomFilter filter in filters) @@ -23,6 +33,11 @@ public static bool PassesAllFilters(IRoomPlan plan, List filters return true; } + /// + /// Determines whether a room plan passes this filter. + /// + /// The room plan to check. + /// True if the room passes the filter; otherwise, false. public abstract bool PassesFilter(IRoomPlan plan); } } diff --git a/RogueElements/MapGen/Rooms/ComponentCollection.cs b/RogueElements/MapGen/Rooms/ComponentCollection.cs index 483e1c7f..1c6d515a 100644 --- a/RogueElements/MapGen/Rooms/ComponentCollection.cs +++ b/RogueElements/MapGen/Rooms/ComponentCollection.cs @@ -6,13 +6,24 @@ namespace RogueElements { + /// + /// A collection of instances, keyed by their type. + /// Provides type-safe storage and retrieval of room components. + /// [Serializable] public class ComponentCollection : TypeDict { + /// + /// Initializes a new instance of the class. + /// public ComponentCollection() { } + /// + /// Creates a deep copy of this collection and all contained components. + /// + /// A new with cloned components. public ComponentCollection Clone() { ComponentCollection newCollection = new ComponentCollection(); diff --git a/RogueElements/MapGen/Rooms/Halls/BaseHallBrush.cs b/RogueElements/MapGen/Rooms/Halls/BaseHallBrush.cs index feddd0b8..fce3edeb 100644 --- a/RogueElements/MapGen/Rooms/Halls/BaseHallBrush.cs +++ b/RogueElements/MapGen/Rooms/Halls/BaseHallBrush.cs @@ -8,21 +8,36 @@ namespace RogueElements { + /// + /// Base class for brushes that paint hallway tiles. + /// Brushes define the shape and terrain of hall segments drawn by hall generators. + /// [Serializable] public abstract class BaseHallBrush { /// - /// Communicates to the Room/HallGen the size of the brush, for alignment purposes. + /// Gets the size of the brush in tiles, used for alignment purposes. /// public abstract Loc Size { get; } /// - /// Communicates to the Hoom/HallGen the center location of the brush, for alignment purposes. + /// Gets the center offset of the brush, used for alignment purposes. /// public abstract Loc Center { get; } + /// + /// Creates a deep copy of this brush. + /// + /// A new instance that is a copy of this brush. public abstract BaseHallBrush Clone(); + /// + /// Draws a hallway segment on the map using this brush. + /// + /// The map context to draw on. + /// The bounding rectangle of the hall area. + /// The starting point and direction of the hall segment. + /// The length of the hall segment in tiles. public abstract void DrawHallBrush(ITiledGenContext map, Rect bounds, LocRay4 ray, int length); } } diff --git a/RogueElements/MapGen/Rooms/Halls/DefaultHallBrush.cs b/RogueElements/MapGen/Rooms/Halls/DefaultHallBrush.cs index fb3fd6e6..d906ebf2 100644 --- a/RogueElements/MapGen/Rooms/Halls/DefaultHallBrush.cs +++ b/RogueElements/MapGen/Rooms/Halls/DefaultHallBrush.cs @@ -8,18 +8,26 @@ namespace RogueElements { + /// + /// A single-tile brush for painting hallways. + /// Paints one tile at a time using the map's room terrain. + /// [Serializable] public class DefaultHallBrush : BaseHallBrush { + /// public override Loc Size { get => Loc.One; } + /// public override Loc Center { get => Loc.Zero; } + /// public override BaseHallBrush Clone() { return new DefaultHallBrush(); } + /// public override void DrawHallBrush(ITiledGenContext map, Rect bounds, LocRay4 ray, int length) { for (int ii = 0; ii < length; ii++) diff --git a/RogueElements/MapGen/Rooms/Halls/README.md b/RogueElements/MapGen/Rooms/Halls/README.md new file mode 100644 index 00000000..e096a1f2 --- /dev/null +++ b/RogueElements/MapGen/Rooms/Halls/README.md @@ -0,0 +1,221 @@ +# Halls + +[![RogueElements](https://img.shields.io/nuget/v/RogueElements?label=RogueElements)](https://www.nuget.org/packages/RogueElements) + +Hall connector generators for procedural roguelike map generation. This module provides classes for connecting rooms with hallways of various styles and widths. + +## Purpose + +The Halls module generates hallways that connect rooms. It handles complex scenarios including: +- Straight hallways between aligned rooms +- Angled/bent hallways between misaligned rooms +- Multi-way intersections (3-way, 4-way) +- Right-angle connections +- Variable hallway widths via brushes + +## Hall Generator + +### RoomGenAngledHall + +The primary hall generator that connects room exits with narrow hallways. It handles all combinations of exits from all directions. + +```csharp +// Basic 1-tile wide hall +var hall = new RoomGenAngledHall(turnBias: 0); + +// Hall with 50% chance of making turns +var angledHall = new RoomGenAngledHall(turnBias: 50); + +// Hall with custom dimensions +var wideHall = new RoomGenAngledHall( + turnBias: 0, + width: new RandRange(3, 7), + height: new RandRange(3, 7) +); + +// Hall with custom brush +var customHall = new RoomGenAngledHall( + turnBias: 50, + brush: new SquareHallBrush(new Loc(2, 2)) +); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `HallTurnBias` | `int` | Percentage chance (0-100) for the hall to make a turn | +| `Brush` | `BaseHallBrush` | The brush used to draw the hall tiles | +| `Width` | `RandRange` | Preferred width of the hall area | +| `Height` | `RandRange` | Preferred height of the hall area | + +#### How It Works + +1. **Side Requirements**: Analyzes which directions need connections based on adjacent rooms +2. **Path Selection**: Chooses between straight paths, angled paths, or multi-way intersections +3. **Brush Drawing**: Uses the configured brush to paint tiles along the chosen path + +## Hall Brushes + +Hall brushes define how hallway tiles are painted. They control the width, shape, and terrain of hallways. + +### BaseHallBrush (Abstract) + +```csharp +public abstract class BaseHallBrush +{ + public abstract Loc Size { get; } // Brush dimensions + public abstract Loc Center { get; } // Center point for alignment + + public abstract BaseHallBrush Clone(); + public abstract void DrawHallBrush(ITiledGenContext map, Rect bounds, LocRay4 ray, int length); +} +``` + +### DefaultHallBrush + +A simple 1x1 tile brush. The most common choice for standard hallways. + +```csharp +var brush = new DefaultHallBrush(); +// Size: 1x1 +// Draws single-tile wide corridors +``` + +### SquareHallBrush + +A rectangular brush for wider hallways. + +```csharp +// 2x2 tile brush for wider halls +var brush = new SquareHallBrush(new Loc(2, 2)); + +// 3x1 horizontal corridor +var horizontalBrush = new SquareHallBrush(new Loc(3, 1)); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Dims` | `Loc` | Dimensions of the brush in tiles | + +### TerrainHallBrush + +A rectangular brush that paints a specific terrain type instead of the default room terrain. + +```csharp +// Create a brush that paints water tiles +var waterBrush = new TerrainHallBrush(new Loc(2, 2), waterTile); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Dims` | `Loc` | Dimensions of the brush in tiles | +| `Terrain` | `ITile` | The terrain type to paint | + +## Usage Example + +From `Ex2_Rooms`: + +```csharp +// Create hall types with spawn weights +var genericHalls = new SpawnList> +{ + // Angled halls that may turn, with variable dimensions + { new RoomGenAngledHall(0, new RandRange(3, 7), new RandRange(3, 7)), 10 }, + + // Simple 1x1 connector rooms + { new RoomGenSquare(new RandRange(1), new RandRange(1)), 20 }, +}; + +// Use in a floor path +var path = new FloorPathBranch(genericRooms, genericHalls) +{ + HallPercent = 50, // 50% chance to use halls between rooms + FillPercent = new RandRange(45), + BranchRatio = new RandRange(0, 25), +}; +``` + +Grid-based example from `Ex3_Grid`: + +```csharp +var genericHalls = new SpawnList> +{ + // 50% turn bias for more interesting layouts + { new RoomGenAngledHall(50), 10 } +}; +path.GenericHalls = genericHalls; +``` + +## Creating Custom Hall Brushes + +1. Inherit from `BaseHallBrush` +2. Implement `Size` and `Center` properties +3. Implement `DrawHallBrush()` to paint tiles along the ray + +```csharp +[Serializable] +public class DiagonalHallBrush : BaseHallBrush +{ + public override Loc Size => new Loc(2, 2); + public override Loc Center => Loc.Zero; + + public override BaseHallBrush Clone() => new DiagonalHallBrush(); + + public override void DrawHallBrush(ITiledGenContext map, Rect bounds, LocRay4 ray, int length) + { + for (int ii = 0; ii < length; ii++) + { + Loc point = ray.Traverse(ii); + + // Draw main tile + map.SetTile(point, map.RoomTerrain.Copy()); + + // Add diagonal accent based on direction + Loc offset = ray.Dir.ToAxis() == Axis4.Horiz + ? new Loc(0, ii % 2 == 0 ? 1 : -1) + : new Loc(ii % 2 == 0 ? 1 : -1, 0); + + Loc accentLoc = point + offset; + if (Collision.InBounds(bounds, accentLoc)) + map.SetTile(accentLoc, map.RoomTerrain.Copy()); + } + } +} +``` + +## Hall Connection Logic + +The `RoomGenAngledHall` handles several connection scenarios: + +### Straight Connections +When rooms align on one axis, a direct straight hall is drawn. + +### Right-Angle Connections +When only two non-opposite directions have connections, an L-shaped path is created. + +### Multi-Way Intersections +For 3-way or 4-way connections, the hall draws: +1. Primary hall (first pair of opposite sides) +2. Secondary hall (remaining sides, connecting to primary) + +### Turn Bias +The `HallTurnBias` property controls whether aligned rooms get straight or bent connections: +- `0`: Prefer straight halls when possible +- `50`: 50/50 chance of turn vs straight +- `100`: Always turn when possible + +## Related Modules + +- **[../](../)** - Parent Rooms module (RoomGen base classes) +- **[FloorPlan/](../../FloorPlan/)** - Freeform room placement using halls +- **[Grid/](../../Grid/)** - Grid-based room layouts using halls + +## See Also + +- `Ex2_Rooms` - Freeform hall generation example +- `Ex3_Grid` - Grid-based hall generation example diff --git a/RogueElements/MapGen/Rooms/Halls/RoomGenAngledHall.cs b/RogueElements/MapGen/Rooms/Halls/RoomGenAngledHall.cs index ba355163..f0f5569c 100644 --- a/RogueElements/MapGen/Rooms/Halls/RoomGenAngledHall.cs +++ b/RogueElements/MapGen/Rooms/Halls/RoomGenAngledHall.cs @@ -12,28 +12,46 @@ namespace RogueElements /// A room that connects its exits with a narrow hallway. /// It is able to handle all combinations of exits from all combination of directions. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public class RoomGenAngledHall : PermissiveRoomGen where T : ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public RoomGenAngledHall() { this.Brush = new DefaultHallBrush(); } + /// + /// Initializes a new instance of the class with a specified turn bias. + /// + /// The percentage chance (0-100) for the hall to make a turn. public RoomGenAngledHall(int turnBias) { this.HallTurnBias = turnBias; this.Brush = new DefaultHallBrush(); } + /// + /// Initializes a new instance of the class with a turn bias and brush. + /// + /// The percentage chance (0-100) for the hall to make a turn. + /// The brush used to draw the hall. public RoomGenAngledHall(int turnBias, BaseHallBrush brush) { this.HallTurnBias = turnBias; this.Brush = brush; } + /// + /// Initializes a new instance of the class with turn bias and dimensions. + /// + /// The percentage chance (0-100) for the hall to make a turn. + /// The preferred width range of the hall area. + /// The preferred height range of the hall area. public RoomGenAngledHall(int turnBias, RandRange width, RandRange height) { this.HallTurnBias = turnBias; @@ -42,6 +60,10 @@ public RoomGenAngledHall(int turnBias, RandRange width, RandRange height) this.Height = height; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. protected RoomGenAngledHall(RoomGenAngledHall other) { this.HallTurnBias = other.HallTurnBias; @@ -51,32 +73,35 @@ protected RoomGenAngledHall(RoomGenAngledHall other) } /// - /// A percentage chance 0 to 100 for the hall making a turn. + /// Gets or sets the percentage chance (0-100) for the hall to make a turn instead of going straight. /// public int HallTurnBias { get; set; } /// - /// The brush to draw the hall with. + /// Gets or sets the brush used to draw the hall tiles. /// public BaseHallBrush Brush { get; set; } /// - /// The preferred width of the area covered by the hall. + /// Gets or sets the preferred width range of the area covered by the hall. /// public RandRange Width { get; set; } /// - /// The preferred height of the area covered by the hall. + /// Gets or sets the preferred height range of the area covered by the hall. /// public RandRange Height { get; set; } + /// public override RoomGen Copy() => new RoomGenAngledHall(this); + /// public override Loc ProposeSize(IRandom rand) { return new Loc(this.Width.Pick(rand), this.Height.Pick(rand)); } + /// public override void DrawOnMap(T map) { // check if there are any sides that have intersections such that straight lines are possible @@ -246,11 +271,11 @@ public override void DrawOnMap(T map) /// /// Draws a bundle of halls from one direction, going up to the specified point, and connects them. /// - /// - /// - /// - /// - /// todo: describe drawnRays parameter on DrawCombinedHall + /// The map context to draw on. + /// The list of previously drawn ray segments for collision detection. + /// The direction from which the halls originate. + /// The coordinate where the halls should end. + /// The starting positions of each hall along the perpendicular axis. public void DrawCombinedHall(ITiledGenContext map, List<(LocRay4, int)> drawnRays, Dir4 dir, int forwardEnd, int[] starts) { bool vertical = dir.ToAxis() == Axis4.Vert; @@ -279,11 +304,21 @@ public void DrawCombinedHall(ITiledGenContext map, List<(LocRay4, int)> drawnRay this.DrawHall(map, drawnRays, combineStart, combineEnd, vertical ? Dir4.Right : Dir4.Down); } + /// public override string ToString() { return string.Format("{0}: Angle:{1}%", this.GetType().GetFormattedTypeName(), this.HallTurnBias); } + /// + /// Chooses starting tiles for a bent hall with exactly one start and one end. + /// Ensures the start and end are not aligned to force a bend. + /// + /// The map context for random number generation. + /// The set of possible start positions. + /// The set of possible end positions. + /// Output array to store the chosen start tile. + /// Output array to store the chosen end tile. private static void Choose1on1BentHallStarts(T map, HashSet starts, HashSet ends, int[] startTiles, int[] endTiles) { // special case; make sure that start and end are NOT aligned to each other because we want a bend @@ -321,11 +356,11 @@ private static void Choose1on1BentHallStarts(T map, HashSet starts, HashSet } /// - /// Returns the intersection of two hashsets IF they both contain only one hashset. Returns an empty hashset otherwise. + /// Returns the intersection of two hashsets IF they both contain only one hashset. Returns an empty hashset otherwise. /// - /// - /// - /// + /// The first list of hash sets. + /// The second list of hash sets. + /// The intersection if both lists have exactly one set; otherwise, an empty set. private static HashSet GetIntersectedTiles(List> opening1, List> opening2) { HashSet intersect = new HashSet(); @@ -339,6 +374,13 @@ private static HashSet GetIntersectedTiles(List> opening1, Lis return intersect; } + /// + /// Gets the minimum or maximum value from an array of choices based on direction and extension preference. + /// + /// The array of position choices. + /// The direction of the hall. + /// Whether to extend to the farthest position. + /// The selected minimum or maximum position. private static int GetHallMinMax(int[] choices, Dir4 dir, bool extendFar) { bool useMax = extendFar; @@ -353,6 +395,12 @@ private static int GetHallMinMax(int[] choices, Dir4 dir, bool extendFar) return result; } + /// + /// Checks if a location collides with any previously drawn hall segment. + /// + /// The list of previously drawn ray segments. + /// The location to check for collision. + /// True if the location collides with a drawn hall; otherwise, false. private static bool CollidesWithHall(List<(LocRay4, int)> drawnRays, Loc endLoc) { foreach ((LocRay4 ray, int range) drawn in drawnRays) @@ -367,11 +415,11 @@ private static bool CollidesWithHall(List<(LocRay4, int)> drawnRays, Loc endLoc) /// /// Draws a hall in a straight cardinal direction, starting with one point and ending with another (inclusive). /// - /// - /// - /// - /// - /// + /// The map context to draw on. + /// The list to add the drawn ray segment to. + /// The starting point of the hall. + /// The ending point of the hall. + /// The direction of the hall. private void DrawHall(ITiledGenContext map, List<(LocRay4, int)> drawnRays, Loc point1, Loc point2, Dir4 dir) { LocRay4 ray = new LocRay4(point1, dir); @@ -420,12 +468,12 @@ private void DrawHall(ITiledGenContext map, List<(LocRay4, int)> drawnRays, Loc /// /// In a 4- or 3-way hall situation, this method is called to add the remaining ways after the first two have been added. /// - /// - /// - /// - /// - /// - /// todo: describe drawnRays parameter on DrawSecondaryHall + /// The map context to draw on. + /// The list of previously drawn ray segments. + /// The set of crossing positions for straight halls. + /// The dictionary of possible starting positions by direction. + /// Whether this is a vertical hall. + /// Whether the hall should turn. private void DrawSecondaryHall(T map, List<(LocRay4, int)> drawnRays, HashSet cross, Dictionary>> possibleStarts, bool vertical, bool turn) { if (!turn) @@ -502,12 +550,12 @@ private void DrawSecondaryHall(T map, List<(LocRay4, int)> drawnRays, HashSet /// Draws the hall connecting the first opposite pair of sides. /// - /// - /// - /// - /// - /// - /// todo: describe drawnRays parameter on DrawPrimaryHall + /// The map context to draw on. + /// The list to add drawn ray segments to. + /// The set of crossing positions for straight halls. + /// The dictionary of possible starting positions by direction. + /// Whether this is a vertical hall. + /// Whether the hall should turn. private void DrawPrimaryHall(T map, List<(LocRay4, int)> drawnRays, HashSet cross, Dictionary>> possibleStarts, bool vertical, bool turn) { if (!turn) @@ -588,10 +636,10 @@ private void DrawPrimaryHall(T map, List<(LocRay4, int)> drawnRays, HashSet /// /// Draws a single straight hall in the specified direction, choosing ONE of the scalars provided in cross. /// - /// - /// - /// - /// todo: describe drawnRays parameter on DrawStraightHall + /// The map context to draw on. + /// The list to add drawn ray segments to. + /// The set of possible crossing positions to choose from. + /// Whether this is a vertical hall. private void DrawStraightHall(T map, List<(LocRay4, int)> drawnRays, HashSet cross, bool vertical) { int startSideDist = MathUtils.ChooseFromHash(cross, map.Rand); diff --git a/RogueElements/MapGen/Rooms/Halls/SquareHallBrush.cs b/RogueElements/MapGen/Rooms/Halls/SquareHallBrush.cs index 09c2bb21..6bc5f437 100644 --- a/RogueElements/MapGen/Rooms/Halls/SquareHallBrush.cs +++ b/RogueElements/MapGen/Rooms/Halls/SquareHallBrush.cs @@ -10,38 +10,54 @@ namespace RogueElements { /// /// A rectangular brush for painting hallways. + /// Paints a rectangular area of tiles using the map's room terrain. /// [Serializable] public class SquareHallBrush : BaseHallBrush { + /// + /// Initializes a new instance of the class. + /// public SquareHallBrush() { } + /// + /// Initializes a new instance of the class with specified dimensions. + /// + /// The dimensions of the brush in tiles. public SquareHallBrush(Loc size) { this.Dims = size; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. public SquareHallBrush(SquareHallBrush other) { this.Dims = other.Dims; } /// - /// Dimensions of the brush, in Tiles + /// Gets or sets the dimensions of the brush in tiles. /// public Loc Dims { get; set; } + /// public override Loc Size { get => this.Dims; } + /// public override Loc Center { get => Loc.Zero; } + /// public override BaseHallBrush Clone() { return new SquareHallBrush(this); } + /// public override void DrawHallBrush(ITiledGenContext map, Rect bounds, LocRay4 ray, int length) { for (int ii = 0; ii < length; ii++) diff --git a/RogueElements/MapGen/Rooms/Halls/TerrainHallBrush.cs b/RogueElements/MapGen/Rooms/Halls/TerrainHallBrush.cs index 781945f9..870d76a3 100644 --- a/RogueElements/MapGen/Rooms/Halls/TerrainHallBrush.cs +++ b/RogueElements/MapGen/Rooms/Halls/TerrainHallBrush.cs @@ -9,43 +9,63 @@ namespace RogueElements { /// - /// A rectangular brush for painting hallways. + /// A rectangular brush for painting hallways with a custom terrain type. + /// Paints a rectangular area of tiles using a specified terrain instead of the default room terrain. /// [Serializable] public class TerrainHallBrush : BaseHallBrush { + /// + /// Initializes a new instance of the class. + /// public TerrainHallBrush() { } + /// + /// Initializes a new instance of the class with specified dimensions and terrain. + /// + /// The dimensions of the brush in tiles. + /// The terrain type to paint. public TerrainHallBrush(Loc size, ITile terrain) { this.Dims = size; this.Terrain = terrain; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. public TerrainHallBrush(TerrainHallBrush other) { this.Dims = other.Dims; this.Terrain = other.Terrain; } + /// + /// Gets or sets the terrain type to paint with this brush. + /// public ITile Terrain { get; set; } /// - /// Dimensions of the brush, in Tiles + /// Gets or sets the dimensions of the brush in tiles. /// public Loc Dims { get; set; } + /// public override Loc Size { get => this.Dims; } + /// public override Loc Center { get => Loc.Zero; } + /// public override BaseHallBrush Clone() { return new TerrainHallBrush(this); } + /// public override void DrawHallBrush(ITiledGenContext map, Rect bounds, LocRay4 ray, int length) { for (int ii = 0; ii < length; ii++) diff --git a/RogueElements/MapGen/Rooms/IPermissiveRoomGen.cs b/RogueElements/MapGen/Rooms/IPermissiveRoomGen.cs index 04f2cd57..53800694 100644 --- a/RogueElements/MapGen/Rooms/IPermissiveRoomGen.cs +++ b/RogueElements/MapGen/Rooms/IPermissiveRoomGen.cs @@ -5,14 +5,28 @@ namespace RogueElements { + /// + /// Marker interface for room generators that can accept connections from any border tile. + /// Permissive rooms have all border tiles marked as fulfillable, allowing maximum flexibility + /// in hallway connections. + /// public interface IPermissiveRoomGen : IRoomGen { } + /// + /// Defines a room generator with configurable width and height ranges. + /// public interface ISizedRoomGen : IRoomGen { + /// + /// Gets or sets the range of possible widths for the room. + /// RandRange Width { get; set; } + /// + /// Gets or sets the range of possible heights for the room. + /// RandRange Height { get; set; } } } \ No newline at end of file diff --git a/RogueElements/MapGen/Rooms/IRoomGen.cs b/RogueElements/MapGen/Rooms/IRoomGen.cs index 1617738d..2c18a1ae 100644 --- a/RogueElements/MapGen/Rooms/IRoomGen.cs +++ b/RogueElements/MapGen/Rooms/IRoomGen.cs @@ -7,26 +7,81 @@ namespace RogueElements { + /// + /// Defines the contract for room generation algorithms. + /// Implementations generate room shapes and manage border connections to adjacent rooms. + /// public interface IRoomGen { + /// + /// Gets the bounding rectangle where the room is drawn on the map. + /// Rect Draw { get; } + /// + /// Requests that a given range of tiles be fulfilled by this room. + /// Adds a side requirement and considers all tiles in the range as eligible for fulfillment. + /// + /// The range of tile positions to request, in absolute coordinates. + /// The direction from this room toward the range. void AskBorderRange(IntRange range, Dir4 dir); + /// + /// Requests that border tiles be fulfilled based on another room's opened borders. + /// Creates a side requirement using the edge location of the source room. + /// + /// The bounding rectangle of the source room. + /// A function to query if a specific border tile is open in the source room. + /// The direction from this room to the source room. void AskBorderFromRoom(Rect sourceDraw, Func borderQuery, Dir4 dir); + /// + /// Gets whether a specific border tile has been opened for connection. + /// + /// The direction of the border. + /// The index of the tile along the border. + /// True if the border tile is opened; otherwise, false. bool GetOpenedBorder(Dir4 dir, int index); + /// + /// Gets whether a specific border tile can potentially be opened for connection. + /// + /// The direction of the border. + /// The index of the tile along the border. + /// True if the border tile can be opened; otherwise, false. bool GetFulfillableBorder(Dir4 dir, int index); + /// + /// Returns the preferred dimensions for this room. + /// + /// The random number generator to use. + /// The proposed size as a . Loc ProposeSize(IRandom rand); + /// + /// Initializes the room with the specified size. + /// If the proposed size is not used, the room may draw a default empty square. + /// + /// The random number generator to use. + /// The size to initialize the room with. void PrepareSize(IRandom rand, Loc size); + /// + /// Sets the location where the room will be drawn on the map. + /// + /// The top-left corner location of the room. void SetLoc(Loc loc); + /// + /// Draws the room onto the specified map context. + /// + /// The map context to draw on. void DrawOnMap(ITiledGenContext map); + /// + /// Creates a deep copy of this room generator. + /// + /// A new instance that is a copy of this room generator. IRoomGen Copy(); } } \ No newline at end of file diff --git a/RogueElements/MapGen/Rooms/IRoomGenCross.cs b/RogueElements/MapGen/Rooms/IRoomGenCross.cs index 62064db0..4b82698d 100644 --- a/RogueElements/MapGen/Rooms/IRoomGenCross.cs +++ b/RogueElements/MapGen/Rooms/IRoomGenCross.cs @@ -8,21 +8,38 @@ namespace RogueElements { + /// + /// Defines a room generator that creates cross-shaped rooms. + /// The room is composed of two intersecting rectangles: one horizontal (major width x minor height) + /// and one vertical (minor width x major height). + /// public interface IRoomGenCross : IRoomGen { + /// + /// Gets or sets the range of possible widths for the horizontal rectangle. + /// RandRange MajorWidth { get; set; } + /// + /// Gets or sets the range of possible heights for the vertical rectangle. + /// RandRange MajorHeight { get; set; } + /// + /// Gets or sets the range of possible heights for the horizontal rectangle. + /// RandRange MinorHeight { get; set; } + /// + /// Gets or sets the range of possible widths for the vertical rectangle. + /// RandRange MinorWidth { get; set; } } /// /// Generates a room composed of two rectangles, one vertical and one horizontal. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public class RoomGenCross : RoomGen, IRoomGenCross where T : ITiledGenContext @@ -39,10 +56,20 @@ public class RoomGenCross : RoomGen, IRoomGenCross [NonSerialized] private int chosenOffsetY; + /// + /// Initializes a new instance of the class. + /// public RoomGenCross() { } + /// + /// Initializes a new instance of the class with specified dimensions. + /// + /// The width range of the horizontal rectangle. + /// The height range of the vertical rectangle. + /// The height range of the horizontal rectangle. + /// The width range of the vertical rectangle. public RoomGenCross(RandRange majorWidth, RandRange majorHeight, RandRange minorHeight, RandRange minorWidth) { this.MajorWidth = majorWidth; @@ -51,6 +78,10 @@ public RoomGenCross(RandRange majorWidth, RandRange majorHeight, RandRange minor this.MinorHeight = minorHeight; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. protected RoomGenCross(RoomGenCross other) { this.MajorWidth = other.MajorWidth; @@ -60,40 +91,55 @@ protected RoomGenCross(RoomGenCross other) } /// - /// The width of the horizontal rectangle. + /// Gets or sets the width of the horizontal rectangle. /// public RandRange MajorWidth { get; set; } /// - /// The height of the horizontal rectangle. + /// Gets or sets the height of the horizontal rectangle. /// public RandRange MinorHeight { get; set; } /// - /// The height of the vertical rectangle. + /// Gets or sets the height of the vertical rectangle. /// public RandRange MajorHeight { get; set; } /// - /// The width of the vertical rectangle. + /// Gets or sets the width of the vertical rectangle. /// public RandRange MinorWidth { get; set; } + /// + /// Gets or sets the chosen width of the vertical rectangle after size preparation. + /// protected int ChosenMinorWidth { get => this.chosenMinorWidth; set => this.chosenMinorWidth = value; } + /// + /// Gets or sets the chosen height of the horizontal rectangle after size preparation. + /// protected int ChosenMinorHeight { get => this.chosenMinorHeight; set => this.chosenMinorHeight = value; } + /// + /// Gets or sets the X offset of the vertical rectangle within the bounding box. + /// protected int ChosenOffsetX { get => this.chosenOffsetX; set => this.chosenOffsetX = value; } + /// + /// Gets or sets the Y offset of the horizontal rectangle within the bounding box. + /// protected int ChosenOffsetY { get => this.chosenOffsetY; set => this.chosenOffsetY = value; } + /// public override RoomGen Copy() => new RoomGenCross(this); + /// public override Loc ProposeSize(IRandom rand) { return new Loc(this.MajorWidth.Pick(rand), this.MajorHeight.Pick(rand)); } + /// public override void DrawOnMap(T map) { Loc size1 = new Loc(this.Draw.Width, this.ChosenMinorHeight); @@ -121,11 +167,13 @@ public override void DrawOnMap(T map) this.SetRoomBorders(map); } + /// public override string ToString() { return string.Format("{0}: {1}x{2}+{3}x{4}", this.GetType().GetFormattedTypeName(), this.MajorWidth, this.MinorHeight, this.MinorWidth, this.MajorHeight); } + /// protected override void PrepareFulfillableBorders(IRandom rand) { this.ChosenMinorWidth = Math.Min(this.Draw.Width, this.MinorWidth.Pick(rand)); diff --git a/RogueElements/MapGen/Rooms/IRoomGenDefault.cs b/RogueElements/MapGen/Rooms/IRoomGenDefault.cs index 937e7db4..ee046117 100644 --- a/RogueElements/MapGen/Rooms/IRoomGenDefault.cs +++ b/RogueElements/MapGen/Rooms/IRoomGenDefault.cs @@ -8,29 +8,40 @@ namespace RogueElements { + /// + /// Marker interface for the default room generator. + /// Used to identify rooms that should be treated as default/placeholder rooms. + /// public interface IRoomGenDefault { } /// /// Generates a one-tile room. + /// Serves as the simplest possible room generator and default placeholder. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public class RoomGenDefault : PermissiveRoomGen, IRoomGenDefault where T : ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public RoomGenDefault() { } + /// public override RoomGen Copy() => new RoomGenDefault(); + /// public override Loc ProposeSize(IRandom rand) { return Loc.One; } + /// public override void DrawOnMap(T map) { this.DrawMapDefault(map); diff --git a/RogueElements/MapGen/Rooms/IRoomPlan.cs b/RogueElements/MapGen/Rooms/IRoomPlan.cs index 4ece8910..5a71f4ec 100644 --- a/RogueElements/MapGen/Rooms/IRoomPlan.cs +++ b/RogueElements/MapGen/Rooms/IRoomPlan.cs @@ -5,10 +5,20 @@ namespace RogueElements { + /// + /// Defines a plan for a room within a floor layout. + /// Contains the room generator and associated metadata components. + /// public interface IRoomPlan { + /// + /// Gets the room generator that creates the room's physical structure. + /// IRoomGen RoomGen { get; } + /// + /// Gets the collection of components that provide metadata and behavior for the room. + /// ComponentCollection Components { get; } } } \ No newline at end of file diff --git a/RogueElements/MapGen/Rooms/PermissiveRoomGen.cs b/RogueElements/MapGen/Rooms/PermissiveRoomGen.cs index 21b3eaab..d7a1544b 100644 --- a/RogueElements/MapGen/Rooms/PermissiveRoomGen.cs +++ b/RogueElements/MapGen/Rooms/PermissiveRoomGen.cs @@ -9,16 +9,21 @@ namespace RogueElements { /// /// Subclass of RoomGen that can fulfill any combination of paths leading into it. + /// All border tiles are marked as fulfillable, allowing connections from any direction. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public abstract class PermissiveRoomGen : RoomGen, IPermissiveRoomGen where T : ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// protected PermissiveRoomGen() { } + /// protected override void PrepareFulfillableBorders(IRandom rand) { foreach (Dir4 dir in DirExt.VALID_DIR4) diff --git a/RogueElements/MapGen/Rooms/README.md b/RogueElements/MapGen/Rooms/README.md new file mode 100644 index 00000000..15098833 --- /dev/null +++ b/RogueElements/MapGen/Rooms/README.md @@ -0,0 +1,243 @@ +# Rooms + +[![RogueElements](https://img.shields.io/nuget/v/RogueElements?label=RogueElements)](https://www.nuget.org/packages/RogueElements) + +Room shape generators for procedural roguelike map generation. This module provides the core abstraction for generating room shapes and managing room-to-room connections. + +## Purpose + +The Rooms module generates individual room shapes that can be placed into floor plans. Each room generator defines its own shape (square, round, cave, etc.) and manages how hallways can connect to it from any of its four cardinal sides. + +## Core Interface + +### IRoomGen + +The fundamental interface that all room generators implement: + +```csharp +public interface IRoomGen +{ + Rect Draw { get; } // The rectangle the room occupies + + Loc ProposeSize(IRandom rand); // Returns preferred dimensions + void PrepareSize(IRandom rand, Loc size); // Initialize to specified size + void SetLoc(Loc loc); // Position the room + void DrawOnMap(ITiledGenContext map); // Render tiles to the map + + // Border management for hallway connections + void AskBorderRange(IntRange range, Dir4 dir); + void AskBorderFromRoom(Rect sourceDraw, Func borderQuery, Dir4 dir); + bool GetOpenedBorder(Dir4 dir, int index); + bool GetFulfillableBorder(Dir4 dir, int index); + + IRoomGen Copy(); +} +``` + +## Room Generator Classes + +### RoomGenSquare + +Generates simple rectangular rooms. + +```csharp +// 4-8 tiles wide, 4-8 tiles tall +var room = new RoomGenSquare( + new RandRange(4, 8), // Width + new RandRange(4, 8) // Height +); +``` + +### RoomGenRound + +Generates rounded rooms. Square dimensions produce circles; rectangular dimensions produce capsules. + +```csharp +// 5-9 tiles in each dimension +var room = new RoomGenRound( + new RandRange(5, 9), + new RandRange(5, 9) +); +``` + +### RoomGenCave + +Generates organic cave-like rooms using cellular automata. Falls back to a square if forced to a size it did not propose. + +```csharp +var cave = new RoomGenCave( + new RandRange(6, 12), // Max width + new RandRange(6, 12) // Max height +); +``` + +### RoomGenBump + +Generates rectangular rooms with randomly blocked perimeter tiles, creating irregular edges. + +```csharp +var bump = new RoomGenBump( + new RandRange(5, 10), // Width + new RandRange(5, 10), // Height + new RandRange(20, 50) // Bump percent (chance of perimeter blocks) +); +``` + +### RoomGenBlocked + +Generates rectangular rooms with a rectangular obstacle block inside. + +```csharp +var blocked = new RoomGenBlocked( + blockTerrain, // Tile for the block + new RandRange(6, 10), // Room width + new RandRange(6, 10), // Room height + new RandRange(2, 4), // Block width + new RandRange(2, 4) // Block height +); +``` + +### RoomGenSpecific + +Generates rooms with exact tile-by-tile specifications. Useful for hand-crafted special rooms. + +```csharp +var specific = new RoomGenSpecific(width, height, roomTerrain); +specific.Tiles[x][y] = customTile; +``` + +## Base Classes + +### RoomGen<T> + +Abstract base class providing the common logic for all room generators. Key responsibilities: + +- **Border Management**: Tracks which border tiles can accept hallway connections +- **Size Preparation**: Validates and applies room dimensions +- **Fulfillment**: Ensures rooms can connect to adjacent rooms/halls + +All `RoomGen` implementations must follow these rules: +1. Generate solvable rooms (any entrance can reach any exit) +2. Handle any given size without throwing exceptions +3. Provide at least one opening per cardinal direction if asked + +### PermissiveRoomGen<T> + +A subclass of `RoomGen` that can accept connections from any border tile. Used for halls and simple rectangular rooms. + +```csharp +// PermissiveRoomGen allows halls to connect anywhere on its border +public abstract class PermissiveRoomGen : RoomGen +{ + protected override void PrepareFulfillableBorders(IRandom rand) + { + // Mark all border tiles as fulfillable + foreach (Dir4 dir in DirExt.VALID_DIR4) + for (int jj = 0; jj < FulfillableBorder[dir].Length; jj++) + FulfillableBorder[dir][jj] = true; + } +} +``` + +## Usage Example + +From `Ex2_Rooms`: + +```csharp +var layout = new MapGen(); + +// Initialize floor plan +var startGen = new InitFloorPlanStep(54, 40); +layout.GenSteps.Add(-2, startGen); + +// Create room types with spawn weights +var genericRooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, +}; + +// Create hall types +var genericHalls = new SpawnList> +{ + { new RoomGenAngledHall(0, new RandRange(3, 7), new RandRange(3, 7)), 10 }, + { new RoomGenSquare(new RandRange(1), new RandRange(1)), 20 }, +}; + +// Create branching path +var path = new FloorPathBranch(genericRooms, genericHalls) +{ + HallPercent = 50, + FillPercent = new RandRange(45), + BranchRatio = new RandRange(0, 25), +}; +layout.GenSteps.Add(-1, path); + +// Draw to tiles +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); +``` + +## Creating Custom Room Generators + +1. Inherit from `RoomGen` or `PermissiveRoomGen` +2. Implement `ProposeSize()` to return preferred dimensions +3. Implement `DrawOnMap()` to render tiles +4. Override `PrepareFulfillableBorders()` to define valid connection points + +```csharp +[Serializable] +public class RoomGenDiamond : RoomGen + where T : ITiledGenContext +{ + public RandRange Size { get; set; } + + public override Loc ProposeSize(IRandom rand) + { + int size = Size.Pick(rand); + return new Loc(size, size); + } + + public override void DrawOnMap(T map) + { + int center = Draw.Width / 2; + for (int y = 0; y < Draw.Height; y++) + { + int dist = Math.Abs(y - center); + for (int x = dist; x < Draw.Width - dist; x++) + map.SetTile(new Loc(Draw.X + x, Draw.Y + y), map.RoomTerrain.Copy()); + } + SetRoomBorders(map); + } + + protected override void PrepareFulfillableBorders(IRandom rand) + { + // Only allow connections at the diamond's widest points + int center = Draw.Width / 2; + FulfillableBorder[Dir4.Up][center] = true; + FulfillableBorder[Dir4.Down][center] = true; + FulfillableBorder[Dir4.Left][center] = true; + FulfillableBorder[Dir4.Right][center] = true; + } + + public override RoomGen Copy() => new RoomGenDiamond(this); +} +``` + +## Room Filters + +Use `BaseRoomFilter` and its subclasses to control which rooms are eligible for certain operations: + +- `RoomFilterComponent` - Filter by room component type +- `RoomFilterDefaultGen` - Filter by room generator type +- `RoomFilterHall` - Filter halls vs rooms + +## Related Modules + +- **[Halls/](./Halls/)** - Hall connector generators (RoomGenAngledHall, hall brushes) +- **[FloorPlan/](../FloorPlan/)** - Freeform room placement +- **[Grid/](../Grid/)** - Grid-based room layouts + +## See Also + +- `Ex2_Rooms` - Freeform room generation example +- `Ex3_Grid` - Grid-based room generation example diff --git a/RogueElements/MapGen/Rooms/RoomComponent.cs b/RogueElements/MapGen/Rooms/RoomComponent.cs index bfff14fb..6750d87c 100644 --- a/RogueElements/MapGen/Rooms/RoomComponent.cs +++ b/RogueElements/MapGen/Rooms/RoomComponent.cs @@ -6,9 +6,17 @@ namespace RogueElements { + /// + /// Base class for components that can be attached to rooms to provide metadata and behavior. + /// Components are used to tag rooms with additional information for filtering and spawning. + /// [Serializable] public abstract class RoomComponent { + /// + /// Creates a deep copy of this component. + /// + /// A new instance that is a copy of this component. public abstract RoomComponent Clone(); } } diff --git a/RogueElements/MapGen/Rooms/RoomFilterComponent.cs b/RogueElements/MapGen/Rooms/RoomFilterComponent.cs index 415c411a..2c82c35e 100644 --- a/RogueElements/MapGen/Rooms/RoomFilterComponent.cs +++ b/RogueElements/MapGen/Rooms/RoomFilterComponent.cs @@ -15,11 +15,19 @@ namespace RogueElements [Serializable] public class RoomFilterComponent : BaseRoomFilter { + /// + /// Initializes a new instance of the class. + /// public RoomFilterComponent() { this.Components = new ComponentCollection(); } + /// + /// Initializes a new instance of the class with specified components. + /// + /// If true, the filter passes rooms that do NOT have the components. + /// The components to filter by. public RoomFilterComponent(bool negate, params RoomComponent[] components) { this.Negate = negate; @@ -28,10 +36,18 @@ public RoomFilterComponent(bool negate, params RoomComponent[] components) this.Components.Set(component); } + /// + /// Gets or sets a value indicating whether to negate the filter result. + /// When true, the filter passes rooms that do NOT have the specified components. + /// public bool Negate { get; set; } + /// + /// Gets or sets the collection of components to filter by. + /// public ComponentCollection Components { get; set; } + /// public override bool PassesFilter(IRoomPlan plan) { foreach (RoomComponent component in this.Components) @@ -43,6 +59,7 @@ public override bool PassesFilter(IRoomPlan plan) return true; } + /// public override string ToString() { if (this.Negate) diff --git a/RogueElements/MapGen/Rooms/RoomFilterDefaultGen.cs b/RogueElements/MapGen/Rooms/RoomFilterDefaultGen.cs index b5a2abb3..b8346118 100644 --- a/RogueElements/MapGen/Rooms/RoomFilterDefaultGen.cs +++ b/RogueElements/MapGen/Rooms/RoomFilterDefaultGen.cs @@ -11,21 +11,34 @@ namespace RogueElements { /// /// Filters for rooms using the default generator. + /// Matches rooms whose implements . /// [Serializable] public class RoomFilterDefaultGen : BaseRoomFilter { + /// + /// Initializes a new instance of the class. + /// public RoomFilterDefaultGen() { } + /// + /// Initializes a new instance of the class with negation setting. + /// + /// If true, the filter passes rooms that do NOT use the default generator. public RoomFilterDefaultGen(bool negate) { this.Negate = negate; } + /// + /// Gets or sets a value indicating whether to negate the filter result. + /// When true, the filter passes rooms that do NOT use the default generator. + /// public bool Negate { get; set; } + /// public override bool PassesFilter(IRoomPlan plan) { if (plan.RoomGen is IRoomGenDefault) @@ -34,6 +47,7 @@ public override bool PassesFilter(IRoomPlan plan) return this.Negate; } + /// public override string ToString() { if (this.Negate) diff --git a/RogueElements/MapGen/Rooms/RoomFilterHall.cs b/RogueElements/MapGen/Rooms/RoomFilterHall.cs index 11a510d3..2f2c3d63 100644 --- a/RogueElements/MapGen/Rooms/RoomFilterHall.cs +++ b/RogueElements/MapGen/Rooms/RoomFilterHall.cs @@ -11,21 +11,34 @@ namespace RogueElements { /// /// Filters for rooms using the hall plan. + /// Matches rooms that are instances of . /// [Serializable] public class RoomFilterHall : BaseRoomFilter { + /// + /// Initializes a new instance of the class. + /// public RoomFilterHall() { } + /// + /// Initializes a new instance of the class with negation setting. + /// + /// If true, the filter passes rooms that are NOT halls. public RoomFilterHall(bool negate) { this.Negate = negate; } + /// + /// Gets or sets a value indicating whether to negate the filter result. + /// When true, the filter passes rooms that are NOT halls. + /// public bool Negate { get; set; } + /// public override bool PassesFilter(IRoomPlan plan) { if (plan is FloorHallPlan) @@ -34,6 +47,7 @@ public override bool PassesFilter(IRoomPlan plan) return this.Negate; } + /// public override string ToString() { if (this.Negate) diff --git a/RogueElements/MapGen/Rooms/RoomGen.cs b/RogueElements/MapGen/Rooms/RoomGen.cs index 9aa73d06..f61081c5 100644 --- a/RogueElements/MapGen/Rooms/RoomGen.cs +++ b/RogueElements/MapGen/Rooms/RoomGen.cs @@ -90,23 +90,23 @@ protected RoomGen() /// These are the tiles that are allowed to be opened (turned on in openedBorder). /// Unlike openedBorder, fulfillableBorder has not been opened, but has signalled it is able to open if asked. /// - /// - /// - /// + /// The direction of the border. + /// The index of the tile along the border. + /// True if the border tile at the specified index can be opened; otherwise, false. public bool GetFulfillableBorder(Dir4 dir, int index) => this.FulfillableBorder[dir][index]; /// - /// Gets rhe tiles that this room has opened, which can be used to inform other rooms where to connect. + /// Gets the tiles that this room has opened, which can be used to inform other rooms where to connect. /// - /// - /// - /// + /// The direction of the border. + /// The index of the tile along the border. + /// True if the border tile at the specified index is opened; otherwise, false. public bool GetOpenedBorder(Dir4 dir, int index) => this.OpenedBorder[dir][index]; /// /// Creates a copy of the object, to be placed in the generated layout. /// - /// + /// A new instance that is a copy of this room generator. public abstract RoomGen Copy(); IRoomGen IRoomGen.Copy() => this.Copy(); @@ -116,15 +116,15 @@ protected RoomGen() /// /// Returns a Loc that represents the dimensions that this RoomGen prefers to be. /// - /// - /// + /// The random number generator to use. + /// The proposed size as a . public abstract Loc ProposeSize(IRandom rand); /// /// Initializes the room to the specified size. If its proposed size is not used, it may draw a default empty square. /// - /// - /// + /// The random number generator to use. + /// The size to initialize the room with. public virtual void PrepareSize(IRandom rand, Loc size) { if (size.X <= 0 || size.Y <= 0) @@ -162,6 +162,10 @@ public virtual void PrepareSize(IRandom rand, Loc size) } } + /// + /// Sets the location where the room will be drawn on the map. + /// + /// The top-left corner location of the room. public void SetLoc(Loc loc) { Rect currDraw = this.Draw; @@ -180,7 +184,7 @@ public void SetLoc(Loc loc) /// The required X/Y positions that must be touched. /// The width that each chosen tile would cover. Starts from the center. /// center of width. - /// + /// A list of hash sets, each containing potential starting positions. public virtual List> ChoosePossibleStartRanges(IRandom rand, int scalarStart, bool[] permittedRange, List origSideReqs, int reqWidth, int reqCenter) { // Gets the starting X if the direction is vertical, starting Y if the direction is horizontal @@ -240,10 +244,18 @@ public virtual List> ChoosePossibleStartRanges(IRandom rand, int sc return resultStarts; } + /// + /// Draws the room onto the specified map context. + /// + /// The map context to draw on. public abstract void DrawOnMap(T map); void IRoomGen.DrawOnMap(ITiledGenContext map) => this.DrawOnMap((T)map); + /// + /// Updates the opened border arrays based on which border tiles are walkable. + /// + /// The map context to check for tile blockage. public virtual void SetRoomBorders(T map) { for (int ii = 0; ii < this.Draw.Width; ii++) @@ -339,9 +351,9 @@ public virtual void FulfillRoomBorders(T map, bool openAll) /// /// Digs inwards from a border until it reaches a traversible tile. /// - /// + /// The map context to modify. /// The direction of the border, facing outwards. - /// + /// The position along the border to dig from. public virtual void DigAtBorder(ITiledGenContext map, Dir4 dir, int scalar) { Loc curLoc = this.Draw.GetEdgeLoc(dir, scalar); @@ -437,8 +449,16 @@ public virtual void AskBorderFromRoom(Rect sourceDraw, Func bor throw new ArgumentException("Permitted borders needs at least one open tile for each sideReq!"); } + /// + /// Prepares the fulfillable border arrays to indicate which border tiles can accept connections. + /// + /// The random number generator to use. protected abstract void PrepareFulfillableBorders(IRandom rand); + /// + /// Draws a simple rectangular room filling the entire bounding box. + /// + /// The map context to draw on. protected void DrawMapDefault(T map) { // draw on all diff --git a/RogueElements/MapGen/Rooms/RoomGenBlocked.cs b/RogueElements/MapGen/Rooms/RoomGenBlocked.cs index 51d7146e..24168936 100644 --- a/RogueElements/MapGen/Rooms/RoomGenBlocked.cs +++ b/RogueElements/MapGen/Rooms/RoomGenBlocked.cs @@ -11,15 +11,26 @@ namespace RogueElements /// /// Generates a rectangular room with the specified width and height, and with a rectangular block with specified width and height. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public class RoomGenBlocked : PermissiveRoomGen, ISizedRoomGen where T : ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public RoomGenBlocked() { } + /// + /// Initializes a new instance of the class with specified dimensions. + /// + /// The terrain type to use for the blocking rectangle. + /// The range of possible widths for the room. + /// The range of possible heights for the room. + /// The range of possible widths for the block. + /// The range of possible heights for the block. public RoomGenBlocked(ITile blockTerrain, RandRange width, RandRange height, RandRange blockWidth, RandRange blockHeight) { this.BlockTerrain = blockTerrain; @@ -29,6 +40,10 @@ public RoomGenBlocked(ITile blockTerrain, RandRange width, RandRange height, Ran this.BlockHeight = blockHeight; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. protected RoomGenBlocked(RoomGenBlocked other) { this.BlockTerrain = other.BlockTerrain.Copy(); @@ -39,37 +54,40 @@ protected RoomGenBlocked(RoomGenBlocked other) } /// - /// Width of the room. + /// Gets or sets the range of possible widths for the room. /// public RandRange Width { get; set; } /// - /// Height of the room. + /// Gets or sets the range of possible heights for the room. /// public RandRange Height { get; set; } /// - /// Width of the block. + /// Gets or sets the range of possible widths for the block. /// public RandRange BlockWidth { get; set; } /// - /// Height of the block. + /// Gets or sets the range of possible heights for the block. /// public RandRange BlockHeight { get; set; } /// - /// The terrain used for the block. + /// Gets or sets the terrain used for the blocking rectangle in the center. /// public ITile BlockTerrain { get; set; } + /// public override RoomGen Copy() => new RoomGenBlocked(this); + /// public override Loc ProposeSize(IRandom rand) { return new Loc(this.Width.Pick(rand), this.Height.Pick(rand)); } + /// public override void DrawOnMap(T map) { for (int x = 0; x < this.Draw.Size.X; x++) @@ -94,6 +112,7 @@ public override void DrawOnMap(T map) this.SetRoomBorders(map); } + /// public override string ToString() { return string.Format("{0}: {1}x{2}", this.GetType().GetFormattedTypeName(), this.Width, this.Height); diff --git a/RogueElements/MapGen/Rooms/RoomGenBump.cs b/RogueElements/MapGen/Rooms/RoomGenBump.cs index 24a2d5db..8fa2921a 100644 --- a/RogueElements/MapGen/Rooms/RoomGenBump.cs +++ b/RogueElements/MapGen/Rooms/RoomGenBump.cs @@ -11,15 +11,24 @@ namespace RogueElements /// /// Generates a rectangular room with the specified width and height, and with the tiles at the perimeter randomly blocked. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public class RoomGenBump : PermissiveRoomGen, ISizedRoomGen where T : ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public RoomGenBump() { } + /// + /// Initializes a new instance of the class with specified dimensions. + /// + /// The range of possible widths for the room. + /// The range of possible heights for the room. + /// The percentage chance of blocking perimeter tiles. public RoomGenBump(RandRange width, RandRange height, RandRange bumpPercent) { this.Width = width; @@ -27,6 +36,10 @@ public RoomGenBump(RandRange width, RandRange height, RandRange bumpPercent) this.BumpPercent = bumpPercent; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. protected RoomGenBump(RoomGenBump other) { this.Width = other.Width; @@ -35,27 +48,30 @@ protected RoomGenBump(RoomGenBump other) } /// - /// Width of the room. + /// Gets or sets the range of possible widths for the room. /// public RandRange Width { get; set; } /// - /// Height of the room. + /// Gets or sets the range of possible heights for the room. /// public RandRange Height { get; set; } /// - /// Chance of a block tile at the room's perimeter. + /// Gets or sets the percentage chance of blocking tiles at the room's perimeter. /// public RandRange BumpPercent { get; set; } + /// public override RoomGen Copy() => new RoomGenBump(this); + /// public override Loc ProposeSize(IRandom rand) { return new Loc(this.Width.Pick(rand), this.Height.Pick(rand)); } + /// public override void DrawOnMap(T map) { // add peturbations @@ -99,6 +115,7 @@ public override void DrawOnMap(T map) this.SetRoomBorders(map); } + /// public override string ToString() { return string.Format("{0}: {1}x{2}", this.GetType().GetFormattedTypeName(), this.Width, this.Height); diff --git a/RogueElements/MapGen/Rooms/RoomGenCave.cs b/RogueElements/MapGen/Rooms/RoomGenCave.cs index a5efe461..3938aa3a 100644 --- a/RogueElements/MapGen/Rooms/RoomGenCave.cs +++ b/RogueElements/MapGen/Rooms/RoomGenCave.cs @@ -13,7 +13,7 @@ namespace RogueElements /// Will generate a square if asked to generate for a size it did not propose. /// For square-looking rooms, check to make sure the room was not cut down. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public class RoomGenCave : RoomGen, ISizedRoomGen where T : ITiledGenContext @@ -25,16 +25,28 @@ public class RoomGenCave : RoomGen, ISizedRoomGen [NonSerialized] private bool[][] tiles; + /// + /// Initializes a new instance of the class. + /// public RoomGenCave() { } + /// + /// Initializes a new instance of the class with specified dimensions. + /// + /// The range of possible widths for the room. + /// The range of possible heights for the room. public RoomGenCave(RandRange width, RandRange height) { this.Width = width; this.Height = height; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. protected RoomGenCave(RoomGenCave other) { this.Width = other.Width; @@ -42,19 +54,24 @@ protected RoomGenCave(RoomGenCave other) } /// - /// The max width of the room. The actual cave will tend to be smaller. + /// Gets or sets the maximum width range of the room. The actual cave will tend to be smaller. /// public RandRange Width { get; set; } /// - /// The max height of the room. The actual cave will tend to be smaller. + /// Gets or sets the maximum height range of the room. The actual cave will tend to be smaller. /// public RandRange Height { get; set; } + /// + /// Gets or sets the tile map representing the cave shape. + /// protected bool[][] Tiles { get => this.tiles; set => this.tiles = value; } + /// public override RoomGen Copy() => new RoomGenCave(this); + /// public override Loc ProposeSize(IRandom rand) { BlobMap largestMap = null; @@ -128,6 +145,7 @@ public override Loc ProposeSize(IRandom rand) return new Loc(this.Tiles.Length, this.Tiles[0].Length); } + /// public override void DrawOnMap(T map) { if (this.Draw.Width != this.Tiles.Length || this.Draw.Height != this.Tiles[0].Length) @@ -149,11 +167,13 @@ public override void DrawOnMap(T map) this.SetRoomBorders(map); } + /// public override string ToString() { return string.Format("{0}: {1}x{2}", this.GetType().GetFormattedTypeName(), this.Width, this.Height); } + /// protected override void PrepareFulfillableBorders(IRandom rand) { // accept nothing but the randomly chosen size diff --git a/RogueElements/MapGen/Rooms/RoomGenRound.cs b/RogueElements/MapGen/Rooms/RoomGenRound.cs index 41ad8774..2247d0d9 100644 --- a/RogueElements/MapGen/Rooms/RoomGenRound.cs +++ b/RogueElements/MapGen/Rooms/RoomGenRound.cs @@ -8,23 +8,35 @@ namespace RogueElements { /// - /// Generates a rounded room. Square dimensions result in a circle, while rectangular dimensions result in capsules. + /// Generates a rounded room. Square dimensions result in a circle, while rectangular dimensions result in capsules. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public class RoomGenRound : RoomGen, ISizedRoomGen where T : ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public RoomGenRound() { } + /// + /// Initializes a new instance of the class with specified dimensions. + /// + /// The range of possible widths for the room. + /// The range of possible heights for the room. public RoomGenRound(RandRange width, RandRange height) { this.Width = width; this.Height = height; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. protected RoomGenRound(RoomGenRound other) { this.Width = other.Width; @@ -32,22 +44,25 @@ protected RoomGenRound(RoomGenRound other) } /// - /// Width of the room. + /// Gets or sets the range of possible widths for the room. /// public RandRange Width { get; set; } /// - /// Height of the room. + /// Gets or sets the range of possible heights for the room. /// public RandRange Height { get; set; } + /// public override RoomGen Copy() => new RoomGenRound(this); + /// public override Loc ProposeSize(IRandom rand) { return new Loc(this.Width.Pick(rand), this.Height.Pick(rand)); } + /// public override void DrawOnMap(T map) { int diameter = Math.Min(this.Draw.Width, this.Draw.Height); @@ -65,11 +80,13 @@ public override void DrawOnMap(T map) this.SetRoomBorders(map); } + /// public override string ToString() { return string.Format("{0}: {1}x{2}", this.GetType().GetFormattedTypeName(), this.Width.ToString(), this.Height.ToString()); } + /// protected override void PrepareFulfillableBorders(IRandom rand) { int diameter = Math.Min(this.Draw.Width, this.Draw.Height); @@ -92,6 +109,14 @@ protected override void PrepareFulfillableBorders(IRandom rand) } } + /// + /// Determines whether a tile at the specified coordinates falls within the rounded room shape. + /// + /// The X coordinate of the tile relative to the room. + /// The Y coordinate of the tile relative to the room. + /// The diameter used for corner rounding. + /// The size of the room. + /// True if the tile is within the room; otherwise, false. private static bool IsTileWithinRoom(int baseX, int baseY, int diameter, Loc size) { Loc sizeX2 = size * 2; diff --git a/RogueElements/MapGen/Rooms/RoomGenSpecific.cs b/RogueElements/MapGen/Rooms/RoomGenSpecific.cs index b613a573..84e19016 100644 --- a/RogueElements/MapGen/Rooms/RoomGenSpecific.cs +++ b/RogueElements/MapGen/Rooms/RoomGenSpecific.cs @@ -10,17 +10,26 @@ namespace RogueElements { /// /// Generates a room with specific tiles and borders. - /// EDITOR UNFRIENDLY + /// Allows defining a precise tile layout for the room. + /// Note: This class is not editor-friendly due to direct tile array manipulation. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public class RoomGenSpecific : RoomGen where T : ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public RoomGenSpecific() { } + /// + /// Initializes a new instance of the class with specified dimensions. + /// + /// The width of the room in tiles. + /// The height of the room in tiles. public RoomGenSpecific(int width, int height) { this.Tiles = new ITile[width][]; @@ -28,12 +37,22 @@ public RoomGenSpecific(int width, int height) this.Tiles[xx] = new ITile[height]; } + /// + /// Initializes a new instance of the class with specified dimensions and terrain. + /// + /// The width of the room in tiles. + /// The height of the room in tiles. + /// The terrain type that represents walkable floor tiles. public RoomGenSpecific(int width, int height, ITile roomTerrain) : this(width, height) { this.RoomTerrain = roomTerrain; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. protected RoomGenSpecific(RoomGenSpecific other) { this.RoomTerrain = other.RoomTerrain; @@ -46,17 +65,27 @@ protected RoomGenSpecific(RoomGenSpecific other) } } + /// + /// Gets or sets the terrain type that represents walkable floor tiles. + /// Used to determine which tiles are open for border connections. + /// public ITile RoomTerrain { get; set; } + /// + /// Gets or sets the 2D array of tiles that define the room layout. + /// public ITile[][] Tiles { get; set; } + /// public override RoomGen Copy() => new RoomGenSpecific(this); + /// public override Loc ProposeSize(IRandom rand) { return new Loc(this.Tiles.Length, this.Tiles[0].Length); } + /// public override void DrawOnMap(T map) { if (this.Draw.Width != this.Tiles.Length || this.Draw.Height != this.Tiles[0].Length) @@ -74,11 +103,13 @@ public override void DrawOnMap(T map) this.SetRoomBorders(map); } + /// public override string ToString() { return string.Format("{0}: {1}x{2}", this.GetType().GetFormattedTypeName(), this.Tiles.Length, this.Tiles[0].Length); } + /// protected override void PrepareFulfillableBorders(IRandom rand) { // NOTE: Because the context is not passed in when preparing borders, diff --git a/RogueElements/MapGen/Rooms/RoomGenSquare.cs b/RogueElements/MapGen/Rooms/RoomGenSquare.cs index 8e05491c..a8f0ae0c 100644 --- a/RogueElements/MapGen/Rooms/RoomGenSquare.cs +++ b/RogueElements/MapGen/Rooms/RoomGenSquare.cs @@ -10,21 +10,33 @@ namespace RogueElements /// /// Generates a rectangular room with the specified width and height. /// - /// + /// The type of map context that supports tiled generation. [Serializable] public class RoomGenSquare : PermissiveRoomGen, ISizedRoomGen where T : ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public RoomGenSquare() { } + /// + /// Initializes a new instance of the class with specified dimensions. + /// + /// The range of possible widths for the room. + /// The range of possible heights for the room. public RoomGenSquare(RandRange width, RandRange height) { this.Width = width; this.Height = height; } + /// + /// Initializes a new instance of the class as a copy of another. + /// + /// The instance to copy from. protected RoomGenSquare(RoomGenSquare other) { this.Width = other.Width; @@ -32,27 +44,31 @@ protected RoomGenSquare(RoomGenSquare other) } /// - /// Width of the room. + /// Gets or sets the range of possible widths for the room. /// public RandRange Width { get; set; } /// - /// Height of the room. + /// Gets or sets the range of possible heights for the room. /// public RandRange Height { get; set; } + /// public override RoomGen Copy() => new RoomGenSquare(this); + /// public override Loc ProposeSize(IRandom rand) { return new Loc(this.Width.Pick(rand), this.Height.Pick(rand)); } + /// public override void DrawOnMap(T map) { this.DrawMapDefault(map); } + /// public override string ToString() { return string.Format("{0}: {1}x{2}", this.GetType().GetFormattedTypeName(), this.Width.ToString(), this.Height.ToString()); diff --git a/RogueElements/MapGen/Spawning/DueSpawnStep.cs b/RogueElements/MapGen/Spawning/DueSpawnStep.cs index b0b931bc..64b3f037 100644 --- a/RogueElements/MapGen/Spawning/DueSpawnStep.cs +++ b/RogueElements/MapGen/Spawning/DueSpawnStep.cs @@ -21,11 +21,20 @@ public class DueSpawnStep : RoomSpawnStep + /// Initializes a new instance of the class. + /// public DueSpawnStep() : base() { } + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The spawner that generates the list of items to place. + /// The percentage to reduce a room's spawn chance after successfully spawning. + /// Whether to include halls as eligible spawn locations. public DueSpawnStep(IStepSpawner spawn, int successPercent, bool includeHalls = false) : base(spawn) { @@ -34,15 +43,21 @@ public DueSpawnStep(IStepSpawner spawn, int successPerc } /// - /// The percentage chance to multiply a room's spawning chance when it successfully spawns an item. + /// Gets or sets the percentage to multiply a room's spawning chance when it successfully spawns an item. /// public int SuccessPercent { get; set; } /// - /// Makes halls eligible for spawn. + /// Gets or sets a value indicating whether halls are eligible for spawn. /// public bool IncludeHalls { get; set; } + /// + /// Distributes spawns by weighting rooms based on their distance from the entrance. + /// Rooms farther from the entrance have higher spawn probability. + /// + /// The generation context to place spawns in. + /// The list of spawnable entities to distribute. public override void DistributeSpawns(TGenContext map, List spawns) { // gather up all rooms and put in a spawn list diff --git a/RogueElements/MapGen/Spawning/IBaseSpawnStep.cs b/RogueElements/MapGen/Spawning/IBaseSpawnStep.cs index e4600291..a29957d8 100644 --- a/RogueElements/MapGen/Spawning/IBaseSpawnStep.cs +++ b/RogueElements/MapGen/Spawning/IBaseSpawnStep.cs @@ -9,10 +9,20 @@ namespace RogueElements { + /// + /// Provides a non-generic interface for spawn steps, enabling type-agnostic access to spawn configuration. + /// + /// public interface IBaseSpawnStep { + /// + /// Gets the spawner that generates the list of items to place. + /// IStepSpawner Spawn { get; } + /// + /// Gets the type of spawnable entity this step handles. + /// Type SpawnType { get; } } @@ -20,17 +30,24 @@ public interface IBaseSpawnStep /// Spawns objects of type E to IPlaceableGenContext T. /// Child classes offer a different way to place the list of spawns provided by Spawn. /// - /// - /// + /// The generation context type, which must support placing spawnable entities. + /// The type of spawnable entity to place on the map. [Serializable] public abstract class BaseSpawnStep : GenStep, IBaseSpawnStep where TGenContext : class, IPlaceableGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// protected BaseSpawnStep() { } + /// + /// Initializes a new instance of the class with the specified spawner. + /// + /// The spawner that generates the list of items to place. protected BaseSpawnStep(IStepSpawner spawn) { this.Spawn = spawn; @@ -41,12 +58,23 @@ protected BaseSpawnStep(IStepSpawner spawn) /// public IStepSpawner Spawn { get; set; } + /// IStepSpawner IBaseSpawnStep.Spawn => this.Spawn; + /// public Type SpawnType => typeof(TSpawnable); + /// + /// Distributes the given spawns across the map using a placement strategy defined by the subclass. + /// + /// The generation context to place spawns in. + /// The list of spawnable entities to distribute. public abstract void DistributeSpawns(TGenContext map, List spawns); + /// + /// Applies the spawn step to the map by generating spawns and distributing them. + /// + /// The generation context to apply spawns to. public override void Apply(TGenContext map) { if (this.Spawn is null) @@ -58,6 +86,7 @@ public override void Apply(TGenContext map) this.DistributeSpawns(map, spawns); } + /// public override string ToString() { if (this.Spawn == null) diff --git a/RogueElements/MapGen/Spawning/IContextSpawner.cs b/RogueElements/MapGen/Spawning/IContextSpawner.cs index 6b522dbe..632fbda9 100644 --- a/RogueElements/MapGen/Spawning/IContextSpawner.cs +++ b/RogueElements/MapGen/Spawning/IContextSpawner.cs @@ -8,10 +8,14 @@ namespace RogueElements { + /// + /// Provides a non-generic interface for spawners that draw from the map's own spawn tables. + /// + /// public interface IContextSpawner { /// - /// The amount of spawns to roll from the spawn tables. + /// Gets or sets the amount of spawns to roll from the spawn tables. /// RandRange Amount { get; set; } } @@ -19,25 +23,38 @@ public interface IContextSpawner /// /// Spawns items from the map's own spawn tables. /// - /// - /// + /// The generation context type, which must provide spawn tables. + /// The type of spawnable entity to generate. [Serializable] public class ContextSpawner : IStepSpawner, IContextSpawner where TGenContext : ISpawningGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// public ContextSpawner() { this.Amount = RandRange.Empty; } + /// + /// Initializes a new instance of the class with the specified amount. + /// + /// The range of spawns to generate. public ContextSpawner(RandRange amount) { this.Amount = amount; } + /// public RandRange Amount { get; set; } + /// + /// Generates spawns by picking from the map's own spawn tables. + /// + /// The generation context providing the spawn tables. + /// A list of spawnable entities picked from the context's spawner. public List GetSpawns(TGenContext map) { int chosenAmount = this.Amount.Pick(map.Rand); @@ -52,6 +69,7 @@ public List GetSpawns(TGenContext map) return results; } + /// public override string ToString() { return string.Format("{0}[{1}]", this.GetType().GetFormattedTypeName(), this.Amount.ToString()); diff --git a/RogueElements/MapGen/Spawning/IEntrance.cs b/RogueElements/MapGen/Spawning/IEntrance.cs index cc243c61..5f2e39a1 100644 --- a/RogueElements/MapGen/Spawning/IEntrance.cs +++ b/RogueElements/MapGen/Spawning/IEntrance.cs @@ -7,6 +7,12 @@ namespace RogueElements { + /// + /// Represents an entrance point on the map, such as stairs going up or a dungeon entry. + /// Players typically start at an entrance when entering a floor. + /// + /// + /// public interface IEntrance : ISpawnable { } diff --git a/RogueElements/MapGen/Spawning/IExit.cs b/RogueElements/MapGen/Spawning/IExit.cs index d84ffffa..18bf35ba 100644 --- a/RogueElements/MapGen/Spawning/IExit.cs +++ b/RogueElements/MapGen/Spawning/IExit.cs @@ -7,6 +7,12 @@ namespace RogueElements { + /// + /// Represents an exit point on the map, such as stairs going down or a dungeon exit. + /// Players typically leave a floor by reaching an exit. + /// + /// + /// public interface IExit : ISpawnable { } diff --git a/RogueElements/MapGen/Spawning/IMultiStepSpawner.cs b/RogueElements/MapGen/Spawning/IMultiStepSpawner.cs index 949919f8..77e6fe49 100644 --- a/RogueElements/MapGen/Spawning/IMultiStepSpawner.cs +++ b/RogueElements/MapGen/Spawning/IMultiStepSpawner.cs @@ -8,25 +8,39 @@ namespace RogueElements { + /// + /// Provides a non-generic interface for multi-step spawners that select from multiple spawner options. + /// + /// public interface IMultiStepSpawner { + /// + /// Gets or sets the picker that selects which spawners to use. + /// IMultiRandPicker Picker { get; set; } } /// /// Randomly chooses an IStepSpawner from a spawner of spawners, then generates the objects from the chosen IStepSpawner. /// - /// - /// + /// The generation context type used to inform spawn generation. + /// The type of spawnable entity to generate. [Serializable] public class MultiStepSpawner : IStepSpawner, IMultiStepSpawner where TGenContext : IGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// public MultiStepSpawner() { } + /// + /// Initializes a new instance of the class with the specified picker. + /// + /// The picker that selects which spawners to use. public MultiStepSpawner(IMultiRandPicker> picker) { this.Picker = picker; @@ -37,12 +51,18 @@ public MultiStepSpawner(IMultiRandPicker> /// public IMultiRandPicker> Picker { get; set; } + /// IMultiRandPicker IMultiStepSpawner.Picker { get { return this.Picker; } set { this.Picker = (IMultiRandPicker>)value; } } + /// + /// Generates spawns by randomly selecting spawners from the picker and aggregating their results. + /// + /// The generation context used to inform spawn generation. + /// A list of spawnable entities generated by the selected spawners. public List GetSpawns(TGenContext map) { if (this.Picker is null) @@ -62,6 +82,7 @@ public List GetSpawns(TGenContext map) return copyResults; } + /// public override string ToString() { return string.Format("{0}: {1}", this.GetType().GetFormattedTypeName(), this.Picker.ToString()); diff --git a/RogueElements/MapGen/Spawning/IPickerSpawner.cs b/RogueElements/MapGen/Spawning/IPickerSpawner.cs index b329b0ea..f2644d5e 100644 --- a/RogueElements/MapGen/Spawning/IPickerSpawner.cs +++ b/RogueElements/MapGen/Spawning/IPickerSpawner.cs @@ -8,25 +8,39 @@ namespace RogueElements { + /// + /// Provides a non-generic interface for spawners that use a random picker to select entities directly. + /// + /// public interface IPickerSpawner { + /// + /// Gets or sets the picker that selects which entities to spawn. + /// IMultiRandPicker Picker { get; set; } } /// /// Generates spawnables from a specifically defined IMultiRandPicker. /// - /// - /// + /// The generation context type used for randomization. + /// The type of spawnable entity to generate. [Serializable] public class PickerSpawner : IStepSpawner, IPickerSpawner where TGenContext : IGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// public PickerSpawner() { } + /// + /// Initializes a new instance of the class with the specified picker. + /// + /// The picker that selects which entities to spawn. public PickerSpawner(IMultiRandPicker picker) { this.Picker = picker; @@ -37,12 +51,18 @@ public PickerSpawner(IMultiRandPicker picker) /// public IMultiRandPicker Picker { get; set; } + /// IMultiRandPicker IPickerSpawner.Picker { get { return this.Picker; } set { this.Picker = (IMultiRandPicker)value; } } + /// + /// Generates spawns by rolling the picker and copying the selected entities. + /// + /// The generation context used for randomization. + /// A list of spawnable entity copies selected from the picker. public List GetSpawns(TGenContext map) { if (this.Picker is null) @@ -57,6 +77,7 @@ public List GetSpawns(TGenContext map) return copyResults; } + /// public override string ToString() { return string.Format("{0}: {1}", this.GetType().GetFormattedTypeName(), this.Picker.ToString()); diff --git a/RogueElements/MapGen/Spawning/IPlaceableGenContext.cs b/RogueElements/MapGen/Spawning/IPlaceableGenContext.cs index 9fa3b486..acd5f7bd 100644 --- a/RogueElements/MapGen/Spawning/IPlaceableGenContext.cs +++ b/RogueElements/MapGen/Spawning/IPlaceableGenContext.cs @@ -8,15 +8,42 @@ namespace RogueElements { + /// + /// Represents a generation context that supports placing spawnable entities at specific locations. + /// Provides methods to query available placement locations and place items on the map. + /// + /// The type of spawnable entity that can be placed. + /// + /// + /// public interface IPlaceableGenContext : IGenContext where T : ISpawnable { + /// + /// Gets all tile locations on the map where items can be placed. + /// + /// A list of all valid placement locations. List GetAllFreeTiles(); + /// + /// Gets tile locations within a specified rectangular region where items can be placed. + /// + /// The rectangular region to search within. + /// A list of valid placement locations within the specified rectangle. List GetFreeTiles(Rect rect); + /// + /// Determines whether an item can be placed at the specified location. + /// + /// The location to check. + /// true if an item can be placed at the location; otherwise, false. bool CanPlaceItem(Loc loc); + /// + /// Places a spawnable item at the specified location on the map. + /// + /// The location where the item should be placed. + /// The spawnable item to place. void PlaceItem(Loc loc, T item); } } diff --git a/RogueElements/MapGen/Spawning/IReplaceableGenContext.cs b/RogueElements/MapGen/Spawning/IReplaceableGenContext.cs index 77950e80..f542bcd5 100644 --- a/RogueElements/MapGen/Spawning/IReplaceableGenContext.cs +++ b/RogueElements/MapGen/Spawning/IReplaceableGenContext.cs @@ -8,11 +8,27 @@ namespace RogueElements { + /// + /// Extends with the ability to modify or remove already-placed items. + /// Provides write access to the collection of spawned entities. + /// + /// The type of spawnable entity that can be placed, viewed, and replaced. + /// + /// public interface IReplaceableGenContext : IViewPlaceableGenContext where T : ISpawnable { + /// + /// Replaces the item at the specified index with a new item. + /// + /// The zero-based index of the item to replace. + /// The new item to place at the index. void SetItem(int index, T item); + /// + /// Removes the item at the specified index from the map. + /// + /// The zero-based index of the item to remove. void RemoveItemAt(int index); } } diff --git a/RogueElements/MapGen/Spawning/ISpawnable.cs b/RogueElements/MapGen/Spawning/ISpawnable.cs index 86d81441..5b0126d1 100644 --- a/RogueElements/MapGen/Spawning/ISpawnable.cs +++ b/RogueElements/MapGen/Spawning/ISpawnable.cs @@ -7,12 +7,16 @@ namespace RogueElements { + /// + /// Represents an entity that can be spawned and placed on a generated map. + /// Implementations include items, enemies, stairs, and other placeable objects. + /// public interface ISpawnable { /// - /// Creates a copy of the object, to be placed in the generated layout. + /// Creates a copy of the object to be placed in the generated layout. /// - /// + /// A new instance that is a copy of this spawnable object. ISpawnable Copy(); } } diff --git a/RogueElements/MapGen/Spawning/ISpawningGenContext.cs b/RogueElements/MapGen/Spawning/ISpawningGenContext.cs index eb4b9c8f..e9bf0f56 100644 --- a/RogueElements/MapGen/Spawning/ISpawningGenContext.cs +++ b/RogueElements/MapGen/Spawning/ISpawningGenContext.cs @@ -8,9 +8,20 @@ namespace RogueElements { + /// + /// Represents a generation context that provides its own spawn tables for generating entities. + /// Contexts implementing this interface can supply spawnable objects via a random picker. + /// + /// The type of spawnable entity this context can generate. + /// + /// + /// public interface ISpawningGenContext : IGenContext where T : ISpawnable { + /// + /// Gets the random picker used to select spawnable entities from this context's spawn tables. + /// IRandPicker Spawner { get; } } } diff --git a/RogueElements/MapGen/Spawning/IStepSpawner.cs b/RogueElements/MapGen/Spawning/IStepSpawner.cs index 34620049..67c97323 100644 --- a/RogueElements/MapGen/Spawning/IStepSpawner.cs +++ b/RogueElements/MapGen/Spawning/IStepSpawner.cs @@ -17,9 +17,18 @@ public interface IStepSpawner : IStepSpawner where TGenContext : IGenContext where TSpawnable : ISpawnable { + /// + /// Generates a list of spawnable entities for placement on the map. + /// + /// The generation context used to inform spawn generation. + /// A list of spawnable entities to be placed. List GetSpawns(TGenContext map); } + /// + /// Provides a non-generic marker interface for step spawners. + /// + /// public interface IStepSpawner { } diff --git a/RogueElements/MapGen/Spawning/IViewPlaceableGenContext.cs b/RogueElements/MapGen/Spawning/IViewPlaceableGenContext.cs index 48a586a7..71a7c66f 100644 --- a/RogueElements/MapGen/Spawning/IViewPlaceableGenContext.cs +++ b/RogueElements/MapGen/Spawning/IViewPlaceableGenContext.cs @@ -8,13 +8,33 @@ namespace RogueElements { + /// + /// Extends with the ability to query already-placed items. + /// Provides read access to the collection of spawned entities and their locations. + /// + /// The type of spawnable entity that can be placed and viewed. + /// + /// public interface IViewPlaceableGenContext : IPlaceableGenContext where T : ISpawnable { + /// + /// Gets the number of items currently placed on the map. + /// int Count { get; } + /// + /// Gets the item at the specified index. + /// + /// The zero-based index of the item to retrieve. + /// The spawnable item at the specified index. T GetItem(int index); + /// + /// Gets the location of the item at the specified index. + /// + /// The zero-based index of the item whose location to retrieve. + /// The location of the item at the specified index. Loc GetLoc(int index); } } diff --git a/RogueElements/MapGen/Spawning/README.md b/RogueElements/MapGen/Spawning/README.md new file mode 100644 index 00000000..455279f1 --- /dev/null +++ b/RogueElements/MapGen/Spawning/README.md @@ -0,0 +1,314 @@ +# Spawning + +[![RogueElements](https://img.shields.io/nuget/v/RogueElements?label=RogueElements)](https://www.nuget.org/packages/RogueElements) + +Entity placement system for procedural roguelike map generation. This module provides classes for spawning items, stairs, mobs, and other entities onto generated maps. + +## Purpose + +The Spawning module places entities (items, monsters, stairs, etc.) onto the generated map tiles. It separates the concerns of: +1. **What to spawn** - Determined by spawner classes +2. **Where to spawn** - Determined by spawn step classes + +## Core Interfaces + +### ISpawnable + +The base interface for anything that can be spawned on the map: + +```csharp +public interface ISpawnable +{ + ISpawnable Copy(); +} +``` + +### IPlaceableGenContext<T> + +The context interface required for entity placement: + +```csharp +public interface IPlaceableGenContext : IGenContext + where T : ISpawnable +{ + List GetAllFreeTiles(); + List GetFreeTiles(Rect rect); + bool CanPlaceItem(Loc loc); + void PlaceItem(Loc loc, T item); +} +``` + +### IStepSpawner<TGenContext, TSpawnable> + +Generates the list of what entities to spawn (but not where): + +```csharp +public interface IStepSpawner +{ + List GetSpawns(TGenContext map); +} +``` + +## Spawn Steps + +Spawn steps are `GenStep` implementations that determine both what and where to spawn. + +### RandomSpawnStep + +Spawns objects on randomly chosen tiles from the set of valid placement locations. + +```csharp +// Create item spawn list +var itemSpawns = new SpawnList +{ + { new Item((int)'!'), 10 }, // Potion, weight 10 + { new Item((int)'$'), 10 }, // Gold, weight 10 + { new Item((int)'*'), 50 }, // Food, weight 50 +}; + +// Random spawn with 10-18 items +var itemPlacement = new RandomSpawnStep( + new PickerSpawner( + new LoopedRand(itemSpawns, new RandRange(10, 19)) + ) +); + +layout.GenSteps.Add(6, itemPlacement); +``` + +### TerminalSpawnStep + +Spawns objects preferentially in terminal (dead-end) rooms. Falls back to normal rooms if all dead-ends are occupied. + +```csharp +var terminalSpawn = new TerminalSpawnStep(spawner) +{ + IncludeHalls = false // Only consider rooms, not halls +}; +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `IncludeHalls` | `bool` | Whether to consider halls as valid spawn locations | + +### RoomSpawnStep + +Base class for room-aware spawning. Spawns entities within specific rooms based on filters. + +```csharp +public abstract class RoomSpawnStep +{ + public List Filters { get; set; } + + public virtual void SpawnRandInCandRooms( + TGenContext map, + SpawnList spawningRooms, + List spawns, + int decayPercent // After spawning, room likelihood is multiplied by this % + ); +} +``` + +### SpecificSpawnStep + +Spawns objects at exact specified locations. + +```csharp +var locs = new List { new Loc(5, 5), new Loc(10, 10) }; +var specificSpawn = new SpecificSpawnStep(spawner, locs); +``` + +### TerrainSpawnStep + +Spawns objects on tiles matching specific terrain types. + +```csharp +// Spawn items on water tiles +var waterSpawn = new TerrainSpawnStep(spawner, waterTerrain); +``` + +## Spawner Classes + +### PickerSpawner + +Generates spawns using a randomized picker: + +```csharp +var spawner = new PickerSpawner( + new LoopedRand(itemSpawns, new RandRange(10, 19)) +); +``` + +### ContextSpawner + +Generates spawns based on context state: + +```csharp +var spawner = new ContextSpawner(); +``` + +## Usage Example + +From `Ex6_Items`: + +```csharp +var layout = new MapGen(); + +// ... room and path setup ... + +// Apply Items with weighted spawn list +var itemSpawns = new SpawnList +{ + { new Item((int)'!'), 10 }, // Potion + { new Item((int)']'), 10 }, // Armor + { new Item((int)'='), 10 }, // Ring + { new Item((int)'?'), 10 }, // Scroll + { new Item((int)'$'), 10 }, // Gold + { new Item((int)'/'), 10 }, // Wand + { new Item((int)'*'), 50 }, // Food (higher weight) +}; + +// Spawn 10-18 items at random locations +RandomSpawnStep itemPlacement = + new RandomSpawnStep( + new PickerSpawner( + new LoopedRand(itemSpawns, new RandRange(10, 19)) + ) + ); +layout.GenSteps.Add(6, itemPlacement); + +// Apply Mobs with weighted spawn list +var mobSpawns = new SpawnList +{ + { new Mob((int)'r'), 20 }, // Rat (common) + { new Mob((int)'T'), 10 }, // Troll + { new Mob((int)'D'), 5 }, // Dragon (rare) +}; + +RandomSpawnStep mobPlacement = + new RandomSpawnStep( + new PickerSpawner( + new LoopedRand(mobSpawns, new RandRange(10, 19)) + ) + ); +layout.GenSteps.Add(6, mobPlacement); +``` + +## Creating Custom Spawnables + +1. Implement `ISpawnable` +2. Add placement logic to your context + +```csharp +[Serializable] +public class Trap : ISpawnable +{ + public int TrapType { get; set; } + public Loc Loc { get; set; } + + public Trap(int trapType) + { + TrapType = trapType; + } + + public ISpawnable Copy() + { + return new Trap(TrapType); + } +} +``` + +## Creating Custom Spawn Steps + +1. Inherit from `BaseSpawnStep` +2. Override `DistributeSpawns()` to define placement logic + +```csharp +[Serializable] +public class CornerSpawnStep : BaseSpawnStep + where TGenContext : class, IPlaceableGenContext, ITiledGenContext + where TSpawnable : ISpawnable +{ + public override void DistributeSpawns(TGenContext map, List spawns) + { + // Find corner tiles (3 adjacent walls) + var corners = new List(); + for (int x = 1; x < map.Width - 1; x++) + { + for (int y = 1; y < map.Height - 1; y++) + { + if (!map.TileBlocked(new Loc(x, y)) && IsCorner(map, x, y)) + corners.Add(new Loc(x, y)); + } + } + + // Spawn in corners + for (int i = 0; i < spawns.Count && corners.Count > 0; i++) + { + int idx = map.Rand.Next(corners.Count); + map.PlaceItem(corners[idx], spawns[i]); + corners.RemoveAt(idx); + } + } + + private bool IsCorner(TGenContext map, int x, int y) + { + int wallCount = 0; + foreach (Dir8 dir in DirExt.VALID_DIR8) + { + if (map.TileBlocked(new Loc(x, y) + dir.GetLoc())) + wallCount++; + } + return wallCount >= 5; // At least 5 of 8 neighbors are walls + } +} +``` + +## Stair Interfaces + +Special interfaces for entrance/exit spawning: + +### IEntrance + +```csharp +public interface IEntrance : ISpawnable +{ + Loc Loc { get; set; } +} +``` + +### IExit + +```csharp +public interface IExit : ISpawnable +{ + Loc Loc { get; set; } +} +``` + +## Room Filters + +Control which rooms are eligible for spawning: + +```csharp +var spawn = new TerminalSpawnStep(spawner); + +// Only spawn in rooms with specific components +spawn.Filters.Add(new RoomFilterComponent(true, typeof(TreasureRoom))); + +// Exclude halls +spawn.Filters.Add(new RoomFilterHall(true)); +``` + +## Related Modules + +- **[Rooms/](../Rooms/)** - Room generators that create spawnable areas +- **[FloorPlan/](../FloorPlan/)** - Floor plans that track room locations +- **[Tiles/](../Tiles/)** - Tile operations for terrain-based spawning + +## See Also + +- `Ex4_Stairs` - Stair placement example +- `Ex6_Items` - Item and mob spawning example diff --git a/RogueElements/MapGen/Spawning/RandomRoomSpawnStep.cs b/RogueElements/MapGen/Spawning/RandomRoomSpawnStep.cs index 48f33f54..bf639611 100644 --- a/RogueElements/MapGen/Spawning/RandomRoomSpawnStep.cs +++ b/RogueElements/MapGen/Spawning/RandomRoomSpawnStep.cs @@ -12,19 +12,28 @@ namespace RogueElements /// Spawns objects in randomly chosen rooms. /// Large rooms have the same probability as small rooms. /// - /// - /// + /// The generation context type, which must support floor plans and placing spawnable entities. + /// The type of spawnable entity to place on the map. [Serializable] public class RandomRoomSpawnStep : RoomSpawnStep where TGenContext : class, IFloorPlanGenContext, IPlaceableGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// public RandomRoomSpawnStep() : base() { this.SuccessPercent = 100; } + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The spawner that generates the list of items to place. + /// The percentage to reduce a room's spawn chance after successfully spawning. + /// Whether to include halls as eligible spawn locations. public RandomRoomSpawnStep(IStepSpawner spawn, int successPercent = 100, bool includeHalls = false) : base(spawn) { @@ -39,10 +48,15 @@ public RandomRoomSpawnStep(IStepSpawner spawn, int succ public int SuccessPercent { get; set; } /// - /// Makes halls eligible for spawn. + /// Gets or sets a value indicating whether halls are eligible for spawn. /// public bool IncludeHalls { get; set; } + /// + /// Distributes spawns by placing each in a randomly selected eligible room. + /// + /// The generation context to place spawns in. + /// The list of spawnable entities to distribute. public override void DistributeSpawns(TGenContext map, List spawns) { // random per room, not per-tile diff --git a/RogueElements/MapGen/Spawning/RandomSpawnStep.cs b/RogueElements/MapGen/Spawning/RandomSpawnStep.cs index 18e18dc5..f03754a8 100644 --- a/RogueElements/MapGen/Spawning/RandomSpawnStep.cs +++ b/RogueElements/MapGen/Spawning/RandomSpawnStep.cs @@ -12,23 +12,35 @@ namespace RogueElements /// Spawns objects on randomly chosen tiles. /// The tile is chosen from the set of tiles where the object is allowed to be placed. /// - /// - /// + /// The generation context type, which must support placing spawnable entities. + /// The type of spawnable entity to place on the map. [Serializable] public class RandomSpawnStep : BaseSpawnStep where TGenContext : class, IPlaceableGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// public RandomSpawnStep() : base() { } + /// + /// Initializes a new instance of the class with the specified spawner. + /// + /// The spawner that generates the list of items to place. public RandomSpawnStep(IStepSpawner spawn) : base(spawn) { } + /// + /// Distributes spawns by placing each on a randomly selected free tile. + /// + /// The generation context to place spawns in. + /// The list of spawnable entities to distribute. public override void DistributeSpawns(TGenContext map, List spawns) { List freeTiles = map.GetAllFreeTiles(); diff --git a/RogueElements/MapGen/Spawning/RoomSpawnStep.cs b/RogueElements/MapGen/Spawning/RoomSpawnStep.cs index c5e2c183..a8b35ffb 100644 --- a/RogueElements/MapGen/Spawning/RoomSpawnStep.cs +++ b/RogueElements/MapGen/Spawning/RoomSpawnStep.cs @@ -8,17 +8,33 @@ namespace RogueElements { + /// + /// Base class for spawn steps that distribute entities across rooms in a floor plan. + /// Provides filtering and room-based placement logic used by subclasses. + /// + /// The type of generation context, which must support floor plans and item placement. + /// The type of spawnable entity to place. + /// + /// + /// [Serializable] public abstract class RoomSpawnStep : BaseSpawnStep where TGenContext : class, IFloorPlanGenContext, IPlaceableGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// protected RoomSpawnStep() : base() { this.Filters = new List(); } + /// + /// Initializes a new instance of the class with the specified spawner. + /// + /// The spawner that generates the list of items to place. protected RoomSpawnStep(IStepSpawner spawn) : base(spawn) { @@ -26,17 +42,17 @@ protected RoomSpawnStep(IStepSpawner spawn) } /// - /// Determines the rooms eligible to spawn the objects in. + /// Gets or sets the filters that determine which rooms are eligible for spawning. /// public List Filters { get; set; } /// - /// Spawns the chosen spawnables into the chosen collection of rooms + /// Spawns entities into randomly selected rooms from the candidate list. /// - /// - /// - /// - /// After spawning, the room is multiplied by this percentage to make it less likely to spawn. + /// The generation context to place spawns in. + /// The weighted list of candidate rooms. + /// The list of spawnable entities to place. + /// The percentage to reduce a room's spawn weight after successful placement. Use 0 to remove the room entirely. public virtual void SpawnRandInCandRooms(TGenContext map, SpawnList spawningRooms, List spawns, int decayPercent) { while (spawningRooms.Count > 0 && spawns.Count > 0) @@ -69,6 +85,13 @@ public virtual void SpawnRandInCandRooms(TGenContext map, SpawnList + /// Attempts to spawn an entity at a random free tile within the specified room. + /// + /// The generation context to place the spawn in. + /// The index of the room to spawn in. + /// The spawnable entity to place. + /// true if the spawn was successfully placed; otherwise, false. public virtual bool SpawnInRoom(TGenContext map, RoomHallIndex roomIndex, TSpawnable spawn) { IRoomGen room = map.RoomPlan.GetRoomHall(roomIndex).RoomGen; diff --git a/RogueElements/MapGen/Spawning/SpecificSpawnStep.cs b/RogueElements/MapGen/Spawning/SpecificSpawnStep.cs index d9948d89..ce0d828d 100644 --- a/RogueElements/MapGen/Spawning/SpecificSpawnStep.cs +++ b/RogueElements/MapGen/Spawning/SpecificSpawnStep.cs @@ -11,19 +11,27 @@ namespace RogueElements /// /// Spawns objects on specific locations. /// - /// - /// + /// The generation context type, which must support placing spawnable entities. + /// The type of spawnable entity to place on the map. [Serializable] public class SpecificSpawnStep : BaseSpawnStep where TGenContext : class, IPlaceableGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// public SpecificSpawnStep() : base() { this.SpawnLocs = new List(); } + /// + /// Initializes a new instance of the class with the specified spawner and locations. + /// + /// The spawner that generates the list of items to place. + /// The specific locations where spawns should be placed. public SpecificSpawnStep(IStepSpawner spawn, List spawnLocs) : base(spawn) { @@ -31,10 +39,15 @@ public SpecificSpawnStep(IStepSpawner spawn, List } /// - /// The locations to spawn the objects. + /// Gets the specific locations where objects will be spawned. /// public List SpawnLocs { get; } + /// + /// Distributes spawns by placing each at the corresponding location in . + /// + /// The generation context to place spawns in. + /// The list of spawnable entities to distribute. public override void DistributeSpawns(TGenContext map, List spawns) { for (int ii = 0; ii < spawns.Count && ii < this.SpawnLocs.Count; ii++) diff --git a/RogueElements/MapGen/Spawning/TerminalSpawnStep.cs b/RogueElements/MapGen/Spawning/TerminalSpawnStep.cs index b273efe7..55661dda 100644 --- a/RogueElements/MapGen/Spawning/TerminalSpawnStep.cs +++ b/RogueElements/MapGen/Spawning/TerminalSpawnStep.cs @@ -12,26 +12,42 @@ namespace RogueElements /// Spawns the objects in terminal (dead-end) rooms. /// Falls back on normal rooms if all dead-end rooms are taken. /// - /// - /// + /// The generation context type, which must support floor plans and placing spawnable entities. + /// The type of spawnable entity to place on the map. [Serializable] public class TerminalSpawnStep : RoomSpawnStep where TGenContext : class, IFloorPlanGenContext, IPlaceableGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// public TerminalSpawnStep() : base() { } + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The spawner that generates the list of items to place. + /// Whether to include halls as eligible spawn locations. public TerminalSpawnStep(IStepSpawner spawn, bool includeHalls = false) : base(spawn) { this.IncludeHalls = includeHalls; } + /// + /// Gets or sets a value indicating whether halls are eligible for spawn. + /// public bool IncludeHalls { get; set; } + /// + /// Distributes spawns by prioritizing terminal (dead-end) rooms, then falling back to other rooms. + /// + /// The generation context to place spawns in. + /// The list of spawnable entities to distribute. public override void DistributeSpawns(TGenContext map, List spawns) { // random per room, not per-tile diff --git a/RogueElements/MapGen/Spawning/TerrainSpawnStep.cs b/RogueElements/MapGen/Spawning/TerrainSpawnStep.cs index 1837f0a5..3e8f6f30 100644 --- a/RogueElements/MapGen/Spawning/TerrainSpawnStep.cs +++ b/RogueElements/MapGen/Spawning/TerrainSpawnStep.cs @@ -11,24 +11,36 @@ namespace RogueElements /// /// Spawns objects randomly on tiles of a specific terrain. /// - /// - /// + /// The generation context type, which must support placing spawnable entities and tile access. + /// The type of spawnable entity to place on the map. [Serializable] public class TerrainSpawnStep : BaseSpawnStep where TGenContext : class, IPlaceableGenContext, ITiledGenContext where TSpawnable : ISpawnable { + /// + /// Initializes a new instance of the class. + /// public TerrainSpawnStep() : base() { } + /// + /// Initializes a new instance of the class with the specified terrain type. + /// + /// The terrain type to spawn objects on. public TerrainSpawnStep(ITile terrain) : base() { this.Terrain = terrain; } + /// + /// Initializes a new instance of the class with the specified terrain type and spawner. + /// + /// The terrain type to spawn objects on. + /// The spawner that generates the list of items to place. public TerrainSpawnStep(ITile terrain, IStepSpawner spawn) : base(spawn) { @@ -36,10 +48,15 @@ public TerrainSpawnStep(ITile terrain, IStepSpawner spa } /// - /// The type of tile to spawn in. + /// Gets or sets the terrain type that tiles must match for spawn placement. /// public ITile Terrain { get; set; } + /// + /// Distributes spawns by placing each on a randomly selected tile matching the specified terrain. + /// + /// The generation context to place spawns in. + /// The list of spawnable entities to distribute. public override void DistributeSpawns(TGenContext map, List spawns) { List freeTiles = new List(); @@ -66,6 +83,7 @@ public override void DistributeSpawns(TGenContext map, List spawns) } } + /// public override string ToString() { if (this.Spawn == null || this.Terrain == null) diff --git a/RogueElements/MapGen/Tiles/DetectIsolatedStairsStep.cs b/RogueElements/MapGen/Tiles/DetectIsolatedStairsStep.cs index ca282abe..7b690961 100644 --- a/RogueElements/MapGen/Tiles/DetectIsolatedStairsStep.cs +++ b/RogueElements/MapGen/Tiles/DetectIsolatedStairsStep.cs @@ -8,21 +8,29 @@ namespace RogueElements { /// - /// A debug step that can be used to generate an error if the map generator created an unreachable stairs. + /// Detects unreachable stairs on the map and raises an error if entrances cannot reach all exits. /// - /// - /// - /// + /// The type of map context that implements tile and placement contexts. + /// The type of entrance to check connectivity from. + /// The type of exit to check connectivity to. + /// + /// This is a debug step useful for validating that every entrance can reach at least one exit. + /// Supports wrapped maps by extending the search rectangle. + /// [Serializable] public class DetectIsolatedStairsStep : GenStep where TGenContext : class, ITiledGenContext, IViewPlaceableGenContext, IViewPlaceableGenContext where TEntrance : IEntrance where TExit : IExit { + /// + /// Initializes a new instance of the class. + /// public DetectIsolatedStairsStep() { } + /// public override void Apply(TGenContext map) { int lX = map.Width; diff --git a/RogueElements/MapGen/Tiles/DetectIsolatedStep.cs b/RogueElements/MapGen/Tiles/DetectIsolatedStep.cs index 15ffe231..97881ab3 100644 --- a/RogueElements/MapGen/Tiles/DetectIsolatedStep.cs +++ b/RogueElements/MapGen/Tiles/DetectIsolatedStep.cs @@ -8,19 +8,26 @@ namespace RogueElements { /// - /// A debug step that can be used to generate an error if the map generator created a map with unreachable walkable tiles. + /// Detects unreachable walkable tiles on the map and raises an error if any are found. /// - /// - /// + /// The type of map context that implements and . + /// The type of entrance used to determine the starting point for connectivity checks. + /// + /// This is a debug step useful for validating that map generation produces fully connected walkable areas. + /// [Serializable] public class DetectIsolatedStep : GenStep where TGenContext : class, ITiledGenContext, IViewPlaceableGenContext where TEntrance : IEntrance { + /// + /// Initializes a new instance of the class. + /// public DetectIsolatedStep() { } + /// public override void Apply(TGenContext map) { const int offX = 0; diff --git a/RogueElements/MapGen/Tiles/DropDiagonalBlockStep.cs b/RogueElements/MapGen/Tiles/DropDiagonalBlockStep.cs index b060de12..a12bc77e 100644 --- a/RogueElements/MapGen/Tiles/DropDiagonalBlockStep.cs +++ b/RogueElements/MapGen/Tiles/DropDiagonalBlockStep.cs @@ -8,24 +8,39 @@ namespace RogueElements { /// - /// Merges blobs of terrain that touch diagonally. + /// Connects terrain blobs that touch only diagonally by filling in adjacent tiles. /// - /// + /// The type of map context that implements . + /// + /// When two regions of the specified terrain touch only at diagonal corners, this step + /// randomly fills in one or both adjacent tiles to create a cardinal connection. + /// [Serializable] public class DropDiagonalBlockStep : GenStep where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public DropDiagonalBlockStep() { } + /// + /// Initializes a new instance of the class with the specified terrain. + /// + /// The terrain type to check for diagonal-only connections. public DropDiagonalBlockStep(ITile terrain) { this.Terrain = terrain; } + /// + /// Gets or sets the terrain type used to detect and merge diagonal connections. + /// public ITile Terrain { get; set; } + /// public override void Apply(T map) { for (int xx = 0; xx < map.Width - 1; xx++) diff --git a/RogueElements/MapGen/Tiles/EraseIsolatedFromSpawnStep.cs b/RogueElements/MapGen/Tiles/EraseIsolatedFromSpawnStep.cs index 5b80c810..087206c9 100644 --- a/RogueElements/MapGen/Tiles/EraseIsolatedFromSpawnStep.cs +++ b/RogueElements/MapGen/Tiles/EraseIsolatedFromSpawnStep.cs @@ -9,26 +9,41 @@ namespace RogueElements { /// - /// Erases blobs of terrain that do not touch walkable ground. + /// Erases terrain blobs that are not reachable from the spawn point by replacing them with wall terrain. /// - /// - /// + /// The type of map context that implements tile and placement contexts. + /// The type of entrance used to determine the spawn point. + /// + /// Unlike , this step uses the entrance location as the starting + /// point for connectivity checks rather than any walkable tile. + /// [Serializable] public class EraseIsolatedFromSpawnStep : GenStep where TGenContext : class, ITiledGenContext, IViewPlaceableGenContext where TEntrance : IEntrance { + /// + /// Initializes a new instance of the class. + /// public EraseIsolatedFromSpawnStep() { } + /// + /// Initializes a new instance of the class with the specified terrain. + /// + /// The terrain type to check for isolation from spawn. public EraseIsolatedFromSpawnStep(ITile terrain) { this.Terrain = terrain; } + /// + /// Gets or sets the terrain type to check for isolation and erase if not reachable from spawn. + /// public ITile Terrain { get; set; } + /// public override void Apply(TGenContext map) { bool[][] connectionGrid = new bool[map.Width][]; diff --git a/RogueElements/MapGen/Tiles/EraseIsolatedStep.cs b/RogueElements/MapGen/Tiles/EraseIsolatedStep.cs index 75252c30..309e5a5c 100644 --- a/RogueElements/MapGen/Tiles/EraseIsolatedStep.cs +++ b/RogueElements/MapGen/Tiles/EraseIsolatedStep.cs @@ -9,24 +9,35 @@ namespace RogueElements { /// - /// Erases blobs of terrain that do not touch walkable ground. + /// Erases terrain blobs that are not connected to any walkable ground by replacing them with wall terrain. /// - /// + /// The type of map context that implements . [Serializable] public class EraseIsolatedStep : GenStep where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public EraseIsolatedStep() { } + /// + /// Initializes a new instance of the class with the specified terrain. + /// + /// The terrain type to check for isolation. public EraseIsolatedStep(ITile terrain) { this.Terrain = terrain; } + /// + /// Gets or sets the terrain type to check for isolation and erase if disconnected. + /// public ITile Terrain { get; set; } + /// public override void Apply(T map) { bool[][] connectionGrid = new bool[map.Width][]; diff --git a/RogueElements/MapGen/Tiles/ITile.cs b/RogueElements/MapGen/Tiles/ITile.cs index 54ff011f..dff45605 100644 --- a/RogueElements/MapGen/Tiles/ITile.cs +++ b/RogueElements/MapGen/Tiles/ITile.cs @@ -7,14 +7,22 @@ namespace RogueElements { + /// + /// Represents a single tile in the map grid. + /// public interface ITile { + /// + /// Determines whether this tile is equivalent to another tile for comparison purposes. + /// + /// The other tile to compare against. + /// true if the tiles are equivalent; otherwise, false. bool TileEquivalent(ITile other); /// - /// Creates a copy of the object, to be placed in the generated layout. + /// Creates a copy of this tile for placement in the generated layout. /// - /// + /// A new instance that is a copy of this tile. ITile Copy(); } } diff --git a/RogueElements/MapGen/Tiles/ITiledGenContext.cs b/RogueElements/MapGen/Tiles/ITiledGenContext.cs index dff4735c..8997b005 100644 --- a/RogueElements/MapGen/Tiles/ITiledGenContext.cs +++ b/RogueElements/MapGen/Tiles/ITiledGenContext.cs @@ -8,32 +8,92 @@ namespace RogueElements { + /// + /// Provides context for tile-based map generation operations. + /// public interface ITiledGenContext : IGenContext { + /// + /// Gets the tile type representing walkable room terrain. + /// ITile RoomTerrain { get; } + /// + /// Gets the tile type representing impassable wall terrain. + /// ITile WallTerrain { get; } + /// + /// Gets the width of the map in tiles. + /// int Width { get; } + /// + /// Gets the height of the map in tiles. + /// int Height { get; } + /// + /// Gets a value indicating whether the map wraps around at the edges. + /// bool Wrap { get; } + /// + /// Gets a value indicating whether the tile array has been initialized. + /// bool TilesInitialized { get; } + /// + /// Determines whether movement is blocked at the specified location for cardinal directions. + /// + /// The location to check. + /// true if the tile blocks movement; otherwise, false. bool TileBlocked(Loc loc); + /// + /// Determines whether movement is blocked at the specified location. + /// + /// The location to check. + /// Whether to check for diagonal movement blocking. + /// true if the tile blocks movement; otherwise, false. bool TileBlocked(Loc loc, bool diagonal); + /// + /// Gets the tile at the specified location. + /// + /// The location to retrieve the tile from. + /// The at the specified location. ITile GetTile(Loc loc); + /// + /// Determines whether the specified tile can be placed at the given location. + /// + /// The location to check. + /// The tile to place. + /// true if the tile can be placed; otherwise, false. bool CanSetTile(Loc loc, ITile tile); + /// + /// Attempts to set the tile at the specified location. + /// + /// The location to set the tile at. + /// The tile to place. + /// true if the tile was successfully set; otherwise, false. bool TrySetTile(Loc loc, ITile tile); + /// + /// Sets the tile at the specified location. + /// + /// The location to set the tile at. + /// The tile to place. void SetTile(Loc loc, ITile tile); + /// + /// Creates a new map with the specified dimensions. + /// + /// The width of the map in tiles. + /// The height of the map in tiles. + /// Whether the map wraps around at the edges. void CreateNew(int tileWidth, int tileHeight, bool wrap = false); } } diff --git a/RogueElements/MapGen/Tiles/InitTilesStep.cs b/RogueElements/MapGen/Tiles/InitTilesStep.cs index 10ad826e..32a720bf 100644 --- a/RogueElements/MapGen/Tiles/InitTilesStep.cs +++ b/RogueElements/MapGen/Tiles/InitTilesStep.cs @@ -9,27 +9,42 @@ namespace RogueElements { /// - /// Initializes a map of Width x Height tiles. + /// Initializes a map of Width x Height tiles filled with wall terrain. /// - /// + /// The type of map context that implements . [Serializable] public class InitTilesStep : GenStep where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public InitTilesStep() { } + /// + /// Initializes a new instance of the class with the specified dimensions. + /// + /// The width of the map in tiles. + /// The height of the map in tiles. public InitTilesStep(int width, int height) { this.Width = width; this.Height = height; } + /// + /// Gets or sets the width of the map in tiles. + /// public int Width { get; set; } + /// + /// Gets or sets the height of the map in tiles. + /// public int Height { get; set; } + /// public override void Apply(T map) { // initialize map array to empty diff --git a/RogueElements/MapGen/Tiles/README.md b/RogueElements/MapGen/Tiles/README.md new file mode 100644 index 00000000..ffbcb666 --- /dev/null +++ b/RogueElements/MapGen/Tiles/README.md @@ -0,0 +1,306 @@ +# Tiles + +[![RogueElements](https://img.shields.io/nuget/v/RogueElements?label=RogueElements)](https://www.nuget.org/packages/RogueElements) + +Tile manipulation operations for procedural roguelike map generation. This module provides classes for initializing, modifying, and cleaning up tile-based maps. + +## Purpose + +The Tiles module handles direct tile operations including: +- Initializing empty maps with wall tiles +- Drawing specific tile patterns +- Cleaning up terrain anomalies (diagonal blocks, isolated areas) +- Detecting and handling isolated regions + +## Core Interface + +### ITiledGenContext + +The context interface required for tile operations: + +```csharp +public interface ITiledGenContext : IGenContext +{ + ITile RoomTerrain { get; } // Default floor/room tile + ITile WallTerrain { get; } // Default wall tile + + int Width { get; } + int Height { get; } + bool Wrap { get; } + bool TilesInitialized { get; } + + bool TileBlocked(Loc loc); + bool TileBlocked(Loc loc, bool diagonal); + ITile GetTile(Loc loc); + bool CanSetTile(Loc loc, ITile tile); + bool TrySetTile(Loc loc, ITile tile); + void SetTile(Loc loc, ITile tile); + void CreateNew(int tileWidth, int tileHeight, bool wrap = false); +} +``` + +### ITile + +Interface for individual tiles: + +```csharp +public interface ITile +{ + ITile Copy(); + bool TileEquivalent(ITile other); +} +``` + +## Tile Steps + +### InitTilesStep + +Initializes a blank map filled with wall tiles. This is typically the first step in any tile-based generation pipeline. + +```csharp +// Initialize a 30x25 map filled with walls +var initStep = new InitTilesStep(30, 25); +layout.GenSteps.Add(0, initStep); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Width` | `int` | Width of the map in tiles | +| `Height` | `int` | Height of the map in tiles | + +### SpecificTilesStep + +Draws a specific array of tiles onto the map at a given offset. Useful for hand-crafted areas. + +```csharp +// Create tile pattern +string[] level = { + ".........................", + "...........#.............", + "....###...###...###......", + "...#.#.....#.....#.#.....", +}; + +ITile[][] tiles = new ITile[level[0].Length][]; +for (int xx = 0; xx < level[0].Length; xx++) +{ + tiles[xx] = new ITile[level.Length]; + for (int yy = 0; yy < level.Length; yy++) + { + int id = level[yy][xx] == '.' ? Map.ROOM_TERRAIN_ID : Map.WALL_TERRAIN_ID; + tiles[xx][yy] = new Tile(id); + } +} + +// Draw at offset (2, 3) +var drawStep = new SpecificTilesStep(tiles, new Loc(2, 3)); +layout.GenSteps.Add(0, drawStep); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Tiles` | `ITile[][]` | 2D array of tiles to draw | +| `Offset` | `Loc` | Position offset for drawing | + +### DropDiagonalBlockStep + +Merges blobs of terrain that touch only diagonally by filling in one or both of the adjacent wall tiles. Prevents visual artifacts and pathfinding issues. + +```csharp +// Fix diagonal water connections +const int terrain = 2; // Water terrain ID +var dropStep = new DropDiagonalBlockStep(new Tile(terrain)); +layout.GenSteps.Add(4, dropStep); +``` + +**Before:** +``` +.~#~. +~#.#~ +#...# +~#.#~ +.~#~. +``` + +**After (one possible result):** +``` +.~~. +~~.~~ +~...~ +~~.~~ +.~~. +``` + +### EraseIsolatedStep + +Erases blobs of a specific terrain that do not touch walkable ground. Removes floating terrain patches trapped in walls. + +```csharp +// Remove isolated water patches +var eraseStep = new EraseIsolatedStep(new Tile(terrain)); +layout.GenSteps.Add(4, eraseStep); +``` + +**Before:** +``` +##### +#~~~# +##### +#...# +##### +``` + +**After:** +``` +##### +##### +##### +#...# +##### +``` + +### DetectIsolatedStep + +Detects isolated walkable areas that cannot be reached from the main floor. + +```csharp +var detectStep = new DetectIsolatedStep(); +layout.GenSteps.Add(5, detectStep); +``` + +### DetectIsolatedStairsStep + +Specifically detects when stairs are placed in isolated areas unreachable from other stairs. + +```csharp +var detectStairsStep = new DetectIsolatedStairsStep(); +layout.GenSteps.Add(5, detectStairsStep); +``` + +### EraseIsolatedFromSpawnStep + +Removes spawn points that are in isolated (unreachable) areas. + +```csharp +var eraseSpawnStep = new EraseIsolatedFromSpawnStep(); +layout.GenSteps.Add(5, eraseSpawnStep); +``` + +### StairsStep + +Places stairs on the map. Works with entrance and exit types. + +```csharp +var stairsStep = new StairsStep( + new StairsUp(), + new StairsDown() +); +layout.GenSteps.Add(2, stairsStep); +``` + +## Usage Example + +From `Ex1_Tiles`: + +```csharp +var layout = new MapGen(); + +// Initialize a 30x25 blank map full of Wall tiles +InitTilesStep startStep = new InitTilesStep(30, 25); +layout.GenSteps.Add(0, startStep); + +// Draw a specific array of tiles onto the map at offset X2,Y3 +string[] level = { + ".........................", + "...........#.............", + "....###...###...###......", + "...#.#.....#.....#.#.....", + "...####...###...####.....", + // ... more rows ... +}; + +ITile[][] tiles = new ITile[level[0].Length][]; +for (int xx = 0; xx < level[0].Length; xx++) +{ + tiles[xx] = new ITile[level.Length]; + for (int yy = 0; yy < level.Length; yy++) + { + int id = Map.WALL_TERRAIN_ID; + if (level[yy][xx] == '.') + id = Map.ROOM_TERRAIN_ID; + tiles[xx][yy] = new Tile(id); + } +} + +var drawStep = new SpecificTilesStep(tiles, new Loc(2, 3)); +layout.GenSteps.Add(0, drawStep); + +// Run the generator +MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); +``` + +## Terrain Cleanup Pipeline + +A typical terrain cleanup sequence after water generation: + +```csharp +// Generate water terrain +const int terrain = 2; +var waterStep = new PerlinWaterStep( + new RandRange(35), 3, + new Tile(terrain), + new MapTerrainStencil(false, true, false, false), + 1 +); +layout.GenSteps.Add(3, waterStep); + +// Fix diagonal water touching +layout.GenSteps.Add(4, new DropDiagonalBlockStep(new Tile(terrain))); + +// Remove isolated water in walls +layout.GenSteps.Add(4, new EraseIsolatedStep(new Tile(terrain))); +``` + +## Creating Custom Tile Steps + +1. Inherit from `GenStep` where T implements `ITiledGenContext` +2. Override `Apply()` to modify tiles + +```csharp +[Serializable] +public class BorderWallStep : GenStep + where T : class, ITiledGenContext +{ + public int BorderWidth { get; set; } = 1; + + public override void Apply(T map) + { + // Draw wall border around entire map + for (int x = 0; x < map.Width; x++) + { + for (int y = 0; y < map.Height; y++) + { + bool isBorder = x < BorderWidth || x >= map.Width - BorderWidth + || y < BorderWidth || y >= map.Height - BorderWidth; + if (isBorder) + map.SetTile(new Loc(x, y), map.WallTerrain.Copy()); + } + } + } +} +``` + +## Related Modules + +- **[Water/](./Water/)** - Water and terrain generation (Perlin noise, blob placement) +- **[Rooms/](../Rooms/)** - Room generators that draw tiles +- **[FloorPlan/](../FloorPlan/)** - Floor plan to tile conversion + +## See Also + +- `Ex1_Tiles` - Static tile map example +- `Ex5_Terrain` - Terrain generation with cleanup steps diff --git a/RogueElements/MapGen/Tiles/SpecificTilesStep.cs b/RogueElements/MapGen/Tiles/SpecificTilesStep.cs index 95ec2e75..393dac61 100644 --- a/RogueElements/MapGen/Tiles/SpecificTilesStep.cs +++ b/RogueElements/MapGen/Tiles/SpecificTilesStep.cs @@ -9,35 +9,57 @@ namespace RogueElements { /// - /// Creates a map out of specific tiles. - /// Not very editor-friendly. + /// Places a predefined array of tiles onto the map at a specified offset. /// - /// + /// The type of map context that implements . + /// + /// This step is useful for placing hand-designed map sections but is not very editor-friendly + /// due to the raw tile array format. + /// [Serializable] public class SpecificTilesStep : GenStep where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public SpecificTilesStep() { this.Tiles = Array.Empty(); } + /// + /// Initializes a new instance of the class with the specified tiles. + /// + /// The 2D array of tiles to place on the map. public SpecificTilesStep(ITile[][] tiles) { this.Tiles = tiles; this.Offset = Loc.Zero; } + /// + /// Initializes a new instance of the class with the specified tiles and offset. + /// + /// The 2D array of tiles to place on the map. + /// The position offset for placing the tiles. public SpecificTilesStep(ITile[][] tiles, Loc offset) { this.Tiles = tiles; this.Offset = offset; } + /// + /// Gets or sets the 2D array of tiles to place on the map. + /// public ITile[][] Tiles { get; set; } + /// + /// Gets or sets the position offset for placing the tiles. + /// public Loc Offset { get; set; } + /// public override void Apply(T map) { // initialize map array to empty diff --git a/RogueElements/MapGen/Tiles/StairsStep.cs b/RogueElements/MapGen/Tiles/StairsStep.cs index b34757d1..1339ba4c 100644 --- a/RogueElements/MapGen/Tiles/StairsStep.cs +++ b/RogueElements/MapGen/Tiles/StairsStep.cs @@ -9,33 +9,51 @@ namespace RogueElements { /// - /// Adds the entrance and exit to the floor. Is not room-conscious and only picks random tiles. + /// Places entrance and exit stairs on the floor at random walkable tile locations. /// - /// - /// - /// + /// The type of map context that implements placement for entrances and exits. + /// The type of entrance to place. + /// The type of exit to place. + /// + /// This step is not room-conscious and selects random free tiles for stair placement. + /// [Serializable] public class StairsStep : GenStep where TGenContext : class, IPlaceableGenContext, IPlaceableGenContext where TEntrance : IEntrance where TExit : IExit { + /// + /// Initializes a new instance of the class. + /// public StairsStep() { this.Entrance = new List(); this.Exit = new List(); } + /// + /// Initializes a new instance of the class with the specified entrance and exit. + /// + /// The entrance to place on the map. + /// The exit to place on the map. public StairsStep(TEntrance entrance, TExit exit) { this.Entrance = new List { entrance }; this.Exit = new List { exit }; } + /// + /// Gets the list of entrances to place on the map. + /// public List Entrance { get; } + /// + /// Gets the list of exits to place on the map. + /// public List Exit { get; } + /// public override void Apply(TGenContext map) { Loc defaultLoc = Loc.Zero; diff --git a/RogueElements/MapGen/Tiles/Water/BlobTilePercentStencil.cs b/RogueElements/MapGen/Tiles/Water/BlobTilePercentStencil.cs index 1a794482..ef0ba1e9 100644 --- a/RogueElements/MapGen/Tiles/Water/BlobTilePercentStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/BlobTilePercentStencil.cs @@ -9,30 +9,43 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// Checking the bounds is checking each individual tile. - /// The amount of tiles in the blob placement that pass the stencil test must be over the specified percent. + /// Provides a blob stencil that requires a minimum percentage of tiles to pass a terrain stencil. /// - /// + /// The type of map context that implements . [Serializable] public class BlobTilePercentStencil : IBlobStencil where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public BlobTilePercentStencil() { this.TileStencil = new DefaultTerrainStencil(); } + /// + /// Initializes a new instance of the class with the specified percent and stencil. + /// + /// The minimum percentage of tiles that must pass the stencil. + /// The terrain stencil to apply to each tile. public BlobTilePercentStencil(int percent, ITerrainStencil tileStencil) { this.Percent = percent; this.TileStencil = tileStencil; } + /// + /// Gets or sets the terrain stencil to apply to each tile in the blob. + /// public ITerrainStencil TileStencil { get; set; } + /// + /// Gets or sets the minimum percentage of tiles that must pass the stencil for the blob to be eligible. + /// public int Percent { get; set; } + /// public bool Test(T map, Rect rect, Grid.LocTest blobTest) { int amount = 0; diff --git a/RogueElements/MapGen/Tiles/Water/BlobTileStencil.cs b/RogueElements/MapGen/Tiles/Water/BlobTileStencil.cs index 513d5314..8c61f5a4 100644 --- a/RogueElements/MapGen/Tiles/Water/BlobTileStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/BlobTileStencil.cs @@ -9,34 +9,53 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// Checking the bounds is checking each individual tile. + /// Provides a blob stencil that applies a terrain stencil to each individual tile in the blob. /// - /// + /// The type of map context that implements . [Serializable] public class BlobTileStencil : IBlobStencil where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public BlobTileStencil() { this.TileStencil = new DefaultTerrainStencil(); } + /// + /// Initializes a new instance of the class with the specified terrain stencil. + /// + /// The terrain stencil to apply to each tile. public BlobTileStencil(ITerrainStencil tileStencil) { this.TileStencil = tileStencil; } + /// + /// Initializes a new instance of the class with the specified terrain stencil and logic mode. + /// + /// The terrain stencil to apply to each tile. + /// Whether any single tile passing is sufficient (OR logic), or all must pass (AND logic). public BlobTileStencil(ITerrainStencil tileStencil, bool requireAny) { this.TileStencil = tileStencil; this.RequireAny = requireAny; } + /// + /// Gets or sets the terrain stencil to apply to each tile in the blob. + /// public ITerrainStencil TileStencil { get; set; } + /// + /// Gets or sets a value indicating whether any single tile passing is sufficient. + /// When true, uses OR logic; when false, uses AND logic requiring all tiles to pass. + /// public bool RequireAny { get; set; } + /// public bool Test(T map, Rect rect, Grid.LocTest blobTest) { for (int xx = rect.X; xx < rect.End.X; xx++) diff --git a/RogueElements/MapGen/Tiles/Water/BlobWaterStep.cs b/RogueElements/MapGen/Tiles/Water/BlobWaterStep.cs index 56d2eef1..a7ca09ec 100644 --- a/RogueElements/MapGen/Tiles/Water/BlobWaterStep.cs +++ b/RogueElements/MapGen/Tiles/Water/BlobWaterStep.cs @@ -9,9 +9,9 @@ namespace RogueElements { /// - /// Creates blobs of water using cellular automata, and places them around the map. + /// Generates water blobs using cellular automata and places them at random locations on the map. /// - /// + /// The type of map context that implements . [Serializable] public class BlobWaterStep : WaterStep, IBlobWaterStep where T : class, ITiledGenContext @@ -19,12 +19,24 @@ public class BlobWaterStep : WaterStep, IBlobWaterStep private const int AUTOMATA_CHANCE = 55; private const int AUTOMATA_ROUNDS = 5; + /// + /// Initializes a new instance of the class. + /// public BlobWaterStep() : base() { this.BlobStencil = new DefaultBlobStencil(); } + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The range for the number of blobs to generate. + /// The water terrain tile to place. + /// The per-tile stencil for eligible placement locations. + /// The blob-wide stencil for valid blob placement. + /// The acceptable area range for each blob. + /// The generation area range for creating blobs. public BlobWaterStep(RandRange blobs, ITile terrain, ITerrainStencil stencil, IBlobStencil blobStencil, IntRange areaScale, IntRange generateScale) : base(terrain, stencil) { @@ -35,25 +47,29 @@ public BlobWaterStep(RandRange blobs, ITile terrain, ITerrainStencil stencil, } /// - /// The number of blobs to place. + /// Gets or sets the range for the number of blobs to place. /// public RandRange Blobs { get; set; } /// - /// The NxN size range of the area creating the blob. It is measured in tiles. It is recommended to pick a range with at least 4 between min and max. + /// Gets or sets the NxN size range of the generation area for creating blobs. + /// It is recommended to pick a range with at least 4 between min and max. /// public IntRange GenerateScale { get; set; } /// - /// The NxN size range of the acceptable area the blob takes. It is measured in tiles. It is recommended to pick a range with at least 4 between min and max. Must be equal to or smaller than Generate Scale. + /// Gets or sets the NxN size range of the acceptable blob area. + /// Must be equal to or smaller than . /// public IntRange AreaScale { get; set; } /// - /// Blob-wide stencil. All-or-nothing: If the blob position passes this stencil, it is drawn. Otherwise it is not. + /// Gets or sets the blob-wide stencil for placement validation. + /// If the blob position fails this stencil, the entire blob is rejected. /// public IBlobStencil BlobStencil { get; set; } + /// public override void Apply(T map) { int blobs = this.Blobs.Pick(map.Rand); @@ -121,13 +137,13 @@ public override string ToString() } /// - /// Attempts to place a blob from the blob map. + /// Attempts to place a blob from the blob map at the specified offset. /// - /// - /// - /// - /// - /// + /// The map context to place the blob on. + /// The blob map containing the blob shapes. + /// The index of the blob to place. + /// The position offset for blob placement. + /// true if the blob was successfully placed; otherwise, false. protected virtual bool AttemptBlob(T map, BlobMap blobMap, int blobIdx, Loc offset) { BlobMap.Blob mapBlob = blobMap.Blobs[blobIdx]; diff --git a/RogueElements/MapGen/Tiles/Water/BorderTerrainStencil.cs b/RogueElements/MapGen/Tiles/Water/BorderTerrainStencil.cs index 2a473118..5c83c114 100644 --- a/RogueElements/MapGen/Tiles/Water/BorderTerrainStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/BorderTerrainStencil.cs @@ -9,19 +9,29 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// Eligible if bordering a certain tile type. + /// Provides a terrain stencil that tests tiles based on their adjacent neighbors. /// - /// + /// The type of map context that implements . + /// + /// A tile is eligible if it borders at least one tile matching the specified types. + /// [Serializable] public class BorderTerrainStencil : ITerrainStencil where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public BorderTerrainStencil() { this.MatchTiles = new List(); } + /// + /// Initializes a new instance of the class with the specified tiles. + /// + /// Whether to invert the match result. + /// The tile types to match against in neighboring tiles. public BorderTerrainStencil(bool negate, params ITile[] tiles) : this() { @@ -30,12 +40,16 @@ public BorderTerrainStencil(bool negate, params ITile[] tiles) } /// - /// The allowed tile types. + /// Gets the list of tile types to match against in neighboring tiles. /// public List MatchTiles { get; private set; } + /// + /// Gets or sets a value indicating whether to invert the match result. + /// public bool Negate { get; set; } + /// public bool Test(T map, Loc loc) { foreach (Dir8 dir in DirExt.VALID_DIR8) diff --git a/RogueElements/MapGen/Tiles/Water/DefaultBlobStencil.cs b/RogueElements/MapGen/Tiles/Water/DefaultBlobStencil.cs index 42e0c0ad..5795f55c 100644 --- a/RogueElements/MapGen/Tiles/Water/DefaultBlobStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/DefaultBlobStencil.cs @@ -9,14 +9,14 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// All tiles are eligible. + /// Provides a blob stencil that allows all blob placements to pass. /// - /// + /// The type of map context that implements . [Serializable] public class DefaultBlobStencil : IBlobStencil where T : class, ITiledGenContext { + /// public bool Test(T map, Rect rect, Grid.LocTest blobTest) { return true; diff --git a/RogueElements/MapGen/Tiles/Water/DefaultTerrainStencil.cs b/RogueElements/MapGen/Tiles/Water/DefaultTerrainStencil.cs index 7cdc56f8..a246eea8 100644 --- a/RogueElements/MapGen/Tiles/Water/DefaultTerrainStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/DefaultTerrainStencil.cs @@ -9,14 +9,14 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// All tiles are eligible. + /// Provides a terrain stencil that allows all tiles to pass. /// - /// + /// The type of map context that implements . [Serializable] public class DefaultTerrainStencil : ITerrainStencil where T : class, ITiledGenContext { + /// public bool Test(T map, Loc loc) { return true; diff --git a/RogueElements/MapGen/Tiles/Water/IBlobStencil.cs b/RogueElements/MapGen/Tiles/Water/IBlobStencil.cs index 965ebcff..49e4b0d8 100644 --- a/RogueElements/MapGen/Tiles/Water/IBlobStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/IBlobStencil.cs @@ -8,9 +8,20 @@ namespace RogueElements { + /// + /// Defines a filter for testing blob-wide eligibility during terrain operations. + /// + /// The type of map context that implements . public interface IBlobStencil where T : class, ITiledGenContext { + /// + /// Tests whether a blob within the specified bounds is eligible for placement. + /// + /// The map context containing the tiles. + /// The bounding rectangle of the blob. + /// A function that tests whether a location belongs to the blob. + /// true if the blob placement is eligible; otherwise, false. bool Test(T map, Rect rect, Grid.LocTest blobTest); } } diff --git a/RogueElements/MapGen/Tiles/Water/IBlobWaterStep.cs b/RogueElements/MapGen/Tiles/Water/IBlobWaterStep.cs index 32738ce6..a2e90ca0 100644 --- a/RogueElements/MapGen/Tiles/Water/IBlobWaterStep.cs +++ b/RogueElements/MapGen/Tiles/Water/IBlobWaterStep.cs @@ -8,12 +8,24 @@ namespace RogueElements { + /// + /// Defines the interface for blob-based water generation steps that place discrete water regions. + /// public interface IBlobWaterStep : IWaterStep { + /// + /// Gets or sets the range for the number of blobs to generate. + /// RandRange Blobs { get; set; } + /// + /// Gets or sets the acceptable area range for each blob in tiles. + /// IntRange AreaScale { get; set; } + /// + /// Gets or sets the generation area range used to create each blob. + /// IntRange GenerateScale { get; set; } } } \ No newline at end of file diff --git a/RogueElements/MapGen/Tiles/Water/IPerlinWaterStep.cs b/RogueElements/MapGen/Tiles/Water/IPerlinWaterStep.cs index ff1eecd0..b07fdda4 100644 --- a/RogueElements/MapGen/Tiles/Water/IPerlinWaterStep.cs +++ b/RogueElements/MapGen/Tiles/Water/IPerlinWaterStep.cs @@ -8,14 +8,29 @@ namespace RogueElements { + /// + /// Defines the interface for Perlin noise-based water generation steps. + /// public interface IPerlinWaterStep : IWaterStep { + /// + /// Gets or sets the number of Perlin noise iterations for height map complexity. + /// int OrderComplexity { get; set; } + /// + /// Gets or sets the minimum unit size of water tiles. + /// int OrderSoftness { get; set; } + /// + /// Gets or sets the target percentage of the map to cover with water. + /// RandRange WaterPercent { get; set; } + /// + /// Gets or sets a value indicating whether to apply bowl distortion to prevent edge cutoffs. + /// bool Bowl { get; set; } } } diff --git a/RogueElements/MapGen/Tiles/Water/ITerrainStencil.cs b/RogueElements/MapGen/Tiles/Water/ITerrainStencil.cs index 2884182f..5f4e397b 100644 --- a/RogueElements/MapGen/Tiles/Water/ITerrainStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/ITerrainStencil.cs @@ -8,9 +8,19 @@ namespace RogueElements { + /// + /// Defines a filter for testing individual tile eligibility during terrain operations. + /// + /// The type of map context that implements . public interface ITerrainStencil where T : class, ITiledGenContext { + /// + /// Tests whether a tile at the specified location is eligible for the terrain operation. + /// + /// The map context containing the tile. + /// The location of the tile to test. + /// true if the tile is eligible; otherwise, false. bool Test(T map, Loc loc); } } diff --git a/RogueElements/MapGen/Tiles/Water/IWaterStep.cs b/RogueElements/MapGen/Tiles/Water/IWaterStep.cs index 57607c91..cd036ec2 100644 --- a/RogueElements/MapGen/Tiles/Water/IWaterStep.cs +++ b/RogueElements/MapGen/Tiles/Water/IWaterStep.cs @@ -8,8 +8,14 @@ namespace RogueElements { + /// + /// Defines the base interface for water generation steps. + /// public interface IWaterStep { + /// + /// Gets or sets the tile representing the water terrain to place. + /// ITile Terrain { get; set; } } } diff --git a/RogueElements/MapGen/Tiles/Water/MapTerrainStencil.cs b/RogueElements/MapGen/Tiles/Water/MapTerrainStencil.cs index c999a722..469e037b 100644 --- a/RogueElements/MapGen/Tiles/Water/MapTerrainStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/MapTerrainStencil.cs @@ -9,18 +9,27 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// Tiles of a certain type are eligible. + /// Provides a terrain stencil that tests tiles based on the map's terrain definitions. /// - /// + /// The type of map context that implements . [Serializable] public class MapTerrainStencil : ITerrainStencil where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public MapTerrainStencil() { } + /// + /// Initializes a new instance of the class with the specified filters. + /// + /// Whether to allow tiles matching the map's walkable terrain. + /// Whether to allow tiles matching the map's wall terrain. + /// Whether to allow tiles that block movement. + /// Whether to invert the filter to exclude matching tiles. public MapTerrainStencil(bool room, bool wall, bool blocked, bool not) { this.Room = room; @@ -30,25 +39,28 @@ public MapTerrainStencil(bool room, bool wall, bool blocked, bool not) } /// - /// Allows tiles specified as the map's walkable terrain. + /// Gets a value indicating whether to allow tiles matching the map's walkable terrain. /// public bool Room { get; private set; } /// - /// Allows tiles specified as the map's wall terrain. + /// Gets a value indicating whether to allow tiles matching the map's wall terrain. /// public bool Wall { get; private set; } /// - /// Allows tiles specified as the blocked terrain. This relies on the map's own definition of TileBlocked. + /// Gets a value indicating whether to allow tiles that block movement. + /// This relies on the map's definition. /// public bool Blocked { get; private set; } /// - /// Reverses the policy, allowing all tiles EXCEPT the ones selected above. + /// Gets a value indicating whether to invert the filter. + /// When set, allows all tiles except those matching the specified criteria. /// public bool Not { get; private set; } + /// public bool Test(T map, Loc loc) { bool result = false; diff --git a/RogueElements/MapGen/Tiles/Water/MatchTerrainStencil.cs b/RogueElements/MapGen/Tiles/Water/MatchTerrainStencil.cs index b7083fdd..dcc8a610 100644 --- a/RogueElements/MapGen/Tiles/Water/MatchTerrainStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/MatchTerrainStencil.cs @@ -9,19 +9,26 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// Tiles in a list of allowed tile types are eligible. + /// Provides a terrain stencil that tests tiles against a list of allowed tile types. /// - /// + /// The type of map context that implements . [Serializable] public class MatchTerrainStencil : ITerrainStencil where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public MatchTerrainStencil() { this.MatchTiles = new List(); } + /// + /// Initializes a new instance of the class with the specified tiles. + /// + /// Whether to invert the match result. + /// The tile types to match against. public MatchTerrainStencil(bool negate, params ITile[] tiles) : this() { @@ -30,12 +37,16 @@ public MatchTerrainStencil(bool negate, params ITile[] tiles) } /// - /// The allowed tile types. + /// Gets the list of tile types to match against. /// public List MatchTiles { get; private set; } + /// + /// Gets or sets a value indicating whether to invert the match result. + /// public bool Negate { get; set; } + /// public bool Test(T map, Loc loc) { ITile checkTile = map.GetTile(loc); diff --git a/RogueElements/MapGen/Tiles/Water/MultiBlobStencil.cs b/RogueElements/MapGen/Tiles/Water/MultiBlobStencil.cs index 7defbc2a..6070dcc8 100644 --- a/RogueElements/MapGen/Tiles/Water/MultiBlobStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/MultiBlobStencil.cs @@ -9,19 +9,26 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// Only considers tiles eligible if they fit any/all conditions. + /// Provides a blob stencil that combines multiple stencils with AND/OR logic. /// - /// + /// The type of map context that implements . [Serializable] public class MultiBlobStencil : IBlobStencil where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public MultiBlobStencil() { this.List = new List>(); } + /// + /// Initializes a new instance of the class with the specified stencils. + /// + /// Whether any single stencil passing is sufficient (OR logic), or all must pass (AND logic). + /// The stencils to combine. public MultiBlobStencil(bool requireAny, params IBlobStencil[] stencils) { this.RequireAny = requireAny; @@ -30,12 +37,17 @@ public MultiBlobStencil(bool requireAny, params IBlobStencil[] stencils) } /// - /// Determines if the entire map should be checked for connectivity, or just the immediate surrounding tiles. + /// Gets or sets the list of blob stencils to combine. /// public List> List { get; set; } + /// + /// Gets or sets a value indicating whether any single stencil passing is sufficient. + /// When true, uses OR logic; when false, uses AND logic. + /// public bool RequireAny { get; set; } + /// public bool Test(T map, Rect rect, Grid.LocTest blobTest) { foreach (IBlobStencil subReq in this.List) diff --git a/RogueElements/MapGen/Tiles/Water/MultiTerrainStencil.cs b/RogueElements/MapGen/Tiles/Water/MultiTerrainStencil.cs index eac65889..8bb752ec 100644 --- a/RogueElements/MapGen/Tiles/Water/MultiTerrainStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/MultiTerrainStencil.cs @@ -9,19 +9,26 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// Only considers tiles eligible if they fit any/all conditions. + /// Provides a terrain stencil that combines multiple stencils with AND/OR logic. /// - /// + /// The type of map context that implements . [Serializable] public class MultiTerrainStencil : ITerrainStencil where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public MultiTerrainStencil() { this.List = new List>(); } + /// + /// Initializes a new instance of the class with the specified stencils. + /// + /// Whether any single stencil passing is sufficient (OR logic), or all must pass (AND logic). + /// The stencils to combine. public MultiTerrainStencil(bool requireAny, params ITerrainStencil[] stencils) { this.RequireAny = requireAny; @@ -30,12 +37,17 @@ public MultiTerrainStencil(bool requireAny, params ITerrainStencil[] stencils } /// - /// Determines if the entire map should be checked for connectivity, or just the immediate surrounding tiles. + /// Gets or sets the list of stencils to combine. /// public List> List { get; set; } + /// + /// Gets or sets a value indicating whether any single stencil passing is sufficient. + /// When true, uses OR logic; when false, uses AND logic. + /// public bool RequireAny { get; set; } + /// public bool Test(T map, Loc loc) { foreach (ITerrainStencil subReq in this.List) diff --git a/RogueElements/MapGen/Tiles/Water/NoChokepointStencil.cs b/RogueElements/MapGen/Tiles/Water/NoChokepointStencil.cs index a2d535b8..b2d3b0d6 100644 --- a/RogueElements/MapGen/Tiles/Water/NoChokepointStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/NoChokepointStencil.cs @@ -9,38 +9,52 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// Locations that, if all made unwalkable, do not cause a chokepoint to be removed, are eligible. + /// Provides a blob stencil that prevents blob placement from creating chokepoints. /// - /// + /// The type of map context that implements . + /// + /// This stencil tests whether placing the blob would disconnect walkable areas or create + /// impassable barriers. + /// [Serializable] public class NoChokepointStencil : IBlobStencil where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public NoChokepointStencil() { this.TileStencil = new DefaultTerrainStencil(); } + /// + /// Initializes a new instance of the class with the specified terrain stencil. + /// + /// The terrain stencil that defines walkable tiles. public NoChokepointStencil(ITerrainStencil tileStencil) { this.TileStencil = tileStencil; } /// - /// The stencil should return true if the tile is considered walkable. (Tile-being-choked) + /// Gets or sets the terrain stencil that determines which tiles are considered walkable. /// public ITerrainStencil TileStencil { get; set; } /// - /// Determines if the entire map should be checked for connectivity, or just the immediate surrounding tiles. - /// If turned off, the gen will refuse to break ANY chokepoint period. - /// If turned on, it will break any chokepoint that has another way around somewhere, while still preventing true unsolvability. + /// Gets or sets a value indicating whether to check the entire map for connectivity. + /// When false, refuses to break any chokepoint. + /// When true, allows breaking chokepoints that have alternate paths, preventing only true disconnections. /// public bool Global { get; set; } + /// + /// Gets or sets a value indicating whether to invert the chokepoint detection result. + /// public bool Negate { get; set; } + /// public bool Test(T map, Rect rect, Grid.LocTest blobTest) { bool IsMapValid(Loc loc) => this.TileStencil.Test(map, loc); diff --git a/RogueElements/MapGen/Tiles/Water/NoChokepointTerrainStencil.cs b/RogueElements/MapGen/Tiles/Water/NoChokepointTerrainStencil.cs index 6bbf9b77..de30c7dd 100644 --- a/RogueElements/MapGen/Tiles/Water/NoChokepointTerrainStencil.cs +++ b/RogueElements/MapGen/Tiles/Water/NoChokepointTerrainStencil.cs @@ -9,36 +9,52 @@ namespace RogueElements { /// - /// A filter for determining the eligible tiles for an operation. - /// Locations that, if all made unwalkable, do not cause a chokepoint to be removed, are eligible. + /// Provides a terrain stencil that prevents tile placement from creating chokepoints. /// - /// + /// The type of map context that implements . + /// + /// This stencil tests whether placing a tile at a location would disconnect walkable areas + /// or create impassable barriers. + /// [Serializable] public class NoChokepointTerrainStencil : ITerrainStencil where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// public NoChokepointTerrainStencil() { this.TileStencil = new DefaultTerrainStencil(); } + /// + /// Initializes a new instance of the class with the specified terrain stencil. + /// + /// The terrain stencil that defines walkable tiles. public NoChokepointTerrainStencil(ITerrainStencil tileStencil) { this.TileStencil = tileStencil; } /// - /// Filters for valid path tiles + /// Gets or sets the terrain stencil that determines which tiles are considered valid path tiles. /// public ITerrainStencil TileStencil { get; set; } /// - /// Determines if the entire map should be checked for connectivity, or just the immediate surrounding tiles. + /// Gets or sets a value indicating whether to check the entire map for connectivity. + /// When false, only checks immediate surrounding tiles. + /// When true, checks the entire map for disconnections. /// public bool Global { get; set; } + /// + /// Gets or sets a value indicating whether to invert the chokepoint detection result. + /// public bool Negate { get; set; } + /// public bool Test(T map, Loc testLoc) { bool IsMapValid(Loc loc) => this.TileStencil.Test(map, loc); diff --git a/RogueElements/MapGen/Tiles/Water/PerlinWaterStep.cs b/RogueElements/MapGen/Tiles/Water/PerlinWaterStep.cs index 8722bc32..d3298ede 100644 --- a/RogueElements/MapGen/Tiles/Water/PerlinWaterStep.cs +++ b/RogueElements/MapGen/Tiles/Water/PerlinWaterStep.cs @@ -9,21 +9,36 @@ namespace RogueElements { /// - /// Generates a random spread of water on the map. This is achieved by generating a heightContext using Perlin Noise, - /// then converting all tiles with a height value below the specified threshold to water. + /// Generates water coverage on the map using Perlin noise to create natural-looking terrain. /// - /// + /// The type of map context that implements . + /// + /// This step generates a height map using Perlin noise, then converts all tiles with height values + /// below a calculated threshold into water tiles, achieving the target water percentage. + /// [Serializable] public class PerlinWaterStep : WaterStep, IPerlinWaterStep where T : class, ITiledGenContext { private const int BUFFER_SIZE = 5; + /// + /// Initializes a new instance of the class. + /// public PerlinWaterStep() : base() { } + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The target percentage of water coverage. + /// The number of Perlin noise iterations for height map generation. + /// The water terrain tile to place. + /// The stencil that determines eligible placement locations. + /// The minimum unit size of water tiles (0 = 1x1, 1 = 2x2, etc.). + /// Whether to apply bowl distortion to prevent edge cutoffs. public PerlinWaterStep(RandRange waterPercent, int complexity, ITile terrain, ITerrainStencil stencil, int softness = default, bool bowl = true) : base(terrain, stencil) { @@ -34,25 +49,29 @@ public PerlinWaterStep(RandRange waterPercent, int complexity, ITile terrain, IT } /// - /// Determines how many iterations of Perlin noise to generate the heighTContext with. Higher complexity = higher variation of heights and more natural looking terrain. + /// Gets or sets the number of Perlin noise iterations for height map generation. + /// Higher values produce more varied heights and more natural-looking terrain. /// public int OrderComplexity { get; set; } /// - /// Determines the smallest unit of water tiles on the map. 0 = 1x1 tile of water, 1 = 2x2 tile of water, etc. + /// Gets or sets the minimum unit size of water tiles. + /// A value of 0 produces 1x1 tiles, 1 produces 2x2 tiles, and so on. /// public int OrderSoftness { get; set; } /// - /// The percent chance of water occurring. + /// Gets or sets the target percentage of the map to cover with water. /// public RandRange WaterPercent { get; set; } /// - /// Distorts the water such that it becomes like a bowl-shape, preventing awkward cutoffs at the edge of the map. + /// Gets or sets a value indicating whether to apply bowl distortion. + /// When enabled, water values are interpolated upward near map edges to prevent awkward cutoffs. /// public bool Bowl { get; set; } + /// public override void Apply(T map) { int waterPercent = this.WaterPercent.Pick(map.Rand); diff --git a/RogueElements/MapGen/Tiles/Water/README.md b/RogueElements/MapGen/Tiles/Water/README.md new file mode 100644 index 00000000..c90ff6a0 --- /dev/null +++ b/RogueElements/MapGen/Tiles/Water/README.md @@ -0,0 +1,336 @@ +# Water / Terrain Generation + +[![RogueElements](https://img.shields.io/nuget/v/RogueElements?label=RogueElements)](https://www.nuget.org/packages/RogueElements) + +Water and terrain pattern generation for procedural roguelike maps. This module provides classes for generating natural-looking terrain features using Perlin noise and cellular automata. + +## Purpose + +The Water module generates terrain patterns (water, lava, chasms, etc.) on existing tile maps. It supports: +- Perlin noise-based continuous terrain generation +- Cellular automata blob generation +- Stencil-based placement rules (which tiles can be painted) +- Chokepoint-aware placement (prevents breaking map connectivity) + +## Water Generation Steps + +### PerlinWaterStep + +Generates random terrain spread using Perlin noise, creating natural-looking continuous patterns. + +```csharp +const int waterTerrain = 2; + +// Generate water covering approximately 35% of eligible tiles +var waterStep = new PerlinWaterStep( + waterPercent: new RandRange(35), + complexity: 3, // Higher = more variation + terrain: new Tile(waterTerrain), + stencil: new MapTerrainStencil(false, true, false, false), // Only on floor tiles + softness: 1 // Minimum water tile size (2^softness) +); +waterStep.Bowl = true; // Prevents edge cutoffs + +layout.GenSteps.Add(3, waterStep); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `WaterPercent` | `RandRange` | Percentage of map to cover with terrain | +| `OrderComplexity` | `int` | Perlin noise iterations (higher = more varied) | +| `OrderSoftness` | `int` | Minimum tile group size (0 = 1x1, 1 = 2x2, etc.) | +| `Bowl` | `bool` | Distort edges to prevent awkward boundary cutoffs | + +### BlobWaterStep + +Creates distinct blobs of terrain using cellular automata, then places them randomly around the map. + +```csharp +var blobStep = new BlobWaterStep( + blobs: new RandRange(3, 6), // Number of blobs to place + terrain: new Tile(waterTerrain), + stencil: new MapTerrainStencil(false, true, false, false), + blobStencil: new DefaultBlobStencil(), + areaScale: new IntRange(16, 25), // Final blob size range (NxN tiles) + generateScale: new IntRange(20, 30) // Generation size range (larger than final) +); + +layout.GenSteps.Add(3, blobStep); +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Blobs` | `RandRange` | Number of blobs to place | +| `GenerateScale` | `IntRange` | Size of area to generate blob in | +| `AreaScale` | `IntRange` | Acceptable final blob size | +| `BlobStencil` | `IBlobStencil` | Blob-level placement validation | + +## Terrain Stencils + +Stencils determine which tiles are eligible for terrain placement. + +### ITerrainStencil<T> + +```csharp +public interface ITerrainStencil + where T : class, ITiledGenContext +{ + bool Test(T map, Loc loc); +} +``` + +### MapTerrainStencil + +Filters by tile type (room, wall, blocked): + +```csharp +// Allow painting on floor tiles only +var floorOnly = new MapTerrainStencil( + room: false, // Don't allow room terrain + wall: true, // Allow where floor currently is + blocked: false, // Don't allow blocked tiles + not: false // Don't invert +); + +// Allow painting anywhere EXCEPT walls +var notWalls = new MapTerrainStencil( + room: false, wall: true, blocked: false, not: true +); +``` + +### DefaultTerrainStencil + +Allows all tiles (no filtering): + +```csharp +var allowAll = new DefaultTerrainStencil(); +``` + +### MultiTerrainStencil + +Combines multiple stencils with AND/OR logic: + +```csharp +var combined = new MultiTerrainStencil( + stencil1, + stencil2 +); +``` + +### MatchTerrainStencil + +Matches specific terrain types: + +```csharp +var matchWater = new MatchTerrainStencil(waterTile); +``` + +### BorderTerrainStencil + +Only allows placement at map borders: + +```csharp +var borderOnly = new BorderTerrainStencil(borderWidth: 2); +``` + +### NoChokepointTerrainStencil + +Prevents painting tiles that would break map connectivity: + +```csharp +// Prevent water from blocking paths +var noChokepoint = new NoChokepointTerrainStencil( + new MapTerrainStencil(false, true, false, false) +); +``` + +## Blob Stencils + +Blob stencils validate entire blob placements (all-or-nothing). + +### IBlobStencil<T> + +```csharp +public interface IBlobStencil + where T : class, ITiledGenContext +{ + bool Test(T map, Rect rect, Grid.LocTest blobTest); +} +``` + +### DefaultBlobStencil + +Allows all blob placements: + +```csharp +var allowAll = new DefaultBlobStencil(); +``` + +### NoChokepointStencil + +Prevents blobs that would disconnect the map: + +```csharp +// Only place blobs that don't block paths +var safeBlobs = new NoChokepointStencil( + new MapTerrainStencil(false, true, false, false) +); +safeBlobs.Global = true; // Check entire map connectivity +``` + +### MultiBlobStencil + +Combines multiple blob stencils: + +```csharp +var combined = new MultiBlobStencil(stencil1, stencil2); +``` + +### BlobTileStencil + +Validates blobs based on tile-level stencil for all tiles in blob: + +```csharp +var tileCheck = new BlobTileStencil( + new MapTerrainStencil(false, true, false, false) +); +``` + +### BlobTilePercentStencil + +Requires a percentage of blob tiles to pass stencil: + +```csharp +var percentCheck = new BlobTilePercentStencil( + new MapTerrainStencil(false, true, false, false), + minPercent: 80 // At least 80% must be valid +); +``` + +## Usage Example + +From `Ex5_Terrain`: + +```csharp +var layout = new MapGen(); + +// ... grid and path setup ... + +// Add stairs +layout.GenSteps.Add(2, new FloorStairsStep( + 0, new StairsUp(), new StairsDown() +)); + +// Generate water (terrain ID 2) with 35% coverage +const int terrain = 2; +var waterPostProc = new PerlinWaterStep( + new RandRange(35), + 3, // Complexity + new Tile(terrain), + new MapTerrainStencil(false, true, false, false), // Floor tiles only + 1 // Softness +); +layout.GenSteps.Add(3, waterPostProc); + +// Fix diagonal water connections +layout.GenSteps.Add(4, new DropDiagonalBlockStep(new Tile(terrain))); + +// Remove isolated water in walls +layout.GenSteps.Add(4, new EraseIsolatedStep(new Tile(terrain))); + +MapGenContext context = layout.GenMap(MathUtils.Rand.NextUInt64()); +``` + +## Creating Custom Terrain Steps + +1. Inherit from `WaterStep` +2. Override `Apply()` and use `DrawBlob()` or `DrawLocs()` helpers + +```csharp +[Serializable] +public class RiverStep : WaterStep + where T : class, ITiledGenContext +{ + public int RiverWidth { get; set; } = 2; + + public override void Apply(T map) + { + // Generate river path from top to bottom + var riverLocs = new List(); + int x = map.Rand.Next(RiverWidth, map.Width - RiverWidth); + + for (int y = 0; y < map.Height; y++) + { + // Meander left or right + x += map.Rand.Next(-1, 2); + x = Math.Clamp(x, RiverWidth, map.Width - RiverWidth - 1); + + // Add river width + for (int w = -RiverWidth / 2; w <= RiverWidth / 2; w++) + riverLocs.Add(new Loc(x + w, y)); + } + + DrawLocs(map, riverLocs.ToArray()); + } +} +``` + +## Creating Custom Stencils + +### Terrain Stencil + +```csharp +[Serializable] +public class DistanceFromEdgeStencil : ITerrainStencil + where T : class, ITiledGenContext +{ + public int MinDistance { get; set; } + + public bool Test(T map, Loc loc) + { + int distX = Math.Min(loc.X, map.Width - 1 - loc.X); + int distY = Math.Min(loc.Y, map.Height - 1 - loc.Y); + return Math.Min(distX, distY) >= MinDistance; + } +} +``` + +### Blob Stencil + +```csharp +[Serializable] +public class MaxAreaBlobStencil : IBlobStencil + where T : class, ITiledGenContext +{ + public int MaxArea { get; set; } + + public bool Test(T map, Rect rect, Grid.LocTest blobTest) + { + int count = 0; + for (int x = rect.X; x < rect.End.X; x++) + { + for (int y = rect.Y; y < rect.End.Y; y++) + { + if (blobTest(new Loc(x, y))) + count++; + } + } + return count <= MaxArea; + } +} +``` + +## Related Modules + +- **[../](../)** - Parent Tiles module (tile initialization, cleanup) +- **[Rooms/](../../Rooms/)** - Room generators +- **[Rand/](../../../Rand/)** - Noise generation utilities + +## See Also + +- `Ex5_Terrain` - Water generation with Perlin noise example +- `NoiseGen` - Perlin noise and cellular automata algorithms diff --git a/RogueElements/MapGen/Tiles/Water/WaterStep.cs b/RogueElements/MapGen/Tiles/Water/WaterStep.cs index c101410f..777533ca 100644 --- a/RogueElements/MapGen/Tiles/Water/WaterStep.cs +++ b/RogueElements/MapGen/Tiles/Water/WaterStep.cs @@ -8,15 +8,27 @@ namespace RogueElements { + /// + /// Provides the base class for water generation steps that place terrain on the map. + /// + /// The type of map context that implements . [Serializable] public abstract class WaterStep : GenStep, IWaterStep where T : class, ITiledGenContext { + /// + /// Initializes a new instance of the class. + /// protected WaterStep() { this.TerrainStencil = new DefaultTerrainStencil(); } + /// + /// Initializes a new instance of the class with the specified terrain and stencil. + /// + /// The water terrain tile to place. + /// The stencil that determines which tiles are eligible for placement. protected WaterStep(ITile terrain, ITerrainStencil check) { this.Terrain = terrain; @@ -24,21 +36,21 @@ protected WaterStep(ITile terrain, ITerrainStencil check) } /// - /// Tile representing the water terrain to paint with. + /// Gets or sets the tile representing the water terrain to paint. /// public ITile Terrain { get; set; } /// - /// Determines which tiles are eligible to be painted on. + /// Gets or sets the stencil that determines which tiles are eligible for water placement. /// public ITerrainStencil TerrainStencil { get; set; } /// - /// Draws a blob with the specified bounds and test method + /// Draws a blob of water terrain within the specified bounds. /// - /// - /// - /// The method to test for terrain presence. Passes in global location on the map. + /// The map context to draw on. + /// The bounding rectangle for the blob. + /// The test function that determines which tiles within the bounds belong to the blob. protected void DrawBlob(T map, Rect rect, Grid.LocTest blobTest) { for (int xx = Math.Max(0, rect.X); xx < Math.Min(map.Width, rect.End.X); xx++) @@ -57,6 +69,11 @@ protected void DrawBlob(T map, Rect rect, Grid.LocTest blobTest) GenContextDebug.DebugProgress("Draw Blob"); } + /// + /// Draws water terrain at the specified array of locations. + /// + /// The map context to draw on. + /// The array of locations to place water terrain. protected void DrawLocs(T map, Loc[] locs) { foreach (Loc loc in locs) diff --git a/RogueElements/MathUtils.cs b/RogueElements/MathUtils.cs index 9c64aa2e..aaf2bafc 100644 --- a/RogueElements/MathUtils.cs +++ b/RogueElements/MathUtils.cs @@ -7,11 +7,17 @@ namespace RogueElements { + /// + /// Provides mathematical utility methods and global random number generation. + /// public static class MathUtils { private static IRandom rand = new ReRandom(); private static INoise noise = new ReNoise(); + /// + /// Gets the global random number generator. + /// public static IRandom Rand { get @@ -20,6 +26,9 @@ public static IRandom Rand } } + /// + /// Gets the global noise generator. + /// public static INoise Noise { get @@ -28,6 +37,10 @@ public static INoise Noise } } + /// + /// Re-seeds both the global random and noise generators. + /// + /// The seed value. public static void ReSeedRand(ulong seed) { rand = new ReRandom(seed); @@ -48,18 +61,43 @@ public static T ChooseFromHash(HashSet hash, IRandom rand) return crossArray[rand.Next(crossArray.Length)]; } + /// + /// Adds an amount to a dictionary entry, creating it if needed. + /// + /// The key type. + /// The dictionary. + /// The key. + /// The amount to add. public static void AddToDictionary(Dictionary dict, T key, int amt) { dict.TryGetValue(key, out int currentCount); dict[key] = currentCount + amt; } + /// + /// Merges counts from one dictionary into another. + /// + /// The key type. + /// The destination dictionary. + /// The source dictionary. public static void AddToDictionary(Dictionary dict1, Dictionary dict2) { foreach (T key in dict2.Keys) AddToDictionary(dict1, key, dict2[key]); } + /// + /// Performs bilinear interpolation between four corner values. + /// + /// Top-left corner value. + /// Top-right corner value. + /// Bottom-left corner value. + /// Bottom-right corner value. + /// X position within the cell. + /// Total X divisions. + /// Y position within the cell. + /// Total Y divisions. + /// The interpolated value. public static int BiInterpolate(int topleft, int topright, int bottomleft, int bottomright, int degreeX, int xTotal, int degreeY, int yTotal) { int bottom = ((topleft * (xTotal - degreeX)) + (topright * degreeX)) * (yTotal - degreeY) / xTotal; @@ -67,11 +105,25 @@ public static int BiInterpolate(int topleft, int topright, int bottomleft, int b return (bottom + top) / yTotal; } + /// + /// Performs linear interpolation between two values. + /// + /// Start value. + /// End value. + /// Position between a and b. + /// Total divisions. + /// The interpolated value. public static int Interpolate(int a, int b, int degree, int total) { return ((a * (total - degree)) + (b * degree)) / total; } + /// + /// Computes integer exponentiation. + /// + /// The base. + /// The exponent. + /// num raised to the power of factor. public static int IntPow(int num, int factor) { int result = 1; @@ -114,6 +166,12 @@ public static int DivUp(int num, int den) return num / den; } + /// + /// Wraps a number to be within [0, size). + /// + /// The number to wrap. + /// The wrap boundary. + /// The wrapped value. public static int Wrap(int num, int size) { return ((num % size) + size) % size; diff --git a/RogueElements/NoiseGen.cs b/RogueElements/NoiseGen.cs index 812be074..a219ea9c 100644 --- a/RogueElements/NoiseGen.cs +++ b/RogueElements/NoiseGen.cs @@ -11,9 +11,13 @@ namespace RogueElements { + /// + /// Flags representing cellular automata neighbor count rules. + /// [Flags] public enum CellRule { + /// No neighbors. None = 0, Eq0 = 1, Eq1 = 2, @@ -45,6 +49,9 @@ public enum CellRule All = 511, } + /// + /// Provides methods for procedural noise generation and cellular automata. + /// public static class NoiseGen { /// @@ -106,6 +113,15 @@ public static int[][] PerlinNoise(IRandom rand, int width, int height, int degre return noise; } + /// + /// Runs cellular automata iterations on a grid. + /// + /// The initial grid state. + /// Rules for cell birth based on neighbor count. + /// Rules for cell survival based on neighbor count. + /// Number of iterations to run. + /// Whether to wrap edges toroidally. + /// The final grid state. public static bool[][] IterateAutomata(bool[][] startGrid, CellRule birth, CellRule survive, int iterations, bool wrap = false) { int width = startGrid.Length; diff --git a/RogueElements/Priority/IPriorityList.cs b/RogueElements/Priority/IPriorityList.cs index b6a01fd3..1554b1e2 100644 --- a/RogueElements/Priority/IPriorityList.cs +++ b/RogueElements/Priority/IPriorityList.cs @@ -10,6 +10,22 @@ namespace RogueElements { + /// + /// Defines the non-generic contract for a priority-ordered collection. + /// + /// + /// + /// provides a type-agnostic interface for priority-ordered collections, + /// enabling reflection-based access and serialization scenarios where the item type is not known + /// at compile time. + /// + /// + /// For typed access, use the generic interface or the + /// implementation directly. + /// + /// + /// + /// [SuppressMessage( "Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface", @@ -17,39 +33,134 @@ namespace RogueElements Justification = "Non-generic interface for typically generic classes")] public interface IPriorityList { + /// + /// Gets the number of distinct priority levels that contain items. + /// int PriorityCount { get; } + /// + /// Gets the total number of items across all priority levels. + /// int Count { get; } + /// + /// Adds an item at the specified priority level. + /// + /// The priority at which to add the item. + /// The item to add. void Add(Priority priority, object item); + /// + /// Inserts an item at a specific index within a priority level. + /// + /// The priority at which to insert the item. + /// The zero-based index at which to insert the item within that priority. + /// The item to insert. void Insert(Priority priority, int index, object item); + /// + /// Removes the item at the specified index within a priority level. + /// + /// The priority level containing the item to remove. + /// The zero-based index of the item to remove within that priority. void RemoveAt(Priority priority, int index); + /// + /// Gets the item at the specified index within a priority level. + /// + /// The priority level to access. + /// The zero-based index of the item within that priority. + /// The item at the specified position. object Get(Priority priority, int index); + /// + /// Sets the item at the specified index within a priority level. + /// + /// The priority level to modify. + /// The zero-based index of the item to replace. + /// The new item value. void Set(Priority priority, int index, object item); + /// + /// Removes all items from the priority list. + /// void Clear(); + /// + /// Gets the number of items at a specific priority level. + /// + /// The priority level to count items at. + /// The number of items at the specified priority, or 0 if the priority does not exist. int GetCountAtPriority(Priority priority); + /// + /// Enumerates all priority levels that contain items, in ascending order. + /// + /// An enumerable of all values with at least one item. IEnumerable GetPriorities(); + /// + /// Gets all items at the specified priority level. + /// + /// The priority level to retrieve items from. + /// An enumerable of items at the specified priority. IEnumerable GetItems(Priority priority); } + /// + /// Defines the generic contract for a priority-ordered collection. + /// + /// The type of items stored in the collection. + /// + /// + /// extends with strongly-typed + /// methods for adding, retrieving, and modifying items by priority. + /// + /// + /// The primary implementation is , which is used by + /// to store instances. + /// + /// + /// + /// public interface IPriorityList : IPriorityList { + /// + /// Adds an item at the specified priority level. + /// + /// The priority at which to add the item. + /// The item to add. void Add(Priority priority, T item); + /// + /// Inserts an item at a specific index within a priority level. + /// + /// The priority at which to insert the item. + /// The zero-based index at which to insert the item within that priority. + /// The item to insert. void Insert(Priority priority, int index, T item); + /// + /// Gets the item at the specified index within a priority level. + /// + /// The priority level to access. + /// The zero-based index of the item within that priority. + /// The item at the specified position. new T Get(Priority priority, int index); + /// + /// Sets the item at the specified index within a priority level. + /// + /// The priority level to modify. + /// The zero-based index of the item to replace. + /// The new item value. void Set(Priority priority, int index, T item); + /// + /// Gets all items at the specified priority level. + /// + /// The priority level to retrieve items from. + /// An enumerable of items at the specified priority, in insertion order. new IEnumerable GetItems(Priority priority); } } diff --git a/RogueElements/Priority/Priority.cs b/RogueElements/Priority/Priority.cs index 8af423fa..c2f2ee44 100644 --- a/RogueElements/Priority/Priority.cs +++ b/RogueElements/Priority/Priority.cs @@ -8,15 +8,87 @@ namespace RogueElements { + /// + /// Represents a hierarchical priority value used to order generation steps in the pipeline. + /// + /// + /// + /// enables fine-grained ordering of instances + /// within a pipeline. Unlike simple integer priorities, + /// supports multi-level hierarchical ordering (e.g., "3.1.2") for inserting steps between existing ones. + /// + /// + /// Priority comparison follows lexicographic ordering of the integer components: + /// + /// Priority(1) comes before Priority(2) + /// Priority(1, 1) comes after Priority(1) but before Priority(2) + /// Priority(1, 0) is equivalent to Priority(1) (trailing zeros are normalized) + /// + /// + /// + /// The hierarchical structure allows inserting new steps between existing priorities without + /// renumbering. For example, to add a step between priorities 3 and 4, use Priority(3, 1). + /// + /// + /// + /// + /// // Simple integer priorities + /// var early = new Priority(1); + /// var middle = new Priority(5); + /// var late = new Priority(10); + /// + /// // Hierarchical priorities for fine-grained ordering + /// var afterMiddle = new Priority(5, 1); // Between 5 and 6 + /// var wayAfterMiddle = new Priority(5, 2); // Between 5.1 and 6 + /// + /// // Extending an existing priority + /// var subStep = new Priority(middle, 1); // Creates Priority(5, 1) + /// + /// // Use with MapGen + /// layout.GenSteps.Add(early, new InitTilesStep()); + /// layout.GenSteps.Add(middle, new PlaceRoomsStep()); + /// layout.GenSteps.Add(afterMiddle, new AddDoorsStep()); + /// layout.GenSteps.Add(late, new FinalizeStep()); + /// + /// + /// + /// [Serializable] public struct Priority : IComparable, IEquatable { + /// + /// Represents an invalid priority that precedes all valid priorities in comparison. + /// + /// + /// Invalid priorities have a null internal array. When compared, invalid priorities + /// precede all valid priorities and are equal to each other. Use this to represent + /// uninitialized or undefined priority states. + /// public static Priority Invalid = new Priority(null); + /// + /// Represents a priority of zero, equivalent to new Priority(0). + /// public static Priority Zero = new Priority(0); private readonly int[] str; + /// + /// Initializes a new instance of the struct with the specified priority levels. + /// + /// + /// The integer components of the priority, from most significant to least significant. + /// Trailing zeros are automatically normalized (e.g., [1, 0, 0] becomes [1]). + /// Pass or an empty array to create an invalid priority. + /// + /// + /// + /// var p1 = new Priority(5); // Priority "5" + /// var p2 = new Priority(5, 1); // Priority "5.1" + /// var p3 = new Priority(5, 1, 2); // Priority "5.1.2" + /// var p4 = new Priority(5, 0); // Normalized to "5" + /// + /// public Priority(params int[] vals) { if (vals == null || vals.Length == 0) @@ -33,6 +105,25 @@ public Priority(params int[] vals) } } + /// + /// Initializes a new instance of the struct by extending an existing priority. + /// + /// The base priority to extend. + /// + /// Additional priority levels to append after the base priority. + /// Trailing zeros in the combined result are normalized. + /// + /// + /// This constructor is useful for creating sub-priorities that logically follow a parent priority. + /// For example, new Priority(existingPriority, 1) creates a priority that comes after + /// existingPriority but before any higher integer priority. + /// + /// + /// + /// var basePriority = new Priority(5); + /// var extended = new Priority(basePriority, 1, 2); // Creates Priority(5, 1, 2) + /// + /// public Priority(Priority other, params int[] vals) { if (vals == null || vals.Length == 0) @@ -50,26 +141,61 @@ public Priority(Priority other, params int[] vals) } } + /// + /// Gets the number of priority levels in this instance. + /// + /// + /// The count of integer components in this priority. Returns 0 for invalid priorities. + /// public int Length { get { return this.str == null ? 0 : this.str.Length; } } + /// + /// Gets the priority component at the specified level index. + /// + /// The zero-based index of the priority level to retrieve. + /// The integer value at the specified priority level. + /// + /// Thrown when is less than 0 or greater than or equal to . + /// public int this[int ii] { get { return this.str[ii]; } } + /// + /// Determines whether two instances are equal. + /// + /// The first priority to compare. + /// The second priority to compare. + /// if the priorities are equal; otherwise, . public static bool operator ==(Priority value1, Priority value2) { return value1.Equals(value2); } + /// + /// Determines whether two instances are not equal. + /// + /// The first priority to compare. + /// The second priority to compare. + /// if the priorities are not equal; otherwise, . public static bool operator !=(Priority value1, Priority value2) { return !(value1 == value2); } + /// + /// Determines whether one is greater than another. + /// + /// The first priority to compare. + /// The second priority to compare. + /// + /// if is greater than ; + /// otherwise, . Returns if either priority is invalid. + /// public static bool operator >(Priority value1, Priority value2) { // Special case for invalid. It is not greater or less than anything. @@ -78,6 +204,15 @@ public int this[int ii] return value1.CompareTo(value2) > 0; } + /// + /// Determines whether one is less than another. + /// + /// The first priority to compare. + /// The second priority to compare. + /// + /// if is less than ; + /// otherwise, . Returns if either priority is invalid. + /// public static bool operator <(Priority value1, Priority value2) { // Special case for invalid. It is not greater or less than anything. @@ -86,6 +221,15 @@ public int this[int ii] return value1.CompareTo(value2) < 0; } + /// + /// Determines whether one is greater than or equal to another. + /// + /// The first priority to compare. + /// The second priority to compare. + /// + /// if is greater than or equal to ; + /// otherwise, . Returns if either priority is invalid. + /// public static bool operator >=(Priority value1, Priority value2) { // Special case for invalid. It is not greater or less than anything. @@ -94,6 +238,15 @@ public int this[int ii] return value1.CompareTo(value2) >= 0; } + /// + /// Determines whether one is less than or equal to another. + /// + /// The first priority to compare. + /// The second priority to compare. + /// + /// if is less than or equal to ; + /// otherwise, . Returns if either priority is invalid. + /// public static bool operator <=(Priority value1, Priority value2) { // Special case for invalid. It is not greater or less than anything. @@ -102,6 +255,12 @@ public int this[int ii] return value1.CompareTo(value2) <= 0; } + /// + /// Returns a string representation of this priority. + /// + /// + /// A dot-separated string of the priority levels (e.g., "5.1.2"), or "NULL" for invalid priorities. + /// public override string ToString() { if (this.str == null) @@ -118,6 +277,20 @@ public override string ToString() return s.ToString(); } + /// + /// Compares this priority to another and returns their relative ordering. + /// + /// The priority to compare against. + /// + /// A negative value if this priority comes before ; + /// zero if they are equal; + /// a positive value if this priority comes after . + /// Invalid priorities compare as preceding all valid priorities. + /// + /// + /// Comparison is performed lexicographically across priority levels. + /// Missing levels are treated as zero (e.g., Priority(5) equals Priority(5, 0)). + /// public int CompareTo(Priority other) { // Invalid precedes everything else @@ -149,6 +322,14 @@ public int CompareTo(Priority other) } } + /// + /// Determines whether this priority is equal to the specified object. + /// + /// The object to compare with this priority. + /// + /// if is a + /// and equals this instance; otherwise, . + /// public override bool Equals(object obj) { if (!(obj is Priority)) @@ -157,11 +338,24 @@ public override bool Equals(object obj) return this.Equals((Priority)obj); } + /// + /// Determines whether this priority is equal to another priority. + /// + /// The priority to compare with this instance. + /// + /// if the priorities have the same value; otherwise, . + /// public bool Equals(Priority other) { return this.CompareTo(other) == 0; } + /// + /// Returns a hash code for this priority. + /// + /// + /// A hash code computed from the priority levels, or 0 for invalid priorities. + /// public override int GetHashCode() { int hash = 0; diff --git a/RogueElements/Priority/PriorityList.cs b/RogueElements/Priority/PriorityList.cs index 8a8cee62..f1b74962 100644 --- a/RogueElements/Priority/PriorityList.cs +++ b/RogueElements/Priority/PriorityList.cs @@ -10,27 +10,78 @@ namespace RogueElements { /// - /// Stores and retrieves values with an associated priority, abstracting out the list-of-lists logic behind them. + /// Stores and retrieves items organized by , providing ordered access for generation pipelines. /// - /// + /// The type of items stored in the list. + /// + /// + /// is the primary container used by to store + /// instances. It allows multiple items at the same priority while maintaining + /// consistent ordering during enumeration. + /// + /// + /// Key features: + /// + /// Items are grouped by and enumerated in priority order + /// Multiple items can share the same priority and are returned in insertion order + /// Supports both simple integer priorities and hierarchical values + /// Efficient priority-based access and modification + /// + /// + /// + /// The class implements both for typed access and + /// for LINQ compatibility. + /// + /// + /// + /// + /// var steps = new PriorityList<GenStep<MyContext>>(); + /// + /// // Add items with simple integer priorities + /// steps.Add(1, new InitTilesStep()); + /// steps.Add(5, new PlaceRoomsStep()); + /// steps.Add(10, new FinalizeStep()); + /// + /// // Add items with hierarchical priorities (between existing priorities) + /// steps.Add(new Priority(5, 1), new AddDoorsStep()); // After PlaceRoomsStep, before FinalizeStep + /// + /// // Enumerate in priority order + /// foreach (var step in steps.EnumerateInOrder()) + /// { + /// step.Apply(context); + /// } + /// + /// + /// + /// + /// [Serializable] public class PriorityList : IPriorityList, ICollection> { private readonly Dictionary> dict; + /// + /// Initializes a new instance of the class with no items. + /// public PriorityList() { this.dict = new Dictionary>(); } /// - /// Retrieves the total amount of priorities being occupied with items. + /// Gets the number of distinct priority levels that contain items. /// + /// + /// The count of unique values with at least one item. + /// public int PriorityCount => this.dict.Count; /// - /// Retrieves the total number of items in the PriorityList + /// Gets the total number of items across all priority levels. /// + /// + /// The sum of items at all priority levels. + /// public int Count { get @@ -42,13 +93,31 @@ public int Count } } + /// bool ICollection>.IsReadOnly => false; + /// + /// Adds an item with a simple integer priority. + /// + /// The integer priority value. Lower values execute earlier. + /// The item to add. + /// + /// This is a convenience overload that wraps the integer in a struct. + /// public void Add(int priority, T item) { this.Add(new Priority(priority), item); } + /// + /// Adds an item at the specified priority level. + /// + /// The priority at which to add the item. + /// The item to add. + /// + /// If other items already exist at this priority, the new item is added after them. + /// Items at the same priority are returned in insertion order during enumeration. + /// public void Add(Priority priority, T item) { if (!this.dict.ContainsKey(priority)) @@ -56,15 +125,33 @@ public void Add(Priority priority, T item) this.dict[priority].Add(item); } + /// void ICollection>.Add(KeyValuePair item) => this.Add(item.Key, item.Value); + /// void IPriorityList.Add(Priority priority, object item) => this.Add(priority, (T)item); + /// + /// Inserts an item at a specific index within an integer priority level. + /// + /// The integer priority value. + /// The zero-based index at which to insert the item within that priority. + /// The item to insert. public void Insert(int priority, int index, T item) { this.Insert(new Priority(priority), index, item); } + /// + /// Inserts an item at a specific index within a priority level. + /// + /// The priority at which to insert the item. + /// The zero-based index at which to insert the item within that priority. + /// The item to insert. + /// + /// Thrown when is not zero for a new priority level, + /// or when it exceeds the count of items at an existing priority. + /// public void Insert(Priority priority, int index, T item) { if (!this.dict.ContainsKey(priority)) @@ -77,8 +164,17 @@ public void Insert(Priority priority, int index, T item) this.dict[priority].Insert(index, item); } + /// void IPriorityList.Insert(Priority priority, int index, object item) => this.Insert(priority, index, (T)item); + /// + /// Removes the item at the specified index within a priority level. + /// + /// The priority level containing the item to remove. + /// The zero-based index of the item to remove within that priority. + /// + /// If removing the item leaves the priority level empty, the priority is automatically removed. + /// public void RemoveAt(Priority priority, int index) { this.dict[priority].RemoveAt(index); @@ -86,6 +182,7 @@ public void RemoveAt(Priority priority, int index) this.dict.Remove(priority); } + /// bool ICollection>.Remove(KeyValuePair item) { List val; @@ -94,11 +191,20 @@ bool ICollection>.Remove(KeyValuePair ite return false; } + /// + /// Gets the item at the specified index within a priority level. + /// + /// The priority level to access. + /// The zero-based index of the item within that priority. + /// The item at the specified position. + /// Thrown when the priority level does not exist. + /// Thrown when the index is out of range. public T Get(Priority priority, int index) { return this.dict[priority][index]; } + /// bool ICollection>.Contains(KeyValuePair item) { List val; @@ -107,24 +213,40 @@ bool ICollection>.Contains(KeyValuePair i return false; } + /// object IPriorityList.Get(Priority priority, int index) => this.Get(priority, index); + /// + /// Sets the item at the specified index within a priority level. + /// + /// The priority level to modify. + /// The zero-based index of the item to replace. + /// The new item value. + /// Thrown when the priority level does not exist. + /// Thrown when the index is out of range. public void Set(Priority priority, int index, T item) { this.dict[priority][index] = item; } + /// void IPriorityList.Set(Priority priority, int index, object item) => this.Set(priority, index, (T)item); + /// + /// Removes all items from the priority list. + /// public void Clear() { this.dict.Clear(); } /// - /// Enumerates all priorities. Returns in order. + /// Enumerates all priority levels in ascending order. /// - /// + /// An enumerable of all values that contain items, sorted in ascending order. + /// + /// Use this method with to iterate through items by priority level. + /// public IEnumerable GetPriorities() { List priorities = new List(); @@ -137,18 +259,30 @@ public IEnumerable GetPriorities() yield return key; } + /// + /// Gets all items at the specified priority level. + /// + /// The priority level to retrieve items from. + /// An enumerable of items at the specified priority, in insertion order. + /// Thrown when the priority level does not exist. public IEnumerable GetItems(Priority priority) { foreach (T item in this.dict[priority]) yield return item; } + /// IEnumerable IPriorityList.GetItems(Priority priority) => this.GetItems(priority); /// - /// Enumerates all items. Does so in priority order. + /// Enumerates all items in priority order. /// - /// + /// + /// An enumerable of all items, first sorted by priority (ascending), then by insertion order within each priority. + /// + /// + /// This is the primary method used by to iterate through generation steps. + /// public IEnumerable EnumerateInOrder() { foreach (Priority key in this.GetPriorities()) @@ -158,8 +292,14 @@ public IEnumerable EnumerateInOrder() } } + /// IEnumerator IEnumerable.GetEnumerator() => this.EnumerateKeyValuePairs(); + /// + /// Gets the number of items at a specific priority level. + /// + /// The priority level to count items at. + /// The number of items at the specified priority, or 0 if the priority does not exist. public int GetCountAtPriority(Priority priority) { if (this.dict.TryGetValue(priority, out List items)) @@ -167,6 +307,7 @@ public int GetCountAtPriority(Priority priority) return 0; } + /// void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { foreach (Priority key in this.GetPriorities()) @@ -179,13 +320,22 @@ void ICollection>.CopyTo(KeyValuePair[] a } } + /// IEnumerator> IEnumerable>.GetEnumerator() { return this.EnumerateKeyValuePairs(); } + /// int IPriorityList.GetCountAtPriority(Priority priority) => this.GetCountAtPriority(priority); + /// + /// Enumerates all items as key-value pairs of priority and item. + /// + /// + /// An enumerator of key-value pairs where the key is the + /// and the value is the item, ordered by priority. + /// private IEnumerator> EnumerateKeyValuePairs() { foreach (Priority key in this.GetPriorities()) diff --git a/RogueElements/Priority/README.md b/RogueElements/Priority/README.md new file mode 100644 index 00000000..2e68e36d --- /dev/null +++ b/RogueElements/Priority/README.md @@ -0,0 +1,227 @@ +# Priority + +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](../../../LICENSE) + +Priority queue system for ordering GenSteps in the map generation pipeline. + +## Overview + +The `Priority` folder provides the ordering mechanism that determines when each GenStep executes. This is critical for roguelike generation where steps must occur in a specific order (e.g., create rooms before placing stairs). + +## Core Concepts + +### Why Priority Ordering? + +Map generation requires careful sequencing: + +1. Initialize the grid structure +2. Create rooms and hallways +3. Draw rooms to tiles +4. Add terrain features (water, lava) +5. Place stairs +6. Spawn items and enemies + +Priority values control this order. Lower priorities execute first. + +### The Priority Struct + +A hierarchical ordering value supporting dot-notation for fine-grained control: + +```csharp +// Simple integer priorities +var init = new Priority(-4); // Early +var draw = new Priority(0); // Middle +var spawn = new Priority(6); // Late + +// Hierarchical priorities for sub-ordering +var water = new Priority(3); // 3 +var waterCleanup = new Priority(3, 1); // 3.1 (after water) +var waterErode = new Priority(3, 2); // 3.2 (after cleanup) + +// Extend existing priority +var subStep = new Priority(water, 1); // Creates 3.1 +``` + +**Comparison:** +``` +-4 < 0 < 3 < 3.1 < 3.2 < 6 +``` + +## Key Classes + +### Priority + +A comparable, multi-level priority value: + +```csharp +[Serializable] +public struct Priority : IComparable, IEquatable +{ + public static Priority Invalid; // No value + public static Priority Zero; // Priority(0) + + public Priority(params int[] vals); + public Priority(Priority other, params int[] vals); // Extend existing + + public int Length { get; } // Number of levels + public int this[int ii] { get; } // Access level value + + // Full comparison operators: ==, !=, <, >, <=, >= +} +``` + +### PriorityList + +A dictionary-backed collection that maintains items by priority: + +```csharp +[Serializable] +public class PriorityList : IPriorityList +{ + public int PriorityCount { get; } // Number of distinct priorities + public int Count { get; } // Total items + + public void Add(Priority priority, T item); + public void Add(int priority, T item); // Convenience overload + + public void Insert(Priority priority, int index, T item); + public void RemoveAt(Priority priority, int index); + + public T Get(Priority priority, int index); + public void Set(Priority priority, int index, T item); + + public IEnumerable GetPriorities(); // Sorted + public IEnumerable GetItems(Priority priority); + public IEnumerable EnumerateInOrder(); // All items, sorted by priority +} +``` + +## Usage in MapGen + +`MapGen` uses `PriorityList>` to hold generation steps: + +```csharp +public class MapGen where T : class, IGenContext +{ + public PriorityList> GenSteps { get; } + + public T GenMap(ulong seed) + { + // Steps are executed in priority order + foreach (Priority priority in GenSteps.GetPriorities()) + { + foreach (IGenStep step in GenSteps.GetItems(priority)) + { + step.Apply(map); + } + } + } +} +``` + +## Example: Building a Generation Pipeline + +```csharp +var layout = new MapGen(); + +// Priority -4: Initialize grid structure +layout.GenSteps.Add(-4, new InitGridPlanStep(1) +{ + CellX = 6, CellY = 4, + CellWidth = 9, CellHeight = 9 +}); + +// Priority -4: Create room paths (same priority, runs after init) +layout.GenSteps.Add(-4, new GridPathBranch +{ + RoomRatio = new RandRange(70), + BranchRatio = new RandRange(0, 50), + GenericRooms = roomSpawnList, + GenericHalls = hallSpawnList +}); + +// Priority -2: Convert grid to floor plan +layout.GenSteps.Add(-2, new DrawGridToFloorStep()); + +// Priority 0: Draw floor plan to tiles +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + +// Priority 2: Place stairs +layout.GenSteps.Add(2, new FloorStairsStep( + 0, new StairsUp(), new StairsDown() +)); + +// Priority 3: Generate water terrain +layout.GenSteps.Add(3, new PerlinWaterStep( + new RandRange(35), 3, new Tile(2), stencil, 1 +)); + +// Priority 4: Clean up water artifacts +layout.GenSteps.Add(4, new DropDiagonalBlockStep(new Tile(2))); +layout.GenSteps.Add(4, new EraseIsolatedStep(new Tile(2))); + +// Priority 6: Spawn items and mobs +layout.GenSteps.Add(6, itemPlacementStep); +layout.GenSteps.Add(6, mobPlacementStep); +``` + +### Execution Order + +``` +Priority -4: InitGridPlanStep +Priority -4: GridPathBranch +Priority -2: DrawGridToFloorStep +Priority 0: DrawFloorToTileStep +Priority 2: FloorStairsStep +Priority 3: PerlinWaterStep +Priority 4: DropDiagonalBlockStep +Priority 4: EraseIsolatedStep +Priority 6: RandomSpawnStep (items) +Priority 6: RandomSpawnStep (mobs) +``` + +## Advanced: Hierarchical Priorities + +For complex pipelines, use dot-notation to insert steps between existing ones: + +```csharp +// Original water step at priority 3 +layout.GenSteps.Add(new Priority(3), waterStep); + +// Later, need to add a step between water (3) and cleanup (4) +// Use 3.1 instead of renumbering everything +layout.GenSteps.Add(new Priority(3, 1), waterEdgeStep); +``` + +Execution order: `3 -> 3.1 -> 4` + +## Multiple Steps at Same Priority + +Items at the same priority execute in insertion order: + +```csharp +// Both at priority 6, items runs first (added first) +layout.GenSteps.Add(6, itemPlacement); +layout.GenSteps.Add(6, mobPlacement); +``` + +## Common Priority Conventions + +| Priority | Purpose | +|----------|---------| +| -4 | Grid/structure initialization | +| -2 | Floor plan generation | +| 0 | Tile drawing | +| 2 | Stairs placement | +| 3-4 | Terrain features (water, lava) | +| 6 | Entity spawning (items, mobs) | + +## See Also + +- [MapGen/MapGen.cs](../MapGen/MapGen.cs) - How priorities drive generation +- [Rand/](../Rand/) - Random utilities used alongside priority ordering +- [Examples/](../../RogueElements.Examples/) - Full pipeline examples + +--- + +![Repobeats analytics](https://repobeats.axiom.co/api/embed/3c5a3b7f5e0c1d8a9b7c5e3a1f9d8b7c6e5a4d3c2b1a0.svg "Repobeats analytics image") diff --git a/RogueElements/README.md b/RogueElements/README.md new file mode 100644 index 00000000..6a32f5aa --- /dev/null +++ b/RogueElements/README.md @@ -0,0 +1,270 @@ +# RogueElements + +[![NuGet](https://img.shields.io/nuget/v/RogueElements.svg)](https://www.nuget.org/packages/RogueElements/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-blue.svg)](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) + +The core library for procedural roguelike map generation using a pipeline architecture. This library is game-agnostic and designed to be integrated into any roguelike or procedural generation project. + +## Overview + +RogueElements provides a flexible, composable system for generating dungeon-like maps. The architecture follows a pipeline pattern where a `MapGen` orchestrator executes a prioritized sequence of `GenStep` operations that progressively build up map state in an `IGenContext`. + +## Architecture + +```mermaid +flowchart TB + subgraph Orchestrator + MapGen["MapGen<T>
Holds PriorityList of GenSteps"] + end + + subgraph Pipeline["Generation Pipeline"] + direction TB + GenMap["GenMap(seed)"] --> Init["InitSeed(seed)"] + Init --> Step1["GenStep.Apply(context)"] + Step1 --> Step2["GenStep.Apply(context)"] + Step2 --> StepN["... more steps ..."] + StepN --> Finish["FinishGen()"] + end + + subgraph Context["Map Context (IGenContext)"] + direction LR + Rand["IRandom Rand"] + State["Map State"] + end + + subgraph Steps["GenStep Types"] + direction TB + TileSteps["Tile Steps
InitTilesStep, SpecificTilesStep"] + FloorSteps["FloorPlan Steps
InitFloorPlanStep, FloorPathBranch"] + GridSteps["Grid Steps
InitGridPlanStep, GridPathBranch"] + SpawnSteps["Spawning Steps
FloorStairsStep, RandomSpawnStep"] + TerrainSteps["Terrain Steps
PerlinWaterStep, DropDiagonalBlockStep"] + end + + MapGen --> Pipeline + Pipeline --> Context + Steps -.-> Pipeline +``` + +### Core Abstractions + +| Class/Interface | Purpose | +|-----------------|---------| +| `MapGen` | Orchestrator - holds priority-ordered GenSteps, calls `GenMap(seed)` | +| `GenStep` | Base class for generation passes - implement `Apply(T map)` | +| `IGenContext` | Base interface for map state - provides `Rand`, `InitSeed()`, `FinishGen()` | +| `Priority` | Ordering mechanism for GenSteps (lower values = earlier execution) | +| `PriorityList` | Container holding GenSteps organized by Priority | + +### Context Interfaces + +The library uses interface composition to enable specific capabilities. Implement the interfaces your GenSteps require: + +| Interface | Enables | Key Members | +|-----------|---------|-------------| +| `ITiledGenContext` | Tile-based operations | `GetTile()`, `SetTile()`, `TileBlocked()` | +| `IFloorPlanGenContext` | Freeform room placement | `FloorPlan` property | +| `IRoomGridGenContext` | Grid-based room layouts | `GridPlan` property | +| `IPlaceableGenContext` | Spawning entities | `PlaceItem()`, spawn location queries | + +## Directory Structure + +``` +RogueElements/ +├── MapGen/ # Generation pipeline +│ ├── MapGen.cs # Main orchestrator +│ ├── GenStep.cs # Base step class +│ ├── IGenContext.cs # Core context interface +│ ├── FloorPlan/ # Freeform room-based generation +│ │ ├── FloorPlan.cs # Room/hall container +│ │ ├── FloorPathBranch.cs # Branching path algorithm +│ │ └── Paths/ # Path generation strategies +│ ├── Grid/ # Grid-based room layouts +│ │ ├── GridPlan.cs # Grid cell container +│ │ ├── GridPathBranch.cs # Grid branching algorithm +│ │ └── Paths/ # Grid path strategies +│ ├── Rooms/ # Room shape generators +│ │ ├── RoomGenSquare.cs # Rectangular rooms +│ │ ├── RoomGenRound.cs # Circular/elliptical rooms +│ │ ├── RoomGenCave.cs # Organic cave shapes +│ │ └── Halls/ # Hall connectors +│ ├── Spawning/ # Entity placement +│ │ ├── RandomSpawnStep.cs # Random item/mob placement +│ │ ├── FloorStairsStep.cs # Stair placement +│ │ └── RoomSpawnStep.cs # Room-based spawning +│ └── Tiles/ # Tile manipulation +│ ├── InitTilesStep.cs # Initialize tile grid +│ ├── Water/ # Water/terrain generation +│ └── PerlinWaterStep.cs # Noise-based terrain +├── Rand/ # RNG utilities +│ ├── RandRange.cs # Random integer ranges +│ ├── SpawnList.cs # Weighted random selection +│ ├── RNG/ # Random number generators +│ └── Noise/ # Perlin noise utilities +├── Priority/ # Priority queue system +│ ├── Priority.cs # Priority value type +│ └── PriorityList.cs # Ordered collection +├── Loc.cs # 2D coordinate struct +├── Rect.cs # Rectangle operations +├── Grid.cs # Grid utilities (pathfinding, flood fill) +└── Detection.cs # Shape detection algorithms +``` + +## Quick Start + +### 1. Create a Map Context + +Your map context implements the interfaces your GenSteps need: + +```csharp +public class MyMapContext : IGenContext, ITiledGenContext +{ + private Tile[,] tiles; + public IRandom Rand { get; private set; } + + public void InitSeed(ulong seed) => Rand = new ReRandom(seed); + public void FinishGen() { } + + // ITiledGenContext implementation + public ITile RoomTerrain => new Tile(1); + public ITile WallTerrain => new Tile(0); + public int Width => tiles.GetLength(0); + public int Height => tiles.GetLength(1); + public bool Wrap => false; + + public void CreateNew(int width, int height, bool wrap = false) + { + tiles = new Tile[width, height]; + } + + public ITile GetTile(Loc loc) => tiles[loc.X, loc.Y]; + public bool TrySetTile(Loc loc, ITile tile) + { + tiles[loc.X, loc.Y] = (Tile)tile; + return true; + } + // ... other members +} +``` + +### 2. Build a Generation Pipeline + +```csharp +var layout = new MapGen(); + +// Priority determines execution order (lower = earlier) +// Step 1: Initialize a 50x50 tile grid +layout.GenSteps.Add(-4, new InitTilesStep(50, 50)); + +// Step 2: Set up grid-based room layout +var gridInit = new InitGridPlanStep(1) +{ + CellX = 5, + CellY = 4, + CellWidth = 9, + CellHeight = 9 +}; +layout.GenSteps.Add(-3, gridInit); + +// Step 3: Generate branching room path +var path = new GridPathBranch +{ + RoomRatio = new RandRange(70), + BranchRatio = new RandRange(0, 50), + GenericRooms = new SpawnList> + { + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 } + } +}; +layout.GenSteps.Add(-2, path); + +// Step 4: Convert grid to floor plan +layout.GenSteps.Add(-1, new DrawGridToFloorStep()); + +// Step 5: Draw to tiles +layout.GenSteps.Add(0, new DrawFloorToTileStep(1)); + +// Generate! +MyMapContext map = layout.GenMap(seed); +``` + +### 3. Create Custom GenSteps + +```csharp +[Serializable] +public class MyCustomStep : GenStep +{ + public int Parameter { get; set; } + + public override void Apply(ITiledGenContext map) + { + // Use map.Rand for randomness + int x = map.Rand.Next(map.Width); + int y = map.Rand.Next(map.Height); + + // Modify map state + map.SetTile(new Loc(x, y), map.RoomTerrain); + } +} + +// Add to pipeline +layout.GenSteps.Add(5, new MyCustomStep { Parameter = 42 }); +``` + +## Key Patterns + +### Priority-Based Ordering + +GenSteps execute in priority order. Use negative priorities for setup, zero for main generation, positive for post-processing: + +``` +-4: Grid/FloorPlan initialization +-2: Room path generation + 0: Tile drawing + 2: Stair placement + 3: Terrain (water, lava) + 6: Item/mob spawning +``` + +### Generic Constraints + +GenSteps use generic constraints to declare their context requirements: + +```csharp +// Requires only basic context +public class BasicStep : GenStep { } + +// Requires tile support +public class TileStep : GenStep { } + +// Requires multiple capabilities +public class SpawnStep : GenStep + where T : IFloorPlanGenContext, IPlaceableGenContext { } +``` + +### Serialization + +All GenSteps are marked `[Serializable]` for save/load support. Your context should also be serializable if you need to save generation state. + +## Debug Support + +Hook into the generation process for debugging: + +```csharp +GenContextDebug.OnInit += (context) => Console.WriteLine("Map initialized"); +GenContextDebug.OnStepIn += (stepName) => Console.WriteLine($"Starting: {stepName}"); +GenContextDebug.OnStepOut += () => Console.WriteLine("Step complete"); +GenContextDebug.OnStep += (context) => RenderMap(context); +``` + +## See Also + +- **[RogueElements.Examples](../RogueElements.Examples/)** - 8 progressive examples demonstrating library usage +- **[RogueElements.Tests](../RogueElements.Tests/)** - Unit tests showing expected behavior +- **[CLAUDE.md](../CLAUDE.md)** - Full architecture documentation + +--- + +![Repobeats analytics](https://repobeats.axiom.co/api/embed/placeholder.svg "Repobeats analytics image") diff --git a/RogueElements/Rand/ILoopedRand.cs b/RogueElements/Rand/ILoopedRand.cs index 9cec6cd5..0e6e1feb 100644 --- a/RogueElements/Rand/ILoopedRand.cs +++ b/RogueElements/Rand/ILoopedRand.cs @@ -8,44 +8,70 @@ namespace RogueElements { + /// + /// Non-generic interface for looped random generators. + /// public interface ILoopedRand { + /// + /// Gets or sets the picker that determines how many items to generate. + /// IRandPicker AmountSpawner { get; set; } } /// /// Generates a list of items by repeatedly calling an IRandPicker /// - /// + /// The type of items to generate. [Serializable] public class LoopedRand : IMultiRandPicker, IRandPicker, ILoopedRand { + /// + /// Initializes a new instance of the class. + /// public LoopedRand() { } + /// + /// Initializes a new instance of the class with a spawner and amount picker. + /// + /// The picker to use for generating each item. + /// The picker that determines how many items to generate. public LoopedRand(IRandPicker spawner, IRandPicker amountSpawner) { this.Spawner = spawner; this.AmountSpawner = amountSpawner; } + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The instance to copy. protected LoopedRand(LoopedRand other) { this.Spawner = other.Spawner.CopyState(); this.AmountSpawner = other.AmountSpawner.CopyState(); } + /// public bool ChangesState => this.Spawner.ChangesState || this.AmountSpawner.ChangesState; + /// + /// Gets or sets the picker used to generate each item. + /// public IRandPicker Spawner { get; set; } + /// public IRandPicker AmountSpawner { get; set; } + /// public bool CanPick => this.AmountSpawner.CanPick; + /// public IMultiRandPicker CopyState() => new LoopedRand(this); + /// public List Roll(IRandom rand) { List result = new List(); @@ -60,6 +86,7 @@ public List Roll(IRandom rand) return result; } + /// public override string ToString() { if (this.AmountSpawner == null) diff --git a/RogueElements/Rand/IMultiRandPicker.cs b/RogueElements/Rand/IMultiRandPicker.cs index 67a42922..92fbd737 100644 --- a/RogueElements/Rand/IMultiRandPicker.cs +++ b/RogueElements/Rand/IMultiRandPicker.cs @@ -11,24 +11,27 @@ namespace RogueElements /// /// A random generator of a list of items. /// - /// + /// The type of items to generate. public interface IMultiRandPicker : IMultiRandPicker { /// /// Randomly generates a list of items of type T. /// - /// - /// + /// The random number generator to use. + /// A list of randomly generated items. List Roll(IRandom rand); /// /// Returns a IMultiRandPicker of the same state as this instance. /// If this instance holds a collection of items, the items themselves are not duplicated. /// - /// + /// A copy of this picker with the same state. IMultiRandPicker CopyState(); } + /// + /// Non-generic base interface for random list pickers. + /// public interface IMultiRandPicker { /// diff --git a/RogueElements/Rand/IPresetMultiRand.cs b/RogueElements/Rand/IPresetMultiRand.cs index ddafb2f6..1d48023b 100644 --- a/RogueElements/Rand/IPresetMultiRand.cs +++ b/RogueElements/Rand/IPresetMultiRand.cs @@ -9,32 +9,55 @@ namespace RogueElements { + /// + /// Non-generic interface for preset multi-item random generators. + /// public interface IPresetMultiRand { + /// + /// Gets the list of items to spawn. + /// IList ToSpawn { get; } + /// + /// Gets a value indicating whether items can be picked. + /// bool CanPick { get; } + /// + /// Gets the number of items in the spawn list. + /// int Count { get; } } /// /// Generates a list of items predefined by the user. /// - /// + /// The type of items to generate. [Serializable] public class PresetMultiRand : IMultiRandPicker, IPresetMultiRand { + /// + /// Initializes a new instance of the class. + /// public PresetMultiRand() { this.ToSpawn = new List>(); } + /// + /// Initializes a new instance of the class with the specified pickers. + /// + /// The pickers to include in the list. public PresetMultiRand(params IRandPicker[] toSpawn) { this.ToSpawn = new List>(toSpawn); } + /// + /// Initializes a new instance of the class with preset items. + /// + /// The items to include in the list. public PresetMultiRand(params T[] toSpawn) { this.ToSpawn = new List>(); @@ -42,11 +65,19 @@ public PresetMultiRand(params T[] toSpawn) this.ToSpawn.Add(new PresetPicker(item)); } + /// + /// Initializes a new instance of the class with a list of pickers. + /// + /// The list of pickers. public PresetMultiRand(List> toSpawn) { this.ToSpawn = toSpawn; } + /// + /// Initializes a new instance of the class with a list of items. + /// + /// The list of items. public PresetMultiRand(List toSpawn) { this.ToSpawn = new List>(); @@ -54,23 +85,36 @@ public PresetMultiRand(List toSpawn) this.ToSpawn.Add(new PresetPicker(item)); } + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The instance to copy. protected PresetMultiRand(PresetMultiRand other) { this.ToSpawn = new List>(other.ToSpawn); } + /// + /// Gets the list of pickers to spawn from. + /// public List> ToSpawn { get; } + /// public bool ChangesState => false; + /// public bool CanPick => this.ToSpawn != null; + /// public int Count => this.ToSpawn != null ? this.ToSpawn.Count : 0; + /// IList IPresetMultiRand.ToSpawn => this.ToSpawn; + /// public IMultiRandPicker CopyState() => new PresetMultiRand(this); + /// public List Roll(IRandom rand) { List result = new List(); @@ -83,6 +127,7 @@ public List Roll(IRandom rand) return result; } + /// public override string ToString() { if (this.Count == 1) diff --git a/RogueElements/Rand/IRandPicker.cs b/RogueElements/Rand/IRandPicker.cs index 5deb1214..e78b975a 100644 --- a/RogueElements/Rand/IRandPicker.cs +++ b/RogueElements/Rand/IRandPicker.cs @@ -12,26 +12,33 @@ namespace RogueElements /// /// A random generator of a single item. /// - /// + /// The type of item to generate. public interface IRandPicker : IRandPicker { /// /// Randomly generates an item of type T. /// - /// - /// + /// The random number generator to use. + /// A randomly generated item. T Pick(IRandom rand); /// /// Returns a IRandPicker of the same state as this instance. /// If this instance holds a collection of items, the items themselves are not duplicated. /// - /// + /// A copy of this picker with the same state. IRandPicker CopyState(); + /// + /// Enumerates all possible outcomes this picker can produce. + /// + /// An enumerable of all possible items. IEnumerable EnumerateOutcomes(); } + /// + /// Non-generic base interface for random item pickers. + /// public interface IRandPicker { /// diff --git a/RogueElements/Rand/ISpawnList.cs b/RogueElements/Rand/ISpawnList.cs index aae4a123..ac058384 100644 --- a/RogueElements/Rand/ISpawnList.cs +++ b/RogueElements/Rand/ISpawnList.cs @@ -9,49 +9,144 @@ namespace RogueElements { + /// + /// Represents a weighted list of spawnable items. + /// + /// The type of items in the list. public interface ISpawnList : IEnumerable { + /// + /// Gets the number of items in the list. + /// int Count { get; } + /// + /// Gets the sum of all spawn rates in the list. + /// int SpawnTotal { get; } + /// + /// Inserts an item at the specified index with a spawn rate. + /// + /// The index at which to insert. + /// The item to insert. + /// The spawn rate weight. void Insert(int index, T spawn, int rate); + /// + /// Adds an item to the list with a spawn rate. + /// + /// The item to add. + /// The spawn rate weight. void Add(T spawn, int rate); + /// + /// Removes all items from the list. + /// void Clear(); + /// + /// Gets the item at the specified index. + /// + /// The index of the item. + /// The item at the index. T GetSpawn(int index); + /// + /// Gets the spawn rate of the item at the specified index. + /// + /// The index of the item. + /// The spawn rate weight. int GetSpawnRate(int index); + /// + /// Sets the item at the specified index. + /// + /// The index of the item. + /// The new item value. void SetSpawn(int index, T spawn); + /// + /// Sets the spawn rate of the item at the specified index. + /// + /// The index of the item. + /// The new spawn rate weight. void SetSpawnRate(int index, int rate); + /// + /// Removes the item at the specified index. + /// + /// The index of the item to remove. void RemoveAt(int index); } + /// + /// Non-generic interface for a weighted list of spawnable items. + /// public interface ISpawnList : IEnumerable { + /// + /// Gets the number of items in the list. + /// int Count { get; } + /// + /// Gets the sum of all spawn rates in the list. + /// int SpawnTotal { get; } + /// + /// Inserts an item at the specified index with a spawn rate. + /// + /// The index at which to insert. + /// The item to insert. + /// The spawn rate weight. void Insert(int index, object spawn, int rate); + /// + /// Adds an item to the list with a spawn rate. + /// + /// The item to add. + /// The spawn rate weight. void Add(object spawn, int rate); + /// + /// Removes all items from the list. + /// void Clear(); + /// + /// Gets the item at the specified index. + /// + /// The index of the item. + /// The item at the index. object GetSpawn(int index); + /// + /// Gets the spawn rate of the item at the specified index. + /// + /// The index of the item. + /// The spawn rate weight. int GetSpawnRate(int index); + /// + /// Sets the item at the specified index. + /// + /// The index of the item. + /// The new item value. void SetSpawn(int index, object spawn); + /// + /// Sets the spawn rate of the item at the specified index. + /// + /// The index of the item. + /// The new spawn rate weight. void SetSpawnRate(int index, int rate); + /// + /// Removes the item at the specified index. + /// + /// The index of the item to remove. void RemoveAt(int index); } } diff --git a/RogueElements/Rand/ISpawnRangeList.cs b/RogueElements/Rand/ISpawnRangeList.cs index 1d0bce20..045b91bd 100644 --- a/RogueElements/Rand/ISpawnRangeList.cs +++ b/RogueElements/Rand/ISpawnRangeList.cs @@ -9,53 +9,166 @@ namespace RogueElements { + /// + /// Represents a weighted list of spawnable items with level range constraints. + /// + /// The type of items in the list. public interface ISpawnRangeList { + /// + /// Gets the number of items in the list. + /// int Count { get; } + /// + /// Inserts an item at the specified index with a level range and spawn rate. + /// + /// The index at which to insert. + /// The item to insert. + /// The level range where the item can spawn. + /// The spawn rate weight. void Insert(int index, T spawn, IntRange range, int rate); + /// + /// Adds an item to the list with a level range and spawn rate. + /// + /// The item to add. + /// The level range where the item can spawn. + /// The spawn rate weight. void Add(T spawn, IntRange range, int rate); + /// + /// Removes all items from the list. + /// void Clear(); + /// + /// Gets the item at the specified index. + /// + /// The index of the item. + /// The item at the index. T GetSpawn(int index); + /// + /// Gets the level range of the item at the specified index. + /// + /// The index of the item. + /// The level range constraint. IntRange GetSpawnRange(int index); + /// + /// Gets the spawn rate of the item at the specified index. + /// + /// The index of the item. + /// The spawn rate weight. int GetSpawnRate(int index); + /// + /// Sets the item at the specified index. + /// + /// The index of the item. + /// The new item value. void SetSpawn(int index, T spawn); + /// + /// Sets the level range of the item at the specified index. + /// + /// The index of the item. + /// The new level range constraint. void SetSpawnRange(int index, IntRange range); + /// + /// Sets the spawn rate of the item at the specified index. + /// + /// The index of the item. + /// The new spawn rate weight. void SetSpawnRate(int index, int rate); + /// + /// Removes the item at the specified index. + /// + /// The index of the item to remove. void RemoveAt(int index); } + /// + /// Non-generic interface for a weighted list of spawnable items with level range constraints. + /// public interface ISpawnRangeList { + /// + /// Gets the number of items in the list. + /// int Count { get; } + /// + /// Inserts an item at the specified index with a level range and spawn rate. + /// + /// The index at which to insert. + /// The item to insert. + /// The level range where the item can spawn. + /// The spawn rate weight. void Insert(int index, object spawn, IntRange range, int rate); + /// + /// Adds an item to the list with a level range and spawn rate. + /// + /// The item to add. + /// The level range where the item can spawn. + /// The spawn rate weight. void Add(object spawn, IntRange range, int rate); + /// + /// Removes all items from the list. + /// void Clear(); + /// + /// Gets the item at the specified index. + /// + /// The index of the item. + /// The item at the index. object GetSpawn(int index); + /// + /// Gets the level range of the item at the specified index. + /// + /// The index of the item. + /// The level range constraint. IntRange GetSpawnRange(int index); + /// + /// Gets the spawn rate of the item at the specified index. + /// + /// The index of the item. + /// The spawn rate weight. int GetSpawnRate(int index); + /// + /// Sets the item at the specified index. + /// + /// The index of the item. + /// The new item value. void SetSpawn(int index, object spawn); + /// + /// Sets the level range of the item at the specified index. + /// + /// The index of the item. + /// The new level range constraint. void SetSpawnRange(int index, IntRange range); + /// + /// Sets the spawn rate of the item at the specified index. + /// + /// The index of the item. + /// The new spawn rate weight. void SetSpawnRate(int index, int rate); + /// + /// Removes the item at the specified index. + /// + /// The index of the item to remove. void RemoveAt(int index); } } diff --git a/RogueElements/Rand/Noise/INoise.cs b/RogueElements/Rand/Noise/INoise.cs index d740a993..376126d9 100644 --- a/RogueElements/Rand/Noise/INoise.cs +++ b/RogueElements/Rand/Noise/INoise.cs @@ -11,20 +11,60 @@ namespace RogueElements { + /// + /// Defines a noise function that generates deterministic random values based on position. + /// public interface INoise { + /// + /// Gets the seed value that the noise function was initialized with. + /// ulong FirstSeed { get; } + /// + /// Gets a non-negative random integer at the specified position. + /// + /// The position in the noise function. + /// A non-negative random integer. int GetInt(ulong position); + /// + /// Gets a non-negative random integer less than the specified maximum at the given position. + /// + /// The position in the noise function. + /// The exclusive upper bound. + /// A random integer from 0 to maxValue - 1. int GetInt(ulong position, int maxValue); + /// + /// Gets a random integer within the specified range at the given position. + /// + /// The position in the noise function. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// A random integer from minValue to maxValue - 1. int GetInt(ulong position, int minValue, int maxValue); + /// + /// Gets a random 64-bit unsigned integer at the specified position. + /// + /// The position in the noise function. + /// A random 64-bit unsigned integer. ulong GetUInt64(ulong position); + /// + /// Gets a random 64-bit unsigned integer at the specified 2D coordinates. + /// + /// The X coordinate. + /// The Y coordinate. + /// A random 64-bit unsigned integer. ulong Get2DUInt64(ulong x, ulong y); + /// + /// Gets a random double between 0.0 and 1.0 at the specified position. + /// + /// The position in the noise function. + /// A random double from 0.0 to 1.0. double GetDouble(ulong position); } } diff --git a/RogueElements/Rand/Noise/README.md b/RogueElements/Rand/Noise/README.md new file mode 100644 index 00000000..ed746c13 --- /dev/null +++ b/RogueElements/Rand/Noise/README.md @@ -0,0 +1,151 @@ +# Noise + +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](../../../../LICENSE) + +Noise generation utilities for natural-looking procedural terrain. + +## Overview + +The `Noise` folder provides deterministic noise functions used primarily for terrain generation. Unlike sequential RNG, noise functions allow querying random values at any position without affecting other positions - essential for coherent terrain patterns. + +## Key Difference: RNG vs Noise + +| Aspect | RNG (ReRandom) | Noise (ReNoise) | +|--------|----------------|-----------------| +| **Access** | Sequential | Position-based | +| **Use case** | Picking items, shuffling | Terrain heightmaps | +| **State** | Changes each call | Stateless | +| **Example** | "Give me the next random number" | "What's the value at (x, y)?" | + +## Core Classes + +### INoise Interface + +```csharp +public interface INoise +{ + ulong FirstSeed { get; } + + int GetInt(ulong position); + int GetInt(ulong position, int maxValue); + int GetInt(ulong position, int minValue, int maxValue); + + ulong GetUInt64(ulong position); + ulong Get2DUInt64(ulong x, ulong y); // 2D noise + + double GetDouble(ulong position); +} +``` + +### ReNoise + +The default noise implementation based on murmur3 hash. Key properties: + +- **Deterministic** - Same seed + position always yields same result +- **Uniform distribution** - Values evenly spread across range +- **Fast** - Single hash computation per query +- **2D support** - `Get2DUInt64(x, y)` for spatial coherence + +```csharp +// Create noise generator with seed +var noise = new ReNoise(12345); + +// Query values at specific positions +int a = noise.GetInt(0); // Always same for position 0 +int b = noise.GetInt(1000); // Independent of position 0 +int c = noise.GetInt(0); // Same as 'a' - deterministic + +// 2D queries for terrain +ulong height = noise.Get2DUInt64(x: 10, y: 20); +``` + +## Usage in Terrain Generation + +The noise utilities power the Perlin noise-based water generation: + +```csharp +// From PerlinWaterStep.Apply() +int[][] noise = NoiseGen.PerlinNoise( + map.Rand, + map.Width, + map.Height, + orderComplexity, // Number of octaves + orderSoftness // Minimum feature size +); + +// Convert to water based on threshold +for (int xx = 0; xx < map.Width; xx++) +{ + for (int yy = 0; yy < map.Height; yy++) + { + if (noise[xx][yy] < waterMark) + map.SetTile(new Loc(xx, yy), waterTile); + } +} +``` + +### Perlin Noise Parameters + +| Parameter | Effect | Typical Value | +|-----------|--------|---------------| +| `OrderComplexity` | Number of noise octaves - higher = more detail | 2-4 | +| `OrderSoftness` | Minimum blob size (2^n tiles) | 0-2 | +| `WaterPercent` | Target coverage percentage | 20-50 | + +## Example: Ex5_Terrain + +Water generation in action: + +```csharp +// Generate water covering ~35% of walkable tiles +const int terrain = 2; // Water terrain ID +var waterStep = new PerlinWaterStep( + new RandRange(35), // 35% water coverage + 3, // Complexity: 3 octaves + new Tile(terrain), + new MapTerrainStencil(false, true, false, false), + 1 // Softness: 2x2 minimum blobs +); +layout.GenSteps.Add(3, waterStep); +``` + +Result: +``` +#################### +#........~~........# +#...~~~~~~~~~~~~~~~# +#...~~~~~~~~~~~~~~~# +#....~~~......~~~..# +#......~~~....~~~..# +#..........~~~~....# +#################### +``` + +## Why Noise Matters for Roguelikes + +1. **Natural Terrain** - Perlin noise creates organic-looking lakes, forests, elevation +2. **Reproducibility** - Same seed regenerates identical terrain patterns +3. **Position Independence** - Query any tile without computing all previous tiles +4. **Scalability** - Works efficiently at any map size + +## Global Access + +Noise is accessible via `MathUtils.Noise`: + +```csharp +// Global noise instance (seeded automatically) +double value = MathUtils.Noise.GetDouble(position); + +// Reseed both RNG and Noise together +MathUtils.ReSeedRand(seed); +``` + +## See Also + +- [RNG/](../RNG/) - Sequential random number generation +- [MapGen/Tiles/Water/](../../MapGen/Tiles/Water/) - Water generation steps using noise +- [Examples/Ex5_Terrain](../../../RogueElements.Examples/Ex5_Terrain/) - Complete terrain example + +--- + +![Repobeats analytics](https://repobeats.axiom.co/api/embed/3c5a3b7f5e0c1d8a9b7c5e3a1f9d8b7c6e5a4d3c2b1a0.svg "Repobeats analytics image") diff --git a/RogueElements/Rand/Noise/ReNoise.cs b/RogueElements/Rand/Noise/ReNoise.cs index 4a797235..9e46b783 100644 --- a/RogueElements/Rand/Noise/ReNoise.cs +++ b/RogueElements/Rand/Noise/ReNoise.cs @@ -23,11 +23,18 @@ public class ReNoise : INoise private const ulong C1 = 0x87c37b91114253d5; private const ulong C2 = 0x4cf5ad432745937f; + /// + /// Initializes a new instance of the class with the current time as seed. + /// public ReNoise() : this(unchecked((ulong)System.DateTime.Now.Ticks)) { } + /// + /// Initializes a new instance of the class with a specified seed. + /// + /// The seed value for the noise function. public ReNoise(ulong seed) { this.FirstSeed = seed; @@ -36,8 +43,10 @@ public ReNoise(ulong seed) /// /// The seed value that the class was initialized with. /// + /// public ulong FirstSeed { get; private set; } + /// public ulong GetUInt64(ulong position) { byte[] data = BitConverter.GetBytes(position); @@ -46,6 +55,11 @@ public ulong GetUInt64(ulong position) return this.Hash(data)[0]; } + /// + /// Gets two random 64-bit unsigned integers at the specified position. + /// + /// The position in the noise function. + /// An array of two random 64-bit unsigned integers. public ulong[] GetTwoUInt64(ulong position) { byte[] data = BitConverter.GetBytes(position); @@ -54,6 +68,7 @@ public ulong[] GetTwoUInt64(ulong position) return this.Hash(data); } + /// public ulong Get2DUInt64(ulong x, ulong y) { byte[] data1 = BitConverter.GetBytes(x); @@ -70,11 +85,13 @@ public ulong Get2DUInt64(ulong x, ulong y) return this.Hash(data)[0]; } + /// public int GetInt(ulong position) { return (int)(this.GetUInt64(position) % int.MaxValue); } + /// public int GetInt(ulong position, int maxValue) { if (maxValue < 0) @@ -86,6 +103,7 @@ public int GetInt(ulong position, int maxValue) return (int)(this.GetUInt64(position) % (ulong)maxValue); } + /// public int GetInt(ulong position, int minValue, int maxValue) { if (minValue > maxValue) @@ -100,6 +118,7 @@ public int GetInt(ulong position, int minValue, int maxValue) return (int)((long)(this.GetUInt64(position) % (ulong)range) + minValue); } + /// public double GetDouble(ulong position) { return (double)this.GetUInt64(position) / ((double)ulong.MaxValue + 1); diff --git a/RogueElements/Rand/PresetPicker.cs b/RogueElements/Rand/PresetPicker.cs index a427d32b..c64be9e2 100644 --- a/RogueElements/Rand/PresetPicker.cs +++ b/RogueElements/Rand/PresetPicker.cs @@ -12,39 +12,59 @@ namespace RogueElements /// /// Generates an item that is predefined by the user. /// - /// + /// The type of item to generate. [Serializable] public class PresetPicker : IRandPicker { + /// + /// Initializes a new instance of the class. + /// public PresetPicker() { } + /// + /// Initializes a new instance of the class with a preset item. + /// + /// The item to return when picked. public PresetPicker(T toSpawn) { this.ToSpawn = toSpawn; } + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The instance to copy. protected PresetPicker(PresetPicker other) { this.ToSpawn = other.ToSpawn; } + /// + /// Gets or sets the item to return when picked. + /// public T ToSpawn { get; set; } + /// public bool ChangesState => false; + /// public bool CanPick => true; + /// public IRandPicker CopyState() => new PresetPicker(this); + /// public IEnumerable EnumerateOutcomes() { yield return this.ToSpawn; } + /// public T Pick(IRandom rand) => this.ToSpawn; + /// public override string ToString() { return string.Format("{{{0}}}", this.ToSpawn != null ? this.ToSpawn.ToString() : "NULL"); diff --git a/RogueElements/Rand/README.md b/RogueElements/Rand/README.md new file mode 100644 index 00000000..74789abe --- /dev/null +++ b/RogueElements/Rand/README.md @@ -0,0 +1,188 @@ +# Rand + +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](../../../LICENSE) + +Random number generation and weighted selection utilities for procedural roguelike map generation. + +## Overview + +The `Rand` folder provides all randomization infrastructure used throughout RogueElements. This includes: + +- **Seedable RNG** - Deterministic random number generation for reproducible maps +- **Weighted Selection** - Pick items from lists with different spawn rates +- **Noise Generation** - Perlin noise for natural terrain features +- **Range Generators** - Generate random integers within specified bounds + +## Core Concepts + +### IRandPicker Interface Hierarchy + +All random pickers implement `IRandPicker`, enabling polymorphic random selection: + +``` +IRandPicker +├── RandRange - Pick integer from [min, max) +├── RandBinomial - Pick from binomial distribution +├── RandBag - Pick from unweighted list +├── SpawnList - Pick from weighted list +└── PresetPicker - Always return same value + +IMultiRandPicker +├── LoopedRand - Repeat picks N times +└── PresetMultiRand - Return preset list +``` + +Key interface members: + +```csharp +public interface IRandPicker +{ + T Pick(IRandom rand); // Get random item + bool CanPick { get; } // Has items to pick? + bool ChangesState { get; } // Modifies internal state on pick? + IRandPicker CopyState(); // Clone for stateful pickers +} +``` + +### Why This Matters for Roguelikes + +1. **Reproducibility** - Same seed produces identical maps, essential for sharing seeds and debugging +2. **Weighted Spawns** - Control rarity of items, enemies, room types naturally +3. **Floor Variation** - `SpawnRangeList` allows different spawn tables per dungeon floor +4. **Extensibility** - Implement `IRandPicker` to create custom distributions + +## Key Classes + +### RandRange + +Generate random integers within a range (exclusive max): + +```csharp +// Single value (always returns 5) +var exact = new RandRange(5); + +// Range: returns 3, 4, 5, 6, or 7 +var range = new RandRange(3, 8); + +// Use in generation +int roomWidth = new RandRange(4, 10).Pick(map.Rand); +``` + +### RandBinomial + +Generate values from a binomial distribution - useful for "attempt N times with X% chance": + +```csharp +// Roll 5 dice, each with 50% success, add 2 as baseline +var loot = new RandBinomial(trials: 5, percent: 50, offset: 2); +int goldPiles = loot.Pick(rand); // Returns 2-7 +``` + +### SpawnList + +Weighted random selection - the workhorse for roguelike item/enemy spawning: + +```csharp +// Create spawn table for room types +var rooms = new SpawnList>(); +rooms.Add(new RoomGenSquare(), 10); // Common +rooms.Add(new RoomGenRound(), 10); // Common +rooms.Add(new RoomGenCave(), 3); // Rare + +// Pick randomly (weighted) +RoomGen room = rooms.Pick(map.Rand); +``` + +With removal (bag without replacement): + +```csharp +var uniqueItems = new SpawnList(remove: true); +uniqueItems.Add(new Sword(), 10); +uniqueItems.Add(new Shield(), 5); + +// First pick might return Sword +// Sword is removed, second pick can only return Shield +``` + +### SpawnRangeList + +Weighted selection that varies by dungeon floor - items appear only on certain floors: + +```csharp +var enemies = new SpawnRangeList(); + +// Slimes appear floors 1-5 with rate 20 +enemies.Add(new Slime(), new IntRange(1, 6), 20); + +// Dragons appear floors 8-10 with rate 5 +enemies.Add(new Dragon(), new IntRange(8, 11), 5); + +// Get spawn table for floor 3 (only contains Slime) +SpawnList floor3 = enemies.GetSpawnList(3); + +// Pick from floor 9 (contains both, weighted) +Enemy enemy = enemies.Pick(rand, level: 9); +``` + +### LoopedRand + +Combine a spawner with an amount picker to generate multiple items: + +```csharp +// Spawn 10-18 items from weighted list +var itemSpawns = new SpawnList(); +itemSpawns.Add(new Potion(), 50); +itemSpawns.Add(new Scroll(), 30); +itemSpawns.Add(new Weapon(), 10); + +var spawner = new LoopedRand( + itemSpawns, // What to spawn + new RandRange(10, 19) // How many (10-18) +); + +List items = spawner.Roll(rand); +``` + +## Integration with MapGen Pipeline + +These utilities are used throughout the generation pipeline: + +```csharp +// Room type selection uses SpawnList +var path = new GridPathBranch(); +path.GenericRooms = new SpawnList> +{ + { new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 10 }, + { new RoomGenRound(new RandRange(5, 9), new RandRange(5, 9)), 10 }, +}; + +// Item spawning uses LoopedRand with SpawnList +var itemPlacement = new RandomSpawnStep( + new PickerSpawner( + new LoopedRand(itemSpawns, new RandRange(10, 19)) + ) +); + +// Water generation uses RandRange for percentage +var water = new PerlinWaterStep( + new RandRange(35), // 35% water coverage + 3, // Complexity + new Tile(terrain), + stencil +); +``` + +## Subfolders + +- **[Noise/](Noise/)** - Perlin noise generation for natural terrain +- **[RNG/](RNG/)** - Core seedable random number generator implementation + +## See Also + +- [Priority/](../Priority/) - Step ordering system that also uses these random utilities +- [MapGen/Spawning/](../MapGen/Spawning/) - How spawn lists integrate with placement +- [Examples/Ex6_Items](../../RogueElements.Examples/Ex6_Items/) - Item spawning example + +--- + +![Repobeats analytics](https://repobeats.axiom.co/api/embed/3c5a3b7f5e0c1d8a9b7c5e3a1f9d8b7c6e5a4d3c2b1a0.svg "Repobeats analytics image") diff --git a/RogueElements/Rand/RNG/IRandom.cs b/RogueElements/Rand/RNG/IRandom.cs index 7000afe1..779bb2f2 100644 --- a/RogueElements/Rand/RNG/IRandom.cs +++ b/RogueElements/Rand/RNG/IRandom.cs @@ -11,18 +11,47 @@ namespace RogueElements { + /// + /// Defines a random number generator with repeatable seed support. + /// public interface IRandom { + /// + /// Gets the seed value that the generator was initialized with. + /// ulong FirstSeed { get; } + /// + /// Gets the next random 64-bit unsigned integer. + /// + /// A random 64-bit unsigned integer. ulong NextUInt64(); + /// + /// Gets a non-negative random integer. + /// + /// A non-negative random integer. int Next(); + /// + /// Gets a random integer within the specified range. + /// + /// The inclusive lower bound. + /// The exclusive upper bound. + /// A random integer from minValue to maxValue - 1. int Next(int minValue, int maxValue); + /// + /// Gets a non-negative random integer less than the specified maximum. + /// + /// The exclusive upper bound. + /// A random integer from 0 to maxValue - 1. int Next(int maxValue); + /// + /// Gets a random double between 0.0 and 1.0. + /// + /// A random double from 0.0 to 1.0. double NextDouble(); } } diff --git a/RogueElements/Rand/RNG/README.md b/RogueElements/Rand/RNG/README.md new file mode 100644 index 00000000..fc2e7ca9 --- /dev/null +++ b/RogueElements/Rand/RNG/README.md @@ -0,0 +1,173 @@ +# RNG + +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](../../../../LICENSE) + +Core seedable random number generator implementation for deterministic roguelike generation. + +## Overview + +The `RNG` folder contains the foundational random number generation used throughout RogueElements. The key requirement is **determinism** - given the same seed, the generator must produce identical sequences across platforms and runs. + +## Why Deterministic Seeds Matter for Roguelikes + +1. **Seed Sharing** - Players can share seeds to experience the same dungeon +2. **Debugging** - Reproduce bugs exactly by saving the seed +3. **Replay Systems** - Record only player inputs, regenerate map from seed +4. **Testing** - Unit tests produce consistent results +5. **Fair Competition** - Daily/weekly challenge runs with identical maps + +## Core Classes + +### IRandom Interface + +The abstraction used throughout the library: + +```csharp +public interface IRandom +{ + ulong FirstSeed { get; } // Original seed for reproduction + + ulong NextUInt64(); // Raw 64-bit value + int Next(); // 0 to int.MaxValue + int Next(int maxValue); // 0 to maxValue (exclusive) + int Next(int minValue, int maxValue); // minValue to maxValue (exclusive) + double NextDouble(); // 0.0 to 1.0 +} +``` + +### ReRandom + +The default RNG implementation using xoshiro256** algorithm: + +```csharp +// Create with explicit seed +var rand = new ReRandom(12345UL); + +// Create with time-based seed +var rand = new ReRandom(); + +// Access original seed for reproduction +ulong seed = rand.FirstSeed; + +// Generate values +int roll = rand.Next(1, 7); // 1-6 (d6) +int index = rand.Next(items.Count); // Random array index +double percent = rand.NextDouble(); // 0.0-1.0 +``` + +**Algorithm Properties:** + +- **xoshiro256**** - Modern, fast PRNG with excellent statistical properties +- **256-bit state** - Large enough for any parallel application +- **Sub-nanosecond speed** - Minimal overhead +- **Passes all known tests** - Including BigCrush statistical test suite + +### SplitMix64 + +Used internally to initialize xoshiro256** state from a single seed: + +```csharp +// Internal usage in ReRandom constructor +SplitMix64 sm = new SplitMix64(seed); +s[0] = sm.Next(); +s[1] = sm.Next(); +s[2] = sm.Next(); +s[3] = sm.Next(); +``` + +## Usage in Map Generation + +Every `IGenContext` provides access to `Rand`: + +```csharp +public class MyStep : GenStep +{ + public override void Apply(ITiledGenContext map) + { + // Use map.Rand for all randomness + int roomWidth = map.Rand.Next(4, 10); + int roomHeight = map.Rand.Next(4, 10); + + // Never create new Random() instances! + // This breaks determinism. + } +} +``` + +### Initialization Flow + +```csharp +// 1. Call GenMap with a seed +MapGenContext context = layout.GenMap(seed); + +// 2. Context initializes its RNG +public void InitSeed(ulong seed) +{ + this.Rand = new ReRandom(seed); +} + +// 3. All GenSteps use context.Rand +// 4. Same seed = identical map every time +``` + +## Global Access + +For convenience, `MathUtils` provides a global RNG: + +```csharp +// Get global RNG instance +IRandom rand = MathUtils.Rand; + +// Reseed both RNG and Noise +MathUtils.ReSeedRand(seed); + +// Generate initial seed for new maps +ulong newSeed = MathUtils.Rand.NextUInt64(); +MapGenContext context = layout.GenMap(newSeed); +``` + +## Platform Considerations + +**Warning about NextDouble():** + +```csharp +/// +/// Floating point operations, including doubles, are non-deterministic. +/// They will vary by compiler, architecture, etc. +/// Understand the risks before using. +/// +public virtual double NextDouble() +``` + +For strict cross-platform determinism, prefer integer operations: + +```csharp +// Instead of: if (rand.NextDouble() < 0.3) +// Use: if (rand.Next(100) < 30) +``` + +## Reproducibility Pattern + +Save and restore seeds for debugging: + +```csharp +// Save seed before generation +ulong seed = MathUtils.Rand.NextUInt64(); +Console.WriteLine($"Generating with seed: {seed}"); + +// Generate map +var context = layout.GenMap(seed); + +// Later, reproduce exact same map +var sameContext = layout.GenMap(seed); // Identical! +``` + +## See Also + +- [Noise/](../Noise/) - Position-based noise (different use case than sequential RNG) +- [IRandPicker](../) - Higher-level random selection utilities +- [MapGen/](../../MapGen/) - How RNG integrates with the generation pipeline + +--- + +![Repobeats analytics](https://repobeats.axiom.co/api/embed/3c5a3b7f5e0c1d8a9b7c5e3a1f9d8b7c6e5a4d3c2b1a0.svg "Repobeats analytics image") diff --git a/RogueElements/Rand/RNG/ReRandom.cs b/RogueElements/Rand/RNG/ReRandom.cs index 11adb334..06e2e1a5 100644 --- a/RogueElements/Rand/RNG/ReRandom.cs +++ b/RogueElements/Rand/RNG/ReRandom.cs @@ -25,11 +25,18 @@ public class ReRandom : IRandom { private readonly ulong[] s; + /// + /// Initializes a new instance of the class with the current time as seed. + /// public ReRandom() : this(unchecked((ulong)System.DateTime.Now.Ticks)) { } + /// + /// Initializes a new instance of the class with a specified seed. + /// + /// The seed value for the random number generator. public ReRandom(ulong seed) { this.FirstSeed = seed; @@ -45,8 +52,10 @@ public ReRandom(ulong seed) /// /// The seed value that the class was initialized with. /// + /// public ulong FirstSeed { get; private set; } + /// public virtual ulong NextUInt64() { ulong result = Rotl(this.s[1] * 5, 7) * 9; @@ -64,11 +73,13 @@ public virtual ulong NextUInt64() return result; } + /// public virtual int Next() { return (int)(this.NextUInt64() % int.MaxValue); } + /// public virtual int Next(int minValue, int maxValue) { if (minValue > maxValue) @@ -82,6 +93,7 @@ public virtual int Next(int minValue, int maxValue) return (int)((long)(this.NextUInt64() % (ulong)range) + minValue); } + /// public virtual int Next(int maxValue) { if (maxValue < 0) @@ -93,6 +105,7 @@ public virtual int Next(int maxValue) return (int)(this.NextUInt64() % (ulong)maxValue); } + /// public override string ToString() { return string.Format("ReRandom: {0} {1} {2} {3}", this.s[0], this.s[1], this.s[2], this.s[3]); diff --git a/RogueElements/Rand/RNG/SplitMix64.cs b/RogueElements/Rand/RNG/SplitMix64.cs index 17b48b9b..8eda2d9c 100644 --- a/RogueElements/Rand/RNG/SplitMix64.cs +++ b/RogueElements/Rand/RNG/SplitMix64.cs @@ -8,15 +8,26 @@ namespace RogueElements { + /// + /// A splitmix64 random number generator used for initializing other RNG states. + /// public class SplitMix64 { private ulong x; + /// + /// Initializes a new instance of the class with a specified seed. + /// + /// The seed value for the generator. public SplitMix64(ulong seed) { this.x = seed; } + /// + /// Gets the next random 64-bit unsigned integer. + /// + /// A random 64-bit unsigned integer. public ulong Next() { ulong z = this.x += 0x9E3779B97F4A7C15; diff --git a/RogueElements/Rand/RandBag.cs b/RogueElements/Rand/RandBag.cs index 11d4af8b..d26c3fc2 100644 --- a/RogueElements/Rand/RandBag.cs +++ b/RogueElements/Rand/RandBag.cs @@ -12,33 +12,53 @@ namespace RogueElements /// /// Selects an item randomly from a list. /// - /// + /// The type of items in the bag. [Serializable] public class RandBag : IRandPicker { private bool removeOnRoll; + /// + /// Initializes a new instance of the class. + /// public RandBag() { this.ToSpawn = new List(); } + /// + /// Initializes a new instance of the class with specified items. + /// + /// The items to include in the bag. public RandBag(params T[] toSpawn) { this.ToSpawn = new List(toSpawn); } + /// + /// Initializes a new instance of the class with optional removal on pick. + /// + /// If true, items are removed after being picked. + /// The list of items. public RandBag(bool remove, List toSpawn) { this.removeOnRoll = remove; this.ToSpawn = toSpawn; } + /// + /// Initializes a new instance of the class with a list of items. + /// + /// The list of items. public RandBag(List toSpawn) { this.ToSpawn = toSpawn; } + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The instance to copy. protected RandBag(RandBag other) { this.ToSpawn = new List(other.ToSpawn); @@ -55,18 +75,23 @@ protected RandBag(RandBag other) /// public bool RemoveOnRoll => this.removeOnRoll; + /// public bool ChangesState => this.RemoveOnRoll; + /// public bool CanPick => this.ToSpawn.Count > 0; + /// public IRandPicker CopyState() => new RandBag(this); + /// public IEnumerable EnumerateOutcomes() { foreach (T spawn in this.ToSpawn) yield return spawn; } + /// public T Pick(IRandom rand) { int index = rand.Next(this.ToSpawn.Count); @@ -76,6 +101,7 @@ public T Pick(IRandom rand) return choice; } + /// public override string ToString() { if (this.ToSpawn.Count == 1) diff --git a/RogueElements/Rand/RandBinomial.cs b/RogueElements/Rand/RandBinomial.cs index cffbd8b8..345e48e6 100644 --- a/RogueElements/Rand/RandBinomial.cs +++ b/RogueElements/Rand/RandBinomial.cs @@ -15,22 +15,40 @@ namespace RogueElements [Serializable] public class RandBinomial : IRandPicker { + /// + /// Initializes a new instance of the class. + /// public RandBinomial() { } + /// + /// Initializes a new instance of the class with trial count and probability. + /// + /// The number of trials. + /// The probability percentage (0-100) for each trial. public RandBinomial(int trials, int percent) { this.Trials = trials; this.Percent = percent; } + /// + /// Initializes a new instance of the class with trial count, probability, and offset. + /// + /// The number of trials. + /// The probability percentage (0-100) for each trial. + /// The value to add to the result. public RandBinomial(int trials, int percent, int offset) : this(trials, percent) { this.Offset = offset; } + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The instance to copy. protected RandBinomial(RandBinomial other) { this.Offset = other.Offset; @@ -53,18 +71,23 @@ protected RandBinomial(RandBinomial other) /// public int Percent { get; set; } + /// public bool ChangesState => false; + /// public bool CanPick => true; + /// public IRandPicker CopyState() => new RandBinomial(this); + /// public IEnumerable EnumerateOutcomes() { for (int ii = 0; ii < this.Trials; ii++) yield return ii; } + /// public int Pick(IRandom rand) { int total = 0; @@ -77,6 +100,7 @@ public int Pick(IRandom rand) return this.Offset + total; } + /// public override string ToString() { return string.Format("{0}+{1}%x{2}", this.Offset, this.Percent, this.Trials); diff --git a/RogueElements/Rand/RandRange.cs b/RogueElements/Rand/RandRange.cs index 7a6fe25a..8ca39821 100644 --- a/RogueElements/Rand/RandRange.cs +++ b/RogueElements/Rand/RandRange.cs @@ -15,39 +15,78 @@ namespace RogueElements [Serializable] public struct RandRange : IRandPicker, IEquatable { + /// + /// The minimum value (inclusive). + /// public int Min; + + /// + /// The maximum value (exclusive). + /// public int Max; + /// + /// Initializes a new instance of the struct with a single value. + /// + /// The exact value to return. public RandRange(int num) { this.Min = num; this.Max = num; } + /// + /// Initializes a new instance of the struct with a range. + /// + /// The minimum value (inclusive). + /// The maximum value (exclusive). public RandRange(int min, int max) { this.Min = min; this.Max = max; } + /// + /// Initializes a new instance of the struct by copying another. + /// + /// The instance to copy. public RandRange(RandRange other) { this.Min = other.Min; this.Max = other.Max; } + /// + /// Gets an empty range representing zero. + /// public static RandRange Empty => new RandRange(0); + /// public bool ChangesState => false; + /// public bool CanPick => this.Min <= this.Max; + /// + /// Determines whether two ranges are equal. + /// + /// The left-hand side range. + /// The right-hand side range. + /// True if the ranges are equal. public static bool operator ==(RandRange lhs, RandRange rhs) => lhs.Equals(rhs); + /// + /// Determines whether two ranges are not equal. + /// + /// The left-hand side range. + /// The right-hand side range. + /// True if the ranges are not equal. public static bool operator !=(RandRange lhs, RandRange rhs) => !lhs.Equals(rhs); + /// public IRandPicker CopyState() => new RandRange(this); + /// public IEnumerable EnumerateOutcomes() { yield return this.Min; @@ -55,14 +94,19 @@ public IEnumerable EnumerateOutcomes() yield return ii; } + /// public int Pick(IRandom rand) => rand.Next(this.Min, this.Max); + /// public bool Equals(RandRange other) => this.Min == other.Min && this.Max == other.Max; + /// public override bool Equals(object obj) => (obj is RandRange) && this.Equals((RandRange)obj); + /// public override int GetHashCode() => unchecked(191 + (this.Min.GetHashCode() * 313) ^ (this.Max.GetHashCode() * 739)); + /// public override string ToString() { if (this.Min + 1 >= this.Max) diff --git a/RogueElements/Rand/SpawnList.cs b/RogueElements/Rand/SpawnList.cs index a4e43c49..eec23383 100644 --- a/RogueElements/Rand/SpawnList.cs +++ b/RogueElements/Rand/SpawnList.cs @@ -12,7 +12,7 @@ namespace RogueElements /// /// Selects an item randomly from a weighted list. /// - /// + /// The type of items in the list. [Serializable] public class SpawnList : IRandPicker, ISpawnList, ICollection.SpawnRate>, ISpawnList { @@ -20,17 +20,28 @@ public class SpawnList : IRandPicker, ISpawnList, ICollection + /// Initializes a new instance of the class. + ///
public SpawnList() { this.spawns = new List(); } + /// + /// Initializes a new instance of the class with optional removal on pick. + /// + /// If true, items are removed after being picked. public SpawnList(bool remove) { this.removeOnRoll = remove; this.spawns = new List(); } + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The instance to copy. protected SpawnList(SpawnList other) { this.removeOnRoll = other.removeOnRoll; @@ -40,12 +51,16 @@ protected SpawnList(SpawnList other) this.spawns.Add(new SpawnRate(item.Spawn, item.Rate)); } + /// public int Count => this.spawns.Count; + /// bool ICollection.IsReadOnly => false; + /// public int SpawnTotal => this.spawnTotal; + /// public bool CanPick => this.spawnTotal > 0; /// @@ -53,14 +68,16 @@ protected SpawnList(SpawnList other) /// public bool RemoveOnRoll => this.removeOnRoll; + /// public bool ChangesState => this.RemoveOnRoll; /// - /// This is a shallow copy. + /// Creates a shallow copy of this instance. /// - /// + /// A new instance with the same state. public IRandPicker CopyState() => new SpawnList(this); + /// void ICollection.Add(SpawnRate spawnRate) { if (spawnRate.Rate < 0) @@ -69,11 +86,13 @@ void ICollection.Add(SpawnRate spawnRate) this.spawnTotal += spawnRate.Rate; } + /// bool ICollection.Contains(SpawnRate item) { return this.spawns.Contains(item); } + /// public void Add(T spawn, int rate) { if (rate < 0) @@ -82,6 +101,7 @@ public void Add(T spawn, int rate) this.spawnTotal += rate; } + /// public void Insert(int index, T spawn, int rate) { if (rate < 0) @@ -90,24 +110,28 @@ public void Insert(int index, T spawn, int rate) this.spawnTotal += rate; } + /// public void Clear() { this.spawns.Clear(); this.spawnTotal = 0; } + /// public IEnumerable EnumerateOutcomes() { foreach (SpawnRate element in this.spawns) yield return element.Spawn; } + /// public IEnumerator GetEnumerator() { foreach (SpawnRate element in this.spawns) yield return element; } + /// void ICollection.CopyTo(SpawnRate[] array, int arrayIndex) { foreach (SpawnRate element in this.spawns) @@ -117,8 +141,10 @@ void ICollection.CopyTo(SpawnRate[] array, int arrayIndex) } } + /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + /// public T Pick(IRandom random) { int ii = this.PickIndex(random); @@ -131,6 +157,12 @@ public T Pick(IRandom random) return spawn; } + /// + /// Picks a random index from the list based on spawn rates. + /// + /// The random number generator to use. + /// The index of the selected item. + /// Thrown when the spawn total is zero. public int PickIndex(IRandom random) { if (this.spawnTotal > 0) @@ -148,11 +180,17 @@ public int PickIndex(IRandom random) throw new InvalidOperationException("Cannot spawn from a spawnlist of total rate 0!"); } + /// public T GetSpawn(int index) { return this.spawns[index].Spawn; } + /// + /// Gets the spawn rate of a specific item. + /// + /// The item to find. + /// The spawn rate of the item, or 0 if not found. public int GetSpawnRate(T spawn) { for (int ii = 0; ii < this.spawns.Count; ii++) @@ -164,16 +202,19 @@ public int GetSpawnRate(T spawn) return 0; } + /// public int GetSpawnRate(int index) { return this.spawns[index].Rate; } + /// public void SetSpawn(int index, T spawn) { this.spawns[index] = new SpawnRate(spawn, this.spawns[index].Rate); } + /// public void SetSpawnRate(int index, int rate) { if (rate < 0) @@ -182,17 +223,20 @@ public void SetSpawnRate(int index, int rate) this.spawns[index] = new SpawnRate(this.spawns[index].Spawn, rate); } + /// public void RemoveAt(int index) { this.spawnTotal -= this.spawns[index].Rate; this.spawns.RemoveAt(index); } + /// bool ICollection.Remove(SpawnRate spawnRate) { return this.spawns.Remove(spawnRate); } + /// public override bool Equals(object obj) { if (!(obj is SpawnList other)) @@ -210,6 +254,7 @@ public override bool Equals(object obj) return true; } + /// public override int GetHashCode() { int code = 0; @@ -218,37 +263,57 @@ public override int GetHashCode() return code; } + /// public override string ToString() { return string.Format("{0}[{1}]", this.GetType().GetFormattedTypeName(), this.spawns.Count); } + /// void ISpawnList.Add(object spawn, int rate) { this.Add((T)spawn, rate); } + /// void ISpawnList.Insert(int index, object spawn, int rate) { this.Insert(index, (T)spawn, rate); } + /// object ISpawnList.GetSpawn(int index) { return this.GetSpawn(index); } + /// void ISpawnList.SetSpawn(int index, object spawn) { this.SetSpawn(index, (T)spawn); } + /// + /// Represents an item with its spawn rate weight. + /// [Serializable] public struct SpawnRate { + /// + /// The spawnable item. + /// public T Spawn; + + /// + /// The spawn rate weight for this item. + /// public int Rate; + /// + /// Initializes a new instance of the struct. + /// + /// The spawnable item. + /// The spawn rate weight. public SpawnRate(T item, int rate) { this.Spawn = item; diff --git a/RogueElements/Rand/SpawnRangeList.cs b/RogueElements/Rand/SpawnRangeList.cs index 5dc88f18..183edebc 100644 --- a/RogueElements/Rand/SpawnRangeList.cs +++ b/RogueElements/Rand/SpawnRangeList.cs @@ -12,18 +12,25 @@ namespace RogueElements /// /// A data structure representing spawn rates of items spread across a range of floors. /// - /// + /// The type of items in the list. // TODO: Binary Space Partition Tree [Serializable] public class SpawnRangeList : ISpawnRangeList, ICollection.SpawnRange>, ISpawnRangeList { private readonly List spawns; + /// + /// Initializes a new instance of the class. + /// public SpawnRangeList() { this.spawns = new List(); } + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The instance to copy. public SpawnRangeList(SpawnRangeList other) { this.spawns = new List(); @@ -31,16 +38,19 @@ public SpawnRangeList(SpawnRangeList other) this.spawns.Add(new SpawnRange(item.Spawn, item.Rate, item.Range)); } + /// public int Count => this.spawns.Count; + /// bool ICollection.IsReadOnly => false; /// - /// This is a shallow copy. + /// Creates a shallow copy of this instance. /// - /// + /// A new instance with the same state. public SpawnRangeList CopyState() => new SpawnRangeList(this); + /// void ICollection.Add(SpawnRange range) { if (range.Rate < 0) @@ -50,6 +60,7 @@ void ICollection.Add(SpawnRange range) this.spawns.Add(range); } + /// public void Add(T spawn, IntRange range, int rate) { if (rate < 0) @@ -59,6 +70,7 @@ public void Add(T spawn, IntRange range, int rate) this.spawns.Add(new SpawnRange(spawn, rate, range)); } + /// public void Insert(int index, T spawn, IntRange range, int rate) { if (rate < 0) @@ -66,11 +78,17 @@ public void Insert(int index, T spawn, IntRange range, int rate) this.spawns.Insert(index, new SpawnRange(spawn, rate, range)); } + /// bool ICollection.Remove(SpawnRange randRange) { return this.spawns.Remove(randRange); } + /// + /// Removes the first occurrence of the specified item. + /// + /// The item to remove. + /// Thrown when the item is not found. public void Remove(T spawn) { for (int ii = 0; ii < this.spawns.Count; ii++) @@ -85,11 +103,13 @@ public void Remove(T spawn) throw new InvalidOperationException("Cannot find spawn!"); } + /// public void Clear() { this.spawns.Clear(); } + /// void ICollection.CopyTo(SpawnRange[] array, int arrayIndex) { foreach (SpawnRange spawn in this.spawns) @@ -99,24 +119,35 @@ void ICollection.CopyTo(SpawnRange[] array, int arrayIndex) } } + /// + /// Enumerates all possible spawn outcomes. + /// + /// An enumerable of all items in the list. public IEnumerable EnumerateOutcomes() { foreach (SpawnRange spawn in this.spawns) yield return spawn.Spawn; } + /// IEnumerator IEnumerable.GetEnumerator() { foreach (SpawnRange spawn in this.spawns) yield return spawn; } + /// IEnumerator IEnumerable.GetEnumerator() { foreach (SpawnRange spawn in this.spawns) yield return spawn; } + /// + /// Creates a spawn list containing only items valid for the specified level. + /// + /// The level to filter by. + /// A spawn list with items applicable to the level. public SpawnList GetSpawnList(int level) { SpawnList newList = new SpawnList(); @@ -129,6 +160,11 @@ public SpawnList GetSpawnList(int level) return newList; } + /// + /// Determines whether any item can be picked at the specified level. + /// + /// The level to check. + /// True if at least one item can spawn at this level. public bool CanPick(int level) { foreach (SpawnRange spawn in this.spawns) @@ -140,6 +176,13 @@ public bool CanPick(int level) return false; } + /// + /// Picks a random item valid for the specified level. + /// + /// The random number generator to use. + /// The level to pick for. + /// A randomly selected item. + /// Thrown when no items can spawn at this level. public T Pick(IRandom random, int level) { int spawnTotal = 0; @@ -165,11 +208,17 @@ public T Pick(IRandom random, int level) throw new InvalidOperationException("Cannot spawn from a spawnlist of total rate 0!"); } + /// public T GetSpawn(int index) { return this.spawns[index].Spawn; } + /// + /// Gets the spawn rate of a specific item. + /// + /// The item to find. + /// The spawn rate of the item, or 0 if not found. public int GetSpawnRate(T spawn) { for (int ii = 0; ii < this.spawns.Count; ii++) @@ -181,21 +230,25 @@ public int GetSpawnRate(T spawn) return 0; } + /// public int GetSpawnRate(int index) { return this.spawns[index].Rate; } + /// public IntRange GetSpawnRange(int index) { return this.spawns[index].Range; } + /// public void SetSpawn(int index, T spawn) { this.spawns[index] = new SpawnRange(spawn, this.spawns[index].Rate, this.spawns[index].Range); } + /// public void SetSpawnRate(int index, int rate) { if (rate < 0) @@ -203,36 +256,43 @@ public void SetSpawnRate(int index, int rate) this.spawns[index] = new SpawnRange(this.spawns[index].Spawn, rate, this.spawns[index].Range); } + /// public void SetSpawnRange(int index, IntRange range) { this.spawns[index] = new SpawnRange(this.spawns[index].Spawn, this.spawns[index].Rate, range); } + /// public void RemoveAt(int index) { this.spawns.RemoveAt(index); } + /// void ISpawnRangeList.Add(object spawn, IntRange range, int rate) { this.Add((T)spawn, range, rate); } + /// void ISpawnRangeList.Insert(int index, object spawn, IntRange range, int rate) { this.Insert(index, (T)spawn, range, rate); } + /// bool ICollection.Contains(SpawnRange item) { return this.spawns.Contains(item); } + /// object ISpawnRangeList.GetSpawn(int index) { return this.GetSpawn(index); } + /// void ISpawnRangeList.SetSpawn(int index, object spawn) { this.SetSpawn(index, (T)spawn); @@ -247,13 +307,33 @@ private IEnumerable GetLevelSpawns(int level) } } + /// + /// Represents an item with its spawn rate weight and level range. + /// [Serializable] public struct SpawnRange { + /// + /// The spawnable item. + /// public T Spawn; + + /// + /// The spawn rate weight for this item. + /// public int Rate; + + /// + /// The level range where this item can spawn. + /// public IntRange Range; + /// + /// Initializes a new instance of the struct. + /// + /// The spawnable item. + /// The spawn rate weight. + /// The level range constraint. public SpawnRange(T item, int rate, IntRange range) { this.Spawn = item; diff --git a/RogueElements/Rect.cs b/RogueElements/Rect.cs index 3a71bf68..b2d18807 100644 --- a/RogueElements/Rect.cs +++ b/RogueElements/Rect.cs @@ -10,15 +10,30 @@ namespace RogueElements { + /// + /// Represents an axis-aligned rectangle with integer coordinates. + /// [Serializable] public struct Rect : IEquatable { + /// + /// The X coordinate of the top-left corner. + /// public int X; + /// + /// The Y coordinate of the top-left corner. + /// public int Y; + /// + /// The width of the rectangle. + /// public int Width; + /// + /// The height of the rectangle. + /// public int Height; /// @@ -69,23 +84,50 @@ public Rect(Loc location, Loc size) this.Height = size.Y; } + /// + /// Gets an empty rectangle at the origin. + /// public static Rect Empty => new Rect(0, 0, 0, 0); + /// + /// Gets the left edge X coordinate. + /// public int Left => this.X; + /// + /// Gets the right edge X coordinate (exclusive). + /// public int Right => this.X + this.Width; + /// + /// Gets the top edge Y coordinate. + /// public int Top => this.Y; + /// + /// Gets the bottom edge Y coordinate (exclusive). + /// // TODO: sniff out off-by-ones with this... public int Bottom => this.Y + this.Height; + /// + /// Gets the area of the rectangle. + /// public int Area => this.Width * this.Height; + /// + /// Gets the perimeter of the rectangle. + /// public int Perimeter => (this.Width * 2) + (this.Height * 2); + /// + /// Gets a value indicating whether this rectangle is empty. + /// public bool IsEmpty => this == Empty; + /// + /// Gets or sets the top-left corner location. + /// public Loc Start { get => new Loc(this.X, this.Y); @@ -96,6 +138,9 @@ public Loc Start } } + /// + /// Gets or sets the size of the rectangle. + /// public Loc Size { get => new Loc(this.Width, this.Height); @@ -106,8 +151,14 @@ public Loc Size } } + /// + /// Gets the bottom-right corner location (exclusive). + /// public Loc End => this.Start + this.Size; + /// + /// Gets the center point of the rectangle. + /// public Loc Center => new Loc(this.X + (this.Width / 2), this.Y + (this.Height / 2)); internal string DebugDisplayString => $"{this.X} {this.Y} {this.Width} {this.Height}"; @@ -236,6 +287,12 @@ public static Rect FromPointRadius(Loc point, int radius) return new Rect(point - new Loc(radius), new Loc((radius * 2) + 1)); } + /// + /// Creates a new that includes both the original bounds and the specified point. + /// + /// The original rectangle. + /// The point to include. + /// A rectangle that contains both the original bounds and the point. // TODO: test this; one point from every quadrant public static Rect IncludeLoc(Rect bounds, Loc point) { @@ -246,6 +303,10 @@ public static Rect IncludeLoc(Rect bounds, Loc point) return FromPoints(new Loc(minX, minY), new Loc(maxX, maxY)); } + /// + /// Returns this rectangle as its own bounding rectangle. + /// + /// This rectangle. public Rect GetBoundingRectangle() { return this; @@ -366,6 +427,11 @@ public void Expand(Dir4 direction, int amount) } } + /// + /// Gets the range along the specified axis perpendicular to the given direction. + /// + /// The axis to get the range for. + /// The range along the perpendicular axis. public IntRange GetSide(Axis4 axis) { switch (axis) @@ -379,6 +445,11 @@ public IntRange GetSide(Axis4 axis) } } + /// + /// Gets the edge coordinate in the specified direction. + /// + /// The direction to get the edge for. + /// The coordinate of the edge in that direction. public int GetScalar(Dir4 direction) { switch (direction) @@ -396,6 +467,11 @@ public int GetScalar(Dir4 direction) } } + /// + /// Sets the edge coordinate in the specified direction, adjusting size accordingly. + /// + /// The direction of the edge to set. + /// The new coordinate value. public void SetScalar(Dir4 direction, int value) { switch (direction) diff --git a/RogueElements/RectExt.cs b/RogueElements/RectExt.cs index 414d7372..692e676b 100644 --- a/RogueElements/RectExt.cs +++ b/RogueElements/RectExt.cs @@ -10,6 +10,9 @@ namespace RogueElements { + /// + /// Provides extension methods for the struct. + /// public static class RectExt { /// @@ -65,6 +68,12 @@ public static Loc GetEdgeLoc(this Rect rect, Dir4 dir, int scalar) } } + /// + /// Gets the length of the border in the specified direction. + /// + /// The rectangle. + /// The direction of the border. + /// The length of the border perpendicular to the direction. public static int GetBorderLength(this Rect rect, Dir4 dir) { return rect.GetSide(dir.ToAxis()).Length; diff --git a/RogueElements/ReflectionUtils.cs b/RogueElements/ReflectionUtils.cs index 1ac6869b..d7e32a17 100644 --- a/RogueElements/ReflectionUtils.cs +++ b/RogueElements/ReflectionUtils.cs @@ -7,8 +7,16 @@ namespace RogueElements { + /// + /// Provides utility methods for reflection operations. + /// public static class ReflectionUtils { + /// + /// Gets a formatted type name without generic arity suffixes. + /// + /// The type. + /// The formatted type name (e.g., "List" instead of "List`1"). public static string GetFormattedTypeName(this Type t) { if (t.IsGenericType) diff --git a/RogueElements/StablePriorityQueue.cs b/RogueElements/StablePriorityQueue.cs index eba821ef..b7ebf2c2 100644 --- a/RogueElements/StablePriorityQueue.cs +++ b/RogueElements/StablePriorityQueue.cs @@ -8,23 +8,42 @@ namespace RogueElements { - // MIN queue + /// + /// A min-heap priority queue that maintains insertion order for equal priorities. + /// + /// The type of priority values. + /// The type of values stored. public class StablePriorityQueue where TPriority : IComparable { - // uses Heap, represented as list private readonly List> data; private uint insertions; + /// + /// Initializes a new instance of the class. + /// public StablePriorityQueue() { this.data = new List>(); } + /// + /// Delegate for operating on priority values. + /// + /// The priority type. + /// The priority value. public delegate void PriorityOp(T inPriority); + /// + /// Gets the number of items in the queue. + /// public int Count => this.data.Count; + /// + /// Adds an item with the specified priority. + /// + /// The priority. + /// The item to add. public void Enqueue(TPriority priority, TValue item) { this.data.Add(new StablePriorityQueueItem(priority, this.insertions, item)); @@ -43,6 +62,10 @@ public void Enqueue(TPriority priority, TValue item) } } + /// + /// Applies an operation to all priorities in the queue. + /// + /// The operation to apply. public void OperateAllPriority(PriorityOp op) { if (op == null) @@ -52,6 +75,11 @@ public void OperateAllPriority(PriorityOp op) op(item.Priority); } + /// + /// Adds an item or updates its priority if it already exists. + /// + /// The priority. + /// The item to add or update. public void AddOrSetPriority(TPriority priority, TValue item) { bool isInQueue = false; @@ -86,15 +114,22 @@ public void AddOrSetPriority(TPriority priority, TValue item) } } + /// + /// Removes and returns the item with the lowest priority. + /// + /// The dequeued item. public TValue Dequeue() { - // assumes pq is not empty; up to calling code - StablePriorityQueueItem frontItem = this.data[0]; // fetch the front + StablePriorityQueueItem frontItem = this.data[0]; this.DeleteAt(0); return frontItem.Value; } + /// + /// Returns the item with the lowest priority without removing it. + /// + /// The front item. public TValue Front() { // assumes pq is not empty; up to calling code @@ -103,14 +138,23 @@ public TValue Front() return frontItem.Value; } + /// + /// Returns the priority of the front item without removing it. + /// + /// The front priority. public TPriority FrontPriority() { - // assumes pq is not empty; up to calling code - StablePriorityQueueItem frontItem = this.data[0]; // fetch the front + StablePriorityQueueItem frontItem = this.data[0]; return frontItem.Priority; } + /// + /// Attempts to get the priority of an item in the queue. + /// + /// The item to find. + /// The priority if found. + /// true if the item was found; otherwise false. public bool TryGetPriority(TValue item, out TPriority priority) { for (int ii = 0; ii < this.data.Count; ii++) @@ -134,6 +178,10 @@ public bool TryGetPriority(TValue item, out TPriority priority) return false; } + /// + /// Removes an item from the queue. + /// + /// The item to remove. public void RemoveItem(TValue item) { for (int ii = this.data.Count - 1; ii >= 0; ii--) @@ -154,6 +202,9 @@ public void RemoveItem(TValue item) } } + /// + /// Removes all items from the queue. + /// public void Clear() { this.data.Clear(); diff --git a/RogueElements/TypeDict.cs b/RogueElements/TypeDict.cs index 83f29f52..f1c55b02 100644 --- a/RogueElements/TypeDict.cs +++ b/RogueElements/TypeDict.cs @@ -10,6 +10,10 @@ namespace RogueElements { + /// + /// A dictionary that stores items keyed by their runtime type, allowing only one instance per type. + /// + /// The base type of items that can be stored. [Serializable] public class TypeDict : ITypeDict, ITypeDict { @@ -18,20 +22,35 @@ public class TypeDict : ITypeDict, ITypeDict private List serializationObjects; + /// + /// Initializes a new instance of the class. + /// public TypeDict() { this.pointers = new Dictionary(); } + /// + /// Gets the number of items in the dictionary. + /// public int Count => this.pointers.Count; + /// bool ICollection.IsReadOnly => false; + /// + /// Removes all items from the dictionary. + /// public void Clear() { this.pointers.Clear(); } + /// + /// Determines whether the dictionary contains an item of the specified type. + /// + /// The type to check. + /// true if an item of that type exists; otherwise false. public bool Contains() where TK : T { @@ -39,11 +58,17 @@ public bool Contains() return this.Contains(type); } + /// + /// Determines whether the dictionary contains an item of the specified type. + /// + /// The type to check. + /// true if an item of that type exists; otherwise false. public bool Contains(Type type) { return this.pointers.ContainsKey(type.AssemblyQualifiedName); } + /// public void CopyTo(T[] array, int idx) { foreach (T element in this.pointers.Values) @@ -53,11 +78,17 @@ public void CopyTo(T[] array, int idx) } } + /// bool ICollection.Contains(T element) { return this.Contains(element.GetType()); } + /// + /// Gets the item of the specified type. + /// + /// The type of item to get. + /// The item of that type. public TK Get() where TK : T { @@ -65,11 +96,22 @@ public TK Get() return (TK)this.pointers[type.AssemblyQualifiedName]; } + /// + /// Gets the item of the specified type. + /// + /// The type of item to get. + /// The item of that type. public T Get(Type type) { return this.pointers[type.AssemblyQualifiedName]; } + /// + /// Attempts to get an item of the specified type. + /// + /// The type of item to get. + /// The item if found. + /// true if found; otherwise false. public bool TryGet(out TK item) where TK : T { @@ -80,11 +122,18 @@ public bool TryGet(out TK item) return success; } + /// + /// Attempts to get an item of the specified type. + /// + /// The type of item to get. + /// The item if found. + /// true if found; otherwise false. public bool TryGet(Type type, out T item) { return this.pointers.TryGetValue(type.AssemblyQualifiedName, out item); } + /// void ICollection.Add(T item) { if (item == null) @@ -92,6 +141,10 @@ void ICollection.Add(T item) this.pointers[item.GetType().AssemblyQualifiedName] = item; } + /// + /// Sets an item in the dictionary, keyed by its runtime type. + /// + /// The item to set. public void Set(T item) { if (item == null) @@ -99,16 +152,23 @@ public void Set(T item) this.pointers[item.GetType().AssemblyQualifiedName] = item; } + /// object ITypeDict.Get(Type type) { return this.Get(type); } + /// void ITypeDict.Set(object item) { this.Set((T)item); } + /// + /// Removes the item of the specified type. + /// + /// The type to remove. + /// true if removed; otherwise false. public bool Remove() where TK : T { @@ -116,21 +176,29 @@ public bool Remove() return this.Remove(type); } + /// + /// Removes the item of the specified type. + /// + /// The type to remove. + /// true if removed; otherwise false. public bool Remove(Type type) { return this.pointers.Remove(type.AssemblyQualifiedName); } + /// bool ICollection.Remove(T element) { return this.Remove(element.GetType()); } + /// public IEnumerator GetEnumerator() { return this.pointers.Values.GetEnumerator(); } + /// IEnumerator IEnumerable.GetEnumerator() { return this.pointers.Values.GetEnumerator(); diff --git a/RogueElements/WrappedCollision.cs b/RogueElements/WrappedCollision.cs index 131f5363..bb4334fc 100644 --- a/RogueElements/WrappedCollision.cs +++ b/RogueElements/WrappedCollision.cs @@ -10,13 +10,32 @@ namespace RogueElements { + /// + /// Provides collision detection methods for wrapped (toroidal) coordinate systems. + /// public static class WrappedCollision { + /// + /// Determines if two rectangles overlap in a wrapped coordinate system. + /// + /// The size of the wrapped area. + /// The first rectangle. + /// The second rectangle. + /// true if the rectangles overlap; otherwise false. public static bool Collides(Loc wrapSize, Rect bound1, Rect bound2) { return Collides(wrapSize, bound1.Start, bound1.Size, bound2.Start, bound2.Size); } + /// + /// Determines if two rectangular regions overlap in a wrapped coordinate system. + /// + /// The size of the wrapped area. + /// Start of the first region. + /// Size of the first region. + /// Start of the second region. + /// Size of the second region. + /// true if the regions overlap; otherwise false. public static bool Collides(Loc wrapSize, Loc start1, Loc size1, Loc start2, Loc size2) { return Collides(wrapSize.X, start1.X, size1.X, start2.X, size2.X) && @@ -45,11 +64,26 @@ public static bool Collides(int wrapSize, int start1, int size1, int start2, int return Collision.Collides(start1, size1, start2 + wrapSize, size2); } + /// + /// Determines if a point is within a rectangle in a wrapped coordinate system. + /// + /// The size of the wrapped area. + /// The rectangle. + /// The point to check. + /// true if the point is within the rectangle; otherwise false. public static bool InBounds(Loc wrapSize, Rect rect, Loc point) { return InBounds(wrapSize.X, rect.Start.X, rect.Size.X, point.X) && InBounds(wrapSize.Y, rect.Start.Y, rect.Size.Y, point.Y); } + /// + /// Determines if a point is within a rectangular region in a wrapped coordinate system. + /// + /// The size of the wrapped area. + /// Start of the region. + /// Size of the region. + /// The point to check. + /// true if the point is within the region; otherwise false. public static bool InBounds(Loc wrapSize, Loc start, Loc size, Loc point) { return InBounds(wrapSize.X, start.X, size.X, point.X) && InBounds(wrapSize.Y, start.Y, size.Y, point.Y); @@ -188,6 +222,13 @@ public static int GetClosestDirWrap(int wrapSize, int pt1, int pt2, int dirSign) return pt1 + wrappedDiff + (dirSign * wrapSize); } + /// + /// Returns all unwrapped versions of a 2D point that exist within a rectangular region. + /// + /// Size of the wrapped area. + /// The region to check. + /// The point to find unwrapped versions of. + /// All unwrapped point locations within the region. public static IEnumerable IteratePointsInBounds(Loc wrapSize, Rect rect, Loc pt) { foreach (int xx in IteratePointsInBounds(wrapSize.X, rect.X, rect.Size.X, pt.X)) diff --git a/docs/claude/architecture.md b/docs/claude/architecture.md new file mode 100644 index 00000000..c3a070b5 --- /dev/null +++ b/docs/claude/architecture.md @@ -0,0 +1,326 @@ +# RogueElements Architecture Reference + +> Claude Code-optimized reference. Tables, diagrams, minimal prose. + +## Interface Hierarchy + +```mermaid +classDiagram + class IGenContext { + <> + +IRandom Rand + +InitSeed(ulong seed) + +FinishGen() + } + + class ITiledGenContext { + <> + +ITile RoomTerrain + +ITile WallTerrain + +int Width + +int Height + +bool Wrap + +TileBlocked(Loc, bool) + +GetTile(Loc) ITile + +SetTile(Loc, ITile) + } + + class IFloorPlanGenContext { + <> + +FloorPlan RoomPlan + +InitPlan(FloorPlan) + } + + class IRoomGridGenContext { + <> + +GridPlan GridPlan + +InitGrid(GridPlan) + } + + class IPlaceableGenContext~T~ { + <> + +GetAllFreeTiles() List~Loc~ + +GetFreeTiles(Rect) List~Loc~ + +CanPlaceItem(Loc) bool + +PlaceItem(Loc, T) + } + + class IViewPlaceableGenContext~T~ { + <> + +GetItem(Loc) T + +GetItems(Rect) List~Loc~ + +GetAllItems() List~Loc~ + } + + class IReplaceableGenContext~T~ { + <> + +RemoveItem(Loc) + } + + class ISpawningGenContext~T~ { + <> + +SpawnList~T~ Spawns + } + + IGenContext <|-- ITiledGenContext + ITiledGenContext <|-- IFloorPlanGenContext + IFloorPlanGenContext <|-- IRoomGridGenContext + + IGenContext <|-- IPlaceableGenContext + IPlaceableGenContext <|-- IViewPlaceableGenContext + IViewPlaceableGenContext <|-- IReplaceableGenContext + + IGenContext <|-- ISpawningGenContext +``` + +## Interface Capabilities + +| Interface | Key Members | Purpose | +|-----------|-------------|---------| +| `IGenContext` | `Rand`, `InitSeed()`, `FinishGen()` | Base context with RNG | +| `ITiledGenContext` | `Width`, `Height`, `GetTile()`, `SetTile()`, `TileBlocked()` | Tile-based map operations | +| `IFloorPlanGenContext` | `RoomPlan`, `InitPlan()` | Freeform room placement | +| `IRoomGridGenContext` | `GridPlan`, `InitGrid()` | Grid-based room layouts | +| `IPlaceableGenContext` | `GetFreeTiles()`, `CanPlaceItem()`, `PlaceItem()` | Entity spawning | +| `IViewPlaceableGenContext` | `GetItem()`, `GetItems()`, `GetAllItems()` | Read placed entities | +| `IReplaceableGenContext` | `RemoveItem()` | Remove/replace entities | +| `ISpawningGenContext` | `Spawns` | Weighted spawn lists | + +## Interface Inheritance Chain + +``` +IGenContext + │ + ├── ITiledGenContext (adds tile operations) + │ │ + │ └── IFloorPlanGenContext (adds FloorPlan) + │ │ + │ └── IRoomGridGenContext (adds GridPlan) + │ + ├── IPlaceableGenContext (adds entity placement) + │ │ + │ └── IViewPlaceableGenContext (adds read access) + │ │ + │ └── IReplaceableGenContext (adds removal) + │ + └── ISpawningGenContext (adds spawn lists) +``` + +## GenStep Categories + +### Initialization Steps + +| Step | Required Context | Purpose | +|------|------------------|---------| +| `InitTilesStep` | `ITiledGenContext` | Initialize tile array with dimensions | +| `InitGridPlanStep` | `IRoomGridGenContext` | Create empty GridPlan | +| `InitFloorPlanStep` | `IFloorPlanGenContext` | Create empty FloorPlan | + +### Grid Steps + +| Step | Required Context | Purpose | +|------|------------------|---------| +| `SetGridDefaultsStep` | `IRoomGridGenContext` | Set default room/hall generators | +| `GridPathBranch` | `IRoomGridGenContext` | Create branching paths in grid | +| `GridPathCircle` | `IRoomGridGenContext` | Create circular path in grid | +| `GridPathTwoSides` | `IRoomGridGenContext` | Connect two sides of grid | +| `ConnectGridBranchStep` | `IRoomGridGenContext` | Add extra connections | +| `SetGridSpecialRoomStep` | `IRoomGridGenContext` | Place special rooms in grid | + +### Conversion Steps + +| Step | Required Context | Purpose | +|------|------------------|---------| +| `DrawGridToFloorStep` | `IRoomGridGenContext` | GridPlan → FloorPlan | +| `DrawFloorToTileStep` | `IFloorPlanGenContext` | FloorPlan → Tiles | + +### FloorPlan Steps + +| Step | Required Context | Purpose | +|------|------------------|---------| +| `AddConnectedRoomsStep` | `IFloorPlanGenContext` | Add rooms with hallway connections | +| `FloorPathBranch` | `IFloorPlanGenContext` | Create branching room paths | +| `SetSpecialRoomStep` | `IFloorPlanGenContext` | Place special rooms | +| `AddBossRoomStep` | `IFloorPlanGenContext` | Add boss room at dead end | + +### Tile Steps + +| Step | Required Context | Purpose | +|------|------------------|---------| +| `SpecificTilesStep` | `ITiledGenContext` | Place specific tiles at locations | +| `StairsStep` | `ITiledGenContext` + `IPlaceableGenContext` | Place stairs | +| `DropDiagonalBlockStep` | `ITiledGenContext` | Remove diagonal wall blocks | +| `FloorStairsStep` | `IFloorPlanGenContext` + `IPlaceableGenContext` | Stairs using room info | + +### Water/Terrain Steps + +| Step | Required Context | Purpose | +|------|------------------|---------| +| `PerlinWaterStep` | `ITiledGenContext` | Perlin noise water generation | +| `BlobWaterStep` | `ITiledGenContext` | Blob-based water generation | + +### Spawning Steps + +| Step | Required Context | Purpose | +|------|------------------|---------| +| `RandomSpawnStep` | `IPlaceableGenContext` | Random entity placement | +| `TerminalSpawnStep` | `IFloorPlanGenContext` + `IPlaceableGenContext` | Spawn at dead ends | +| `RoomSpawnStep` | `IFloorPlanGenContext` + `IPlaceableGenContext` | Per-room spawning | +| `PickerSpawner` | `ISpawningGenContext` + `IPlaceableGenContext` | Spawn from weighted list | + +## Data Flow Pipeline + +```mermaid +flowchart TD + subgraph Init["Initialization Phase"] + A[InitTilesStep] --> B[InitGridPlanStep] + end + + subgraph Grid["Grid Phase"] + B --> C[SetGridDefaultsStep] + C --> D[GridPathBranch] + D --> E[ConnectGridBranchStep] + E --> F[SetGridSpecialRoomStep] + end + + subgraph Convert1["Grid→Floor Conversion"] + F --> G[DrawGridToFloorStep] + end + + subgraph Floor["FloorPlan Phase"] + G --> H[SetSpecialRoomStep] + H --> I[Additional Room Steps] + end + + subgraph Convert2["Floor→Tile Conversion"] + I --> J[DrawFloorToTileStep] + end + + subgraph Tiles["Tile Phase"] + J --> K[DropDiagonalBlockStep] + K --> L[PerlinWaterStep] + end + + subgraph Spawn["Spawning Phase"] + L --> M[StairsStep] + M --> N[RandomSpawnStep] + N --> O[TerminalSpawnStep] + end + + O --> P[Final Map] +``` + +## Data Structures + +### GridPlan + +| Member | Type | Purpose | +|--------|------|---------| +| `GridWidth` | `int` | Number of room columns | +| `GridHeight` | `int` | Number of room rows | +| `CellWall` | `int` | Wall thickness between cells | +| `WidthRange` | `RandRange` | Room width range | +| `HeightRange` | `RandRange` | Room height range | +| `GetRoom(Loc)` | `GridRoomPlan` | Get room at grid position | +| `GetHall(LocRay4)` | `GridHallPlan` | Get hallway between rooms | + +### FloorPlan + +| Member | Type | Purpose | +|--------|------|---------| +| `RoomCount` | `int` | Number of rooms | +| `HallCount` | `int` | Number of hallways | +| `GetRoom(int)` | `IRoomPlan` | Get room by index | +| `GetHall(int)` | `IPermissiveRoomGen` | Get hall by index | +| `GetRoomHall(RoomHallIndex)` | `IRoomPlan` | Get room or hall | +| `AddRoom(IRoomGen, ...)` | `void` | Add room to plan | +| `AddHall(IPermissiveRoomGen, ...)` | `void` | Add hallway to plan | + +### RoomGen Types + +| Type | Purpose | +|------|---------| +| `RoomGenSquare` | Rectangular rooms | +| `RoomGenRound` | Circular/elliptical rooms | +| `RoomGenCave` | Cave-like irregular rooms | +| `RoomGenCross` | Cross-shaped rooms | +| `RoomGenAngledHall` | Angled hallway connections | +| `RoomGenDefault` | Default fallback generator | + +## Key Classes Quick Reference + +| Class | File | Purpose | +|-------|------|---------| +| `MapGen` | `MapGen/MapGen.cs` | Main orchestrator | +| `GenStep` | `MapGen/GenStep.cs` | Base step class | +| `Priority` | `Priority/Priority.cs` | Step ordering | +| `PriorityList` | `Priority/PriorityList.cs` | Ordered step container | +| `FloorPlan` | `MapGen/FloorPlan/FloorPlan.cs` | Freeform room layout | +| `GridPlan` | `MapGen/Grid/GridPlan.cs` | Grid-based layout | +| `SpawnList` | `Rand/SpawnList.cs` | Weighted random selection | +| `RandRange` | `Rand/RandRange.cs` | Random range values | + +## File Locations + +| Component | Path | +|-----------|------| +| Core Pipeline | `RogueElements/MapGen/` | +| Context Interfaces | `RogueElements/MapGen/IGenContext.cs`, `ITiledGenContext.cs`, etc. | +| GenStep Base | `RogueElements/MapGen/GenStep.cs` | +| FloorPlan System | `RogueElements/MapGen/FloorPlan/` | +| Grid System | `RogueElements/MapGen/Grid/` | +| Room Generators | `RogueElements/MapGen/Rooms/` | +| Spawning System | `RogueElements/MapGen/Spawning/` | +| Tile Operations | `RogueElements/MapGen/Tiles/` | +| RNG Utilities | `RogueElements/Rand/` | +| Priority System | `RogueElements/Priority/` | +| Examples | `RogueElements.Examples/` | +| Tests | `RogueElements.Tests/` | + +## Priority Conventions + +| Range | Phase | Example Steps | +|-------|-------|---------------| +| -10 to -1 | Pre-init | Debug setup | +| 0-9 | Initialization | `InitTilesStep`, `InitGridPlanStep` | +| 10-19 | Grid Setup | `SetGridDefaultsStep`, `GridPathBranch` | +| 20-29 | Grid Connections | `ConnectGridBranchStep` | +| 30-39 | Grid→Floor | `DrawGridToFloorStep` | +| 40-49 | Floor Modifications | `SetSpecialRoomStep` | +| 50-59 | Floor→Tile | `DrawFloorToTileStep` | +| 60-69 | Tile Cleanup | `DropDiagonalBlockStep` | +| 70-79 | Terrain | `PerlinWaterStep` | +| 80-89 | Stairs | `StairsStep` | +| 90-99 | Spawning | `RandomSpawnStep`, `TerminalSpawnStep` | + +## Context Constraint Patterns + +```csharp +// Minimal - only needs RNG +public class MyStep : GenStep where T : IGenContext + +// Tile operations +public class MyStep : GenStep where T : ITiledGenContext + +// Room-aware operations +public class MyStep : GenStep where T : IFloorPlanGenContext + +// Grid-based generation +public class MyStep : GenStep where T : IRoomGridGenContext + +// Entity spawning +public class MyStep : GenStep where T : IPlaceableGenContext + +// Multiple constraints +public class MyStep : GenStep + where T : IFloorPlanGenContext, IPlaceableGenContext +``` + +## Debug Hooks + +| Event | Signature | When Fired | +|-------|-----------|------------| +| `GenContextDebug.OnInit` | `Action` | Map initialization | +| `GenContextDebug.OnStep` | `Action` | Each step execution | +| `GenContextDebug.OnStepIn` | `Action` | Step entry | +| `GenContextDebug.OnStepOut` | `Action` | Step exit | diff --git a/docs/claude/flows.md b/docs/claude/flows.md new file mode 100644 index 00000000..df7cf879 --- /dev/null +++ b/docs/claude/flows.md @@ -0,0 +1,487 @@ +# Code Flow Documentation + +Traced execution paths for key operations in RogueElements. + +## 1. MapGen.GenMap(seed) Flow + +The main entry point for generating a map. + +**File:** `RogueElements/MapGen/MapGen.cs` + +```mermaid +sequenceDiagram + participant Client + participant MapGen + participant Context as IGenContext + participant Queue as StablePriorityQueue + participant Steps as GenStep[] + + Client->>MapGen: GenMap(seed) + MapGen->>Context: Activator.CreateInstance(typeof(T)) + MapGen->>Context: InitSeed(seed) + MapGen->>MapGen: GenContextDebug.DebugInit(map) + + loop foreach priority in GenSteps + MapGen->>Queue: Enqueue(priority, genStep) + end + + MapGen->>MapGen: ApplyGenSteps(map, queue) + + loop while queue.Count > 0 + Queue->>MapGen: Dequeue() -> step + MapGen->>MapGen: GenContextDebug.StepIn(step.ToString()) + MapGen->>Steps: step.Apply(map) + MapGen->>MapGen: GenContextDebug.StepOut() + end + + MapGen->>Context: FinishGen() + MapGen-->>Client: return map +``` + +### Key Code Points + +| Location | Description | +|----------|-------------| +| `MapGen.cs:126` | Context created via `Activator.CreateInstance(typeof(T))` | +| `MapGen.cs:127` | `map.InitSeed(seed)` initializes RNG | +| `MapGen.cs:129` | `GenContextDebug.DebugInit(map)` fires OnInit event | +| `MapGen.cs:132-137` | Priority queue built from GenSteps | +| `MapGen.cs:139` | `ApplyGenSteps(map, queue)` executes the pipeline | +| `MapGen.cs:141` | `map.FinishGen()` called after all steps | + +### ApplyGenSteps Loop (lines 164-182) + +```csharp +while (queue.Count > 0) +{ + IGenStep postProc = queue.Dequeue(); // line 168 + GenContextDebug.StepIn(postProc.ToString()); // line 169 + try + { + postProc.Apply(map); // line 173 + } + catch (Exception ex) + { + GenContextDebug.DebugError(ex); // line 177 + } + GenContextDebug.StepOut(); // line 180 +} +``` + +--- + +## 2. Grid-Based Room Generation (GridPathBranch) + +Tree-like layout using terminals/branchables pattern. + +**File:** `RogueElements/MapGen/Grid/Paths/IGridPathBranch.cs` + +```mermaid +flowchart TD + A[ApplyToPath] --> B[Clear GridPlan] + B --> C[Calculate roomsToOpen] + C --> D[Place first room randomly] + D --> E{roomsLeft > 0?} + E -->|Yes| F[Pop from terminals] + F --> G[GetExpandDirChances] + G --> H{Available rays?} + H -->|Yes| I[ExpandPath - add hall + room] + I --> J[Add to terminals] + J --> K[Update branchables if multiple dirs] + K --> L{pendingBranch >= 100?} + L -->|Yes| M[Pop from branchables] + M --> N[ExpandPath for branch] + N --> E + L -->|No| E + H -->|No| O{terminals empty?} + O -->|Yes + NoForcedBranches| P[Break] + O -->|Yes + forced| Q[Set pendingBranch = 100] + Q --> E + O -->|No| E + E -->|No| R[Done] +``` + +### Key Methods + +**ApplyToPath** (`IGridPathBranch.cs:111-205`) + +```csharp +public override void ApplyToPath(IRandom rand, GridPlan floorPlan) +{ + // Retry loop for failed attempts + for (int ii = 0; ii < 10; ii++) // line 113 + { + floorPlan.Clear(); // line 116 + + int roomsToOpen = floorPlan.GridWidth * floorPlan.GridHeight + * this.RoomRatio.Pick(rand) / 100; // line 118 + + // Place first room randomly + Loc sourceRoom = new Loc( + rand.Next(floorPlan.GridWidth), + rand.Next(floorPlan.GridHeight)); // line 128 + floorPlan.AddRoom(sourceRoom, this.GenericRooms.Pick(rand), + this.RoomComponents.Clone()); // line 129 + + // Add to terminals twice (can expand in 2 directions) + terminals.Add(sourceRoom); // line 132 + terminals.Add(sourceRoom); // line 133 + // ... expansion loop + } +} +``` + +**ExpandPath** (`IGridPathBranch.cs:250-257`) + +```csharp +protected bool ExpandPath(IRandom rand, GridPlan floorPlan, LocRay4 chosenRay) +{ + floorPlan.SetHall(chosenRay, this.GenericHalls.Pick(rand), + this.HallComponents.Clone()); // line 252 + floorPlan.AddRoom(chosenRay.Traverse(1), + this.GenericRooms.Pick(rand), + this.RoomComponents.Clone()); // line 253 + return true; +} +``` + +### Decision Points + +| Line | Decision | +|------|----------| +| 118-120 | `roomsToOpen` capped to minimum 1 | +| 147-163 | If rays available, extend path; else check terminals empty | +| 167-170 | `NoForcedBranches` controls whether to break or force branch | +| 173-196 | Branch loop runs while `pendingBranch >= 100` | + +--- + +## 3. GridPlan to FloorPlan to Tiles Conversion + +Three-stage transformation pipeline. + +```mermaid +flowchart LR + subgraph Stage1[DrawGridToFloorStep] + A[GridPlan] --> B[Create FloorPlan] + B --> C[PlaceRoomsOnFloor] + end + + subgraph Stage2[DrawFloorToTileStep] + D[FloorPlan] --> E[CreateNew tiles] + E --> F[Fill with walls] + F --> G[DrawOnMap] + end + + Stage1 --> Stage2 +``` + +### Stage 1: DrawGridToFloorStep + +**File:** `RogueElements/MapGen/Grid/DrawGridToFloorStep.cs` + +```csharp +public override void Apply(T map) // line 45 +{ + var floorPlan = new FloorPlan(); // line 47 + floorPlan.InitSize(map.GridPlan.Size, map.GridPlan.Wrap); // line 48 + map.InitPlan(floorPlan); // line 49 + map.GridPlan.PlaceRoomsOnFloor(map); // line 51 +} +``` + +**PlaceRoomsOnFloor** (`GridPlan.cs:206-329`) + +1. **Choose room bounds** (lines 209-210): + ```csharp + for (int ii = 0; ii < this.ArrayRooms.Count; ii++) + this.ChooseRoomBounds(map.Rand, ii); + ``` + +2. **Choose hall bounds** (lines 213-223): + ```csharp + for (int xx = 0; xx < this.VHalls.Length; xx++) + for (int yy = 0; yy < this.VHalls[xx].Length; yy++) + this.ChooseHallBounds(map.Rand, xx, yy, true); // vertical + // ... same for HHalls (horizontal) + ``` + +3. **Add rooms to FloorPlan** (lines 231-245): + ```csharp + foreach (var plan in this.ArrayRooms) + { + if (plan.PreferHall) + map.RoomPlan.AddHall((IPermissiveRoomGen)plan.RoomGen, plan.Components); + else + map.RoomPlan.AddRoom(plan.RoomGen, plan.Components); + } + ``` + +4. **Connect with halls** (lines 258-321): Links rooms via VHalls and HHalls + +### Stage 2: DrawFloorToTileStep + +**File:** `RogueElements/MapGen/FloorPlan/DrawFloorToTileStep.cs` + +```csharp +public override void Apply(T map) // line 56 +{ + // Create tile array with padding + map.CreateNew( + map.RoomPlan.DrawRect.Width + (2 * this.Padding), + map.RoomPlan.DrawRect.Height + (2 * this.Padding), + map.RoomPlan.Wrap); // line 59-62 + + // Fill entire map with walls + for (int ii = 0; ii < map.Width; ii++) + for (int jj = 0; jj < map.Height; jj++) + map.SetTile(new Loc(ii, jj), map.WallTerrain.Copy()); // line 66-69 + + // Adjust positions for padding + map.RoomPlan.MoveStart(new Loc(this.Padding)); // line 73 + + // Draw all rooms and halls + map.RoomPlan.DrawOnMap(map); // line 75 +} +``` + +### FloorPlan.DrawOnMap (`FloorPlan.cs:618-681`) + +```csharp +public void DrawOnMap(ITiledGenContext map) +{ + GenContextDebug.StepIn("Main Rooms"); // line 620 + + // Draw rooms first + for (int ii = 0; ii < this.Rooms.Count; ii++) // line 623 + { + IFloorRoomPlan plan = this.Rooms[ii]; + // Negotiate borders with adjacent undrawn rooms/halls + foreach (RoomHallIndex adj in plan.Adjacents) // line 627 + { + if (adj.IsHall || adj.Index > ii) + { + // Ask adjacent for fulfillable borders + plan.RoomGen.AskBorderFromRoom(...); // line 635 + } + } + plan.RoomGen.DrawOnMap(map); // line 639 + this.TransferBorderToAdjacents(...); // line 640 + } + + GenContextDebug.StepIn("Connecting Halls"); // line 651 + + // Draw halls after rooms + for (int ii = 0; ii < this.Halls.Count; ii++) // line 654 + { + // Similar negotiation and drawing... + plan.RoomGen.DrawOnMap(map); // line 670 + } +} +``` + +--- + +## 4. Room Generation Lifecycle (RoomGen) + +**File:** `RogueElements/MapGen/Rooms/RoomGen.cs` + +```mermaid +sequenceDiagram + participant Caller + participant RoomGen + participant Map as ITiledGenContext + + Caller->>RoomGen: ProposeSize(rand) + RoomGen-->>Caller: Loc (preferred size) + + Caller->>RoomGen: PrepareSize(rand, size) + Note over RoomGen: Initialize border arrays + RoomGen->>RoomGen: PrepareFulfillableBorders(rand) + + Caller->>RoomGen: SetLoc(loc) + Note over RoomGen: Set Draw.Start position + + Caller->>RoomGen: AskBorderFromRoom(sourceDraw, borderQuery, dir) + Note over RoomGen: Configure connection requirements + + Caller->>RoomGen: DrawOnMap(map) + RoomGen->>Map: SetTile() for each floor tile + RoomGen->>RoomGen: SetRoomBorders(map) + RoomGen->>RoomGen: FulfillRoomBorders(map, openAll) +``` + +### Key Methods + +**ProposeSize** (`RoomGen.cs:121`) - Abstract, implemented by subclasses + +Example: `RoomGenSquare.cs:60-63` +```csharp +public override Loc ProposeSize(IRandom rand) +{ + return new Loc(this.Width.Pick(rand), this.Height.Pick(rand)); +} +``` + +**PrepareSize** (`RoomGen.cs:128-163`) +```csharp +public virtual void PrepareSize(IRandom rand, Loc size) +{ + if (size.X <= 0 || size.Y <= 0) + throw new ArgumentException("Rooms must be of a positive size."); + + Rect currDraw = this.Draw; + currDraw.Size = size; + this.Draw = currDraw; // line 135 + + // Initialize border arrays for each direction + foreach (Dir4 dir in DirExt.VALID_DIR4) // line 138 + { + this.OpenedBorder[dir] = new bool[...]; + this.FulfillableBorder[dir] = new bool[...]; + this.BorderToFulfill[dir] = new bool[...]; + } + + this.PrepareFulfillableBorders(rand); // line 145 + // Validate at least one fulfillable border per direction +} +``` + +**SetLoc** (`RoomGen.cs:169-174`) +```csharp +public void SetLoc(Loc loc) +{ + Rect currDraw = this.Draw; + currDraw.Start = loc; + this.Draw = currDraw; +} +``` + +**AskBorderFromRoom** (`RoomGen.cs:413-450`) +```csharp +public virtual void AskBorderFromRoom(Rect sourceDraw, + Func borderQuery, Dir4 dir) +{ + // Verify rooms are touching + Loc startLoc = this.Draw.GetEdgeLoc(dir, 0); + Loc endLoc = sourceDraw.GetEdgeLoc(dir.Reverse(), 0); + if (startLoc + dir.GetLoc() != endLoc) + throw new ArgumentException("Rooms must touch..."); // line 418 + + // Add side requirement and mark fulfillable borders + this.AskSideReq(sourceSide, dir); // line 422 + // ... transfer border information +} +``` + +**DrawOnMap** - Abstract, example from `RoomGenSquare.cs:66-69` +```csharp +public override void DrawOnMap(T map) +{ + this.DrawMapDefault(map); // fills rectangle with floor tiles +} +``` + +**DrawMapDefault** (`RoomGen.cs:462-472`) +```csharp +protected void DrawMapDefault(T map) +{ + for (int x = 0; x < this.Draw.Size.X; x++) + for (int y = 0; y < this.Draw.Size.Y; y++) + map.SetTile(new Loc(this.Draw.X + x, this.Draw.Y + y), + map.RoomTerrain.Copy()); // line 468 + + this.SetRoomBorders(map); // line 471 +} +``` + +--- + +## 5. Spawning Flow (RandomSpawnStep) + +**Files:** +- `RogueElements/MapGen/Spawning/IBaseSpawnStep.cs` +- `RogueElements/MapGen/Spawning/RandomSpawnStep.cs` + +```mermaid +flowchart TD + A[Apply called] --> B{Spawn is null?} + B -->|Yes| C[Return early] + B -->|No| D[Spawn.GetSpawns] + D --> E{spawns.Count > 0?} + E -->|No| C + E -->|Yes| F[DistributeSpawns] + F --> G[GetAllFreeTiles] + G --> H{For each spawn} + H --> I{freeTiles.Count > 0?} + I -->|No| J[Stop] + I -->|Yes| K[Pick random tile] + K --> L[PlaceItem] + L --> M[Remove tile from list] + M --> H +``` + +### BaseSpawnStep.Apply (`IBaseSpawnStep.cs:78-87`) + +```csharp +public override void Apply(TGenContext map) +{ + if (this.Spawn is null) + return; // line 81 + + List spawns = this.Spawn.GetSpawns(map); // line 83 + + if (spawns.Count > 0) + this.DistributeSpawns(map, spawns); // line 86 +} +``` + +### RandomSpawnStep.DistributeSpawns (`RandomSpawnStep.cs:44-57`) + +```csharp +public override void DistributeSpawns(TGenContext map, List spawns) +{ + List freeTiles = map.GetAllFreeTiles(); // line 46 + + for (int ii = 0; ii < spawns.Count && freeTiles.Count > 0; ii++) + { + TSpawnable item = spawns[ii]; // line 50 + + int randIndex = map.Rand.Next(freeTiles.Count); // line 52 + map.PlaceItem(freeTiles[randIndex], item); // line 53 + freeTiles.RemoveAt(randIndex); // line 54 + GenContextDebug.DebugProgress("Placed Object"); // line 55 + } +} +``` + +### Key Interfaces + +| Interface | Purpose | +|-----------|---------| +| `IStepSpawner` | Generates list of items to spawn | +| `IPlaceableGenContext` | Context that can receive placed items | +| `ISpawnable` | Marker interface for spawnable entities | + +### Spawning Variants + +| Class | Strategy | +|-------|----------| +| `RandomSpawnStep` | Random tile selection | +| `RandomRoomSpawnStep` | Distribute evenly across rooms | +| `RoomSpawnStep` | Spawn in specific room types | +| `TerminalSpawnStep` | Spawn at dead-end rooms | +| `TerrainSpawnStep` | Spawn on specific terrain types | + +--- + +## Quick Reference: File Locations + +| Flow | Primary Files | +|------|---------------| +| MapGen Pipeline | `MapGen/MapGen.cs:123-144` | +| Grid Path | `MapGen/Grid/Paths/IGridPathBranch.cs:111-205` | +| Grid to Floor | `MapGen/Grid/DrawGridToFloorStep.cs:45-52` | +| Floor to Tiles | `MapGen/FloorPlan/DrawFloorToTileStep.cs:56-76` | +| Room Lifecycle | `MapGen/Rooms/RoomGen.cs:121-252` | +| Spawning | `MapGen/Spawning/RandomSpawnStep.cs:44-57` | diff --git a/docs/claude/patterns.md b/docs/claude/patterns.md new file mode 100644 index 00000000..dad893b6 --- /dev/null +++ b/docs/claude/patterns.md @@ -0,0 +1,506 @@ +# Patterns: Step-by-Step Recipes + +Practical recipes for extending RogueElements with custom components. + +--- + +## 1. Add a Custom RoomGen + +Room generators define the shape and layout of individual rooms. + +### When to Use +- Create custom room shapes (L-shaped, circular, cross-shaped) +- Implement procedural room interiors (pillars, pools, furniture) +- Add rooms with special connection constraints + +### Steps + +1. **Choose a base class:** + - `PermissiveRoomGen` - All border tiles accept connections (simple rooms) + - `RoomGen` - Custom control over which borders accept connections + +2. **Create the class with required overrides:** + +```csharp +using System; + +namespace YourGame +{ + [Serializable] + public class RoomGenCross : PermissiveRoomGen, ISizedRoomGen + where T : ITiledGenContext + { + public RoomGenCross() { } + + public RoomGenCross(RandRange armWidth, RandRange armLength) + { + this.ArmWidth = armWidth; + this.ArmLength = armLength; + } + + // Copy constructor for cloning + protected RoomGenCross(RoomGenCross other) + { + this.ArmWidth = other.ArmWidth; + this.ArmLength = other.ArmLength; + } + + public RandRange ArmWidth { get; set; } + public RandRange ArmLength { get; set; } + + // Required: create a copy for placement + public override RoomGen Copy() => new RoomGenCross(this); + + // Required: propose dimensions based on RNG + public override Loc ProposeSize(IRandom rand) + { + int arm = this.ArmLength.Pick(rand); + int width = this.ArmWidth.Pick(rand); + int size = (arm * 2) + width; + return new Loc(size, size); + } + + // Required: draw the room onto the map + public override void DrawOnMap(T map) + { + int armWidth = this.ArmWidth.Min; + int center = this.Draw.Width / 2; + int halfArm = armWidth / 2; + + // Draw horizontal arm + for (int x = 0; x < this.Draw.Width; x++) + { + for (int y = center - halfArm; y <= center + halfArm; y++) + { + map.SetTile(new Loc(this.Draw.X + x, this.Draw.Y + y), + map.RoomTerrain.Copy()); + } + } + + // Draw vertical arm + for (int y = 0; y < this.Draw.Height; y++) + { + for (int x = center - halfArm; x <= center + halfArm; x++) + { + map.SetTile(new Loc(this.Draw.X + x, this.Draw.Y + y), + map.RoomTerrain.Copy()); + } + } + + // Update borders for hallway connections + this.SetRoomBorders(map); + } + } +} +``` + +3. **Register in your pipeline:** + +```csharp +var rooms = new SpawnList>(); +rooms.Add(new RoomGenCross(new RandRange(3, 5), new RandRange(2, 4)), 10); +rooms.Add(new RoomGenSquare(new RandRange(4, 8), new RandRange(4, 8)), 20); + +layout.GenSteps.Add(new Priority(3), new DrawGridRoomsStep(rooms)); +``` + +--- + +## 2. Add a Custom GenStep + +GenSteps are the building blocks of the generation pipeline. + +### When to Use +- Add custom terrain features (lava pools, chasms, grass patches) +- Post-process generated maps (smooth walls, add decorations) +- Implement custom spawning logic + +### Steps + +1. **Identify required interfaces** (constrain `T` appropriately): + - `ITiledGenContext` - Tile manipulation + - `IFloorPlanGenContext` - Room-based layouts + - `IRoomGridGenContext` - Grid-based layouts + - `IPlaceableGenContext` - Entity spawning + +2. **Create the step:** + +```csharp +using System; + +namespace YourGame +{ + [Serializable] + public class ScatterPillarsStep : GenStep + where T : class, ITiledGenContext + { + public ScatterPillarsStep() { } + + public ScatterPillarsStep(int count, ITile pillarTile) + { + this.Count = count; + this.PillarTile = pillarTile; + } + + public int Count { get; set; } + public ITile PillarTile { get; set; } + + public override void Apply(T map) + { + int placed = 0; + int attempts = this.Count * 10; + + while (placed < this.Count && attempts > 0) + { + attempts--; + + // Use map.Rand for reproducibility + int x = map.Rand.Next(1, map.Width - 1); + int y = map.Rand.Next(1, map.Height - 1); + Loc loc = new Loc(x, y); + + // Only place on floor tiles with space around + if (!map.TileBlocked(loc) && this.HasClearance(map, loc)) + { + map.SetTile(loc, this.PillarTile.Copy()); + placed++; + GenContextDebug.DebugProgress("Placed Pillar"); + } + } + } + + private bool HasClearance(T map, Loc loc) + { + // Check 4 cardinal directions are floor + foreach (Dir4 dir in DirExt.VALID_DIR4) + { + if (map.TileBlocked(loc + dir.GetLoc())) + return false; + } + return true; + } + + public override string ToString() + { + return $"{this.GetType().GetFormattedTypeName()}: {this.Count} pillars"; + } + } +} +``` + +3. **Add to pipeline with appropriate priority:** + +```csharp +// Add after room generation but before spawning +layout.GenSteps.Add(new Priority(5, 1), + new ScatterPillarsStep(10, new Tile(PILLAR_TERRAIN_ID))); +``` + +--- + +## 3. Add a Custom Spawnable Type + +Spawnables are entities placed on the map (items, enemies, traps, etc.). + +### When to Use +- Add new entity categories (traps, NPCs, chests) +- Create entities with custom data (leveled monsters, enchanted items) + +### Steps + +1. **Implement ISpawnable:** + +```csharp +using System; + +namespace YourGame +{ + public class Trap : ISpawnable + { + public Trap() { } + + public Trap(int trapType, int damage) + { + this.TrapType = trapType; + this.Damage = damage; + } + + // Copy constructor + protected Trap(Trap other) + { + this.TrapType = other.TrapType; + this.Damage = other.Damage; + this.Loc = other.Loc; + } + + public int TrapType { get; set; } + public int Damage { get; set; } + public Loc Loc { get; set; } + + // Required by ISpawnable + public ISpawnable Copy() => new Trap(this); + } +} +``` + +2. **Add IPlaceableGenContext to your map context:** + +```csharp +public class MapGenContext : BaseMapGenContext, + IRoomGridGenContext, + IPlaceableGenContext // Add this interface +{ + // Your existing implementation... + + // Implement IPlaceableGenContext + List IPlaceableGenContext.GetAllFreeTiles() + { + return Grid.FindTilesInBox( + Loc.Zero, + new Loc(this.Width, this.Height), + loc => !this.IsTileOccupied(loc)); + } + + List IPlaceableGenContext.GetFreeTiles(Rect rect) + { + return Grid.FindTilesInBox(rect.Start, rect.Size, + loc => !this.IsTileOccupied(loc)); + } + + bool IPlaceableGenContext.CanPlaceItem(Loc loc) + { + return !this.IsTileOccupied(loc); + } + + void IPlaceableGenContext.PlaceItem(Loc loc, Trap trap) + { + var newTrap = new Trap(trap.TrapType, trap.Damage) { Loc = loc }; + this.Map.Traps.Add(newTrap); + } +} +``` + +3. **Use existing spawn steps or create custom:** + +```csharp +// Create spawner for traps +var trapSpawner = new PickerSpawner( + new LoopedRand( + new RandRange(3, 6), + new SpawnList + { + { new Trap(SPIKE_TRAP, 5), 10 }, + { new Trap(FIRE_TRAP, 10), 5 }, + })); + +layout.GenSteps.Add(new Priority(7), + new RandomSpawnStep(trapSpawner)); +``` + +--- + +## 4. Create a Custom Map Context + +The map context holds all state during generation and defines available capabilities. + +### Interface Hierarchy + +``` +IGenContext (base - Rand, InitSeed, FinishGen) + | + +-- ITiledGenContext (tiles, RoomTerrain, WallTerrain) + | | + | +-- IFloorPlanGenContext (RoomPlan for freeform rooms) + | | + | +-- IRoomGridGenContext (GridPlan for grid-based rooms) + | + +-- IPlaceableGenContext (spawn entities) + | + +-- IViewPlaceableGenContext (read placed entities) +``` + +### Minimal Context (tiles only) + +```csharp +public class MinimalContext : ITiledGenContext +{ + private int[][] tiles; + private IRandom rand; + + public int Width => this.tiles.Length; + public int Height => this.tiles[0].Length; + public bool Wrap => false; + public bool TilesInitialized => this.tiles != null; + public IRandom Rand => this.rand; + + public ITile RoomTerrain => new Tile(0); // Floor + public ITile WallTerrain => new Tile(1); // Wall + + public void InitSeed(ulong seed) => this.rand = new ReRandom(seed); + public void FinishGen() { } + + public void CreateNew(int width, int height, bool wrap = false) + { + this.tiles = new int[width][]; + for (int x = 0; x < width; x++) + this.tiles[x] = new int[height]; + } + + public ITile GetTile(Loc loc) => new Tile(this.tiles[loc.X][loc.Y]); + + public bool CanSetTile(Loc loc, ITile tile) => true; + + public bool TrySetTile(Loc loc, ITile tile) + { + this.tiles[loc.X][loc.Y] = ((Tile)tile).ID; + return true; + } + + public void SetTile(Loc loc, ITile tile) => this.TrySetTile(loc, tile); + + public bool TileBlocked(Loc loc) => this.tiles[loc.X][loc.Y] == 1; + public bool TileBlocked(Loc loc, bool diagonal) => this.TileBlocked(loc); +} +``` + +### Full Context (grid + spawning) + +See `RogueElements.Examples/Ex6_Items/MapGenContext.cs` for a complete example implementing: +- `IRoomGridGenContext` - Grid-based room layouts +- `IPlaceableGenContext` - Item spawning +- `IPlaceableGenContext` - Enemy spawning +- `IViewPlaceableGenContext` - Stair placement and querying + +--- + +## 5. Integrate with Game Engine + +### General Pattern + +```csharp +public class MapGenerator +{ + public YourGameMap Generate(ulong seed) + { + // 1. Build the layout + var layout = new MapGen(); + this.ConfigureLayout(layout); + + // 2. Generate + MapGenContext context = layout.GenMap(seed); + + // 3. Copy results to game map + var gameMap = new YourGameMap(context.Width, context.Height); + + for (int x = 0; x < context.Width; x++) + { + for (int y = 0; y < context.Height; y++) + { + var tile = context.GetTile(new Loc(x, y)); + gameMap.SetTile(x, y, ConvertTile(tile)); + } + } + + // Copy entities + foreach (var item in context.Map.Items) + gameMap.SpawnItem(item.Loc.X, item.Loc.Y, item.ID); + + return gameMap; + } +} +``` + +### Unity + +```csharp +public class DungeonGenerator : MonoBehaviour +{ + public Tilemap floorTilemap; + public Tilemap wallTilemap; + public TileBase floorTile; + public TileBase wallTile; + + public void Generate() + { + var layout = BuildLayout(); + var context = layout.GenMap((ulong)Random.Range(0, int.MaxValue)); + + for (int x = 0; x < context.Width; x++) + { + for (int y = 0; y < context.Height; y++) + { + Vector3Int pos = new Vector3Int(x, y, 0); + var tile = context.GetTile(new Loc(x, y)); + + if (IsWall(tile)) + wallTilemap.SetTile(pos, wallTile); + else + floorTilemap.SetTile(pos, floorTile); + } + } + } +} +``` + +### MonoGame + +```csharp +public class DungeonScreen : GameScreen +{ + private int[,] tileMap; + + public void Generate() + { + var layout = BuildLayout(); + var context = layout.GenMap((ulong)new Random().Next()); + + this.tileMap = new int[context.Width, context.Height]; + + for (int x = 0; x < context.Width; x++) + { + for (int y = 0; y < context.Height; y++) + { + var tile = (Tile)context.GetTile(new Loc(x, y)); + this.tileMap[x, y] = tile.ID; + } + } + } + + public override void Draw(GameTime gameTime) + { + for (int x = 0; x < this.tileMap.GetLength(0); x++) + { + for (int y = 0; y < this.tileMap.GetLength(1); y++) + { + var sourceRect = GetTileSourceRect(this.tileMap[x, y]); + spriteBatch.Draw(tileset, new Vector2(x * 16, y * 16), sourceRect, Color.White); + } + } + } +} +``` + +### Godot (C#) + +```csharp +public partial class DungeonGenerator : Node2D +{ + [Export] public TileMap tileMap; + + public void Generate() + { + var layout = BuildLayout(); + var context = layout.GenMap((ulong)GD.Randi()); + + for (int x = 0; x < context.Width; x++) + { + for (int y = 0; y < context.Height; y++) + { + var tile = (Tile)context.GetTile(new Loc(x, y)); + int atlasId = tile.ID == 0 ? 0 : 1; // Floor vs Wall + tileMap.SetCell(0, new Vector2I(x, y), 0, new Vector2I(atlasId, 0)); + } + } + } +} +``` diff --git a/mcp-server/.gitignore b/mcp-server/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/mcp-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/mcp-server/README.md b/mcp-server/README.md new file mode 100644 index 00000000..7889eb01 --- /dev/null +++ b/mcp-server/README.md @@ -0,0 +1,296 @@ +# RogueElements MCP Server + +[![MCP](https://img.shields.io/badge/MCP-Compatible-green.svg)](https://modelcontextprotocol.io/) +[![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-blue.svg)](https://nodejs.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +MCP (Model Context Protocol) server providing AI-optimized documentation, code generation, and exploration tools for the RogueElements procedural map generation library. + +## Features + +### Tools + +| Tool | Description | +|------|-------------| +| `rogue_search` | Search for classes across all categories by name or summary | +| `rogue_list_classes` | List all classes in a specific category | +| `rogue_get_class_docs` | Get detailed XML documentation for a specific class | +| `rogue_get_docs` | Access AI-optimized architecture, flows, and patterns documentation | +| `rogue_get_example` | Get annotated source code from the Examples project | +| `rogue_scaffold_roomgen` | Generate boilerplate for a custom RoomGen | +| `rogue_scaffold_genstep` | Generate boilerplate for a custom GenStep | +| `rogue_scaffold_spawnable` | Generate boilerplate for a custom spawnable entity | +| `rogue_list_interfaces` | List context interfaces with their capabilities | + +### Class Categories + +Use with `rogue_list_classes` and `rogue_search`: + +| Category | Description | +|----------|-------------| +| `genstep` | Generation steps (InitTilesStep, GridPathBranch, etc.) | +| `roomgen` | Room shape generators (RoomGenSquare, RoomGenCave, etc.) | +| `gridpath` | Grid-based path algorithms | +| `floorpath` | FloorPlan-based path algorithms | +| `spawning` | Entity placement (RandomSpawnStep, PickerSpawner, etc.) | +| `tiles` | Tile manipulation steps | +| `rand` | RNG and weighted selection utilities | +| `context` | Generation context interfaces | +| `priority` | Priority queue system | +| `floorplan` | FloorPlan data structures | +| `gridplan` | GridPlan data structures | +| `core` | Core utilities (Loc, Rect, Grid, etc.) | + +### Documentation Resources + +Use with `rogue_get_docs`: + +| Document | Description | +|----------|-------------| +| `architecture` | Interface hierarchy, GenStep categories, data flow diagrams | +| `flows` | Traced code paths for key operations | +| `patterns` | Step-by-step recipes for common modifications | + +### Examples + +Use with `rogue_get_example`: + +| Example | Concepts | +|---------|----------| +| `Ex1_Tiles` | Static tiles, InitTilesStep basics | +| `Ex2_Rooms` | Freeform rooms via FloorPlan | +| `Ex3_Grid` | Grid-based layouts via GridPlan | +| `Ex4_Stairs` | Stair placement and spawning | +| `Ex5_Terrain` | Water and terrain via Perlin noise | +| `Ex6_Items` | Item spawning and spawn lists | +| `Ex7_Special` | Special room placement | +| `Ex8_Integration` | Full pipeline combining all concepts | + +## Installation + +```bash +cd mcp-server +npm install +npm run build +``` + +## Usage + +### With Claude Code + +Add to your Claude Code MCP settings: + +**macOS**: `~/.claude/claude_desktop_config.json` +**Linux**: `~/.config/claude/claude_desktop_config.json` + +```json +{ + "mcpServers": { + "rogueelemens": { + "command": "node", + "args": ["/path/to/RogueElements/mcp-server/dist/index.js"] + } + } +} +``` + +### Development + +```bash +# Watch mode with auto-reload +npm run dev + +# Build for production +npm run build + +# Run the server +npm start +``` + +## Example Tool Usage + +### Search for Classes + +``` +Tool: rogue_search +Args: { "query": "water", "limit": 5 } +``` + +Returns classes matching "water" across all categories. + +### List Classes in Category + +``` +Tool: rogue_list_classes +Args: { "category": "spawning" } +``` + +Lists all spawning-related classes with summaries. + +### Get Class Documentation + +``` +Tool: rogue_get_class_docs +Args: { "class_name": "GridPathBranch" } +``` + +Returns detailed XML documentation including: +- Class summary and remarks +- Constructor documentation +- Public properties with types and descriptions +- Public methods with signatures +- Related classes + +### Get Architecture Docs + +``` +Tool: rogue_get_docs +Args: { "name": "architecture" } +``` + +Returns interface hierarchy, GenStep categories, and data flow diagrams. + +### Get Example Code + +``` +Tool: rogue_get_example +Args: { "name": "Ex3_Grid" } +``` + +Returns annotated source code for grid-based generation. + +### Scaffold a RoomGen + +``` +Tool: rogue_scaffold_roomgen +Args: { + "name": "Diamond", + "shape_description": "diamond-shaped room with configurable size" +} +``` + +Generates: + +```csharp +[Serializable] +public class RoomGenDiamond : RoomGen + where T : ITiledGenContext +{ + public RandRange Size { get; set; } + + public RoomGenDiamond() { } + + public RoomGenDiamond(RandRange size) + { + this.Size = size; + } + + protected RoomGenDiamond(RoomGenDiamond other) + { + this.Size = other.Size; + } + + public override RoomGen Copy() => new RoomGenDiamond(this); + + public override Loc ProposeSize(IRandom rand) + { + int size = this.Size.Pick(rand); + return new Loc(size, size); + } + + public override void DrawOnMap(T map) + { + // TODO: Implement diamond-shaped room drawing + // Draw a diamond-shaped room with configurable size + this.DrawMapDefault(map); + this.SetRoomBorders(map); + } +} +``` + +### Scaffold a GenStep + +``` +Tool: rogue_scaffold_genstep +Args: { + "name": "AddPillars", + "context_type": "ITiledGenContext", + "description": "Adds decorative pillars to rooms" +} +``` + +### Scaffold a Spawnable + +``` +Tool: rogue_scaffold_spawnable +Args: { + "name": "Treasure", + "description": "Treasure chest with gold value", + "spawn_type": "terminal" +} +``` + +Generates both a spawnable entity class and a spawn step. + +### List Interfaces + +``` +Tool: rogue_list_interfaces +Args: {} +``` + +Returns the interface hierarchy with capabilities for each: + +``` +IGenContext (base) +├── ITiledGenContext +│ └── IFloorPlanGenContext +│ └── IRoomGridGenContext +├── IPlaceableGenContext +│ └── IViewPlaceableGenContext +│ └── IReplaceableGenContext +└── ISpawningGenContext +``` + +## Architecture + +```mermaid +flowchart LR + subgraph Client["Claude Code / MCP Client"] + Q[Query] + end + + subgraph MCP["MCP Server"] + Tools[Tools] + Resources[Resources] + Parser[Tree-sitter Parser] + end + + subgraph Data["RogueElements"] + Docs[docs/claude/] + Source[RogueElements/] + Examples[Examples/] + end + + Q --> Tools + Q --> Resources + Tools --> Parser + Parser --> Source + Resources --> Docs + Tools --> Examples +``` + +## Requirements + +- Node.js >= 18 +- Built from RogueElements repository root (needs `docs/claude/` and source directories) + +## See Also + +- **[RogueElements](../RogueElements/)** - Core library +- **[RogueElements.Examples](../RogueElements.Examples/)** - Usage examples +- **[CLAUDE.md](../CLAUDE.md)** - Full architecture documentation + +--- + +![Repobeats analytics](https://repobeats.axiom.co/api/embed/placeholder.svg "Repobeats analytics image") diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json new file mode 100644 index 00000000..40be46c7 --- /dev/null +++ b/mcp-server/package-lock.json @@ -0,0 +1,1753 @@ +{ + "name": "rogueelemens-mcp-server", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rogueelemens-mcp-server", + "version": "2.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.1", + "tree-sitter-c-sharp": "^0.23.1", + "web-tree-sitter": "^0.24.7", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.7", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", + "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", + "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz", + "integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-sitter-c-sharp": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/tree-sitter-c-sharp/-/tree-sitter-c-sharp-0.23.1.tgz", + "integrity": "sha512-9zZ4FlcTRWWfRf6f4PgGhG8saPls6qOOt75tDfX7un9vQZJmARjPrAC6yBNCX2T/VKcCjIDbgq0evFaB3iGhQw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-tree-sitter": { + "version": "0.24.7", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.24.7.tgz", + "integrity": "sha512-CdC/TqVFbXqR+C51v38hv6wOPatKEUGxa39scAeFSm98wIhZxAYonhRQPSMmfZ2w7JDI0zQDdzdmgtNk06/krQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 00000000..17524527 --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,27 @@ +{ + "name": "rogueelemens-mcp-server", + "version": "2.2.1", + "description": "MCP server for RogueElements procedural map generation library", + "type": "module", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "tsx watch src/index.ts", + "build": "tsc", + "clean": "rm -rf dist" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.1", + "web-tree-sitter": "^0.24.7", + "tree-sitter-c-sharp": "^0.23.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts new file mode 100644 index 00000000..18042d14 --- /dev/null +++ b/mcp-server/src/index.ts @@ -0,0 +1,2167 @@ +#!/usr/bin/env node +/** + * MCP Server for RogueElements + * + * Provides documentation resources, class browsing, search, and code generation tools + * for working with the RogueElements procedural map generation library. + * + * Uses tree-sitter for proper AST-based C# parsing to extract XML documentation. + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import * as fs from "fs"; +import * as path from "path"; +import { fileURLToPath } from "url"; +import Parser from "web-tree-sitter"; +type Language = Parser.Language; +type SyntaxNode = Parser.SyntaxNode; + +// Get __dirname equivalent for ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// ============================================================================= +// DIRECTORY DISCOVERY +// ============================================================================= + +function findDocsDir(): string { + const candidates = [ + path.resolve(__dirname, "../../docs/claude"), + path.resolve(__dirname, "../docs/claude"), + path.resolve(process.cwd(), "docs/claude"), + ]; + + for (const dir of candidates) { + if (fs.existsSync(dir)) return dir; + } + + return path.resolve(process.cwd(), "docs/claude"); +} + +function findRogueElementsDir(): string { + const candidates = [ + path.resolve(__dirname, "../../RogueElements"), + path.resolve(__dirname, "../RogueElements"), + path.resolve(process.cwd(), "RogueElements"), + ]; + + for (const dir of candidates) { + if (fs.existsSync(dir)) return dir; + } + + return path.resolve(process.cwd(), "RogueElements"); +} + +const DOCS_DIR = findDocsDir(); +const ROGUE_DIR = findRogueElementsDir(); + +// ============================================================================= +// TREE-SITTER INITIALIZATION +// ============================================================================= + +let csharpParser: Parser | null = null; +let csharpLanguage: Language | null = null; + +async function initializeParser(): Promise { + if (csharpParser) return; + + await Parser.init(); + csharpParser = new Parser(); + + // Load C# grammar - try multiple locations + const wasmCandidates = [ + path.resolve(__dirname, "../node_modules/tree-sitter-c-sharp/tree-sitter-c_sharp.wasm"), + path.resolve(__dirname, "../../node_modules/tree-sitter-c-sharp/tree-sitter-c_sharp.wasm"), + path.resolve(process.cwd(), "mcp-server/node_modules/tree-sitter-c-sharp/tree-sitter-c_sharp.wasm"), + ]; + + let wasmPath: string | null = null; + for (const candidate of wasmCandidates) { + if (fs.existsSync(candidate)) { + wasmPath = candidate; + break; + } + } + + if (!wasmPath) { + throw new Error("Could not find tree-sitter-c_sharp.wasm"); + } + + csharpLanguage = await Parser.Language.load(wasmPath); + csharpParser.setLanguage(csharpLanguage); +} + +// ============================================================================= +// TREE-SITTER HELPERS +// ============================================================================= + +function findNodesByType(node: SyntaxNode, type: string, results: SyntaxNode[] = []): SyntaxNode[] { + if (node.type === type) results.push(node); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) findNodesByType(child, type, results); + } + return results; +} + +function getDocComments(node: SyntaxNode): string { + const comments: string[] = []; + let prev = node.previousSibling; + + while (prev && prev.type === "comment") { + comments.unshift(prev.text); + prev = prev.previousSibling; + } + + return comments.join("\n"); +} + +function parseDocComment(docText: string): { summary: string; remarks: string; inheritdoc: boolean } { + const summaryMatch = docText.match(/\s*([\s\S]*?)\s*<\/summary>/); + const summary = summaryMatch + ? summaryMatch[1].replace(/^\s*\/\/\/\s*/gm, "").trim() + : ""; + + const remarksMatch = docText.match(/\s*([\s\S]*?)\s*<\/remarks>/); + const remarks = remarksMatch + ? remarksMatch[1].replace(/^\s*\/\/\/\s*/gm, "").trim() + : ""; + + const inheritdoc = docText.includes("; + summary: string; +} + +interface ClassDoc { + name: string; + namespace: string; + baseClass: string; + interfaces: string[]; + summary: string; + remarks: string; + properties: Array<{ name: string; type: string; summary: string }>; + methods: Array<{ name: string; signature: string; summary: string; returnType: string }>; + constructors: ConstructorDoc[]; + filePath: string; + isInterface: boolean; + isStruct: boolean; + isAbstract: boolean; + isGeneric: boolean; + genericParams: string; +} + +async function parseClassFile(filePath: string): Promise { + try { + await initializeParser(); + if (!csharpParser) return []; + + let content = fs.readFileSync(filePath, "utf-8"); + + // Strip UTF-8 BOM if present + if (content.charCodeAt(0) === 0xFEFF) { + content = content.slice(1); + } + + const tree = csharpParser.parse(content); + if (!tree) return []; + + const results: ClassDoc[] = []; + + // Extract namespace + const namespaceDecls = findNodesByType(tree.rootNode, "namespace_declaration"); + let namespace = ""; + if (namespaceDecls.length > 0) { + namespace = getFieldText(namespaceDecls[0], "name"); + } + // Also check for file-scoped namespace + const fileScopedNs = findNodesByType(tree.rootNode, "file_scoped_namespace_declaration"); + if (fileScopedNs.length > 0) { + namespace = getFieldText(fileScopedNs[0], "name"); + } + + // Find all class, struct, and interface declarations + const classDecls = findNodesByType(tree.rootNode, "class_declaration"); + const structDecls = findNodesByType(tree.rootNode, "struct_declaration"); + const interfaceDecls = findNodesByType(tree.rootNode, "interface_declaration"); + + const allDecls = [ + ...classDecls.map(n => ({ node: n, isInterface: false, isStruct: false })), + ...structDecls.map(n => ({ node: n, isInterface: false, isStruct: true })), + ...interfaceDecls.map(n => ({ node: n, isInterface: true, isStruct: false })) + ]; + + for (const { node: declNode, isInterface, isStruct } of allDecls) { + const className = getFieldText(declNode, "name"); + if (!className) continue; + + // Check for generic type parameters + const typeParams = declNode.childForFieldName("type_parameters"); + const genericParams = typeParams ? typeParams.text : ""; + const isGeneric = !!typeParams; + + // Check modifiers for abstract/public + let isAbstract = false; + let isPublic = false; + for (let i = 0; i < declNode.childCount; i++) { + const child = declNode.child(i); + if (child?.type === "modifier") { + if (child.text === "abstract") isAbstract = true; + if (child.text === "public") isPublic = true; + } + } + + // Skip non-public classes + if (!isPublic) continue; + + // Get base class and interfaces from base_list + const baseLists = findNodesByType(declNode, "base_list"); + let baseClass = ""; + const interfaces: string[] = []; + + if (baseLists.length > 0) { + const baseList = baseLists[0]; + let isFirst = true; + for (let i = 0; i < baseList.childCount; i++) { + const child = baseList.child(i); + if (child && child.type !== ":" && child.type !== ",") { + // Use full type name including generic parameters + const typeName = child.text.trim(); + if (isFirst && !isInterface) { + // First item could be base class or interface + // Check if it's an interface (starts with I followed by uppercase) + const baseTypeName = typeName.replace(/<.*>/, ""); // Strip generics for interface check + if (baseTypeName.startsWith("I") && baseTypeName.length > 1 && baseTypeName[1] === baseTypeName[1].toUpperCase()) { + interfaces.push(typeName); + } else { + baseClass = typeName; + } + isFirst = false; + } else { + interfaces.push(typeName); + } + } + } + } + + // Get doc comments + const classDoc = getDocComments(declNode); + const { summary, remarks } = parseDocComment(classDoc); + + // Extract properties and fields + const properties: ClassDoc["properties"] = []; + + // Fields + const fieldDecls = findNodesByType(declNode, "field_declaration"); + for (const field of fieldDecls) { + // Check if public + let fieldPublic = false; + for (let i = 0; i < field.childCount; i++) { + const child = field.child(i); + if (child?.type === "modifier" && child.text === "public") { + fieldPublic = true; + break; + } + } + if (!fieldPublic) continue; + + const fieldDoc = getDocComments(field); + const { summary: fieldSummary } = parseDocComment(fieldDoc); + const fieldType = getFieldText(field, "type") || "unknown"; + + const variableDeclarators = findNodesByType(field, "variable_declarator"); + for (const declarator of variableDeclarators) { + const varName = getFieldText(declarator, "name") || declarator.text.split("=")[0].trim(); + properties.push({ name: varName, type: fieldType, summary: fieldSummary || "" }); + } + } + + // Properties + const propDecls = findNodesByType(declNode, "property_declaration"); + for (const prop of propDecls) { + // Interface members are implicitly public - skip modifier check for interfaces + let propPublic = isInterface; + if (!propPublic) { + for (let i = 0; i < prop.childCount; i++) { + const child = prop.child(i); + if (child?.type === "modifier" && child.text === "public") { + propPublic = true; + break; + } + } + } + if (!propPublic) continue; + + const propDoc = getDocComments(prop); + const { summary: propSummary } = parseDocComment(propDoc); + const propType = getFieldText(prop, "type") || "unknown"; + const propName = getFieldText(prop, "name"); + + if (propName) { + properties.push({ name: propName, type: propType, summary: propSummary || "" }); + } + } + + // Extract constructors + const constructors: ConstructorDoc[] = []; + const constructorDecls = findNodesByType(declNode, "constructor_declaration"); + + for (const ctor of constructorDecls) { + // Check if public + let ctorPublic = false; + for (let i = 0; i < ctor.childCount; i++) { + const child = ctor.child(i); + if (child?.type === "modifier" && child.text === "public") { + ctorPublic = true; + break; + } + } + if (!ctorPublic) continue; + + const ctorDoc = getDocComments(ctor); + const { summary: ctorSummary } = parseDocComment(ctorDoc); + + // Extract parameter documentation from tags + const paramDocs: Record = {}; + const paramMatches = ctorDoc.matchAll(/\s*([\s\S]*?)\s*<\/param>/g); + for (const match of paramMatches) { + paramDocs[match[1]] = match[2].replace(/^\s*\/\/\/\s*/gm, "").trim(); + } + + const paramsNode = ctor.childForFieldName("parameters"); + const params = paramsNode ? paramsNode.text : "()"; + + // Parse individual parameters + const parameters: Array<{ name: string; type: string; summary: string }> = []; + if (paramsNode) { + const paramNodes = findNodesByType(paramsNode, "parameter"); + for (const param of paramNodes) { + const paramName = getFieldText(param, "name"); + const paramType = getFieldText(param, "type") || "unknown"; + parameters.push({ + name: paramName, + type: paramType, + summary: paramDocs[paramName] || "" + }); + } + } + + constructors.push({ + signature: `${className}${params}`, + parameters, + summary: ctorSummary || "" + }); + } + + // Extract methods + const methods: ClassDoc["methods"] = []; + const methodDecls = findNodesByType(declNode, "method_declaration"); + + for (const method of methodDecls) { + // Interface members are implicitly public - skip modifier check for interfaces + let methodPublic = isInterface; + if (!methodPublic) { + for (let i = 0; i < method.childCount; i++) { + const child = method.child(i); + if (child?.type === "modifier" && child.text === "public") { + methodPublic = true; + break; + } + } + } + if (!methodPublic) continue; + + const methodDoc = getDocComments(method); + const { summary: methodSummary, inheritdoc } = parseDocComment(methodDoc); + + const methodName = getFieldText(method, "name"); + + // Get return type - try multiple field names used by tree-sitter C# + let returnType = getFieldText(method, "returns") || + getFieldText(method, "return_type") || + getFieldText(method, "type"); + + // If still not found, look for the first type child before the method name + if (!returnType) { + for (let i = 0; i < method.childCount; i++) { + const child = method.child(i); + if (child?.type === "predefined_type" || + child?.type === "identifier" || + child?.type === "generic_name" || + child?.type === "qualified_name" || + child?.type === "nullable_type" || + child?.type === "array_type" || + child?.type === "void_keyword") { + // Check if this is before the name + const nameNode = method.childForFieldName("name"); + if (nameNode && child.endIndex < nameNode.startIndex) { + returnType = child.text; + break; + } + } + } + } + + if (!returnType) returnType = "void"; + + const paramsNode = method.childForFieldName("parameters"); + const params = paramsNode ? paramsNode.text : "()"; + + const signature = `${returnType} ${methodName}${params}`; + + methods.push({ + name: methodName, + signature, + summary: inheritdoc ? "(inherited)" : (methodSummary || ""), + returnType + }); + } + + results.push({ + name: className, + namespace, + baseClass, + interfaces, + summary, + remarks, + properties, + methods, + constructors, + filePath, + isInterface, + isStruct, + isAbstract, + isGeneric, + genericParams + }); + } + + return results; + } catch (err) { + console.error(`Error parsing ${filePath}:`, err); + return []; + } +} + +function matchesBaseClass(classDoc: ClassDoc, targetBaseClasses: string[]): boolean { + if (targetBaseClasses.length === 0) return true; // Match all if no filter + + const { baseClass, interfaces, name } = classDoc; + + for (const target of targetBaseClasses) { + // Direct base class match + if (baseClass === target) return true; + if (baseClass.replace(/<[^>]+>/, "") === target) return true; + + // Interface match + for (const iface of interfaces) { + if (iface === target) return true; + if (iface.replace(/<[^>]+>/, "") === target) return true; + } + + // Name contains target (for inheritance chains) + if (baseClass.includes(target)) return true; + + // Self-match for interfaces + if (classDoc.isInterface && name === target) return true; + } + + return false; +} + +async function findClassesInCategory(category: ClassCategory): Promise { + const categoryInfo = CLASS_CATEGORIES[category]; + const classes: ClassDoc[] = []; + const filesToParse: string[] = []; + + function collectFiles(dir: string, recursive: boolean) { + if (!fs.existsSync(dir)) return; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory() && recursive && !entry.name.startsWith(".") && entry.name !== "obj" && entry.name !== "bin") { + collectFiles(fullPath, recursive); + } else if (entry.isFile() && entry.name.endsWith(".cs")) { + filesToParse.push(fullPath); + } + } + } + + for (const dir of categoryInfo.dirs) { + const fullDir = path.join(ROGUE_DIR, dir); + collectFiles(fullDir, categoryInfo.recursive); + } + + for (const filePath of filesToParse) { + const fileDocs = await parseClassFile(filePath); + for (const classDoc of fileDocs) { + // Filter by interfaces only if specified + if ((categoryInfo as any).interfacesOnly && !classDoc.isInterface) continue; + + // Filter by base class + if (matchesBaseClass(classDoc, [...categoryInfo.baseClasses])) { + classes.push(classDoc); + } + } + } + + return classes; +} + +async function findClassByName(className: string): Promise { + for (const category of Object.keys(CLASS_CATEGORIES) as ClassCategory[]) { + const classes = await findClassesInCategory(category); + const found = classes.find(c => c.name.toLowerCase() === className.toLowerCase()); + if (found) return found; + } + return null; +} + +function levenshteinDistance(a: string, b: string): number { + const matrix: number[][] = []; + + for (let i = 0; i <= b.length; i++) matrix[i] = [i]; + for (let j = 0; j <= a.length; j++) matrix[0][j] = j; + + for (let i = 1; i <= b.length; i++) { + for (let j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j] + 1 + ); + } + } + } + + return matrix[b.length][a.length]; +} + +// ============================================================================= +// ENHANCED SEARCH SCORING +// ============================================================================= + +function calculateRelevanceScore(cls: ClassDoc, queryLower: string): number { + const nameLower = cls.name.toLowerCase(); + const summaryLower = (cls.summary || "").toLowerCase(); + const queryWords = queryLower.split(/\s+/).filter(w => w.length >= 2); + + // Exact name match - best + if (nameLower === queryLower) return 0; + + // Name starts with query + if (nameLower.startsWith(queryLower)) return 10; + + // Name contains query + if (nameLower.includes(queryLower)) return 20; + + // All query words in name (multi-word search) + if (queryWords.length > 1 && queryWords.every(w => nameLower.includes(w))) { + return 25; + } + + // Summary contains query + if (summaryLower.includes(queryLower)) return 50; + + // All query words in summary + if (queryWords.length > 1 && queryWords.every(w => summaryLower.includes(w))) { + return 55; + } + + // Property or method name matches + const propNames = cls.properties.map(p => p.name.toLowerCase()).join(" "); + const methodNames = cls.methods.map(m => m.name.toLowerCase()).join(" "); + if (propNames.includes(queryLower) || methodNames.includes(queryLower)) { + return 70; + } + + // Any query word appears anywhere + const allText = `${nameLower} ${summaryLower} ${propNames} ${methodNames}`; + if (queryWords.some(w => allText.includes(w))) return 80; + + // Fuzzy match (typo tolerance) + const distance = levenshteinDistance(queryLower, nameLower); + if (distance <= 3) return 100 + distance; + + return 1000; // No match +} + +// ============================================================================= +// EXAMPLES DISCOVERY +// ============================================================================= + +interface ExampleInfo { + name: string; + file: string; + description: string; + concepts: string[]; +} + +function findExamplesDir(): string { + const candidates = [ + path.resolve(__dirname, "../../RogueElements.Examples"), + path.resolve(__dirname, "../RogueElements.Examples"), + path.resolve(process.cwd(), "RogueElements.Examples"), + ]; + + for (const dir of candidates) { + if (fs.existsSync(dir)) return dir; + } + + return path.resolve(process.cwd(), "RogueElements.Examples"); +} + +const EXAMPLES_DIR = findExamplesDir(); + +const EXAMPLES: ExampleInfo[] = [ + { name: "Ex1_Tiles", file: "Ex1_Tiles/Example1.cs", description: "Static tiles and InitTilesStep basics", concepts: ["InitTilesStep", "ITiledGenContext", "Priority"] }, + { name: "Ex2_Rooms", file: "Ex2_Rooms/Example2.cs", description: "Freeform rooms via FloorPlan", concepts: ["FloorPlan", "RoomGen", "AddConnectedRoomsStep"] }, + { name: "Ex3_Grid", file: "Ex3_Grid/Example3.cs", description: "Grid-based layouts via GridPlan", concepts: ["GridPlan", "GridPathBranch", "SetGridDefaultsStep"] }, + { name: "Ex4_Stairs", file: "Ex4_Stairs/Example4.cs", description: "Stair placement and spawning", concepts: ["StairsStep", "IPlaceableGenContext", "FloorStairsStep"] }, + { name: "Ex5_Terrain", file: "Ex5_Terrain/Example5.cs", description: "Water and terrain via Perlin noise", concepts: ["PerlinWaterStep", "BlobWaterStep", "ITile"] }, + { name: "Ex6_Items", file: "Ex6_Items/Example6.cs", description: "Item spawning and spawn lists", concepts: ["RandomSpawnStep", "SpawnList", "PickerSpawner"] }, + { name: "Ex7_Special", file: "Ex7_Special/Example7.cs", description: "Special room placement", concepts: ["SetGridSpecialRoomStep", "SetSpecialRoomStep", "ImmutableRoom"] }, + { name: "Ex8_Integration", file: "Ex8_Integration/Example8.cs", description: "Full pipeline combining all concepts", concepts: ["MapGen", "GenStep", "Full pipeline"] }, +]; + +async function findSimilarClasses(searchName: string, limit: number = 5): Promise> { + const searchLower = searchName.toLowerCase(); + const allMatches: Array<{ name: string; category: string; score: number }> = []; + + for (const category of Object.keys(CLASS_CATEGORIES) as ClassCategory[]) { + const classes = await findClassesInCategory(category); + + for (const cls of classes) { + const nameLower = cls.name.toLowerCase(); + let score = levenshteinDistance(searchLower, nameLower); + + if (nameLower.includes(searchLower) || searchLower.includes(nameLower)) { + score = Math.max(0, score - 5); + } + + const searchParts = searchLower.replace(/step|gen|room|path|plan/gi, "").trim(); + if (searchParts && nameLower.includes(searchParts)) { + score = Math.max(0, score - 3); + } + + allMatches.push({ name: cls.name, category, score }); + } + } + + return allMatches.sort((a, b) => a.score - b.score).slice(0, limit); +} + +interface RelatedClass { + name: string; + category: string; + relation: "same-base" | "same-interface" | "sibling" | "similar-name"; + summary: string; +} + +async function findRelatedClasses(classDoc: ClassDoc, limit: number = 8): Promise { + const related: RelatedClass[] = []; + const seen = new Set(); + seen.add(classDoc.name); // Don't include self + + for (const category of Object.keys(CLASS_CATEGORIES) as ClassCategory[]) { + const classes = await findClassesInCategory(category); + + for (const cls of classes) { + if (seen.has(cls.name)) continue; + + // Same base class (e.g., both inherit from GenStep) + if (classDoc.baseClass && cls.baseClass) { + const baseA = classDoc.baseClass.replace(/<[^>]+>/, ""); + const baseB = cls.baseClass.replace(/<[^>]+>/, ""); + if (baseA === baseB && baseA !== "object") { + related.push({ + name: cls.name, + category, + relation: "same-base", + summary: cls.summary || "" + }); + seen.add(cls.name); + continue; + } + } + + // Shares an interface + if (classDoc.interfaces.length > 0 && cls.interfaces.length > 0) { + const docInterfaces = classDoc.interfaces.map(i => i.replace(/<[^>]+>/, "")); + const clsInterfaces = cls.interfaces.map(i => i.replace(/<[^>]+>/, "")); + const shared = docInterfaces.filter(i => clsInterfaces.includes(i)); + if (shared.length > 0) { + related.push({ + name: cls.name, + category, + relation: "same-interface", + summary: cls.summary || "" + }); + seen.add(cls.name); + continue; + } + } + + // Similar name pattern (e.g., RoomGenSquare and RoomGenRound) + const docPrefix = classDoc.name.match(/^([A-Z][a-z]+(?:[A-Z][a-z]+)*?)(?=[A-Z][a-z]+$|Step$|Gen$)/)?.[1]; + const clsPrefix = cls.name.match(/^([A-Z][a-z]+(?:[A-Z][a-z]+)*?)(?=[A-Z][a-z]+$|Step$|Gen$)/)?.[1]; + if (docPrefix && clsPrefix && docPrefix === clsPrefix && docPrefix.length > 3) { + related.push({ + name: cls.name, + category, + relation: "sibling", + summary: cls.summary || "" + }); + seen.add(cls.name); + continue; + } + } + } + + // Sort: same-base first, then same-interface, then sibling + const priority: Record = { + "same-base": 0, + "same-interface": 1, + "sibling": 2, + "similar-name": 3 + }; + + return related + .sort((a, b) => priority[a.relation] - priority[b.relation]) + .slice(0, limit); +} + +// ============================================================================= +// MCP SERVER SETUP +// ============================================================================= + +const server = new McpServer({ + name: "rogueelemens-mcp-server", + version: "2.0.0" +}); + +// ============================================================================= +// RESOURCES - Documentation files +// ============================================================================= + +const DOC_FILES = ["architecture", "flows", "patterns"] as const; + +for (const docName of DOC_FILES) { + server.resource( + `rogue-docs-${docName}`, + `rogue://docs/${docName}`, + async () => { + const filePath = path.join(DOCS_DIR, `${docName}.md`); + try { + const content = fs.readFileSync(filePath, "utf-8"); + return { + contents: [{ + uri: `rogue://docs/${docName}`, + mimeType: "text/markdown", + text: content + }] + }; + } catch { + return { + contents: [{ + uri: `rogue://docs/${docName}`, + mimeType: "text/plain", + text: `Error: Could not read ${docName}.md` + }] + }; + } + } + ); +} + +// ============================================================================= +// RESOURCES - Class categories (browsable) +// ============================================================================= + +for (const [category, info] of Object.entries(CLASS_CATEGORIES)) { + server.resource( + `rogue-classes-${category}`, + `rogue://classes/${category}`, + async () => { + const classes = await findClassesInCategory(category as ClassCategory); + + const lines = [ + `# ${category} Classes`, + "", + `**Description:** ${info.description}`, + `**Base Classes:** ${info.baseClasses.length > 0 ? info.baseClasses.map(b => `\`${b}\``).join(", ") : "(all public classes)"}`, + `**Directories:** ${info.dirs.map(d => `\`RogueElements/${d}\``).join(", ")}`, + `**Count:** ${classes.length}`, + "", + "## Classes", + "" + ]; + + for (const cls of classes) { + const typeLabel = cls.isInterface ? "[interface]" : cls.isAbstract ? "[abstract]" : ""; + const genericLabel = cls.isGeneric ? cls.genericParams : ""; + lines.push(`### ${cls.name}${genericLabel} ${typeLabel}`); + if (cls.summary) lines.push(cls.summary); + if (cls.baseClass) lines.push(`- **Base:** \`${cls.baseClass}\``); + if (cls.interfaces.length > 0) { + lines.push(`- **Implements:** ${cls.interfaces.map(i => `\`${i}\``).join(", ")}`); + } + if (cls.properties.length > 0) { + const propNames = cls.properties.slice(0, 5).map(p => p.name).join(", "); + const more = cls.properties.length > 5 ? `, ... (+${cls.properties.length - 5} more)` : ""; + lines.push(`- **Properties:** ${propNames}${more}`); + } + lines.push(""); + } + + return { + contents: [{ + uri: `rogue://classes/${category}`, + mimeType: "text/markdown", + text: lines.join("\n") + }] + }; + } + ); +} + +// ============================================================================= +// PROMPTS +// ============================================================================= + +server.prompt( + "create_roomgen", + "Guide for creating a custom RoomGen in RogueElements", + () => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `Help me create a custom RoomGen for RogueElements. + +A RoomGen defines the shape of a room. Here's what I need to implement: + +1. Inherit from \`RoomGen\` or \`PermissiveRoomGen\` where T : ITiledGenContext +2. Override \`ProposeSize(IRandom rand)\` to return room dimensions +3. Override \`DrawOnMap(T map)\` to render the room shape to tiles +4. Implement \`Copy()\` for cloning + +Use \`rogue_list_classes\` with category "roomgen" to see existing examples. + +What shape of room would you like to create?` + } + }] + }) +); + +server.prompt( + "create_genstep", + "Guide for creating a custom GenStep in RogueElements", + () => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `Help me create a custom GenStep for RogueElements. + +A GenStep is a single step in the map generation pipeline. Here's what I need to implement: + +1. Inherit from \`GenStep\` where T constrains to required context interfaces +2. Add \`[Serializable]\` attribute for save/load support +3. Override \`Apply(T map)\` with generation logic +4. Add to pipeline with \`layout.GenSteps.Add(new Priority(N), step)\` + +Common context interfaces: +- \`ITiledGenContext\` - Basic tile operations +- \`IFloorPlanGenContext\` - Room-based layouts +- \`IRoomGridGenContext\` - Grid-based layouts +- \`IPlaceableGenContext\` - Entity spawning + +Use \`rogue_list_classes\` with category "genstep" to see existing examples. + +What kind of generation step would you like to create?` + } + }] + }) +); + +server.prompt( + "create_spawnable", + "Guide for creating a custom spawnable type in RogueElements", + () => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `Help me create a custom spawnable type for RogueElements. + +A spawnable is any entity that can be placed on the map (items, enemies, traps, etc.). + +Steps: +1. Create a class implementing \`ISpawnable\` (just needs \`Copy()\` method) +2. Add \`IPlaceableGenContext\` to your map context +3. Use spawn steps like \`RandomSpawnStep\` to place them + +Use \`rogue_list_classes\` with category "spawning" to see existing spawning infrastructure. + +What kind of entity would you like to spawn?` + } + }] + }) +); + +server.prompt( + "create_context", + "Guide for creating a custom map context in RogueElements", + () => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `Help me create a custom map context for RogueElements. + +The map context holds all state during generation. Implement interfaces based on what GenSteps you need: + +Interface hierarchy: +\`\`\` +IGenContext (base - Rand, InitSeed, FinishGen) +├── ITiledGenContext (tiles - Width, Height, SetTile, GetTile) +│ └── IFloorPlanGenContext (rooms - RoomPlan, InitPlan) +│ └── IRoomGridGenContext (grid - GridPlan, InitGrid) +└── IPlaceableGenContext (spawning - PlaceItem, GetFreeTiles) +\`\`\` + +Use \`rogue_list_classes\` with category "context" to see the interface definitions. + +What capabilities does your context need?` + } + }] + }) +); + +// ============================================================================= +// TOOLS - Search & Browse +// ============================================================================= + +server.tool( + "rogue_search", + `Search for RogueElements classes across all categories by name or summary. + +Searches class names and XML documentation summaries. Returns matches ranked by relevance. +Use this when you're not sure which category a class belongs to, or to find classes related to a concept. + +Categories searched: ${Object.keys(CLASS_CATEGORIES).join(", ")}`, + { + query: z.string() + .min(2) + .describe("Search query (e.g., 'water', 'spawn', 'grid', 'room')"), + limit: z.number() + .min(1) + .max(50) + .default(10) + .describe("Maximum results to return"), + response_format: z.enum(["markdown", "json"]) + .default("markdown") + .describe("Output format: markdown (default) or json") + }, + async ({ query, limit, response_format }) => { + const queryLower = query.toLowerCase(); + const results: Array<{ + name: string; + category: ClassCategory; + summary: string; + score: number; + isInterface: boolean; + isStruct: boolean; + }> = []; + + for (const category of Object.keys(CLASS_CATEGORIES) as ClassCategory[]) { + const classes = await findClassesInCategory(category); + + for (const cls of classes) { + const score = calculateRelevanceScore(cls, queryLower); + if (score < 1000) { + results.push({ + name: cls.name, + category, + summary: cls.summary || "(no documentation)", + score, + isInterface: cls.isInterface, + isStruct: cls.isStruct + }); + } + } + } + + const sorted = results.sort((a, b) => a.score - b.score).slice(0, limit); + + if (sorted.length === 0) { + return { + content: [{ + type: "text", + text: `No classes found matching '${query}'. Try a different search term or use rogue_list_classes to browse by category.` + }] + }; + } + + if (response_format === "json") { + return { + content: [{ + type: "text", + text: JSON.stringify({ + query, + count: sorted.length, + results: sorted.map(r => ({ + name: r.name, + category: r.category, + type: r.isInterface ? "interface" : r.isStruct ? "struct" : "class", + summary: r.summary + })) + }, null, 2) + }] + }; + } + + const lines = [ + `# Search Results: "${query}"`, + "", + `Found ${sorted.length} matching classes:`, + "", + "| Class | Category | Type | Summary |", + "|-------|----------|------|---------|" + ]; + + for (const result of sorted) { + const summary = result.summary.length > 50 + ? result.summary.substring(0, 47) + "..." + : result.summary; + const typeLabel = result.isInterface ? "interface" : result.isStruct ? "struct" : "class"; + lines.push(`| \`${result.name}\` | ${result.category} | ${typeLabel} | ${summary} |`); + } + + lines.push(""); + lines.push("*Use `rogue_get_class_docs` for full documentation on any class.*"); + + return { + content: [{ type: "text", text: lines.join("\n") }] + }; + } +); + +server.tool( + "rogue_list_classes", + `List all RogueElements classes in a specific category. + +Categories: ${Object.keys(CLASS_CATEGORIES).join(", ")} + +Returns class names with brief summaries from XML documentation.`, + { + category: z.enum(Object.keys(CLASS_CATEGORIES) as [ClassCategory, ...ClassCategory[]]) + .describe("Category of classes to list"), + limit: z.number() + .min(1) + .max(100) + .default(50) + .describe("Maximum results to return (default 50)"), + offset: z.number() + .min(0) + .default(0) + .describe("Number of results to skip (for pagination)"), + response_format: z.enum(["markdown", "json"]) + .default("markdown") + .describe("Output format: markdown (default) or json") + }, + async ({ category, limit, offset, response_format }) => { + const allClasses = await findClassesInCategory(category); + const categoryInfo = CLASS_CATEGORIES[category]; + + if (allClasses.length === 0) { + return { + content: [{ + type: "text", + text: `No classes found in category '${category}' (searched ${categoryInfo.dirs.join(", ")})` + }] + }; + } + + const classes = allClasses.slice(offset, offset + limit); + const hasMore = offset + limit < allClasses.length; + + if (response_format === "json") { + return { + content: [{ + type: "text", + text: JSON.stringify({ + category, + description: categoryInfo.description, + total: allClasses.length, + offset, + limit, + hasMore, + nextOffset: hasMore ? offset + limit : null, + classes: classes.map(c => ({ + name: c.name + (c.isGeneric ? c.genericParams : ""), + type: c.isInterface ? "interface" : c.isStruct ? "struct" : c.isAbstract ? "abstract" : "class", + baseClass: c.baseClass || null, + summary: c.summary || null + })) + }, null, 2) + }] + }; + } + + const lines = [ + `# ${category} Classes`, + "", + `**Description:** ${categoryInfo.description}`, + `**Total:** ${allClasses.length}${offset > 0 ? ` (showing ${offset + 1}-${Math.min(offset + limit, allClasses.length)})` : ""}`, + "", + "| Class | Type | Summary |", + "|-------|------|---------|" + ]; + + for (const cls of classes) { + const summary = cls.summary ? cls.summary.substring(0, 60) : "(no docs)"; + const typeLabel = cls.isInterface ? "interface" : cls.isStruct ? "struct" : cls.isAbstract ? "abstract" : "class"; + const generic = cls.isGeneric ? cls.genericParams : ""; + lines.push(`| \`${cls.name}${generic}\` | ${typeLabel} | ${summary} |`); + } + + if (hasMore) { + lines.push(""); + lines.push(`*More results available. Use offset=${offset + limit} for next page.*`); + } + + return { + content: [{ type: "text", text: lines.join("\n") }] + }; + } +); + +server.tool( + "rogue_get_class_docs", + `Get detailed XML documentation for a specific RogueElements class or interface. + +Extracts from C# source files: +- Class summary and remarks +- Namespace and base class +- Implemented interfaces +- Public properties with their types and documentation +- Public methods with their signatures and documentation`, + { + class_name: z.string() + .min(1) + .describe("Name of the class or interface to get documentation for"), + response_format: z.enum(["markdown", "json"]) + .default("markdown") + .describe("Output format: markdown (default) or json") + }, + async ({ class_name, response_format }) => { + const classDoc = await findClassByName(class_name); + + if (!classDoc) { + const suggestions = await findSimilarClasses(class_name, 5); + + let errorMsg = `Class '${class_name}' not found in RogueElements.\n\n`; + + if (suggestions.length > 0) { + errorMsg += "**Did you mean one of these?**\n\n"; + for (const suggestion of suggestions) { + errorMsg += `- \`${suggestion.name}\` (${suggestion.category})\n`; + } + errorMsg += "\n*Use `rogue_search` for broader search or `rogue_list_classes` to browse by category.*"; + } + + return { + content: [{ type: "text", text: errorMsg }] + }; + } + + // Find related classes + const relatedClasses = await findRelatedClasses(classDoc); + + if (response_format === "json") { + return { + content: [{ + type: "text", + text: JSON.stringify({ + name: classDoc.name, + genericParams: classDoc.genericParams || null, + type: classDoc.isInterface ? "interface" : classDoc.isStruct ? "struct" : classDoc.isAbstract ? "abstract" : "class", + namespace: classDoc.namespace, + baseClass: classDoc.baseClass || null, + interfaces: classDoc.interfaces, + file: classDoc.filePath.replace(ROGUE_DIR, "RogueElements"), + summary: classDoc.summary || null, + remarks: classDoc.remarks || null, + constructors: classDoc.constructors, + properties: classDoc.properties, + methods: classDoc.methods, + relatedClasses: relatedClasses.map(r => ({ + name: r.name, + category: r.category, + relation: r.relation + })) + }, null, 2) + }] + }; + } + + const typeLabel = classDoc.isInterface ? "interface" : classDoc.isStruct ? "struct" : classDoc.isAbstract ? "abstract class" : "class"; + const generic = classDoc.isGeneric ? classDoc.genericParams : ""; + + const lines = [ + `# ${classDoc.name}${generic}`, + "", + `**Type:** ${typeLabel}`, + `**Namespace:** \`${classDoc.namespace}\``, + ]; + + if (classDoc.baseClass) { + lines.push(`**Base Class:** \`${classDoc.baseClass}\``); + } + if (classDoc.interfaces.length > 0) { + lines.push(`**Implements:** ${classDoc.interfaces.map(i => `\`${i}\``).join(", ")}`); + } + lines.push(`**File:** \`${classDoc.filePath.replace(ROGUE_DIR, "RogueElements")}\``); + lines.push(""); + + if (classDoc.summary) { + lines.push("## Summary", "", classDoc.summary, ""); + } + + if (classDoc.remarks) { + lines.push("## Remarks", "", classDoc.remarks, ""); + } + + if (classDoc.constructors.length > 0) { + lines.push("## Constructors", ""); + for (const ctor of classDoc.constructors) { + lines.push(`### \`${ctor.signature}\``); + if (ctor.summary) lines.push(ctor.summary); + if (ctor.parameters.length > 0) { + lines.push(""); + lines.push("**Parameters:**"); + for (const param of ctor.parameters) { + const paramDoc = param.summary ? ` - ${param.summary}` : ""; + lines.push(`- \`${param.name}\`: \`${param.type}\`${paramDoc}`); + } + } + lines.push(""); + } + } + + if (classDoc.properties.length > 0) { + lines.push("## Properties", ""); + for (const prop of classDoc.properties) { + lines.push(`### \`${prop.name}\` : \`${prop.type}\``); + if (prop.summary) lines.push(prop.summary); + lines.push(""); + } + } + + if (classDoc.methods.length > 0) { + lines.push("## Methods", ""); + for (const method of classDoc.methods) { + lines.push(`### \`${method.signature}\``); + if (method.summary) lines.push(method.summary); + lines.push(""); + } + } + + if (relatedClasses.length > 0) { + lines.push("## Related Classes", ""); + lines.push("| Class | Category | Relation |"); + lines.push("|-------|----------|----------|"); + for (const related of relatedClasses) { + const relationLabel = related.relation === "same-base" ? "Same base class" : + related.relation === "same-interface" ? "Implements same interface" : + related.relation === "sibling" ? "Same family" : "Similar name"; + lines.push(`| \`${related.name}\` | ${related.category} | ${relationLabel} |`); + } + lines.push(""); + lines.push("*Use `rogue_get_class_docs` to get details on any related class.*"); + } + + return { + content: [{ type: "text", text: lines.join("\n") }] + }; + } +); + +// ============================================================================= +// TOOLS - Documentation Access +// ============================================================================= + +const DOC_DESCRIPTIONS: Record = { + architecture: "Interface hierarchy, GenStep categories, data flow diagrams, and priority conventions", + flows: "Traced code paths for key operations like map generation and room placement", + patterns: "Step-by-step recipes for common modifications and custom implementations" +}; + +server.tool( + "rogue_get_docs", + `Access AI-optimized documentation for RogueElements. + +Available documents: +- architecture: ${DOC_DESCRIPTIONS.architecture} +- flows: ${DOC_DESCRIPTIONS.flows} +- patterns: ${DOC_DESCRIPTIONS.patterns} + +Omit the name parameter to list all available docs.`, + { + name: z.string() + .optional() + .describe("Document name (architecture, flows, patterns). Omit to list all.") + }, + async ({ name }) => { + if (!name) { + const lines = [ + "# RogueElements Documentation", + "", + "AI-optimized reference documentation for the RogueElements library.", + "", + "| Document | Description |", + "|----------|-------------|" + ]; + for (const [docName, desc] of Object.entries(DOC_DESCRIPTIONS)) { + lines.push(`| \`${docName}\` | ${desc} |`); + } + lines.push(""); + lines.push("*Use `rogue_get_docs` with a name to read a specific document.*"); + return { content: [{ type: "text", text: lines.join("\n") }] }; + } + + const docPath = path.join(DOCS_DIR, `${name}.md`); + if (!fs.existsSync(docPath)) { + return { + content: [{ + type: "text", + text: `Document '${name}' not found.\n\nAvailable documents: ${Object.keys(DOC_DESCRIPTIONS).join(", ")}` + }] + }; + } + + const content = fs.readFileSync(docPath, "utf-8"); + return { content: [{ type: "text", text: content }] }; + } +); + +server.tool( + "rogue_get_example", + `Get annotated source code from the RogueElements.Examples project. + +Examples demonstrate progressive complexity: +${EXAMPLES.map(e => `- ${e.name}: ${e.description}`).join("\n")} + +Use this to see real implementation patterns.`, + { + name: z.string() + .optional() + .describe("Example name (Ex1_Tiles, Ex2_Rooms, etc.). Omit to list all."), + concept: z.string() + .optional() + .describe("Search for examples using a specific concept (e.g., 'GridPlan', 'spawning')") + }, + async ({ name, concept }) => { + // If searching by concept + if (concept) { + const conceptLower = concept.toLowerCase(); + const matches = EXAMPLES.filter(e => + e.concepts.some(c => c.toLowerCase().includes(conceptLower)) || + e.description.toLowerCase().includes(conceptLower) || + e.name.toLowerCase().includes(conceptLower) + ); + + if (matches.length === 0) { + return { + content: [{ + type: "text", + text: `No examples found for concept '${concept}'.\n\nAvailable examples: ${EXAMPLES.map(e => e.name).join(", ")}` + }] + }; + } + + const lines = [ + `# Examples for "${concept}"`, + "", + "| Example | Description | Related Concepts |", + "|---------|-------------|------------------|" + ]; + for (const e of matches) { + lines.push(`| \`${e.name}\` | ${e.description} | ${e.concepts.join(", ")} |`); + } + lines.push(""); + lines.push("*Use `rogue_get_example` with a specific name to read the source code.*"); + return { content: [{ type: "text", text: lines.join("\n") }] }; + } + + // List all examples + if (!name) { + const lines = [ + "# RogueElements Examples", + "", + "Progressive examples from basic to advanced:", + "", + "| Example | Description | Key Concepts |", + "|---------|-------------|--------------|" + ]; + for (const e of EXAMPLES) { + lines.push(`| \`${e.name}\` | ${e.description} | ${e.concepts.join(", ")} |`); + } + lines.push(""); + lines.push("*Use `rogue_get_example` with a name to read source code, or use `concept` to search.*"); + return { content: [{ type: "text", text: lines.join("\n") }] }; + } + + // Find and return specific example + const example = EXAMPLES.find(e => e.name.toLowerCase() === name.toLowerCase()); + if (!example) { + return { + content: [{ + type: "text", + text: `Example '${name}' not found.\n\nAvailable examples: ${EXAMPLES.map(e => e.name).join(", ")}` + }] + }; + } + + const examplePath = path.join(EXAMPLES_DIR, example.file); + if (!fs.existsSync(examplePath)) { + return { + content: [{ + type: "text", + text: `Example file '${example.file}' not found at ${examplePath}` + }] + }; + } + + const content = fs.readFileSync(examplePath, "utf-8"); + const lines = [ + `# ${example.name}`, + "", + `**Description:** ${example.description}`, + `**Key Concepts:** ${example.concepts.join(", ")}`, + "", + "## Source Code", + "", + "```csharp", + content, + "```" + ]; + + return { content: [{ type: "text", text: lines.join("\n") }] }; + } +); + +// ============================================================================= +// TOOLS - Code Generation +// ============================================================================= + +server.tool( + "rogue_scaffold_roomgen", + `Generate boilerplate code for a custom RogueElements RoomGen. + +Creates a properly structured class with: +- Serializable attribute for save/load +- Configurable properties with proper Clone() support +- ProposeSize() and DrawOnMap() methods +- Example shape logic`, + { + name: z.string() + .min(1) + .describe("Name for the RoomGen class (e.g., 'Diamond', 'Cross')"), + shape_description: z.string() + .describe("Description of the room shape to generate"), + has_properties: z.boolean() + .default(true) + .describe("Include configurable size properties") + }, + async ({ name, shape_description, has_properties }) => { + const className = `RoomGen${name}`; + const code = `using System; +using RogueElements; + +namespace YourNamespace +{ + /// + /// Generates ${shape_description.toLowerCase()} shaped rooms. + /// + [Serializable] + public class ${className} : RoomGen + where T : ITiledGenContext + {${has_properties ? ` + /// + /// Range of possible room widths. + /// + public RandRange Width { get; set; } + + /// + /// Range of possible room heights. + /// + public RandRange Height { get; set; } + + public ${className}() + { + this.Width = new RandRange(5, 10); + this.Height = new RandRange(5, 10); + } + + public ${className}(RandRange width, RandRange height) + { + this.Width = width; + this.Height = height; + } + + protected ${className}(${className} other) + { + this.Width = other.Width; + this.Height = other.Height; + } + + public override RoomGen Copy() => new ${className}(this); +` : ` + public override RoomGen Copy() => new ${className}(); +`} + public override Loc ProposeSize(IRandom rand) + {${has_properties ? ` + return new Loc(this.Width.Pick(rand), this.Height.Pick(rand));` : ` + // TODO: Return appropriate dimensions for ${shape_description} + return new Loc(rand.Next(5, 10), rand.Next(5, 10));`} + } + + public override void DrawOnMap(T map) + { + // this.Draw contains the room bounds (X, Y, Width, Height, End) + int centerX = this.Draw.X + this.Draw.Width / 2; + int centerY = this.Draw.Y + this.Draw.Height / 2; + + for (int x = this.Draw.X; x < this.Draw.End.X; x++) + { + for (int y = this.Draw.Y; y < this.Draw.End.Y; y++) + { + Loc loc = new Loc(x, y); + + // TODO: Implement ${shape_description} shape logic + // Example: check distance from center, edges, etc. + bool inShape = true; // Replace with your shape condition + + if (inShape) + { + map.SetTile(loc, map.RoomTerrain.Copy()); + } + } + } + + // Draw border tiles for fulfillables + this.SetRoomBorders(map); + } + } +}`; + + return { + content: [{ + type: "text", + text: `# Generated RoomGen: ${className} + +\`\`\`csharp +${code} +\`\`\` + +## Next Steps + +1. **Implement shape logic** in DrawOnMap(): + - Use \`centerX\`/\`centerY\` for radial shapes + - Use \`this.Draw\` bounds for edge-based shapes + - Set \`inShape\` condition for your pattern + +2. **Add to room pool**: + \`\`\`csharp + var roomGen = new SpawnList>(); + roomGen.Add(new ${className}(new RandRange(5, 8), new RandRange(5, 8)), 10); + \`\`\` + +3. **Use with grid or floor paths**: + \`\`\`csharp + layout.GenSteps.Add(new Priority(15), new SetGridDefaultsStep(roomGen)); + \`\`\` + +*See \`rogue_get_example\` with name "Ex3_Grid" for complete usage.*` + }] + }; + } +); + +server.tool( + "rogue_scaffold_genstep", + `Generate boilerplate code for a custom RogueElements GenStep. + +Creates a properly structured class with: +- Serializable attribute for save/load +- Configurable properties with proper Clone() support +- Context-specific API comments +- Example iteration patterns`, + { + name: z.string() + .min(1) + .describe("Name for the GenStep class (e.g., 'AddPillars', 'ScatterItems')"), + context_type: z.enum(["ITiledGenContext", "IFloorPlanGenContext", "IRoomGridGenContext"]) + .describe("The context interface this step requires"), + description: z.string() + .describe("What this generation step does"), + has_properties: z.boolean() + .default(true) + .describe("Include configurable properties") + }, + async ({ name, context_type, description, has_properties }) => { + // Don't duplicate "Step" suffix if already present + const className = name.endsWith("Step") ? name : `${name}Step`; + const code = `using System; +using RogueElements; + +namespace YourNamespace +{ + /// + /// ${description} + /// + [Serializable] + public class ${className} : GenStep + where T : class, ${context_type} + {${has_properties ? ` + /// + /// Probability of applying the effect (0-100). + /// + public int Chance { get; set; } + + /// + /// Terrain to use for the effect. + /// + public ITile Terrain { get; set; } + + public ${className}() + { + this.Chance = 50; + } + + public ${className}(ITile terrain, int chance = 50) + { + this.Terrain = terrain; + this.Chance = chance; + } +` : ""} + public override void Apply(T map) + { + // Available from ITiledGenContext: + // - map.Rand: Seeded random number generator + // - map.Width, map.Height: Map dimensions + // - map.SetTile(loc, tile): Place a tile + // - map.GetTile(loc): Get tile at location + // - map.TileBlocked(loc): Check if wall + // - map.RoomTerrain, map.WallTerrain: Terrain templates +${context_type === "IFloorPlanGenContext" || context_type === "IRoomGridGenContext" ? ` + // Available from IFloorPlanGenContext: + // - map.RoomPlan: The floor plan with rooms and halls + // - map.RoomPlan.RoomCount: Number of rooms + // - map.RoomPlan.GetRoom(index): Get room by index + // - map.RoomPlan.GetRoomGen(index): Get the RoomGen that created it +` : ""}${context_type === "IRoomGridGenContext" ? ` + // Available from IRoomGridGenContext: + // - map.GridPlan: The grid plan + // - map.GridPlan.GridWidth, GridHeight: Grid dimensions + // - map.GridPlan.GetRoom(Loc): Get room at grid position +` : ""} +${context_type === "IFloorPlanGenContext" || context_type === "IRoomGridGenContext" ? ` + // Iterate over rooms + for (int i = 0; i < map.RoomPlan.RoomCount; i++) + { + IRoomPlan room = map.RoomPlan.GetRoom(i); + Rect bounds = room.RoomGen.Draw; + + // Process tiles in this room + for (int x = bounds.X; x < bounds.End.X; x++) + { + for (int y = bounds.Y; y < bounds.End.Y; y++) + { + Loc loc = new Loc(x, y); + if (!map.TileBlocked(loc, false)) + { + ${has_properties ? `if (map.Rand.Next(100) < this.Chance) + { + // TODO: Apply effect + }` : "// TODO: Apply effect"} + } + } + } + }` : ` + // Iterate over all tiles + for (int x = 0; x < map.Width; x++) + { + for (int y = 0; y < map.Height; y++) + { + Loc loc = new Loc(x, y); + if (!map.TileBlocked(loc, false)) + { + ${has_properties ? `if (map.Rand.Next(100) < this.Chance) + { + // TODO: Apply effect + }` : "// TODO: Apply effect"} + } + } + }`} + } + } +}`; + + const priorityHint = context_type === "IRoomGridGenContext" ? "10-29" : + context_type === "IFloorPlanGenContext" ? "30-59" : "60-89"; + + return { + content: [{ + type: "text", + text: `# Generated GenStep: ${className} + +\`\`\`csharp +${code} +\`\`\` + +## Next Steps + +1. **Implement your logic** in Apply(): + - Use \`map.Rand\` for randomness + - Use \`map.SetTile(loc, tile)\` to modify terrain + - Check \`map.TileBlocked()\` to avoid walls + +2. **Add to pipeline** (priority ${priorityHint} for ${context_type}): + \`\`\`csharp + layout.GenSteps.Add(new Priority(${priorityHint.split("-")[0]}), new ${className}()); + \`\`\` + +3. **Priority guide**: + - 0-9: Initialization + - 10-29: Grid operations + - 30-59: Floor plan operations + - 60-89: Tile modifications + - 90-99: Entity spawning + +*See \`rogue_get_docs\` with name "architecture" for full priority conventions.*` + }] + }; + } +); + +server.tool( + "rogue_list_interfaces", + "List RogueElements context interfaces with their capabilities", + {}, + async () => { + const interfaces = `# RogueElements Context Interfaces + +## Interface Hierarchy + +\`\`\` +IGenContext (base) +├── ITiledGenContext +│ └── IFloorPlanGenContext +│ └── IRoomGridGenContext +├── IPlaceableGenContext +│ └── IViewPlaceableGenContext +│ └── IReplaceableGenContext +└── ISpawningGenContext +\`\`\` + +## Interface Details + +### IGenContext +Base interface for all contexts. +- \`IRandom Rand\` - Seeded random number generator +- \`void InitSeed(ulong seed)\` - Initialize with seed +- \`void FinishGen()\` - Called after generation completes + +### ITiledGenContext : IGenContext +Tile-based map operations. +- \`int Width, Height\` - Map dimensions +- \`ITile RoomTerrain, WallTerrain\` - Terrain templates +- \`bool TilesInitialized\` - Whether tiles are ready +- \`void SetTile(Loc, ITile)\` - Place a tile +- \`ITile GetTile(Loc)\` - Get tile at location +- \`bool TileBlocked(Loc)\` - Check if blocked +- \`void CreateNew(width, height)\` - Create tile array + +### IFloorPlanGenContext : ITiledGenContext +Room-based layouts using FloorPlan. +- \`FloorPlan RoomPlan\` - The floor plan +- \`void InitPlan(FloorPlan)\` - Initialize with plan + +### IRoomGridGenContext : IFloorPlanGenContext +Grid-based layouts using GridPlan. +- \`GridPlan GridPlan\` - The grid plan +- \`void InitGrid(GridPlan)\` - Initialize with plan + +### IPlaceableGenContext : IGenContext +Entity spawning for type T. +- \`List GetAllFreeTiles()\` - Find spawn locations +- \`List GetFreeTiles(Rect)\` - Find in area +- \`bool CanPlaceItem(Loc)\` - Check if valid +- \`void PlaceItem(Loc, T)\` - Place entity + +### IViewPlaceableGenContext : IPlaceableGenContext +Adds ability to query placed items. +- \`int Count\` - Number of placed items +- \`T GetItem(int index)\` - Get item by index +- \`Loc GetLoc(int index)\` - Get location by index + +### IReplaceableGenContext : IViewPlaceableGenContext +Adds ability to remove items. +- \`void RemoveItemAt(int index)\` - Remove item + +### ISpawningGenContext : IGenContext +Weighted spawn lists. +- \`SpawnList Spawns\` - Spawn table +`; + + return { + content: [{ type: "text", text: interfaces }] + }; + } +); + +server.tool( + "rogue_scaffold_spawnable", + `Generate boilerplate code for a custom spawnable entity and spawn step. + +Creates: +- A spawnable entity class implementing ISpawnable +- A spawn step using IPlaceableGenContext +- Integration example with SpawnList`, + { + name: z.string() + .min(1) + .describe("Name for the spawnable entity (e.g., 'Treasure', 'Enemy', 'Trap')"), + description: z.string() + .describe("What this spawnable represents"), + spawn_type: z.enum(["random", "terminal", "room"]) + .default("random") + .describe("Spawn placement strategy: random (anywhere), terminal (dead ends), room (per-room)") + }, + async ({ name, description, spawn_type }) => { + const entityClass = name; + const stepClass = `${name}SpawnStep`; + + const code = `using System; +using System.Collections.Generic; +using RogueElements; + +namespace YourNamespace +{ + /// + /// ${description} + /// + [Serializable] + public class ${entityClass} : ISpawnable + { + /// + /// Display name for this entity. + /// + public string Name { get; set; } + + /// + /// Entity type or category. + /// + public int Type { get; set; } + + public ${entityClass}() + { + this.Name = "${name}"; + this.Type = 0; + } + + public ${entityClass}(string name, int type) + { + this.Name = name; + this.Type = type; + } + + public ISpawnable Copy() + { + return new ${entityClass} + { + Name = this.Name, + Type = this.Type + }; + } + } + + /// + /// Spawns ${description.toLowerCase()} on the map. + /// + [Serializable] + public class ${stepClass} : GenStep + where T : class, ${spawn_type === "room" ? "IFloorPlanGenContext, " : ""}IPlaceableGenContext<${entityClass}> + { + /// + /// Number of entities to spawn. + /// + public RandRange Amount { get; set; } + + /// + /// Weighted spawn table for selecting which ${entityClass} to place. + /// + public SpawnList<${entityClass}> Spawns { get; set; } + + public ${stepClass}() + { + this.Amount = new RandRange(3, 8); + this.Spawns = new SpawnList<${entityClass}>(); + } + + public ${stepClass}(SpawnList<${entityClass}> spawns, RandRange amount) + { + this.Spawns = spawns; + this.Amount = amount; + } + + public override void Apply(T map) + { + int count = this.Amount.Pick(map.Rand); +${spawn_type === "random" ? ` + // Get all available spawn locations + List freeTiles = map.GetAllFreeTiles(); + + for (int i = 0; i < count && freeTiles.Count > 0; i++) + { + // Pick random location + int idx = map.Rand.Next(freeTiles.Count); + Loc loc = freeTiles[idx]; + freeTiles.RemoveAt(idx); + + // Pick random entity from spawn list + ${entityClass} entity = this.Spawns.Pick(map.Rand).Copy() as ${entityClass}; + map.PlaceItem(loc, entity); + }` : spawn_type === "terminal" ? ` + // Find terminal (dead-end) rooms + List terminalRooms = new List(); + for (int i = 0; i < map.RoomPlan.RoomCount; i++) + { + IRoomPlan room = map.RoomPlan.GetRoom(i); + if (room.Adjacents.Count == 1) + terminalRooms.Add(i); + } + + for (int i = 0; i < count && terminalRooms.Count > 0; i++) + { + // Pick random terminal room + int roomIdx = map.Rand.Next(terminalRooms.Count); + int roomId = terminalRooms[roomIdx]; + terminalRooms.RemoveAt(roomIdx); + + IRoomPlan room = map.RoomPlan.GetRoom(roomId); + List freeTiles = map.GetFreeTiles(room.RoomGen.Draw); + + if (freeTiles.Count > 0) + { + Loc loc = freeTiles[map.Rand.Next(freeTiles.Count)]; + ${entityClass} entity = this.Spawns.Pick(map.Rand).Copy() as ${entityClass}; + map.PlaceItem(loc, entity); + } + }` : ` + // Spawn in each room + for (int i = 0; i < map.RoomPlan.RoomCount; i++) + { + IRoomPlan room = map.RoomPlan.GetRoom(i); + List freeTiles = map.GetFreeTiles(room.RoomGen.Draw); + + int roomCount = Math.Min(count, freeTiles.Count); + for (int j = 0; j < roomCount; j++) + { + int idx = map.Rand.Next(freeTiles.Count); + Loc loc = freeTiles[idx]; + freeTiles.RemoveAt(idx); + + ${entityClass} entity = this.Spawns.Pick(map.Rand).Copy() as ${entityClass}; + map.PlaceItem(loc, entity); + } + }`} + } + } +}`; + + const usageCode = `// Create spawn list with weighted entries +var ${name.toLowerCase()}Spawns = new SpawnList<${entityClass}>(); +${name.toLowerCase()}Spawns.Add(new ${entityClass}("Common ${name}", 0), 10); // Weight 10 (common) +${name.toLowerCase()}Spawns.Add(new ${entityClass}("Rare ${name}", 1), 3); // Weight 3 (rare) +${name.toLowerCase()}Spawns.Add(new ${entityClass}("Epic ${name}", 2), 1); // Weight 1 (very rare) + +// Add to pipeline (priority 90+ for spawning) +var spawnStep = new ${stepClass}(${name.toLowerCase()}Spawns, new RandRange(5, 10)); +layout.GenSteps.Add(new Priority(95), spawnStep);`; + + return { + content: [{ + type: "text", + text: `# Generated Spawnable: ${entityClass} + +\`\`\`csharp +${code} +\`\`\` + +## Usage Example + +\`\`\`csharp +${usageCode} +\`\`\` + +## Context Requirements + +Your map context must implement: +\`\`\`csharp +public class MyContext : IGenContext, ${spawn_type !== "random" ? "IFloorPlanGenContext, " : ""}IPlaceableGenContext<${entityClass}> +{ + // IPlaceableGenContext implementation + public List GetAllFreeTiles() { /* ... */ } + public List GetFreeTiles(Rect rect) { /* ... */ } + public bool CanPlaceItem(Loc loc) { /* ... */ } + public void PlaceItem(Loc loc, ${entityClass} item) { /* ... */ } +} +\`\`\` + +*See \`rogue_get_example\` with name "Ex6_Items" for complete spawning example.*` + }] + }; + } +); + +// ============================================================================= +// SERVER STARTUP +// ============================================================================= + +async function main() { + // Initialize parser before connecting + await initializeParser(); + + // Diagnostic info + console.error("═══════════════════════════════════════════════════════════════"); + console.error(" RogueElements MCP Server v2.2.1"); + console.error("═══════════════════════════════════════════════════════════════"); + console.error(` Library: ${fs.existsSync(ROGUE_DIR) ? "✓" : "✗"} ${ROGUE_DIR}`); + console.error(` Docs: ${fs.existsSync(DOCS_DIR) ? "✓" : "✗"} ${DOCS_DIR}`); + console.error(` Examples: ${fs.existsSync(EXAMPLES_DIR) ? "✓" : "✗"} ${EXAMPLES_DIR}`); + console.error(` Parser: ${csharpParser ? "✓ tree-sitter (C#)" : "✗ not initialized"}`); + console.error("───────────────────────────────────────────────────────────────"); + console.error(" Tools: rogue_search, rogue_list_classes, rogue_get_class_docs"); + console.error(" rogue_get_docs, rogue_get_example, rogue_list_interfaces"); + console.error(" rogue_scaffold_roomgen, rogue_scaffold_genstep"); + console.error(" rogue_scaffold_spawnable"); + console.error("═══════════════════════════════════════════════════════════════"); + + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Server connected via stdio transport"); +} + +main().catch(error => { + console.error("Server error:", error); + process.exit(1); +}); diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json new file mode 100644 index 00000000..654b0189 --- /dev/null +++ b/mcp-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}