Fast simulation code is slow to write, so generators write it
I want to talk about code generators, domain specific languages (DSL), and simulating interactions. Me personally, I am a fan of games like Dwarf Fortress, Age of Empires and Might and Magic, and Role Playing Games in the broader sense. Now, no matter how you look at them, they all boil down to having a "state" (like a chess board), some "actors" (the little ants you interact with), and "the rules and relationship" they form to each other. Initially this idea started being explored by looking into cache optimized Go data structures after reading a blog post by Serge Skoredin, and trying to encode them as annotations for generating code out of them. I like meta programming, not gonna lie. Ecclesia is an isometric pilgrimage through a world of countries drawn as Orthodox icons. It is written as annotations on Go structs, about ten lines per entity. Seven code generators, stacked, turn those lines into cache-friendly memory pools, behaviour, the world, the drawings, the network frame, the screens and the rules. The simulation runs on one thread, at 0.21 ms per tick on an Intel i7-9750H. Twenty frames from tick 60 to tick 2,340, rendered headless by the game's own tool, with the camera following the shepherd. The goal How many classic Reynolds boids can you run on 1 CPU at 60 FPS, without using any goroutines? I managed to get around 8192, using Serge Skoredin's approach from the blog post last year. My goal for this experiment was to encode the optimal cache data structures into metaprogramming generators, such that Claude can write a high-level DSL and generate down to this level of simulations. If I had to output the code myself, it would take around 8 hours of constant writing to get around 1k LoC of code. I treat the low-level tasks as building blocks. You need a grasp and understanding of what is possible with them, but you do not need to remember the exact byte order and syntax. The stack I went full meme, and stacked like 3 levels of generators on top of each other. Level 1: A basic generator does the same stuff: raw structs to SoA + wire codecs + bitfields to not waste space. And runs benchmarks to see how many allocs/ops per entity there are. And one more fun thing: it encodes the gamestate on 1400 bytes, in order to fit on a UDP datagram, such that it can support networking. Level 2: Then on top of this one, I got a generator for relations between entities. Level 3: And on top of this one, one for "game" design and schema behavior. And then all this runs on the simulator engine that handles collision, flow fields, events, genetic algorithms, networking, etc. Ecclesia added four more levels. Each generator reads its own annotations and writes Go that the level above calls. Generator Describes Writes gameframegen Memory Struct-of-arrays pools, wire codecs, bitfield accessors simgen Behaviour Actants, relations, and the state machines that move each relation forward gamegen The world Species, spawns, world rules icongen What a thing looks like Each asset as a list of operations over parametric forms, plotted to an 8-bit indexed sprite by integer arithmetic. No image files. netgen What a client is told, and in what order Which pool goes first into the 1,400-byte frame when the world is busy shellgen Everything around the game Screens, render order, layout, typed settings, a versioned save format rulegen Who a person is, and how what they do turns out Hidden dice. Every probability is computed by exact enumeration of the 216 outcomes of three dice. Two checks sit outside the chain. simcheck emits nothing. It refuses a relation that nothing spawns, and an actant that nothing fills, because both generate valid code and never happen. netgen's budget check fails the build when a schema promises more than a frame holds, and it prints the arithmetic. From a story to a running world The flow is something like this. You ask Claude to read a story, a novel, whatever, and extract relations, entities, etc. Then it edits a Schema.go with the high-level DSL annotations, like 10 lines per entity. Then I run the Level 2 generator: that creates the primitives and a high-level API. Then it combines the high-level API rules in main.go and just compiles it to the final gamestate. 1. A story: "fairies that glow and heal deer". 2. Annotations on Go structs, about 10 lines per entity: simgen:actant affordances=Flee,Graze (the deer) simgen:relation Heal actors=Fairy,Creature (the healing) 3. Seven generators write the Go code. 4. simcheck and the frame budget refuse what can never happen. 5. go build, and the world runs. The page view and the eye view The world is drawn from the page, isometric, as in the first animation. The same state can also be seen through the shepherd's eye. There is also a first-person view, similar to the original Might and Magic. Twenty-four frames at tick 600, the eye turning 360 degrees in steps of 15. Threads For all this + networking + disk I/O, it works on 4 cores (threads) only. But the simulation in itself is on 1 core, 1 thread. The networking is offloaded to 1 core (1 thread), disk I/O to a different core (thread), and the painting on screen to a different core (thread). Measured per tick on an Intel i7-9750H, default world, 600 ticks, rendered headless: simulation 0.21 ms, culling 0.28 ms, ground 1.33 ms, sprites 1.14 ms, overlay 0.49 ms. MMORPGs with a local agent It is quite wild, as at some point you realize you can hot swap the views of a running program, and end up letting people create their own fantasy game by prompting on the same gamestate and rules shared by other people. I think we will end up having MMORPGs that ship a small local agent, that allows you to fill in the gaps and build the world and game system tailored to you, but at the same time interact and be a live part of the other people's own game fantasy. I would love to play such games. Limits. The per-tick numbers come from one machine and one world, over 600 ticks; a busier world costs more. The 8,192 boids figure is a separate experiment, one thread at 60 FPS. The engine and the game are not published. Originally published on keibisoft.com: Fast simulation code is slow to write, so generators write it
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to