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

Neolamprologus Similis

- Posted in Cichlids by

To live happily, live hidden.

Let there be graphs

- Posted in Computer by

Some years ago I tried to monitor and display some domestic metrics, with my aquariums temperature as a main goal.
I thus bought a profilux monitor, which was expensive. It was also a close system (at least at the time, no API for external access, I quickly gave up trying to hack it), and... mine is now out of order...
So instead of fixing it, I decided to take the Home Assistant road, and, wow, what a trip :)
And guess what, temperature captors are available, easy to use and some of them are waterproof. See me coming?
Within a few hours, I had my long awaited water temperatures in a nice graph: enter image description here

Another proof, if needed, that happiness definitively lies in simple things.

ps: Yes, the Profilux can also monitor pH values, and I've yet to find a Zigbee device that can do that, other than some huge pool stuff. But unlike my temperatures, my pH is stable, so as long as I have to choose, I'd rather have my sexy temperature curves than ... nothing at all :)
And who knows, maybe tomorrow Amazon will offer me the Zigbee pH sensor I've been waiting for.
Peace out.

Dungeon generator

- Posted in D&D by

I recently stumbled upon this awesome free online Dungeon Map Generator, which gives you really nice ready to use maps like this:
enter image description here

While trying to build such a generator from scratch, I quickly came to the conclusion that this one is in fact much more complete and complex that it appears at first sight (and you realize that once you've tried to build your own):

  • Maps are not just orthogonal, they're nicely rotated
  • Walls are sexy
  • Rooms can be round
  • Rooms have notes

So yes, I'm trying to build my own map generator (got the idea while playing the solo Dungeon Crawler 2D6 Dungeon, which is by the way as cheap as excellent) and after a few hours of Lua, right now my generated maps look like this (beware, it's all but as sexy as above): enter image description here

There's a French proverb that says “when I look at myself I feel sorry, when I compare myself I feel comforted”.
Well, right now I'm much more sorry than comforted :)
So now I've got some work, first debug (the doors are badly placed, the corridors too numerous), rooms should be numbered in a clever way, and then make the output a little prettier (as far as the look is concerned there's definitively room for improvement).
And then generate levels interconnected by stairs :)

Why spend hours trying to do this when very nice free tools already exist?
Because “Its the not the destination, it's the journey.”
Peace out.

R U mine?

- Posted in Mind thoughts by

I recently realized that I really like the R's.
Model of my Mondraker mountain bike?
"Level R", check.
Model of my Honda car?
"Type R", check.
And even my motocross bike is a Kawasaki without R, I do love the 90's Honda models, "CR".
enter image description here So love (at least mine) is definitely in the R.
(please pronounce "R" the french way (air) , so the joke is 100% complete)

Take only pictures, leave only footprints

- Posted in Computer by

Twenty years ago, I roamed the four corners and like everyone else, I took pictures of Antelope Canyon. You know, the ones where yours are always 100 times uglier than everyone else's?
It just so happens that, at the time, I still had a film camera, which doesn't like X-ray scans at airport security checkpoints, which of course I didn't know, in my naivety at the time (and still a bit this day).
As a result, once developed, all my photos of a once-in-a-lifetime trip are all speckled, and therefore terrible.
Furthermore, the light (or lack of it) in Antelope Canyon already makes this kind of photo natively difficult (plus people everywhere, no photo tripod).
So even if the result is personal and therefore emotional, it's undeniably ugly.
And here, 20 years later, playing with Topaz's tools (no, I'm not sponsored), I've come up with a rather nice result, even if inventing pixels inevitably distances the final result from the original reality.

enter image description here
Yes, 20 years... Life begins when you realize you've only got one? Yes :)

Surrounded by CONST

- Posted in Mind thoughts by

Do you remember when you used to write C code?
(yes, back in the 80's)
More specifically the difference between

int aVar = 100;

and

const int aVar =100;

Something (thunder)struck me today: life's quite similar to a C program.
After spending decades worrying about all RealLife™ variables around me, trying to change them, now I've reached an age when things became crystal clear:

DO NOT WAIST TIME WORRYING ABOUT CONSTANTS
(they will out-live you)

Trying to change a CONST variable in everyday life will invariably give you the following error:

gcc life compiler error: line xx, assignment attempt of read-only variable 'aVar'

So the only hope resides in changing non const variables, so focus only on them, the hell with const.

Free tip: Assholes are undoubtedly const variables ¯\_(ツ)_/¯

Where it all began

- Posted in Gillian by

Somewhere around 1998, on a whim, I bought the X-files season 1 box set and discovered Gillian Anderson, and I've been a fan ever since.
Yes, I even have autographed photos...
Lately, I've been thinking I'd mix up my desire to re-watch X-files and play with Stable Diffusion, specifically the Low-Rank Adaptation (aka LORA) that let you model elements in it.
So I spent a few hours making screenshots and building my first LORA, obviously of Gillian Anderson, aka Scully.
The first results are encouraging (I'm far from a specialist, and the power of Stable Diffusion is matched only by its complexity):
enter image description here

As the X-Files show took place over several years, Gillian evolved visually over time, so I challenged myself to do one LORA of Scully per season.
enter image description here

These two images come solely from a LORA made from the captures of the first two episodes, a job that requires ... a lot of time (capture, crop, describe and then sleep while my 2080 is working).
But I'm pretty happy with the first results, even if I'm learning as I go. See you in a few weeks for the release of the hopefully nice Scully_s01.safetensors.

I want to believe.