Atomis Development Blog

jdVOID Part II: The Card Factory

Part I was the game. This is what happened in the five days after, and it is not more content. My son wants three to six new cards a week. That is a tooling problem, not a card problem.

It installs like an app

A web manifest, a service worker, three icons rendered from the key art. Tap once and jdVOID sits on the home screen with no address bar. No store, no wrapper, no second build.

The worker splits traffic three ways, and that split is the whole design:

A card designer, in the browser

The jdVOID card designer: a 40 unit grid with the ship figure and drag handles, the shape list, all card values and traits, and a help column

A ship is three to eleven signed distance primitives on a 40 unit grid. The designer draws them, you drag them. Then the values, the twelve traits the rules actually read, the card colour, the description in two languages. The preview is the game's own rasterizer ported to JavaScript, so it shows what the game will draw rather than something close to it.

It writes nothing. Drafts live in localStorage, the output is a JSON block, and getting a card into the game means pasting that block into cards.json and deploying. I built the server side publish flow on paper and then did not build it. Family policy: no wild growth.

Two cards, the real rules

Two ships in the jdVOID sandbox: a pink beam standing between the attacker and its target

The client is jdBasic in a browser, so the rules module can just run there. sandbox.jdb drops two cards facing each other and plays cards into arena.jdb. No second engine to drift.

Same duel, desktop and WebAssembly: LASER wins with 100 hull left after 9.3 seconds. Identical to the tenth of a second and the hull point.

The beam in that picture is a new card field. Until now the only thing that decided what an attack looked like was reach: over two a travelling bolt, under it a melee slash, and always the same yellow. A card can now ask for SHOTFX: "BEAM" and gets a standing line in its own colour.

One card against all the others

The question a new card raises is not whether it beats the DROID. It is what it is too strong against.

$ jdbasic duels.jdb IRON tmp/iron.json

IRON (6 Elixier) gegen alle anderen

Gegner        Elix  Ausgang                Rest    Zeit
BRUTSCHIFF    5     BRUTSCHIFF gewinnt      770    32.5 s
GLEITER       4     IRON kommt durch       1915    12.4 s
KOLOSS        5     KOLOSS kommt durch     1140     8.9 s
PLASMA        4     Zauber                  325    nimmt 325 von 3400
RAMME         4     RAMME kommt durch       720     3.7 s
TITAN         7     IRON gewinnt           1150    17.9 s

Bilanz: 11 gewonnen, 3 verloren
gegen gleich teure oder teurere: 1 von 1 gewonnen

"Walks through" is not "wins", and the difference is the whole point of a card that ignores troops. The table keeps them apart.

Replays, and what if

A recorded match is its two decks, the card levels and the cards that landed. Inputs, not states. Replaying it has to end the way it ended, which makes every recording a determinism test that runs itself.

Each recording carries a fingerprint of the card table it was played under, so a replay that diverges can say whether the rules moved or the recording is incomplete. Checkpoints every 300 ticks name the second where it started to differ.

Then whatif.jdb: change one number, replay every usable recorded match, and count how many winners flip. A balance change measured against matches that were actually played instead of a feeling.

What the tools said that we did not know

The last one is why the designer's help column was read out of arena.jdb rather than remembered.

Play it

Still live at vvss.jdbasic.tech, now installable to a home screen. Source under jdb/demos/games/basicroyale in the jdBasic repo, and the language itself at jdbasic.org.

Five days. Zero new cards from me. My son has a factory.

← Back to all posts