26-AUG-2026
26-AUG-2026
0940
I’m taking some vacation before Mayhem heads off to kindergarten. This morning is orientation and she is nervous and
excited and off to orientation this morning so I had some time to do some review of lurtz, and something cool
happened.
The design of lurtz has always been a little haphazard owing to the vibe-y nature of it, I knew that going in. I also
knew that it was likely I would step over the SRD/PHB line as I am sure Claude is trained – like all the perpetrators
of the grandest larceny in the history of theft – on all the PHB stuff, and it was going to leak in. My understanding
of the SRD and OGL and all that is this:
- Build whatever you like, these things govern what you distribute.
- Distributing stuff based on the SRD content is fine.
- If you distribute PHB content, or any other content from WOTC, the full force of the legal department of Hasbro will set ruin upon thee and thy house for ten generations.
I do not want to piss off Hasbro, but I am sure lurtz has like, PHB and some Arcana and such.
Now, this may seem like a problem if you want to build a D&D engine to manage balance for all of D&D. I agree it is a
constraint, but lurtz can be clever, and part of why I vibecoded it was to sort out how best to design around this.
I knew I wanted a bytecode machine, I wasn’t sure why. I try not to make design decisions like that without having a reason more than, “Because they’re cool.” When I first started learning Rust, I tried the synacor1 challenge, which required building a weird little bytecode machine among some other fun puzzles I won’t spoil. I’ve really liked these little guys since taking a low-level systems course in college based on some weird little 16b ISA that had a little simulator and stuff, it was pretty rad. I liked the architecture enough I started building hazel along those lines, before getting distracted by other shinies and landing here.
In any case, I knew I wanted to take that approach, but that didn’t really satisfy me for a reason, I decided eventually though that a good enough reason was to see how the tokenmachine managed with the architecture.
Overall, fine, I guess. Not great, not terrible. It took a lot of nudging to push it towards that model, and it took a
lot more to keep it there and get it not to re-invent every single wheel. Normally I think these systems have good
API-sense, but in reality what they have is majority API sense. That is, whatever the greatest common subset of APIs
people use, that is what it will inevitably lean to use; and since the APIs themselves are probably part of it’s
training set, there is a huge sample of DIY in there. Since the sorts of libraries for this less common architecture
aren’t as readily available or used as libraries for a more ‘ad hoc/amorphous’ architecture2, it is more likely to
grab a generic, frequently used library for which it has lots of examples, or just DIY everything. I had to point it to
bitmask a bunch of times.
As I was starting to dig through some of the generated code, I started seeing it – PHB-sign. A little scattering of
Battle Master rules has snuck it’s way into the API. I need this code, it’s not going anywhere, by #1 above I can keep
it I just can’t give it away, so the obvious solution is to stick it in a crate. In a lurtz-fighter crate it is safely
tucked away where I can avoid publishing it, but how can I keep it’s PHB-ness off the core engine?
When you design an ISA, you don’t take responsibility for every program written in that ISA, you just design the ISA. When Intel had its FDIV bug, it was responsible because the ISA was broken. When your software crashes because someone divided by zero, the author of the software is at fault, not Intel.
In the same way, lurtz doesn’t need to provide direct APIs for any of the mechanics – in any system –
lurtz-as-a-VM should be agnostic to the game system entirely. We need that for 2014/2024 support anyway. In a way, you
can think of a battle as an incremental compilation problem. We have a battle-log, I need to turn that sequence of
actions into a sequence of opcodes on a VM such that the state of the VM after executing those codes contains sufficient
information to reconstruct the battle at that state.
That is to say, I can manage some auxillary structure to manage all the things a Battle-Master class would need without
actually implementing the class directly in lurtz. This is a much better reason and I retroactively use this one as
justification for this model. It has some nice knock-on effects too.
Right now lurtz is one big crate with everything piled in. It has an action API for all the things you can do in D&D,
it has the engine itself, all the opcode stuff, everything, one big pile. Obviously I can split out lurtz-$class like
mentioned, but also lurtz-monsters and lurtz-players can be split out. Players and Monster statblocks both translate
to ‘intialization’ opcodes that start a battle log and configure the environment. These translate into datastructures
inside the VM as well. I can then further split out lurtz-srd, which contains the core SRD actions, upon which all
these other crates depend, now lurtz is the core crate with only the engine in it. Each crate can have it’s own
validation set, which we compile and assert on the engine state. These now track the implementations much more tightly,
which will greatly improve my ever growing mutant problem.
It has a natural extension pattern as well, new UA or splatbooks can be included as another crate, independently tested.
It means that lurtz can be optimized aggressively for the GPU and the ‘compiler’ crates can expect to run more or less
uninterrupted on the CPU. I suppose in the final tally the training process probably looks like some number of agents
playing as many games as they can on the GPU; they make a big pile of next move decisions while the CPU grabs those
moves, compiles them, and sends them back to the engine to update in parallel. I’m not sure if I could get 100% of that
loop on the GPU but it doesn’t seem impossible. That’s either inexperience or very clever, I’m not sure which.
It also leaves open a door to test other combat systems. Pathfinder is an obvious target, but there are lots of systems that look like D20-style combat.
All this would’ve probably been findable with a whole bunch of anxious thought about how things might work. By the time
I got there I’d’ve been so worried about implementing it wrong I’d’ve given up. I’ve put about two proper weeks of
thought into lurtz at this point and have a working program. It’s not good, it’s not super well factored (yet), but
it’s reasonably functional and it’s easy enough to change. Because I had a working program, I was able to see this
refactor, instead of having to imagine it ahead of time. I can look at this code and this structure falls out.
I will definitely need to rewrite every one of these crates at some point3, but having them to hand when finding the architecture makes code feel much more play-doh than it used to.
I quit working on hazel partially because I was getting lost down a rabbit hole of screwing with the trait system to
do some weird message passing stuff. I also quit on it because I kept running into problems that needed a big fat
refactor that would take days of churning over error messages and making tiny adjustments. If it weren’t for the fact
that I do want to keep one project as agent-free as I can, I’d be using an LLM for those changes today and probably
still be working on it. That hypothesis is supported by this result with lurtz. My favorite part of coding is
refactoring, because it is really difficult to hold both the observed and the desired in your head at the same time;
much less to plot a course from one to the other. My strategy has been to rely on good observation skills and git to
slowly move from one to another. LLMs offer a new option here. lurtz has been sort of ‘3d printed’ into place. It’s
cheap plastic, it’s all one mold instead of several moving parts, but it gives me an object I can hold in my hands.
I occasionally turn wood; mostly to make handles for stuff. One of the things about turning handles is that – while you need a template to get a repeatable result, no two handles are ever finished exactly alike. You always use the same process, or at least I do:
- Turn stock to the template dimensions
- Cleaning pass with a skew
- Rough pass with 60 grit
- Stop lathe, feel the handle.
- Start lath, skew or sand.
- Repeat 4 and 5 until it’s right, adjust grit as necessary.
- Friction polish with BLO, Wax on the Lathe, Buff.
You can’t make a handle for something without trying to use it first, there is no substitute for holding it in your
hand. Trying to divine this architecture for lurtz ahead of time, especially given my vanishingly close to zero prior
experience in game making, would’ve been like trying to make a perfect handle template – not possible.
In any case, this was a very nice validation of what I hoped would happen when I started this, I had hoped that LLMs would work nicely this way and it has. I have a natural skeleton to build the next version of this against, and all it took was two weeks of prompting, not exclusively on this but majority on this, most of which were status report requests / disambiguating parts of the plan. I’ll talk about how I set up this project another time. I kept all the conversations archived so I should be able to do some forensics on it.
-
The original challenge page is down, this is a mirror I found here. I hope it comes back at some point, but maybe someone oughta just build a new one. :) ↩
-
I couldn’t find a way to make that not sound perjorative. I don’t mean it that way. Ad hoc architecture is not a crime, it’s a choice with trade-offs. ↩
-
I wrote that and immediately went to hedge – maybe this is true, but I could see a world where I rewrite some of the crates, then make the machine copy that style to the others. Still a rewrite, but by what fraction? ↩