Ledger v3 · C# · Godot 4.7 Get started

Ledger v3 is in active development what it isn't yet

Every number has a receipt.

Ledger is a deterministic gameplay ability system. Write your game's stats, abilities and effects as content, check them with plays, and ask the running world why any value is what it is. The core knows no engine; a Godot adapter and editor addon come with it.

ledger new Arena

C# on .NET 10 · fixed-point numbers · same state on every machine · Godot 4.7 .NET addon

content/cookbook/backbone.ledgeryou write this
13stat Damage by type default 1
14stat Resist by type max 0.75
⋮
17stages Attack Armour Resistance
⋮
19effect Hits
20 before Damage by holder at Attack
21 scale holder.Damage
22 before Damage of holder having Physical at Armour
23 scale 100 / (100 + holder.Armour)
24 before Damage of holder at Resistance
25 scale 1 - holder.Resist
⋮
33archetype Brute
34 base MaxHealth 1000
35 base Armour 100
36 base Resist.Fire 0.5
37 base Resist.Cold 0.25
38 full Health
39 has Hits
⋮
41ability Strike
42 target one
43 deal 40 Physical + 40 Fire + 40 Cold + 40 Lightning to target
⋮
48effect Searing
49 Damage.Fire + 20%
⋮
52effect Sharpened
53 Damage + 10%
ledger play --why 23 d.Healththe world answers
#2 Brute · Health at the end, tick 0
875of 1000
the hit125

#10 tick 0 Health −125, sent 160, 125 after its rules, from #1 by Strike

Attack
  1. Hits scale Physical+4160 → 164
  2. Hits scale Fire+12164 → 176
  3. Hits scale Cold+4176 → 180
  4. Hits scale Lightning+4180 → 184
Armour
  1. Hits scale Physical−22184 → 162
Resistance
  1. Hits scale Physical0162 → 162
  2. Hits scale Fire−26162 → 136
  3. Hits scale Cold−11136 → 125
  4. Hits scale Lightning0125 → 125
Health−1251000 → 875

The Attack rule read holder.Damage. Its receipt: --why 23 a.Damage.Fire

base 1 from Damage.Fire1
Searing: percent+0.2
Sharpened: percent+0.1
(1 + 0) x (1 + 0.3) x 11.3

An Exile carrying two affixes strikes a Brute for 40 of each of four types. Every line item names the rule that made it, the rule's line names the stat it read, and the stat's receipt names the affixes. Hover a line item to see the content behind it. The stage labels come from ledger pipeline; the rest is the tool's own output, laid out as a ledger.

The loop

Edit, check, play. Stop at the first that fails.

Each step answers one question about your game. Run them in this order after any change. Errors come back at their file, line and column, all of them at once.

1

Edit

The rules live in content\*.ledger; what they should do lives in a .play beside them.

A recipe from the cookbook is the usual starting point.
2

Check

Does the content load, and do the plays read?

ledger check content/cookbook/backbone.ledger content/cookbook/backbone.play
1 catalog and 5 plays checked, 0 errors
3

Play

Does the content do what the plays say? A failing step is reported at its line.

ledger play content/cookbook/mechanics/windup.play
2 plays (37 steps), 0 failing
4

Run it headless

Does the game around the content still work: a run, a recording replayed, a quickload's round trip?

tools\Arena.Headless\…\Arena.Headless.exe roundtrip
saved again at once: the same file
5

Look in the editor

Does it look and feel right?

The Godot dock lists errors, runs plays and shows a pool's pipeline. The inspector reads the running game.
Plays

Tests that read like the design doc.

A play makes actors, gives orders, moves time and expects values. Every expected number says where it comes from, so a reviewer can check the arithmetic and ledger play checks the content.

mechanics/windup.ledgerthe ability
16effect Burning
17 tags Fire
18 stack add max 5
19 lasts 3s
20 every 0.5s
21 deal 3 * self.stacks Fire to holder from source
⋮
23ability Fireball
24 tags Spell Fire
25 target point within 14
26 cost Mana 20
27 cooldown 1s
28 windup 0.8s
29 broken by Damage of holder when event.landed >= 25
30 deal 30 Fire to all enemies within 2.5 of target
31 apply Burning to all enemies within 2.5 of target
mechanics/windup.playwhat it must do
6play "wound up at (10, 0): nothing lands or is paid until 0.8 s; then 30 to each foe within 2.5, none to one 3 away, 20 Mana paid, each one hit burning"
7 make r Side
8 make b Side
9 make h Hero with team = r at 0 0
10 make a Dummy with team = b at 10 0
11 make c Dummy with team = b at 11.5 1
12 make far Dummy with team = b at 13 0
13 act h Fireball at 10 0
14 expect done
15 expect h active Fireball
16 advance 0.5s
17 expect a.Health = 1000 from content: the Dummy's 1000, nothing landed before the windup's 0.8 s
18 expect h.Mana = 200 from content: the Hero's MaxMana 200, nothing paid before the commit
19 advance 0.3s
20 expect h idle
21 expect a.Health = 970 from content: 1000 less Fireball's 30 at 0.8 s
22 expect c.Health = 970 from content: 1.80 from the point, within 2.5: 1000 less 30
23 expect far.Health = 1000 from content: 3 from the point, past 2.5
24 expect h.Mana = 180 from content: the Hero's 200 less Fireball's 20 at the commit
25 expect a has Burning 1 from content: Fireball's apply Burning to each foe within 2.5
26 expect far lacks Burning
27 act h Fireball at 10 0
28 expect refused OnCooldown

Time is ticks

At 60 ticks a second, advance 0.5s is 30 ticks. Jumping time and stepping it run the same code, so a play can skip ahead without missing a beat.

Refusals are outcomes

Every order gets an outcome. expect refused OnCooldown checks the refusal by name, the same outcome the game receives.

save mid-play

At any step, save saves the world and plays on from the restored copy, as a quickload would. Every step after it checks the round trip.

Ask why

Ask why, and get the answer the world used.

The code that changes a value writes down how it changed. A tooltip, a debugger and a replay all read that same record, so the answer never drifts from what actually happened.

> ledger play --why 23 a.Damage.Fire content/cookbook/backbone.play
== at the end, tick 0: #1 Exile, Damage.Fire
Damage.Fire = 1.3
  base 1 from Damage.Fire
  Searing: percent +0.2
  Sharpened: percent +0.1
  (1 + 0) x (1 + 0.3) x 1

Why a stat is what it is

Its base, each term and the effect that gave it, the strongest of each category, and its bounds. Two percents sum before they multiply, the way an ARPG's increases do.

In a running game, the inspector shows the same receipt for the actor you pick, each effect a button to its line.

Godot

The editor reads your content too.

The Godot adapter hosts the world and ticks it from _PhysicsProcess, and its kit draws what a player may see. The editor addon puts the command line's answers in a dock, next to the game they describe.

A presentation kit

An ability bar, a cast bar and tooltips that preview what the world would do, and cues bound to tags your content already writes. An ability added in content alone shows up and can be used with no C#.

Completion in content files

Highlighting, and the words and names that fit what you typed, each with its kind. After holder. it offers what an actor holds.

The dock

Checks, plays, a preview, the Pipeline tab and the Run tab. Every rule and name is a button to its line.

The inspector

Pick an actor in the running game and read its pools' and stats' receipts and the changes the journal holds.

Reload while it runs

Save a content file and it reloads into the running world between two ticks, with its report as Godot warnings.

ChecksPlaysWordsPipelineRun
Health: a change to it meets 3 rules, in this order
before, at AttackHits: Damage by holder at Attack backbone.ledger:20scales it; held by Exile, Brute
before, at ArmourHits: Damage of holder having Physical at Armour backbone.ledger:22scales it; held by Exile, Brute
before, at ResistanceHits: Damage of holder at Resistance backbone.ledger:24scales it; held by Exile, Brute
In a running game the tab narrows to the inspector's picked actor, with each when shown true or false as it stands.

A sketch of the Pipeline tab. It lists the lines ledger pipeline prints.

The principles

Four rules the kernel keeps, each one checked by a machine.

A rule like these only holds if something refuses to let it slip. Each comes with the check that keeps it.

Deterministic

Time is ticks. Randomness is seeded, drawn from streams the world owns, and saved. Nothing reads the machine: no clock, no culture, no hash order, no floating-point function whose answer differs by machine. Two runs on two machines end in identical state.

Checked bythe same seeds replayed under another culture, time zone and operating system; every outside call the core makes held to an allowlist; its compiled code scanned for floats.

A steady tick allocates nothing

Games with hundreds of actors stutter on garbage collection, and the cost is invisible until it isn't. So it is measured in every build a game runs.

Measureda strike through a build of ten affixes: 3.4 to 3.5 µs, 0 bytes allocated.

Everything round-trips through a save

A restored world holds exactly what the saved one held and plays on exactly as it would have. Saves, quickloads, replays and lockstep joins all depend on it.

Checked byroundtrip: saved again at once, the same file; then both played on to one save.

Every number has a receipt

A game can ask why a value is what it is, and why an action was refused, and get the real answer the world used. The author, the player reading a tooltip and the tester reading a replay all see the same one.

Asked with--why, ledger pipeline, ledger why, and the inspector.

An engine-free core. Ledger knows no game engine. Space, AI and presentation stay the game's, behind ports, and an adapter joins the core to an engine. Godot's comes first.

The cookbook

43 recipes, lifted from real game slices.

Each recipe is a .ledger and its .play that load alone, cut down to one mechanic from a game built on Ledger. Find the one nearest what you want, copy it, then change its numbers and names.

Action RPG11 recipes

Tower defence9 recipes

Shooter6 recipes

Tactics5 recipes

RTS5 recipes

Raid4 recipes

Cards3 recipes

Straight answers

Where Ledger stops.

Better to know before you start a game on it.

Not a game engine

Ledger keeps the numbers on a game's actors and every way they change. Physics, AI and rendering stay your game's. Space sits behind a port: use the plane space it ships with, or bring your own.

Not floating point

Every value is a Num, a fixed-point decimal. The platform's floating-point maths is kept out of the core, since its last digit can differ by machine. The host's trig reads a committed table instead.

Not a network transport

Rollback in place and client prediction under server authority are built, with side views so each player is sent only what their side can see. Ledger makes the bytes; sending them is your game's job. A game moved by Godot's float physics can't be a rollback or lockstep peer.

Not a node graph

Content is plain text. You can diff it, review it and grep it, and a coding agent can write it.

Quick start

A game that builds, checks and plays from the first minute.

ledger new makes a Godot C# game with its content, its plays and a headless runner, ready for the loop above.

For coding agents

Your assistant gets the same guide you do.

The authoring guide is an agent skill, ledger-author, and every new game's CLAUDE.md points to it. ledger check --json and ledger play --json print one object a line, each with a stable id, the line as written and a fix where one is known.

1

Make a game

Add --turns for a turn-based game, or --space own to bring your own space.

ledger new Arena
2

Change the content

Edit content\Arena.ledger and its play, then check and play them.

ledger check content\Arena.catalog content\Arena.play
ledger play content\Arena.play
3

Open it in Godot

Build the game's project first; the build copies the addon in. Then open the folder in Godot 4.7 .NET.

dotnet build Arena.csproj

Write the rules. Ask the world why.

Stats, abilities and effects as content, a world that runs them the same on every machine, and an answer for every number it holds.