Torchbound progress status

- Posted in D&D by

Everyone in Europe saw the solar eclipse. Not me.
I was -and still am- too busy trying to make a playable pre-alpha version of Torchbound.

Right now, it’s not exactly visually that impressive: enter image description here

Despite everything that has been done behind the scenes:

      +-----------------------------------------------+
      |                    SUMMARY                    |
      +-----------------------------------------------+
  Files                    258
  Modules                  -
  CodeGraph nodes          4044
  CodeGraph edges          13291
  Functions analyzed       469
  Call relations           1114

  Entry points
    LOVE callbacks         6
    Bootstrap              1
    Module APIs            463
    Root candidates        217
    Documented             687


And if I try to gauge how far along I am in this project -which seemed small and easy at first but is becoming a bigger and bigger thing every day- I’d say:

Rules / Engine foundations       ████████████████████
Persistence                      ████████████████████
Campaign aggregate / session     ████████████████████
Playable encounters              ████████████████████
Playable travel                  ████████████████████
Interactive travel consequences  ████████████████████
Playable exploration             ████████████░░░░░░░░
Party management                 ░░░░░░░░░░░░░░░░░░░░
Generated encounters             ░░░░░░░░░░░░░░░░░░░░
Rewards / progression loop       ░░░░░░░░░░░░░░░░░░░░
Management UI                    ░░░░░░░░░░░░░░░░░░░░
Game UI                          ░░░░░░░░░░░░░░░░░░░░
Game tests                       ░░░░░░░░░░░░░░░░░░░░
Game assets                      █░░░░░░░░░░░░░░░░░░░
Game sounds                      ██░░░░░░░░░░░░░░░░░░

At least this time—I didn't start with the visuals :)

And of course, I have dozens of brilliant ideas every day that I meticulously log in my personal local instance of LineAgent (yes, Linear is great, but it’s a non-open-source SaaS service, so I switched both the product and the hosting for my issues).
And since LineAgent doesn't expose a user interface -it is headless and REST only- well... I built a little interface on a small self-hosted server, just to take my mind off things for a bit.

enter image description here

(perhaps an english version on day)

So Torchbound will probably never be finished, but deep down inside I’ve always known that :)
Sticks and stones...
Yeah, I admit this post is a bit all over the place. But that's okay.

Peace. Out.

Torchbound

- Posted in D&D by

So I dove into developing my game, which will use the dungeon I now know how to generate.
After much thought, I’ve decided to call it Torchbound, since it’s based on the rules of Five Torches Deep.

Version 1, aka “Optimism and carefree”

I thought to myself, “OK, it’s a simplified version of D&D—this will work easily,” and of course, as always, I started playing around with the UI.
And I built a UI in Lua—how should I put it?—it’s pretty old-school :)
And of course I created the logical sequence of screens for character creation: Race, Attibutes, Classes, Equipment—the usual stuff.
Then, of course, the inevitable happened: “Damn, the rules aren’t that simple,” and “Hello, unmaintainable if… then… else statements.
So, I… stopped everything and… started over from scratch (which just shows that WIS doesn't necessarily come with age).
(Well, OK, I’ll probably keep some assets like logos, buttons, and sounds)

Version 2, “Welcome to the Real World”

The UI will come later; I need to start with the game engine: the FTD rules.

Phase 1: The Rules
So I started by actually reading the rules—which, let’s face it, I’d only skimmed before. Well, even though it’s a minimalist version of D&D 5th Edition, it’s not as straightforward as it seems. I ended up with a rules file that’s… 120 KB, including 24 reference tables. Plain text, mind you—not a Word document with images.

Phase 2: Modeling the Rules
Then, after careful consideration, I settled on the following model:

      ┌→ Specific item (AC) ────────→ Plain text 'Contract' ────→ Lua 'Tests' ────→ Lua 'Logic'
      ├→ Specific item (Armor)──────→ Plain text 'Contract' ────→ Lua 'Tests' ────→ Lua 'Logic'
RULES ┼→ Specific item (Attack) ────→ Plain text 'Contract' ────→ Lua 'Tests' ────→ Lua 'Logic'
      ├→ ...
      └→ Specific item (Weapon) ────→ Plain text 'Contract' ────→ Lua 'Tests' ────→ Lua 'Logic'

And now I think I've covered everything with a little less than... 90 subdomains (from “AC” to “Weapon”). It's just huge compared to what I imagined, but at least the foundations will be isolated, well-defined, and their code and tests will be dedicated.

Phase 3: Implementing the Rules
For example, for AC:

Contract 'contract.md':

# CONTRACT: ARMOR CLASS

Every combatant, PC or NPC, has an Armor Class (AC).
AC is the number an attacker must meet or exceed to hit the target in combat.
AC is therefore the DC used to damage a target.
FTD defines, among others:
    Heavy Armor
        AC 15
    Light Armor
        AC 12 + DEX mod
A shield increases AC.
Further AC calculation details depend on the equipment and applicable rules.

## SOFTWARE INTERPRETATION
The contract receives the values required to calculate AC.
For heavy armor:
    AC = 15
For light armor:
    AC = 12 + DEX mod
The contract does not resolve the attack check, roll dice, select armor,
calculate the DEX modifier, or decide whether an attack hits.

## API
Module:
    Modules.Engine.ac

Entry point:
    ac.heavy()
    ac.light(dex_modifier)

## RETURN
The functions return:
    {ac = <valeur d'AC>}

## INVARIANTS
Heavy armor provides a base AC of 15.
Light armor provides a base AC of 12 before applying the DEX modifier.
The contract never modifies DEX, calculates the DEX modifier, or rolls dice.
It is deterministic for identical inputs.


## TEST CASES
The following cases must be covered:
- heavy armor
- light armor with a zero modifier
- light armor with a positive modifier
- light armor with a negative modifier
- determinism


## OUT OF SCOPE

The AC contract does not perform:
- RNG ;
- CHECK ;
(...)

## VALIDATION
The contract is considered validated when:
    lua Tests/run_tests.lua
returns zero failures and all contract cases are covered.

Tests 'test-AC.lua':

TestAC = {}
-- HEAVY ARMOR
function TestAC:test_heavy_armor()
    local result = ac.heavy()
    lu.assertEquals(result.ac, 15)
end
(...)
return TestAC

Logic 'ac.lua':

local ac = {}
function ac.heavy()
    return {
        ac = 15
    }
end
function ac.light(dex_modifier)
    return {
        ac = 12 + dex_modifier
    }
end
return ac

Phase 4: Writing All These Items All the {specifications, tests, code} for the 90 domains have been written, and I am verifying that:

  • the {specifications, tests, code} actually exist:
.\Specifications\Contracts\Rules_Check.ps1
 TORCHBOUND - RULES CHECK

===== COUNTS =====
Contracts             : 86
Contract modules      : 86
Contract tests        : 86
Infrastructure tests  : 5
Total tests           : 91

===== CONTRACT BIJECTION =====
Contracts          : 86
Modules            : 86
Contract tests     : 86
OK   complete 1:1 contract matrix.

 RULES CHECK : PASS
  • the tests are OK:
lua .\Tests\run_tests.lua
Ran 967 tests in 0.072 seconds, 967 successes, 0 failures

Nearly 1,000 unit tests are run with every change.

Phase 5: The engine that drives all these rules
The approach adopted is as follows:

                        +------------------+
                        |    INPUT / UI    |
                        +--------+---------+
                                 |
                                 | command
                                 v
                        +------------------+
                        | COMMAND VALIDATOR|
                        +--------+---------+
                                 |
                                 | valid command
                                 v
                        +------------------+
                        |  COMMAND QUEUE   |
                        +--------+---------+
                                 |
                                 | one command per update
                                 v
+----------------+      +--------+---------+      +----------------+
| ACTOR STATE    |<---->|      ENGINE      |<---->| COMBAT / TURN  |
| HP, SUP, class |      | orchestration    |      | current actor  |
| actions, etc.  |      +--------+---------+      +----------------+
+----------------+               |
                                 | selects resolver
              +------------------+------------------+
              |                  |                  |
              v                  v                  v
     +----------------+ +----------------+ +----------------+
     | ACTION         | | ATTACK         | | SPELLCASTING   |
     | RESOLVER       | | RESOLVER       | | RESOLVER       |
     +-------+--------+ +-------+--------+ +-------+--------+
             |                  |                  |
             |                  v                  v
             |          +---------------+  +---------------+
             |          | DAMAGE / HP   |  | SPELL CHECK   |
             |          +---------------+  +-------+-------+
             |                                     |
             |                             failure |
             |                                     v
             |                             +---------------+
             |                             | MAGIC MISHAP  |
             |                             +---------------+
             |
             +------------------+
                                |
                                | state changes
                                | and events
                                v
                       +-------------------+
                       |   EVENT BUFFER    |
                       +---------+---------+
                                 |
                                 | dispatch
                                 v
                       +-------------------+
                       | EVENT HANDLERS    |
                       +---------+---------+
                                 |
                    +------------+------------+
                    |                         |
                    v                         v
           +----------------+        +----------------+
           | COMBAT LOG     |        | FOLLOW-UP RULE |
           | observable     |        | death, etc.    |
           +----------------+        +----------------+

In practice, it looks something like this when you're playing around with a Goblin:

[CALL] PUBLIC engine.update(0)
[CALL] command_queue.pop
[RETURN] command_queue.pop
[STATE] queue 1 -> 0
[STATE] consumed command=Attack
[CALL] combat_command_resolver.execute
  [CALL] engine.use_action
  [RETURN] engine.use_action
  [STATE] Active false -> true
  [STATE] action accepted=true
  [CALL] combat_resolver.execute
    [CALL] attack.resolve
    [RETURN] attack.resolve
    [STATE] attack hit=true
    [CALL] damage.resolve
    [RETURN] damage.resolve
    [STATE] damage total=5
    [CALL] hp.apply_damage
    [RETURN] hp.apply_damage
    [STATE] hp result 5 -> 0
    [CALL] events.push
    [RETURN] events.push
    [EVENT] damage_applied
    [STATE] events 0 -> 1
  [RETURN] combat_resolver.execute
  [STATE] goblin.hp 5 -> 0
  [STATE] combat hit=true
[RETURN] combat_command_resolver.execute
[STATE] attack command success=true
[CALL] event_dispatcher.dispatch_all
  [CALL] event_dispatcher.dispatch
    [CALL] combat_log.on_damage_applied
    [RETURN] combat_log.on_damage_applied
    [STATE] logs 0 -> 1
    [CALL] death_handler.on_damage_applied
    [RETURN] death_handler.on_damage_applied
    [STATE] goblin.incapacitated nil -> true
    [STATE] goblin.dying nil -> true
    [STATE] death handler result=false
  [RETURN] event_dispatcher.dispatch
  [EVENT] dispatched damage_applied
[RETURN] event_dispatcher.dispatch_all
[STATE] events 1 -> 0
[STATE] logs 0 -> 1
[RETURN] PUBLIC engine.update(0)
[STATE] queue 1 -> 0
[STATE] logs 0 -> 1
[STATE] Active false -> true
[STATE] goblin.hp 5 -> 0
[STATE] goblin.incapacitated nil -> true
[STATE] goblin.dying nil -> true

So I’m still working on integrating all of this.

Once all the rules are properly implemented (especially since I’ve learned along the way that there’s an SRD for FTD, and of course it doesn’t match my rules 100%), I’ll start working on more visual aspects, but we’re not there yet.

A quick note on tools:

  • It’s obvious, but I’ll say it anyway: without Git, there’s no hope (I’m working entirely locally).
  • CodeGraph is simply indispensable as the code grows; it lets you generate things like this:
combat_resolver
├── actor::get_ac
│   ├── actor::get_ac_bonus
│   ├── armor::ac
│   └── shield::apply
├── actor::is_dead
│   └── actor::has_condition
├── attack::resolve
│   └── check::resolve
├── damage::resolve
├── engine::get_actor
├── engine::get_state
├── equipment::get
├── events::push
├── hp::apply_damage
│   ├── hp
│   │   └── supply::max
│   └── supply::max
├── weapon::attack_bonus
└── weapon::damage_bonus

and:

engine::rest_actor
├── engine::get_actor [Modules/Engine/engine.lua:204]
└── rest_handler::apply [Modules/Engine/engine.lua:273, Modules/Engine/rest_handler.lua:12]
    ├── actor::clear_temporary_injuries [Modules/Engine/actor.lua:142, Modules/Engine/rest_handler.lua:12]
    │   └── actor.ensure_injuries [Modules/Engine/actor.lua:75]
    ├── actor::has_condition [Modules/Engine/actor.lua:201, Modules/Engine/rest_handler.lua:12]
    ├── actor::remove_condition [Modules/Engine/actor.lua:222, Modules/Engine/rest_handler.lua:12]
    ├── hp::apply_healing [Modules/Engine/hp.lua:34, Modules/Engine/rest_handler.lua:12]
    │   └── hp.normalize_amount [Modules/Engine/hp.lua:4]
    │       └── supply::max [Modules/Engine/hp.lua:4]
    └── rest::resolve [Modules/Engine/rest.lua:3, Modules/Engine/rest_handler.lua:12]

And, most importantly, to find out what I've already integrated and what I haven't:

Runtime call tree reachable from main.lua.

main.lua
├── love.draw
│   └── engine.draw
├── love.load
│   └── engine.init
│       ├── combat.create
│       │   ├── combat_round.create
│       │   │   └── combat_turn.create
│       │   └── combat_round.create
│       │       └── combat_turn.create
│       ├── combat.create
│       │   ├── combat_round.create
│       │   │   └── combat_turn.create
│       │   └── combat_round.create
│       │       └── combat_turn.create
│       ├── command_queue.new
│       ├── command_queue.new
│       └── event_setup.init
└── love.update
    └── engine.update
        ├── action_resolver.execute
        │   ├── engine.get_actor
        │   ├── engine.get_state
        │   ├── engine.use_action
        │   │   ├── actions.can_follow
        │   │   ├── actions.can_take
        │   │   ├── actions.use
        │   │   │   └── actions.can_take
        │   │   ├── actions.use
        │   │   │   └── actions.can_take
        │   │   ├── actor.can_act
        │   │   │   ├── actor.create
        │   │   │   │   ├── supply.load
        │   │   │   │   └── supply.max
        │   │   │   └── actor.has_condition
        │   │   ├── engine.build_used_actions
        │   │   ├── engine.get_action_key
        │   │   ├── engine.get_actor
        │   │   └── engine.set_action_used
        │   ├── events.create
        │   └── events.push
        ├── actor.has_condition
        ├── command_queue.pop
        ├── death_handler.resolve_deadline
        │   ├── actor.add_condition
        │   ├── actor.has_condition
        │   ├── actor.remove_condition
        │   ├── death.resolve
        │   └── events.push
        ├── death_handler.resolve_deadline
        │   ├── actor.add_condition
        │   ├── actor.has_condition
        │   ├── actor.remove_condition
        │   ├── death.resolve
        │   └── events.push
        └── event_dispatcher.dispatch_all
  • Last but not least, to keep from getting lost amid development, integration, bugs, and testing, Linear is a lifesaver too. enter image description here

Well, I’ve spent about half my vacation on this, but I’m having fun :)
That was a (very) long post—sorry.
Have a safe trip, and don't forget your torches if you're going to seedy places—especially ones that aren't well-lit.
Peace, out.

The Return of the Dungeon Generator

- Posted in D&D by

So, no, I hadn't forgotten about it, but with my tendency to procrastinate as soon as life gives me a break, this project had been gathering dust for quite some time.
Against all odds, I dug it out from the depths of a folder on my hard drive with the idea of reworking it, or even (crazy and pretentious as I am) finishing it, trying at the same time to conquer—for once—one of my most persistent demons: projects started but never finished. Thanks to global warming, I was able to spend a few days cooped up indoors to escape the heat and work on continuing this project.
Well, actually, I started it all over from scratch :)
Still built on Lua (which reminds me of my first love, Tcl) and its Löve framework (for doing 2D without reinventing the wheel).
So here’s a more or less stable version of Isometric Dungeon Generator (aka IDG). And yes, I know, it’s not an isometric projection, but who cares—it’s just its nickname.

So, what exactly does this revenant from beyond the grave do?
It lets you:

  • randomly generate dungeons with square rooms and straight corridors with turns
  • scatter doors (possibly secret ones), pillars, traps, pits, skeletons, and of course treasures throughout them
  • influence all that randomness by adjusting the weights that govern the spawning of these objects
  • render all of this using sprites that I half-stole (shh) and half-made myself
  • export a superb .png of the whole thing
  • and also export a .json file

What does it look like?
A zoomable overview of the dungeon:

A zoomable “internal” view of the dungeon that lets you visualize the underlying logic behind object generation:

A configuration window to control (to some extent) the parameters of the generated dungeon:



So, in the end, what’s the point of all this?

  • Well, for starters, so I can look at my screen and say out loud: “Did you see that? I did it!” (and, incidentally, to post here once every two years)
  • To accept that I’ll always be light-years away from the quality of projects like Dungeon Map Generator (acceptance's always the first step)
  • And it’s the first step toward my original idea: coding a single-player dungeon crawler, in the style of 2D6 Dungeon, even though lately I’ve been eyeing 5 Torches Deep more (I love the tension of managing torches)

See you in 3 years. And remember:

None of us are promised tomorrow. Life is uncertain. So... eat your dessert first.

Peace. Out.

(yes, the last sentences were a bit Matthew Colville influenced. Just a bit)

3D Gibberosa

- Posted in Cichlids by

It's pretty impressive how I can get a 3D Cyphotiliapia...
Starting with an image generated by NotebookLM, all I have to do is convert it to 3D and print the result.
I go to bed, and the next morning my Gibberosa is there!
Too bad I have no talent for painting :)