CommandBattleKit is a set of Pure C# libraries for building command-based battle systems.
It accepts player or AI input as a CommandEnvelope, checks whether the command is legal, updates battle state, and returns the result as events and presentation data. Game-specific skills, targeting, and formulas live in an IBattleRules implementation.
v0.1.0 is an experimental release. Public APIs and serialized formats may change. The bundled rules are examples of how to extend the Kernel, not finished games. See Current status for details.
- 1. Run the samples
- 2. Minimal example
- 3. How one command is processed
- 4. Writing custom rules
- 5. Data model
- 6. Using CommandBattleKit from Unity and Godot
- 7. Offline, AI, online, and replay
- 8. Repository layout
- 9. Current status
- 10. Creating local packages
- 11. Contributing and license
Suggested reading paths:
- Run something first: sections 1 and 2
- Write a ruleset: sections 3, 4, and 5
- Integrate with Unity or Godot: sections 5 and 6
- Evaluate online play or replay: sections 5, 7, and 9
Install the .NET 8 SDK, then run:
dotnet restore CommandBattleKit.sln
dotnet build CommandBattleKit.sln --configuration Release --no-restore
dotnet test CommandBattleKit.sln --configuration Release --no-buildRun a bundled scenario in automatic demo mode:
dotnet run --project src/Samples/CommandBattle.Cli -- --scenario classic --demoThe built-in scenario names are classic, monster, and row. Without --demo, the Heroes side accepts input from the terminal.
Exercise the Online layer without opening a network connection:
dotnet run --project src/Samples/CommandBattle.Cli -- --scenario monster --online-demoRun the JSON-defined scenario:
dotnet run --project src/Samples/CommandBattle.Cli -- --scenario-json src/Samples/CommandBattle.Scenarios/Examples/monster-json-demo.json --demoMost integrations follow four steps:
- Provide an
IBattleRulesimplementation and aBattleSetup. - Create a
BattleStatewithBattleEngine.Initialize. - Select a command returned by
GetLegalCommands, wrap it in aCommandEnvelope, and pass it toApplyCommand. - Render the resulting
PlayerViewand use theEventLogBatchfor logs and presentation.
The following code uses the Classic RPG sample rules to run that flow and apply one legal command.
using CommandBattle.Kernel;
using CommandBattle.Rules.ClassicRpg;
var rules = new ClassicRpgRules();
var engine = new BattleEngine(rules);
var setup = ClassicRpgScenario.CreateDemoBattle();
var state = engine.Initialize(setup);
var selected = engine.GetLegalCommands(state)[0];
var command = new CommandEnvelope(
new CommandId("example-0001"),
selected.ActorId,
new CommandIntent(selected.Type, selected.SkillId, selected.TargetActorId));
var result = engine.ApplyCommand(state, command);
if (!result.Accepted)
{
throw new InvalidOperationException(result.RejectionReason);
}
var view = engine.CreatePlayerView(state);
foreach (var battleEvent in result.Events)
{
Console.WriteLine(battleEvent.Message);
}BattleSetup
|
| BattleEngine.Initialize
v
BattleState
|
| GetLegalCommands <--- IBattleRules
v
select one LegalCommand
|
v
CommandEnvelope
|
| ValidateCommand <--- IBattleRules
| PlanEffects <--- IBattleRules
v
EffectPlan
|
| applied by the Kernel
v
updated BattleState + EventLogBatch
|
| CreatePlayerView
v
PlayerView
The main types in this flow are:
| Type | Purpose |
|---|---|
BattleSetup |
Defines actors, stats, skills, and resources before a battle starts |
BattleState |
Holds runtime HP, MP, turn, status, and other mutable state |
LegalCommand |
A command that is currently available; local UI or AI selects from this list |
CommandEnvelope |
Input containing a command ID, acting actor, and command intent |
EffectPlan |
An ordered list of damage, healing, movement, and other effects for the Kernel to apply |
EventLogBatch |
Facts produced by one input, used by logs, presentation, and synchronization |
PlayerView |
Data prepared for a local presentation layer |
ApplyCommand checks the current actor and calls the ruleset validation before accepting input. Accepted commands are recorded, then the Kernel applies each node returned by PlanEffects.
Rules read BattleState to make decisions but do not mutate it directly. Returning state changes as EffectNode values keeps event recording, replay, and state hashing on the same execution path.
Game-specific behavior belongs in IBattleRules. The v0.1.0 interface does not make every part of battle progression replaceable.
A ruleset can change:
- the commands and targets offered to UI or AI;
- validation of commands received from outside the Kernel;
- formulas and combinations for MP costs, damage, healing, status effects, and other effects;
- additional effects at the start of a turn;
- how the sample AI selects a legal command;
- command priority when applying simultaneous inputs;
- the Rules Version used for replay compatibility.
The following behavior is fixed in the v0.1.0 Kernel:
| Concern | v0.1.0 behavior |
|---|---|
| Initial turn order | Active actors sorted by descending Speed, then by ActorId |
| Normal turn progression | Advances through TurnOrder and skips defeated actors |
| Victory | Battle ends when either Heroes or Enemies has no living actor |
| Sides | Heroes and Enemies |
| Command types | Attack, Skill, Defend, Switch, Move, and Ultimate |
| Skill kinds | Damage and Heal |
| Built-in status kind | Poison |
GetCommandPriority does not change the initial turn order. It only decides which command resolves first when several commands are passed to ApplySimultaneousCommands.
Custom victory conditions, a different turn model, or arbitrary command and status kinds require Kernel changes in v0.1.0. These are known limits of the current interface.
| Member | Required | Purpose |
|---|---|---|
GetLegalCommands |
Yes | Produces commands available to the current actor |
ValidateCommand |
Yes | Revalidates external input against the current state |
PlanEffects |
Yes | Converts an accepted command into an EffectPlan |
ChooseAiCommand |
Yes | Selects one input for the sample AI |
RulesVersion |
Default provided | Identifies rules compatibility for replay and stored data |
OnTurnStart |
Default provided | Returns additional effects at the start of a turn |
GetCommandPriority |
Default provided | Returns resolution priority for simultaneous inputs |
GetLegalCommandCapacityHint |
Default provided | Tunes initial legal-command buffer capacity without changing game behavior |
A minimal custom implementation has this shape:
public sealed class MyBattleRules : IBattleRules
{
public string RulesVersion => "0.1.0";
public IReadOnlyList<LegalCommand> GetLegalCommands(
BattleState state,
ActorId actorId)
{
// Return attacks, skills, and targets available now.
throw new NotImplementedException();
}
public CommandValidationResult ValidateCommand(
BattleState state,
CommandEnvelope command)
{
// Never trust UI input; validate it against the current state.
throw new NotImplementedException();
}
public EffectPlan PlanEffects(
BattleState state,
CommandEnvelope command)
{
// Convert validated input into effects the Kernel can apply.
throw new NotImplementedException();
}
public CommandEnvelope ChooseAiCommand(
BattleState state,
ActorId actorId,
CommandId commandId)
{
// Select one result from GetLegalCommands.
throw new NotImplementedException();
}
}The following excerpt combines a skill's MP cost, damage, and optional status effect. It is intended to run inside PlanEffects after validation.
var actor = state.GetActor(command.ActorId);
var target = state.GetActor(command.Intent.TargetActorId!.Value);
var skillId = command.Intent.SkillId!.Value;
var skill = actor.Skills.First(value => value.SkillId == skillId);
var damage = Math.Max(1, actor.Attack + skill.Power - target.Defense / 2);
var effects = new EffectPlanBuilder(state);
effects.Add(EffectNode.SpendMp(actor.ActorId, skill.MpCost));
effects.Add(EffectNode.Damage(actor.ActorId, target.ActorId, damage));
if (skill.AppliedStatus is { } status)
{
effects.Add(EffectNode.ApplyStatus(actor.ActorId, target.ActorId, status));
}
return effects.ToPlan();The Kernel currently supports these effect groups:
| Group | Effects |
|---|---|
| Basic | MP spending, damage, healing, and defending |
| Position | Active/reserve switching and row/slot movement |
| Additional stats | Toughness damage, Energy changes, and follow-up damage |
| Named resources | Actor-scoped and team-scoped resource changes |
| Status | Applying a StatusDefinition |
The Kernel applies the plan and handles state updates, defeat, victory checks, and event generation.
| Sample | What it demonstrates | Relevant rules behavior |
|---|---|---|
| Classic RPG | Multiple actors, attack, defend, single/all-target skills, healing, and poison | Target generation, MP spending, damage/heal, status, and a simple healing AI |
| Monster Duel | Active/reserve actors, switching, and simultaneous commands | Switch, switch priority, and validation that only active actors can act or be targeted |
| Row Battle | Front/back rows, movement, and front-row target restrictions | Move, row-aware legal targets, and position-aware damage |
The samples are not complete or balanced games. They are comparison points for authors writing their own rulesets.
CommandBattleKit separates initial definitions, runtime state, outward-facing data, and persistence data.
initial definitions
BattleSetup / ActorSetup / SkillDefinition / StatusDefinition
|
| Initialize
v
runtime state owned by the Kernel
BattleState / ActorState
|
+--> presentation: PlayerView + EventLogBatch
|
+--> persistence/verification: BattleSnapshotView / ReplayPackageView
|
+--> online: OnlinePlayerView / VisibleLegalCommand
| Type | Main contents |
|---|---|
BattleSetup |
Battle ID, participating actors, and initial team resources |
ActorSetup |
Actor ID, name, side, HP/MP, Attack/Defense/Speed, skills, position, and named resources |
SkillDefinition |
Skill ID, target, MP cost, power, all-target flag, extra effects, and optional status |
StatusDefinition |
Status ID, kind, potency, and duration |
ResourceState |
An actor-scoped gauge identified by ResourceId |
TeamRuleResourceState |
A team-scoped gauge identified by ResourceId |
Initial data can be constructed directly in C#:
var strike = new SkillDefinition(
new SkillId("strike"),
"Strike",
SkillKind.Damage,
TargetSide.Enemy,
MpCost: 0,
Power: 8,
Description: "A basic strike.");
var setup = new BattleSetup(
"readme-battle",
new ActorSetup[]
{
new(
new ActorId("hero"),
"Hero",
TeamSide.Heroes,
MaxHp: 40,
MaxMp: 10,
Attack: 12,
Defense: 6,
Speed: 10,
Skills: new[] { strike }),
new(
new ActorId("enemy"),
"Enemy",
TeamSide.Enemies,
MaxHp: 30,
MaxMp: 0,
Attack: 9,
Defense: 4,
Speed: 8,
Skills: Array.Empty<SkillDefinition>())
});ActorId, SkillId, CommandId, and ResourceId are distinct ID types so that unrelated string identifiers are not accidentally mixed.
BattleState and ActorState contain current HP, MP, turn, winner, position, status, resources, accepted command history, and event history.
They are runtime data owned by the Kernel. Rules read them to make decisions; UI, AI, transport code, and rules do not mutate them directly. State changes enter through ApplyCommand and are applied by the Kernel from an EffectPlan.
Do not bind UI directly to BattleState or treat it as a general persistence DTO. Convert it to the view or replay type intended for the consumer.
| Type | Intended consumer | Contents |
|---|---|---|
PlayerView |
Local presentation | Actor display data, current actor, legal commands, timeline, and visible resources |
EventLogBatch |
Presentation, animation, logs | Damage, healing, defeat, and other facts produced by one command |
BattleSnapshotView |
Storage, verification, debugging | State at one point plus schema/rules versions and state fingerprint |
ReplayPackageView |
Storage and playback | Initial setup, accepted commands, final snapshot, and event log |
OnlinePlayerView |
Online client | Viewer-scoped actors, visible commands, input token, and cursor |
VisibleLegalCommand |
Online client | A tokenized command proposal that does not expose the raw legal command |
PlayerView is intended for trusted local use. A client that may not see the full state uses OnlinePlayerView produced by CommandBattle.Online.
var snapshot = engine.CreateSnapshotView(state);
var stateHash = engine.ComputeStateHash(state);
var replay = engine.CreateReplayPackageView(setup, state);- A snapshot captures state for inspection, storage, or comparison.
- A state hash/fingerprint helps compare two battle states.
- A replay runs the accepted command history again from its
BattleSetup. - Replays with incompatible Rules, Kernel, or schema versions are rejected.
BattleSnapshotView and ReplayPackageView have MessagePack formatters. v0.1.0 does not provide an API that restores a running BattleState directly from a snapshot; use replay reconstruction instead.
The bundled scenarios use no battle RNG, so they are deterministic for the same inputs. Replay data does not currently capture RNG state. Custom rules that use randomness must manage deterministic replay themselves.
monster-json-demo.json demonstrates passing actors, stats, skills, and positions through JSON.
{
"name": "monster-json-demo",
"ruleset": "monster",
"battleId": "monster-json-demo",
"actors": [
{
"actorId": "fox",
"name": "Foxling",
"side": "Heroes",
"maxHp": 32,
"attack": 10,
"defense": 4,
"speed": 12,
"skills": []
}
]
}This loader belongs to the CommandBattle.Scenarios sample. Its ruleset field accepts only classic, monster, or row. It is not a general data-driven rules system in the Kernel.
A presentation layer needs to render PlayerView, submit the selected command, and turn returned events into visual feedback. Engine-specific types stay inside the application adapter.
Unity MonoBehaviour / Godot Node
|
| buttons, animation, engine assets
v
application adapter / ViewModel
|
| BattleSetup, CommandEnvelope, PlayerView, EventLogBatch
v
CommandBattle.Kernel + custom IBattleRules
This class has no engine-specific dependency and can be called from either Unity or Godot.
using CommandBattle.Kernel;
public sealed class LocalBattleSession
{
private readonly BattleEngine _engine;
private readonly BattleState _state;
private int _sequence = 1;
public LocalBattleSession(IBattleRules rules, BattleSetup setup)
{
_engine = new BattleEngine(rules);
_state = _engine.Initialize(setup);
}
public PlayerView View => _engine.CreatePlayerView(_state);
public ApplyCommandResult Submit(int legalCommandIndex)
{
var legalCommands = _engine.GetLegalCommands(_state);
if ((uint)legalCommandIndex >= (uint)legalCommands.Count)
{
throw new ArgumentOutOfRangeException(nameof(legalCommandIndex));
}
var selected = legalCommands[legalCommandIndex];
var command = new CommandEnvelope(
new CommandId($"ui-{_sequence++:0000}"),
selected.ActorId,
new CommandIntent(
selected.Type,
selected.SkillId,
selected.TargetActorId));
return _engine.ApplyCommand(_state, command);
}
}Build buttons from View.LegalCommands and pass the pressed button index to Submit. After an accepted command, render the new View and use result.Events for animation and messages.
using CommandBattle.Kernel;
using UnityEngine;
public sealed class BattleScreen : MonoBehaviour
{
private LocalBattleSession _session = null!;
private void Start()
{
_session = new LocalBattleSession(
new MyBattleRules(),
MyBattleSetup.Create());
Render(_session.View);
}
public void OnCommandPressed(int commandIndex)
{
var result = _session.Submit(commandIndex);
if (!result.Accepted)
{
Debug.LogWarning(result.RejectionReason);
return;
}
Render(_session.View);
Play(result.Events);
}
private void Render(PlayerView view)
{
// Map view.Actors and view.LegalCommands to UI.
}
private void Play(EventLogBatch events)
{
// Convert events into damage labels and animation.
}
}If the Unity project stores authoring data in ScriptableObject, convert it to BattleSetup, ActorSetup, and SkillDefinition in the adapter. Rules and Kernel types do not need to inherit from ScriptableObject.
CommandBattle.Kernel also targets netstandard2.1. For a Unity version that supports .NET Standard 2.1, build the Kernel and custom rules for that target, then place the resulting assemblies and their NuGet dependencies in a referenced location such as the Unity project's Assets/Plugins directory. v0.1.0 does not ship a Unity package or installer.
using CommandBattle.Kernel;
using Godot;
public partial class BattleScreen : Control
{
private LocalBattleSession _session = null!;
public override void _Ready()
{
_session = new LocalBattleSession(
new MyBattleRules(),
MyBattleSetup.Create());
Render(_session.View);
}
public void OnCommandPressed(int commandIndex)
{
var result = _session.Submit(commandIndex);
if (!result.Accepted)
{
GD.PushWarning(result.RejectionReason);
return;
}
Render(_session.View);
Play(result.Events);
}
private void Render(PlayerView view)
{
// Map view.Actors and view.LegalCommands to Controls.
}
private void Play(EventLogBatch events)
{
// Convert events into animation and messages.
}
}From a Godot C# project, choose a compatible target framework and reference the Kernel or a custom battle library through ProjectReference or a local package. If authoring data uses Godot Resource, convert it to BattleSetup in the adapter.
LocalBattleSession uses raw LegalCommand because the presentation and Kernel run in the same trusted process.
An online client instead renders OnlinePlayerView.LegalCommands and returns the selected VisibleLegalCommand.Token together with the InputToken. The server resolves and revalidates the token without exposing raw legal commands that may contain hidden information.
All modes eventually use the same Kernel command application. They differ in where commands come from and where the Kernel runs.
| Mode | Input source | Main API |
|---|---|---|
| Offline | Local presentation | GetLegalCommands, ApplyCommand, PlayerView |
| AI | IBattleRules.ChooseAiCommand |
ChooseAiCommand, ApplyCommand |
| Simultaneous input | Multiple players or AI agents | ApplySimultaneousCommands, GetCommandPriority |
| Online | Visible token selected by a client | OnlinePlayerView and SubmitCommand in CommandBattle.Online |
| Replay | Stored AcceptedCommandRecord values |
CreateReplayPackageView, Replay, and ReplayTo |
The online command flow is:
server Kernel
|
| redact and tokenize for one viewer
v
OnlinePlayerView + VisibleLegalCommand
|
| client returns a selected token
v
Online application
|
| validate token, input cursor, and player
v
apply Command to server Kernel
CommandBattle.Online provides transport-independent tokens, idempotency, player-scoped views and updates, reconnect snapshots, simple login, lobbies, and matchmaking primitives. The library itself does not implement HTTP, WebSocket, or RPC transport.
src/Adapters/MagicOnion contains an experimental example that connects the Online layer to MagicOnion.
| Project | Role |
|---|---|
CommandBattle.Transport.MagicOnion |
Shared Unary Service and StreamingHub contracts |
CommandBattle.Server.MagicOnion |
An ASP.NET Core server example that implements the contracts and creates battles from sample scenarios |
Only source code and the project files needed to build it are included. Prebuilt assemblies, bin, obj, server configuration, and a MagicOnion client implementation are not included. Neither project is packable; dotnet restore obtains their dependencies.
Run the server example with:
dotnet run --project src/Adapters/MagicOnion/CommandBattle.Server.MagicOnion -- --urls http://127.0.0.1:5057A client can reference CommandBattle.Transport.MagicOnion and call IBattleService or IBattleHub through a MagicOnion client. The v0.1.0 server still exposes demo-oriented unauthenticated APIs. Do not expose it publicly without application-specific authentication, authorization, persistence, and operational hardening.
| Path | Role | Target | Packable |
|---|---|---|---|
src/Libraries/CommandBattle.Kernel |
Core command processing and state model | net8.0, netstandard2.1 |
Yes |
src/Libraries/CommandBattle.Online |
Transport-independent online application primitives | net8.0 |
Yes |
src/Adapters/MagicOnion/CommandBattle.Transport.MagicOnion |
Shared MagicOnion transport contracts | net8.0 |
No |
src/Adapters/MagicOnion/CommandBattle.Server.MagicOnion |
MagicOnion server implementation example | net8.0 |
No |
src/Samples/Rules (3 projects) |
Classic RPG, Monster Duel, and Row Battle Sample Rules | net8.0 |
No |
src/Samples/CommandBattle.Scenarios |
Built-in scenarios, JSON loader, and JSON example | net8.0 |
No |
src/Samples/CommandBattle.Cli |
Interactive and automatic CLI | net8.0 |
No |
tests/CommandBattle.Tests |
Kernel, scenario, resource, and replay tests | net8.0 |
No |
Assemblies and namespaces use the CommandBattle.* prefix; CommandBattleKit is the project name.
Implemented:
- battle initialization, legal-command generation, validation, rejection, and Kernel-owned state changes;
- effects for damage, healing, resources, statuses, and positions;
- event logs, player views, snapshots, state hashes, versioned serialization contracts, and accepted-command replay;
- three Sample Rules, built-in and JSON scenarios, a CLI, and xUnit tests;
- online tokens, idempotency, player-scoped updates, reconnect snapshots, session login, persistence prototypes, lobbies, and matchmaking primitives;
- source-only MagicOnion transport contracts and a server implementation example.
Experimental or incomplete:
- Sample Rules demonstrate extension points but are not complete or balanced games;
- AI selects legal commands with simple heuristics;
- visibility, authentication, persistence, lobbies, and matchmaking are not hardened for production;
- the MagicOnion server exposes demo-oriented unauthenticated APIs and is not a production-ready host;
BattleStatestill exposes implementation-oriented collections and must be treated as Kernel-owned;- public APIs and serialized formats may change before 1.0.0.
Not implemented or out of scope:
- general rules hooks for arbitrary turn order and victory conditions;
- general interrupt, trigger, rollback, and direct state restoration from snapshots;
- RNG state integrated into replay data;
- graphics, UI, transport adapters other than MagicOnion, production server hosting, and operations.
Create packages for inspection without publishing them:
dotnet pack CommandBattleKit.Packages.slnf --configuration Release --output artifacts/packagesThis creates CommandBattle.Kernel and CommandBattle.Online version 0.1.0. The MagicOnion adapter/server, Rules, scenarios, CLI, and tests are not packable and are provided as repository source.
No package is published to nuget.org or GitHub Packages.
Read CONTRIBUTING.md before proposing a change. Describe the impact of changes to the Kernel/rules boundary, public APIs, and serialized contracts.
CommandBattleKit is available under the MIT License. Dependency license information is recorded in THIRD-PARTY-NOTICES.md.