D&D

Stuff about this very unknown tabletop RPG

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)

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.

More than images, ideas

- Posted in D&D by

Recently I played quite a bit with Stable Diffusion, a program for generating images from text, and it's amazing.
I'm not going to try to show here some unmatched ability (that I don't have) to generate extraordinary images, the Internet is full of tutorials on the subject (I recommend for example Sebastian Kamph's YouTube channel, he is both talented and educational).
However, let's talk about the unexpected fact that these images ... stimulate my imagination.
Indeed, in order to illustrate in an original way the textual descriptions of my imaginary world of Gilianar, I played with Stable Diffusion and besides the quality of the images produced, I discovered with surprise how this tool gave me new ideas.
Let me explain.
Let's say I want an image of a dragon perched on an old tower. With the following prompt

ancient yellow and green dragon with a large wingspan perched on an old broken stone tower

I get this for example:
enter image description here
Not bad for a start.
Stable Diffusion's modus operandi is to generate a lot of images, then once you like one of them, to modify it in a loop until you get a satisfactory result.
So let's modify it, but this time starting not from the textual description of the beginning, but from the image above (yes, Stable Diffusion is amazing, I already said it).
By playing with the enigmatic parameter Denoising strength, one introduce more or less chaos in the following images and so that's where magic happens, it's as if a part of a story was written without us.
After a few generations, an image clicks in my head.
"Oh yeah, that's cool, it looks like a forgotten tomb, let's keep this one!"
enter image description here
So much for the grave, now it looks more like she has boobs and she is instead near the entrance to her nest, on which she watches fiercely.
What if she protects her eggs? Deal!
A few random images later with guarding her eggs added to the description, this one fits my mood:
enter image description here
Definitely a good start for a place and the history that goes with it!

Thank you Stable Diffusion, I was just looking for original images to illustrate my texts, but I actually found more than that, now I own an iterative idea machine!

Life's too short not to explore ideas

- Posted in D&D by

After spending a few hours reading Monte Cook's Ptolus D&D campaign setting, it became obvious that creating a whole world was as appealing to me as playing someone else's. So why not giving it a try?
I don't know anyone who has died of shame, at least not officially, so I'm going for it. Gillianar (do you see the wink?) is a world designed for D&D, but above all to have fun materializing my ideas, my dreams, even my deliriums.
Ideas come quicker than I can write them down, it's quite exhilarating. I know I won't last the distance (let alone approach the 800 pages written by Monte) and that I might get bored pretty quickly, but getting started is pretty exciting :)
This universe will be freely available here, after all I'm writing for me and not to get rich.
It's just a pity that I waited for the advent of ChatGPT to try to write something, but at least it will all come from my fertile brain and not from an AI as good as it is :)
To the question "Where do I start?" I answered with "Draw the world as it is in your head", the first result (there will be a lot of changes I think) is the image bellow. Gillianar, home of tormented heroes Yes, 99% of it is probably just a compilation of what I've read or seen in medieval fantasy, but that's okay. For me :)

See you soon, and while then, roll high or die!