A deterministic, modular C#/.NET engine for roguelike deckbuilders. It covers combat, the run around it, a bot that plays and replays runs, and RogueDeck Studio, a browser editor for building a game without code. The engine has no rendering, input or theme of its own; a frontend hosts it.
The central rule: adding a new effect must not require changing the central resolver. In practice that means
- no central switch over concrete effects,
- no hardcoded fields such as
PoisonStacks,IsStunnedorThorns, - no game-specific rules inside turn processing,
- no effect that bypasses the deterministic queue.
A mechanic is added through its own definitions, handlers, providers and packages. Triggered mechanics are split into five parts:
WHEN What happened?
IF Which conditions must match?
WHO Which combatants are affected?
HOW MUCH How is the value calculated?
WHAT Which primitive effect is requested?
An existing event can be combined with an existing effect without a new engine class:
WHEN damage is received WHEN a skill card is played
IF the target has Thorns IF it is the second skill this turn
WHO the attacker WHO the player
HOW the current Thorns value HOW 1
WHAT deal damage WHAT draw cards
flowchart LR
A[Combat event] --> B[Typed trigger]
B --> C[Filters]
C --> D[Target and value providers]
D --> E[Effect program]
E --> F[Deterministic queue]
F --> G[Typed effect handler]
G --> H[State change and new events]
H --> F
Effects and events are processed through the queue until the chain is complete or combat ends. Chain ancestry, re-entry rules and configurable depth limits prevent accidental infinite loops. The combat state can be hashed at any point, so identical inputs can be shown to produce identical outcomes.
Requirements: .NET 10 SDK.
git clone https://github.com/Paranoidgrinch/RogueDeck-Core.git
cd RogueDeck-Core
dotnet test # about 2,700 testsCI runs the suite on Windows and Linux, checks formatting with dotnet format and builds the benchmarks on
every push and pull request to main.
The Studio runs in the browser at studio.moonvineforge.com (Blazor
WebAssembly, deployed from src/RogueDeck.Studio.Web). To run it locally:
dotnet run --project src/RogueDeck.StudioA project is one document edited through separate tabs: hero, characters, cards, statuses, enemies and
encounters, relics, events, shops, consumables, workbenches and recipes, map rules, meta progression and
balance. The Playtest tab plays the current document on the engine. Effect programs are edited visually,
drafts are saved automatically, and the result is exported as a versioned game.roguedeck.json blueprint.
RogueDeck.Scenario describes a fight as blueprints and scripted turns, runs it on the real combat loop and
renders a readable log:
using RogueDeck.Core.Combat;
using RogueDeck.Scenario.Authoring;
using RogueDeck.Scenario.Dsl;
using RogueDeck.Scenario.Reporting;
using RogueDeck.Scenario.Scripting;
var fight = new ScenarioBlueprint();
// A card: deal 8 damage to the chosen target, for 1 energy.
fight.Cards.Add(new CardBlueprint("strike")
{
Program = Effects.Program(Effects.DealDamage(Targets.EventTarget, 8)),
}.Cost(StandardCombatIds.EnergyResource, 1));
// An enemy action: smash the hero for 12.
fight.EnemyActions.Add(new EnemyActionBlueprint("smash", new ActionIntent("Smash", IntentKind.Attack))
{
Program = new EffectProgram<EnemyActionContext>(
new DealDamageNode<EnemyActionContext>(
CombatantTargetSelectors.EventTarget, new ConstantExpression<EnemyActionContext>(12))),
});
// The hero (with a small deck) and the enemy.
fight.Hero = new HeroBlueprint("knight")
{
MaxHealth = 50,
Deck = { new DeckEntry(new CardDefinitionId("strike"), Count: 3) },
};
fight.Hero.Resources.Add(new ResourceSpec(StandardCombatIds.EnergyResource, Current: 3, Max: 3));
var ogre = new EnemyBlueprint("ogre") { MaxHealth = 40 };
ogre.Actions.Add(new EnemyActionDefinitionId("smash"));
fight.Enemies.Add(ogre);
// Script the turns…
var script = new ScenarioScript()
.HeroPlays("strike", "ogre")
.HeroEndsTurn()
.EnemyActs("ogre", "smash", "knight")
.Build();
// …run them on the real engine, and read the narrative log.
var report = new ScenarioRunner().Run(new Playthrough(fight, script, combatId: "demo"));
Console.WriteLine(new NarrativeLogRenderer().Render(report));For direct control from your own game loop, use RogueDeck.Core: build a CombatDefinitionRegistryBuilder,
register a StandardCombatPackage, and drive a CombatState with the CombatReplayRunner
(PlayCardCommand, EndTurnCommand). src/RogueDeck.ConsoleSandbox/Program.cs is a complete example.
roguedeck-bot plays whole runs of an exported game without a UI, several seeds in parallel, and replays
recorded player runs:
dotnet run --project src/RogueDeck.Bot.Cli -c Release -- --game game.roguedeck.json --runs 20
dotnet run --project src/RogueDeck.Bot.Cli -c Release -- --game game.roguedeck.json --replay-run run.jsonA replay prints REPRODUCED or the first room in which it diverges.
The engine consists of plain .NET 10 libraries without game dependencies. Reference src/RogueDeck.Core
(combat) and src/RogueDeck.Run (runs), or build local NuGet packages with dotnet pack.
docs/godot-export-contract.md describes how a frontend hosts a run.
Combat (RogueDeck.Core):
- deterministic combat state and seeded randomness; snapshot, stable hash and replay
- effect-request and handler registries, ordered effect and event queues
- turn, round and combat-result lifecycles
- cards and zones: draw, discard, exhaust, banish, retain
- card costs and cost modifiers, resources and resource lifecycles
- statuses with stacking, duration and charges
- damage, healing, block and defensive pools
- typed target selectors and value expressions
- card, status, resource and lifecycle triggers with re-entry protection
- modifier and interceptor pipeline (damage, block, cost, status application, death prevention)
- package-based registration, combat logging, architecture tests
Mechanics are written as effect programs: typed trees of nodes (sequence, conditional, causal, repeat, for-each) over operations, value expressions and selectors. They are validated before the fight starts and run on the deterministic queue. Cards, enemy actions, triggers and temporary rules all use the same runtime.
Runs (RogueDeck.Run):
- characters and parties, starting decks, inventories, run resources
- relics with run effects and combat rules, consumables, shops, workbenches and recipes
- events with choices and effects; encounter pools for normal, elite and boss fights
- multi-act runs on generated branching maps (layered, rule-based and strategic generators, all seeded)
- rewards, card upgrades and removal, meta progression between runs, tutorials
- saves, run recording and replay
src/
RogueDeck.Core combat engine
RogueDeck.Run run engine: map, relics, events, shops, meta, saves, replay
RogueDeck.Scenario fight scripting: blueprints, DSL, runner, log renderer
RogueDeck.Sandbox.Combat Studio editors and interactive combat (Razor library)
RogueDeck.Sandbox.Run Studio editors and interactive runs (Razor library)
RogueDeck.Studio Studio, server-hosted
RogueDeck.Studio.Web Studio, WebAssembly
RogueDeck.Bot run-playing bot
RogueDeck.Bot.Cli roguedeck-bot command-line tool
RogueDeck.ConsoleSandbox end-to-end combat example, used as CI smoke test
tests/
RogueDeck.Core.Tests combat engine tests, including architecture tests
RogueDeck.Run.Tests run engine and map generator tests (including fuzz tests)
RogueDeck.Scenario.Tests
RogueDeck.Sandbox.Tests
RogueDeck.Core.Benchmarks BenchmarkDotNet
docs/ design notes: run engine, maps, parties, relics, Godot export contract
.github/workflows/ ci.yml (build, format, test), deploy-studio.yml (Studio to GitHub Pages)
Combat engine v1 is complete. Two behaviours are deliberate v1 decisions: CombatResultChanged can be
observed but does not trigger (the queue stops at a terminal result), and Combatant is the only target
domain. The card composition engine (card generation from snippets) is deferred to a separate project.
The run engine and the Studio are in active development. They carry the complete five-act game Bureaucrats & Broomsticks, and the Studio can author, playtest and export a game.
A satirical fantasy roguelike deckbuilder built on this engine:
| Repository | Content |
|---|---|
| RogueDeck-Core (this repository) | combat and run engine, bot, replay |
| bnb-content | game content, built and validated into game.roguedeck.json |
| bnb-godot | Godot 4 (.NET) frontend |
| bnb-runs | recorded playtest runs, replayable with roguedeck-bot |
The game started as a Python prototype with hardcoded mechanics. The coupling between effects and event-specific action classes in that version is what this engine was designed to avoid.
Mechanics that seem hard to express are useful test cases for the architecture. To propose one, open an issue and describe what triggers it, which conditions apply, whom it targets, how its value is calculated and what it does. Card ideas can also be submitted through the Community Card Forge.
PolyForm Noncommercial License 1.0.0. Noncommercial use, modification and distribution are
permitted with attribution (Required Notice: Copyright (c) 2026 Paranoidgrinch). Commercial use requires a
separate license; contact Moonvine Forge Studios.
Versions published before October 2026 were released under the MIT License and remain available under it.