Skip to content

Repository files navigation

CommandBattleKit

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.

日本語 README

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.

Contents

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

1. Run the samples

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-build

Run a bundled scenario in automatic demo mode:

dotnet run --project src/Samples/CommandBattle.Cli -- --scenario classic --demo

The 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-demo

Run the JSON-defined scenario:

dotnet run --project src/Samples/CommandBattle.Cli -- --scenario-json src/Samples/CommandBattle.Scenarios/Examples/monster-json-demo.json --demo

2. Minimal example

Most integrations follow four steps:

  1. Provide an IBattleRules implementation and a BattleSetup.
  2. Create a BattleState with BattleEngine.Initialize.
  3. Select a command returned by GetLegalCommands, wrap it in a CommandEnvelope, and pass it to ApplyCommand.
  4. Render the resulting PlayerView and use the EventLogBatch for 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);
}

3. How one command is processed

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.

4. Writing custom rules

Game-specific behavior belongs in IBattleRules. The v0.1.0 interface does not make every part of battle progression replaceable.

4.1 Replaceable behavior and Kernel-fixed behavior

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.

4.2 IBattleRules members

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();
    }
}

4.3 Building an effect plan

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.

4.4 The three Sample Rules

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.

5. Data model

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

5.1 Initial definitions

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.

5.2 Runtime state

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.

5.3 Data for presentation, logs, and transport

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.

5.4 Snapshots, hashes, and replay

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.

5.5 JSON scenarios

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.

6. Using CommandBattleKit from Unity and Godot

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

6.1 Shared local adapter

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.

6.2 Unity side

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.

6.3 Godot side

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.

6.4 Local input versus an online client

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.

7. Offline, AI, online, and replay

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.

7.1 MagicOnion adapter (source only)

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:5057

A 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.

8. Repository layout

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.

9. Current status

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;
  • BattleState still 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.

10. Creating local packages

Create packages for inspection without publishing them:

dotnet pack CommandBattleKit.Packages.slnf --configuration Release --output artifacts/packages

This 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.

11. Contributing and license

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.

About

Experimental Pure C# building blocks for command-based battle systems on .NET.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages