A small rogue-like built from the bottom up in a custom game engine on C#/Blazor. Features a tileset renderer using the beautiful Ultimate Fantasy Tileset from Oryx, and a custom-built ASCII renderer, switchable at any time.
- Features
- Screenshots
- Getting started
- How to play
- Tileset
- Project structure
- Architecture
- Game data / configuration
- Contributing
- License
- Procedural dungeon generation, including animated liquid pools — water, mud, acid and lava; walkable, but mud/water slow you, acid burns, and lava is instant death.
- Procedurally-placed fence enclosures — walkable-blocking but see-through, using a generalized edge-blocking primitive (also used by statues) that blocks movement and close combat across a fence line, independent of the tile itself.
- A variety of monsters, animated using CSS animations, with mouse-over descriptions.
- Sounds and music, plus a screen-shake effect on hits.
- Useable environment objects (doors, chests) and field-of-view/vision.
- Pick-up items with a lettered inventory: consumable potions and toggle-equippable gear (e.g. a ring of protection), data-driven from
Data/items.json. - Basic combat, driven by a Warhammer-inspired ruleset.
- A tileset renderer (using the Ultimate Fantasy Tileset) and a from-scratch ASCII renderer (old-school format, with colors), switchable client-side at any time - and auto-selected on load based on whether tileset assets are present.
- Almost everything (monster/hero stats, floor/wall sets, decorations, map generation weights) is data-driven via JSON, rather than hardcoded — see Game data / configuration.
A partially explored sandy dungeon with a number of monsters chasing:
A room with a bunch of chests:
Chased by a skeleton into the arms of a goblin and his two pet black spiders:
The same scene rendered in the ASCII renderer:
- .NET 10 SDK or later.
- No database, no external services, no additional tooling required.
git clone https://github.com/dontrolle/BlazorRogue.git
cd BlazorRogue
dotnet build
dotnet run
By default the app listens on https://localhost:5001 (see Properties/launchSettings.json) — open either URL in a browser to play.
BlazorRogue.Tests is an xUnit test project covering core, UI-independent game logic (dice/combat math, Configuration JSON parsing, Map geometry helpers, and end-to-end dungeon generation smoke tests). Run it with:
dotnet test
There is no separate lint step — rely on .editorconfig conventions and the compiler's nullable-reference-type warnings (the build is currently warning-free; please keep it that way). CI runs dotnet build, dotnet csharpier check . (formatting), then dotnet test on every push/PR to master via GitHub Actions (.github/workflows/CI.yml). Run dotnet csharpier format . locally before committing to match it.
docker build -f docker/Dockerfile -t blazorrogue .
docker run -p 8080:8080 blazorrogue
Open http://localhost:8080 in a browser. The image is a Linux container built via multi-stage
dotnet publish (see docker/Dockerfile) and never contains the proprietary tileset assets at all
— see Tileset — so the containerized game runs in ASCII-renderer mode unless
BLAZORROGUE_ART_PATH is pointed at a separately-deployed tileset atlas.
| Action | Keys |
|---|---|
| Move / attack (8-directional) | Numpad, or qweasdzxc |
| Use (open door, chest, etc.) | Shift + move towards the object |
| Pick up item(s) on your tile | g |
| Open inventory | i — then u use/equip, d drop, Esc close |
| Quick use / equip an item | u — opens the inventory ready to use/equip |
| Start a new game | "New game" button (left panel) |
| Toggle tileset/ASCII rendering | CTRL-A |
| Toggle debug mode | CTRL-D |
| Help overlay | ? |
This project employs the excellent Ultimate Fantasy Tileset.
If you own the UF Tileset, see tools/AtlasPacker/README.md for how to build an atlas bundle from it and point the game at it via BLAZORROGUE_ART_PATH. Without one, the game automatically falls back to the built-in ASCII renderer — no setup needed to get playing.
BlazorRogue.csproj / Program.cs Minimal-hosting entry point, unified Blazor Components hosting
BlazorRogue.Tests/ xUnit test project for core game-logic classes (see below)
App.razor / Routes.razor Root HTML shell + router
Pages/ Blazor pages (GamePage.razor is the main game view)
Shared/ Shared Razor components
GameObjects/ GameObject and its subclasses (Moveable, Door, Chest, ...)
Components/ Component base class, InventoryComponent, UseableComponent
Combat/ Combat system, incl. the Warhammer-inspired ruleset (Combat/Warhammer/)
AI/ Monster AI components
Effects/ EffectsSystem (screen shake) and SoundManager (audio cues)
Vision/ Field-of-view implementation
World/ Map, Tile, Decoration and related types; World/Generation/ holds
the map generators (IMapGenerator and implementors)
Rendering/ SpriteAtlas (licensed-tileset atlas -> CSS), AnimationCssGenerator
and HandAuthoredSpriteAnimations (generate @keyframes CSS from
monster/hero, liquid-pool, and torch/hit-flash animation data)
Entities/ Type definitions parsed from configuration (MoveableType,
LevelConfiguration, SettingsMap, etc.), plus Configuration.cs
which parses Data/*.json into them
Sessions/ Per-browser session state that survives page reloads
Utility/ Small standalone helpers (e.g. string extension methods)
Data/ JSON game data: monsters, heroes, floorsets, wallsets,
liquidsets, decorations, items, levels
Game.cs / References.cs Core game state (see Architecture below)
wwwroot/ Static assets: CSS, JS interop, sounds
docker/ Dockerfile (see Docker below)
tools/AtlasPacker/ Dev-machine-only tool that packs the licensed tileset into an
obfuscated atlas bundle for BLAZORROGUE_ART_PATH
One playthrough is rooted in a Game, which owns the map generator, Map, combat system, and Configuration. A Game is created and held by a GameSession for it to survive page reloads (each reload is a fresh Blazor circuit). GameSessionStore keeps sessions in memory, keyed by an id the browser holds in localStorage. Cross-cutting services (Map, Configuration, SoundManager, EffectsSystem) are reached through static holders in References.cs. GameSession.Activate() re-points them at the active game before any handler runs.
Everything placed on the map is a GameObject (Moveable, Door, Chest, …). Inspired by ECS GameObject's have optional Component's added at construction. The Map holds the Tile grid a map generator builds. Vision/ does field-of-view. GameObject's are rendered to Decoration's. Rendering has a tileset path and an ASCII path, chosen client-side.
See ARCHITECTURE.md for the full picture — engine internals, the map-generation and rendering pipelines, and the gotchas worth knowing before a structural change.
Most game content is data, not code — new monsters, heroes, floor/wall sets, and decorations can usually be added without touching C#:
Data/monsters.json,Data/heroes.json— combat stats, AI behavior, sprites/animations.Data/floorsets.json,Data/wallsets.json— tileset mappings and map-generation weights.Data/liquidsets.json— animated liquid pools (water/mud/acid/lava): frames, ASCII colour, and hazard effect.Data/decorations.json— static decorative objects (torches, carpets, etc.).Data/items.json— pickup-able items: name, kind (use_once/equipable), sprite + ASCII glyph, and effect (heal/armour_bonus) with a magnitude.Data/levels.json— one entry per level: dimensions, which map generator to use (by string id), and that generator's own tuning parameters.Data/game-config.json— optional local knobs:starting_level(the levelnoa new game starts on, default0; set to-1000to start on the test level),debug_mode(initial value of the in-game Ctrl-D debug toggle, defaultfalse), anddebug_level(the levelnoCtrl-G jumps to/from once debug mode is on — e.g.-1002for the fence gallery; omitted by default, in which case Ctrl-G does nothing). The file, or any key, may be omitted.
These are parsed in Entities/Configuration.cs via a Parse*Type method per entity kind — follow the existing pattern (and the GetRequiredString/RequireNonNullString helpers for required fields) when adding a new data-driven concept.
A level's map_generator.parameters in levels.json is different from the rest: It's parsed into a SettingsMap (Entities/SettingsMap.cs) — a small recursive value tree of int/double/string/nested-map, read back via typed getters (GetInt, GetDouble, GetString, GetMap), each with a required form and a (key, defaultValue) form. This keeps the JSON-parsing library out of the map generators entirely — see the Map generation and Map-generator parameters entries in CLAUDE.md for the full design.
masteris protected: every change needs to go through a pull request; CI (dotnet build+dotnet test) must pass before merging.- Please keep the build warning-free — nullable reference types are enabled project-wide.
- Add or update tests in
BlazorRogue.Testsfor changes to game logic (combat, configuration parsing, map/dungeon generation, etc.); for changes that are hard to unit test (rendering, Blazor components, JS interop), please describe how you manually verified the change (e.g. a screenshot or a description of in-browser testing) in your PR description. - Small, focused PRs are preferred over large ones, especially for anything touching rendering or the hosting model — those are the areas most likely to have subtle runtime-only breakage that
dotnet buildwon't catch.
This project's code is licensed under the MIT License. The Ultimate Fantasy Tileset assets referenced in Tileset are © Oryx Design Lab and are not covered by this license.