jdVOID: A Weekend Arena Duel, Written in BASIC
My son and I wanted something to play against each other. Not on the couch. On our phones, from wherever we happen to be.
So we built it. Two days, one language, no engine.
jdVOID is a two-player arena duel in the Clash Royale tradition. You spend elixir, you drop ships, the ships walk at the enemy stations and shoot whatever gets in the way. Three minutes, plus overtime. Whoever knocks down more stations wins.
The whole thing is jdBasic. The match server is jdBasic. The client on the phone is jdBasic. The rules are one module both of them import.
One file owns the truth
The single decision that made the weekend work: arena.jdb knows the rules and nothing else. No graphics, no network, no files. It exports a handful of functions over a state map that is plain JSON all the way down.
IMPORT ARENA
st = ARENA.NEWGAME()
msg$ = ARENA.PLAYCARD(st, pid, "KOLOSS", 4.0, 20.0)
st = ARENA.SIMSTEP(st)
The server imports it. The client imports it. The balance harness imports it. There is exactly one place where a unit decides what to attack, so the phone and the server can never disagree about the rules, only about the clock.
And because the state is a JSON map, the server does not have to serialize anything. It hands the map to JSON.STRINGIFY$ and that is the wire protocol.
The server does nothing until asked
There is no simulation thread. Nothing ticks in the background. When a client asks for the state, the server looks at the clock, works out how many 0.1 second steps it owes, runs them, and answers.
A room nobody is watching costs zero. Twenty idle rooms cost zero. The simulation only exists while somebody is looking at it, which is a very comfortable way to run a game server on a shared box.
Rooms are created on demand by name. That was the answer to "can my colleagues play too". They pick a room name, and two pairs never see each other.
The client is the same language, in a browser
jdBasic compiles to WebAssembly. The page fetches royale.jdb over HTTP, drops it into the virtual filesystem, resolves the modules it imports, and runs it. No transpiling, no second implementation, no JavaScript in the game.
A link carries the session:
play/?prog=royale&fs=1&name=Atomi&room=main&lang=en
That was the actual requirement from my son. Gen Z does not type program names into a REPL. They tap a link. So the site root is a poster page with three fields, and it forwards to the URL above with the canvas taking the whole screen.
The page also hands the program its own origin, and the client turns that into its API base. The same file runs against localhost and against production without a rebuild.
Nobody here can draw
I have no artist. What I do have is an array language.
Every ship is three to five signed distance fields. A disc, a rounded box, a bar. The rasterizer evaluates them over the whole pixel grid at once, adds a glow pass, and hands the finished RGBA buffer to SPRITE.SETBUFFER. It runs once at startup and after that the GPU does the work.
' a card is data, not a special case in the code
"BRUTSCHIFF": { "COST": 5, "HP": 900, "DMG": 55,
"SPAWNS": "NANOS", "SPAWNRATE": 3.0, "SPAWNN": 2 }
Cards are traits, not classes. FLY ignores the river. ONLYTOWERS walks past troops. BUILD never moves. TTL makes a building temporary. SPAWNS hatches a brood on its own clock. A new card is a few lines in a JSON file and a shape in the rasterizer. Fifteen of them shipped over the weekend, the last three on Saturday evening because my son declared that Titan was stupid.
The loser gets the same chest
Every finished match hands both players five common cards, two strong ones and a wildcard. Win or lose. That is not a design principle, it is family diplomacy. I lose most of them.
Levelling a card costs 2, 4, 8 or 16 copies. Wildcards fill the gap. Everything is keyed by player name in one JSON file next to the server.
Four things that bit
The sound died after half a minute. My son heard it before I did. The music bass had a sustain level and never released, so it filled the 64 voice pool and the allocator ran out. Two fixes: the music now uses a long decay instead of a sustain, and the engine steals the quietest sustaining voice when nothing is free. Measured with SOUND.RENDER: peak amplitude 0.001 before, 0.112 after.
Everybody got the same chest. RND() was unseeded, and seeding did not help either, because the C runtime keeps its random state per thread and the chests are dealt on HTTP handler threads. jdBasic now carries its own generator in the VM state.
Taps landed in the wrong place, then nowhere at all. The canvas is scaled on a phone, and the mouse coordinates were window coordinates. After that was fixed, taps still did nothing, because the event handler receives its payload as an array and I was indexing it as a map. One character.
The server crashed mid-match. My main loop was a SLEEP loop, which means the VM was executing bytecode on the main thread while HTTP handlers executed on theirs. jdBasic grew HTTP.SERVER.WAIT for exactly this, and the crash went away.
Play it
It is live at vvss.jdbasic.tech. Pick a name, pick a room, send the link to somebody. It runs in a phone browser, no install, no store.
The whole source is in the jdBasic repo under jdb/demos/games/basicroyale, with a README that covers the architecture, the endpoints and the box setup. And jdBasic itself lives at jdbasic.org, where you can try the language in your browser.
Two days. A language I wrote myself. And a son who beats me at my own game.