← Blog Feed (Atom)

Post two · a project write-up

AI World Gen: thousands of tiny decisions, and a loop that graded them

A map generator that asks a model one question per tile. A new kind of model that answers in milliseconds for a fraction of a cent. And the evaluation loop that took it from a green blur to a village in eleven measured versions, most of them written by an agent.

A 16 by 16 pixel-art village: a stone cottage with an orange door in the top left, a wooden cottage with a door in the middle, trees along the top, a dirt path across the bottom, a villager and two haystacks.
Version 11 drawing the "medieval village" preset on a 16 by 16 grid. Every tile was one question to a decision model. The stone cottage, the wooden cottage and their doors were placed by code first; the model chose everything else.

A model that decides instead of writes

AI World Gen started at the AI Hackathon Barcelona 2026. The idea: describe a place, and watch an AI build its map one tile at a time. The interesting question was never "can a model draw a map". It was: given what is around this tile, what goes here? That question is asked hundreds of times per map. Asking a chat model each time is slow, expensive, and turns a one-word answer into text you have to parse.

Jev, made by TypeSafe and served through OpenRouter's decisions endpoint, is a different kind of model. You send it a state (a JSON object describing the situation) and a typed question: pick one of these options, or place this on a scale. It answers with a choice, a confidence and a probability for every option. It does not write text at all. It is built for speed (TypeSafe quotes tens of milliseconds for the model itself) and it is priced at about $0.04 per million input tokens, roughly fifty times less than the chat models I would otherwise have used for the same call. The exact bill is a section of its own below.

One decision: state in, typed answer out A box on the left holds the state: the world, the cell and its part of the plan, the whole map as letters, the neighbours, counts and a balance sheet. An arrow carries it, with one question ("which type goes here? options: grass, path, oak, fence"), to a box marked Jev. An arrow leaves Jev to a box holding the answer: the choice "path", a confidence, and one probability per option. state world: medieval village cell: (9, 12) · outside map: 16×16 letters neighbours: grass ×5, path ×2 balance: path 18% of 15% missing: well, villager suggested: path (door nearby) one question which type goes here? grass · path · oak · fence Jev one choice answer choice: path confidence: 0.71 path ████████░░ 0.71 grass ██░░░░░░░░ 0.19 fence █░░░░░░░░░ 0.07 oak ░░░░░░░░░░ 0.03
One tile, one call. The state is a few hundred tokens; the answer is a choice with the probabilities behind it. The map samples from those probabilities, so a model that is 70% sure of "path" gives a map with some grass in it, not a solid path.

So the generator splits the work in two. One creative call goes to a language model, once per world: it invents the vocabulary, the six to twenty-four kinds of tile a "space station" or a "haunted mansion" is made of, with their rules. Then one decision goes to Jev, once per tile, with the vocabulary as the options and the map so far as the state. A preset world skips the creative call entirely, because the vocabulary ships with the page.

The numbers make the design possible. The test run I describe below draws 38 maps, 3,008 decisions in total. It costs about 24 cents and takes under ten minutes. A chat model in the same seat would cost dollars and take the better part of an hour, for an answer that has to be parsed back into one of the options. Cheap and fast is not a nicety here; it is what lets you run the whole thing before every change.

What it costs, really

Here is the bill. OpenRouter's usage page for the week in which all eleven versions were drawn, back-filled and re-drawn shows 39.2 thousand requests to Jev, 79.1 million tokens, and a spend of $3.11. That is 2,018 tokens per decision (the state with the whole map in it), $0.039 per million tokens, and $0.00008 per decision: about eight thousandths of a cent. A 16 by 16 map is 256 decisions, so $0.020. A full test run of 38 maps is 3,008 decisions, so $0.24. The language model, Claude Sonnet 5, was called 7 times that week to write vocabularies, and those 7 calls cost $0.33: one vocabulary costs about the same as 594 decisions.

What one dollar buys, logarithmic scale Jev decisions12,605Per-cell calls to DeepSeek V4 Flash13,749Per-cell calls to Claude Sonnet 5244Vocabularies from Claude Sonnet 521
Prices from OpenRouter on 20 September 2026. The Jev figure is measured from the bill; the others are computed from the list price with the same 2,000-token state and a ten-token answer per call.

The fair comparison is not Jev against a chat model, it is three ways of drawing the same map. One: a decision per cell with Jev, which is what the generator does. Two: the same per-cell loop with a chat model, sending the same state and asking for one word back. Three: one chat call that writes the whole 16 by 16 grid as JSON, which is what most people would build first.

Cost of one 16 × 16 map (256 cells), logarithmic scale $0.0010$0.010$0.10$1 Jev, one decision per cell$0.020Claude Sonnet 5, one call per cell$1.05GPT-5.2, one call per cell$0.93DeepSeek V4 Pro, one call per cell$0.22DeepSeek V4 Flash, one call per cell$0.019Claude Sonnet 5, the whole map in one call$0.021GPT-5.2, the whole map in one call$0.027DeepSeek V4 Pro, the whole map in one call$0.0024
Per-cell with a frontier chat model is about fifty times the price of Jev. The whole map in one call is in the same range as Jev, and with the cheapest models it is cheaper.

Two things in that chart surprised me. The first is that the per-cell loop with a chat model is not absurd: with the cheapest models it costs about what Jev costs. Price alone does not justify a decision model. What does is everything around the price. Jev can only answer with one of the options you gave it, so there is nothing to parse and no invented tile type. It returns a probability for every option, which is what the inspector shows and what the sampling uses. And it is built to be fast: TypeSafe quotes tens of milliseconds for the model; the round trip from my laptop, through OpenRouter, measured about 0.35 seconds per decision in the evaluation runs, most of it network. A chat model answering a two-thousand-token prompt takes one to three seconds before it says its one word, so the same 256-cell map takes five to ten minutes instead of a minute and a half.

The second surprise is that the whole map in one call is cheap, too, and it sees everything at once, which is exactly what the per-cell model could not do until the blueprint gave it rooms. A chat model writing the grid in one go might well draw a better village. It might also return a 15 by 16 grid, or a tile type that does not exist, or a valid JSON document with a hole in the middle, and when it does, one failure loses the whole map. There is nothing to watch while it works, no probability behind any cell, and no way to ask again about one tile. The per-cell design is a bet that those properties are worth more than the model's view of the whole, and the blueprint is the admission that the view of the whole still had to come from somewhere. Code gives it for free.

The practical lesson is about the state. At $0.00008 per decision, the question "should I send the whole map in every call?" has an answer that costs nothing to try. The map sketch is about 300 tokens on a 16 by 16 grid, so sending it 256 times adds a few hundredths of a cent to the map. That is why version 5 could put the entire grid in every question and measure what it did, and why the state now carries a balance sheet, the missing landmarks and the excluded types. When each decision costs this little, the state is where you spend.

The assumptions behind the chart: OpenRouter list prices on 20 September 2026, per million tokens, and the cost of one 16 × 16 map each way
Chat modelInputOutput256 calls, one per cellOne call, whole grid
Claude Sonnet 5$2.000$10.000$1.05$0.021
GPT-5.2$1.750$14.000$0.93$0.027
DeepSeek V4 Pro$0.422$0.845$0.22$0.0024
DeepSeek V4 Flash$0.036$0.073$0.019$0.0002
Jev 1.13 (measured)$0.039$0.020

Per cell: a 2,000-token state and a 10-token answer, 256 times. Whole grid: a 2,500-token prompt (vocabulary, rules, instructions) and a 1,600-token JSON answer. Neither includes retries or reasoning tokens, which would raise the chat-model figures.

Version one was a green blur

A 16 by 16 grid entirely of dark grass tiles, with one green tree near the top.
v1, the same village seed. One tree, 255 grass.
The same seed drawn by version 11: two cottages with doors, trees, a path, haystacks and a villager.
v11, the same seed, 20 hours and ten versions later.

The first version worked, in the sense that every call succeeded and a map came out. It was also useless. Thirteen of the fifteen maps in the first test set used one tile type for more than 85% of the cells: a village that is all grass, a station that is all corridor. The reason is obvious once you see it. A model that decides one cell from its neighbours, and whose neighbours are all grass, picks grass. Every correct local decision adds up to a wrong map.

I could have looked at three maps, changed the prompt, looked at three more, and felt good. I have done that before and I know how it ends: the change that fixes the village breaks the station, and nobody notices for a week. So before touching the generator, I built a way to measure it.

Measuring instead of looking

The measurement lives in Galtea, an evaluation platform for AI products (where I work, so take this paragraph as a demonstration of the tool as much as a recommendation). The shape of it is simple. A product has versions. A version is judged against written specifications: sentences about what a good map is. Each specification has metrics that score a map from 0 to 1, and a dataset of test cases. A test case here is one seed: a preset, a grid size, a fill order and a random seed, so every version draws the same 38 worlds.

Almost every metric is plain code in the project, not a model. "Doors sit in walls" is a loop over the door tiles. "One walkable region" is a flood fill. "The route reaches the edge" is a search. The one thing code cannot answer, "does this look like a village?", goes to a judge model, GPT-5.2, which reads the map as text and scores it. The specifications grew as the failures showed up: three at the start (barriers form structures, paths form routes, everything walkable is reachable), a fourth after version 4 ("a place reads as a place": doors in walls, walls as outlines, an enclosed room, ground in patches), and three more after version 7 (the vocabulary's own rules hold, routes lead to doors and off the map, the landmarks are there once and nothing floods the map). By the end there were 28 computed metrics and one judge.

The evaluation and improvement loop Six boxes arranged in a circle, joined by arrows: change the generator; run the 38 test seeds; score every map with the 28 metrics and the judge; compare with the previous version in Galtea; read the failure modes in the pictures and the numbers; form a hypothesis, which leads back to the change. An agent sits inside the circle and turns it. Outside the circle a person sets the direction, adds specifications when a failure has no number, sets the budget, and decides when to stop. 1 · Change state, sampling, rules, plan 2 · Run 38 seeds ~3,000 decisions, about 25 cents 3 · Score 28 metrics in code + 1 judge 4 · Compare against the previous version 5 · Read the failures the pictures and the numbers 6 · Hypothesis one theory, one version an agent turns the loop writes the change and its tests, commits, runs the evaluation, writes down what it saw a person, outside the loop sets the direction · adds a specification when a failure has no number · sets the budget · says stop
Every version is one turn of this loop. The agent (Claude Code, in this case) runs steps 1 to 6 on its own; I read the pull requests, added specifications when I saw a failure the numbers missed, and decided when the loop had earned its rest.

Eleven versions in twenty hours

Version 1 was scored at 14:20 on 19 September. Version 11 at 09:49 the next morning. Each chart below is one specification, and each point is the mean score of that version over the same 38 seeds. The dotted grey chart is the judge. The numbers are in the table at the end.

Structures 0.18 → 0.98 1 0 v1v5v8v11 Paths 0.74 → 0.77 1 0 v1v5v8v11 Reachable 0.69 → 0.88 1 0 v1v5v8v11 Reads as a place 0.48 → 0.88 1 0 v1v5v8v11 Rules hold 0.83 → 0.99 1 0 v1v5v8v11 Routes to doors 1.00 → 0.82 1 0 v1v5v8v11 Landmarks 0.07 → 0.83 1 0 v1v5v8v11 Judge (GPT-5.2) 0.09 → 0.69 1 0 v1v5v8v11

The judge, which never saw a metric, agrees with the code: it moves from 0.09 on version 1 to 0.69 on version 11, and its two big steps land where the computed ones do, at the balance sheet (v2) and at the blueprint (v8). The story the charts tell, in order:

v2 · confetti
A balance sheet in the state, so the model knows which types are under-used. Coverage of the vocabulary went from 0.15 to 0.91, and the model placed the needed types anywhere: single walls, single path cells, nothing joined up.
v3 · floods
Continuation hints: "this wall line reaches you, continue it". The model did, across the whole map. 58 corridor cells in one 8 by 8 station.
v4 · lines
Code judges the one suggested continuation: short lines, closable gaps, never an over-used type. Walls form lines and closed shapes. But 21 of 23 doors stood in open ground, and no map had a room you could enter.
v5 · the model reads the sketch, a little
The whole map goes into the state, one letter per cell. Coherence moved from 0.50 to 0.53. The 16 by 16 maps showed the limit: walls still came as solid slabs, because a model deciding one cell cannot see where a rectangle should end.
v6 · less noise
The sampling had been overriding the model's own choice on 27% of the cells. Raising the floor to a third of the best option cut that to 18%: ground in patches 0.25 to 0.52, and the rare types lost their chances (coverage 0.72 to 0.54).
v7 · the local rules hold, the rooms do not
Typed placement rules (never next to X, only next to Y, on this edge) enforced in code before the question is asked. "Rules hold" went from 0.61 to 0.98. Nothing else moved: "never next to" cannot say "in a wall".
v8 · rooms
The blueprint. Code places each structure as a rectangle with a door before any cell is asked about, and each cell is only offered the types of its part: wall, door, interior, outside. Doors in walls 0.15 to 0.96; an enclosed room on every map; "a place reads as a place" 0.52 to 0.89. The price: coverage down to 0.52.
v9 · the world remembers what it lacks
The state names the things the world still misses; a unique landmark is out once placed; a route is suggested outside a planned door. Landmarks single 0.68 to 1.00, doors with a route 0.56 to 0.88.
v10 · the cap that could not fire
A ground type past twice its target is no longer offered. It changed nothing, because the maps that flooded were the settings whose vocabulary made the route the only floor: a corridor was the station's ground.
v11 · a floor that is not a road
The station gets an open deck, the city block a plaza, and corridor and street become lines. The station's route share fell from 0.53 to 0.12 of the map. The corridors are now short stubs on the deck, which is the next problem.
v1: an 8 by 8 haunted mansion that is almost entirely dark floor.
v1
v2: scattered single wall tiles on dark floor.
v2
v3: wall fragments, a candle, a door in open floor.
v3
v4: wall lines and a block of cobwebs.
v4
v5: a solid slab of wall tiles with windows.
v5
v6: wall slabs with windows, no room inside.
v6
v7: wall slabs again, with a door on the outside.
v7
v8: two rectangular rooms with doors, a candle between them.
v8
v9: two rooms with doors and a candle, a chest in one.
v9
v10: two rooms with doors and a chest.
v10
v11: two rooms with doors, a candle, a chest and a ghost.
v11
The same seed of the "haunted mansion" preset through all eleven versions: nothing, confetti, floods, slabs, and then, from v8, rooms.

Regressions, and what the loop caught

Three things I would not have learnt from looking at pictures.

A new metric rewrites history. Version 1 scored 0.99 on "paths form routes", because a map that is all corridor has no isolated path cell. When the path-share metric arrived later and every old version was rescored, v1 fell to 0.74. A version table is only comparable if every row is scored with the same rules, so every new metric was run back over every old map, and every new test seed was drawn again with each old version's own code. Read the charts knowing that; the scores are today's rules applied to yesterday's maps.

Every fix costs something, and the loop shows you the bill. The sampling floor in v6 made calmer maps and starved the rare types. The blueprint in v8 gave the map rooms and took a third of its vocabulary coverage. Version 11 fixed the flooded station and turned its corridors into stubs: path continuity fell from 0.73 to 0.58 in the same run that raised path share from 0.65 to 0.84. None of these were visible in the one map I would have looked at. All of them were visible in the mean over 38.

v10 space station: a 16 by 16 grid where more than half the cells are corridor, with sealed sections of blue panels.
v10 station: the corridor is the floor, so the cap on over-used types had nothing else to offer.
v11 space station: an open deck with sealed sections and short corridor stubs.
v11 station: an open deck as the ground, corridors as lines. Route share 0.53 to 0.12; continuity paid for it.

A per-cell model cannot draw a rectangle. Seven versions tried to get rooms out of the prompt: more context, the whole map as text, softer sampling, typed rules. Doors stayed in fields. The eighth version stopped asking. Code places the buildings, the way a person drawing a map decides where the houses go before drawing the grass, and the model fills each part. That is the one change in the table that moved a specification by more than 0.3. The lesson generalises: give the model the decisions that are judgements (what stands in this room, who walks here) and give the code the decisions that are geometry.

The same maps, one call each

The cost chart above said the whole map in one call was cheap. Cheap is not the same as good, so I built it as a second mode of the generator and ran it through the same evaluation, as version v-llm. Same 38 seeds, same shipped vocabularies, same blueprint. The prompt gives Claude Sonnet 5 what the per-cell loop gives Jev, written once: every type with its rules and its target share, each planned room with its coordinates and the types its walls, door and inside allow, and the rules a good map follows. The model answers with the grid as rows of letters; a wrong shape or an unknown letter goes back to it with the list of problems, three attempts at most.

The first real call taught me something before it drew anything. Left with its default settings, the model spent its entire 6,000-token budget thinking about an 8 by 8 village and returned an empty string: 68 seconds, $0.064, nothing to parse. With thinking turned off, the same call took under four seconds and half a cent. Every number below is with thinking off.

Mean score per specification over the same 38 seeds 0.250.50.7510.980.94Structures0.770.75Paths0.880.85Reachable0.880.74Reads as a place0.990.76Rules hold0.820.61Routes to doors0.830.90Landmarks0.690.65Judge (GPT-5.2)v11: a decision per cell (Jev)v-llm: the whole map in one call (Claude Sonnet 5)
The mean score per specification over the same 38 seeds, the per-cell loop against the single call. Better in one call: landmarks and story (0.83 to 0.90). Worse: a place reads as a place (0.88 to 0.74), the rules hold (0.99 to 0.76), routes lead to doors (0.82 to 0.61). The judge, which reads the finished map and not the metrics, gives 0.65 against 0.69.
v11, a decision per cell: the 16 by 16 village with two cottages, a path along the bottom and trees at the top.
v11, 256 decisions, 90 seconds, about $0.020.
v-llm, the whole map in one call: the same seed with cottages, fields, a river along the east edge, trees and paths.
v-llm, one call, 6 seconds, $0.010.
Measured cost and time per map (means over the run) 8 × 8 map, cost$0.005 Jev$0.006 one call16 × 16 map, cost$0.020 Jev$0.010 one call8 × 8 map, time22 s Jev4 s one call16 × 16 map, time90 s Jev6 s one call38-map run, cost$0.24 Jev$0.25 one call
Measured, not estimated: the Jev figures from the bill ($0.00008 per decision) and the v11 run's own timings; the one-call figures from the token usage and the timing OpenRouter returned for every v-llm call. The whole run of 38 maps cost $0.25 in one-call mode against $0.24 with Jev, in a fraction of the time.

On the map itself the single call loses, and it loses where I least expected. The plan is in its prompt room by room, with coordinates, and still only 47% of its maps hold an enclosed room, against 92% for the per-cell loop, and 60% of its doors sit in a wall against 87%. Writing 256 letters in one breath, the model drifts: a wall one column off, a door in the grass, a room that never closes. The per-cell loop cannot drift, because the blueprint is applied to each cell before the model is asked. The same holds for the typed rules, 0.76 against 0.99: Jev is never offered a type its rules forbid, and a text model breaks a few every map. Routes fell the most, 0.82 to 0.61, because its doors open onto nothing more often. It won on one specification, landmarks and story, 0.83 to 0.90: a model that sees the whole map places the one well and the one gate, where the per-cell loop still forgets them on half the maps (0.45 to 0.76). The judge, which reads the finished map and not the metrics, gives 0.65 against 0.69.

On time it wins outright: 3.7 seconds instead of 22 for an 8 by 8 map, 6 instead of 90 for 16 by 16, because 256 round trips become one. On money it is a tie: the whole run cost $0.25 in one call against $0.24 with Jev. The small map is cheaper with Jev ($0.005 against $0.006), because a 2,000-token prompt sent once costs more than 64 tiny decisions; the large map is cheaper in one call ($0.010 against $0.020), because the prompt does not grow with the map while the number of decisions does. Over the run, 46 calls did the work of 3,008, and 8 of the 38 maps needed a second answer because the first had the wrong number of rows or letters.

So which one? For a finished small map, fast, and a story that mentions every landmark, one call is the better tool, and it is now a switch in AI Setup. For rooms that close, doors in walls, rules that cannot be broken because the option was never offered, a map that fills in front of the reader, and the ability to ask again about one tile, the per-cell loop is the only one of the two that delivers. The honest summary is that the per-cell design was never justified by price; the two cost the same. It is justified by control, cell by cell, and the price of Jev is what makes that control affordable.

v11 against v-llm, mean score per specification over the same 38 seeds
Specificationv11, a decision per cellv-llm, one callChange
Structures0.980.94-0.04
Paths0.770.75-0.02
Reachable0.880.85-0.04
Reads as a place0.880.74-0.14
Rules hold0.990.76-0.23
Routes to doors0.820.61-0.20
Landmarks0.830.90+0.07
Judge (GPT-5.2)0.690.65-0.04

Running the loop with an agent

Most of the eleven versions were written by an agent, Claude Code, working in a loop I set up and then mostly watched. One turn looked like this. Read the previous version's scores and the pictures where it lost. Write down one theory ("the model does not see enough of the map", "the sampling is adding noise", "the rules are prose, so they are advice"). Change the code with tests. Commit, so the version in Galtea records exactly what was measured. Run the 38 seeds. Compare. Write the result into the project's README, in one paragraph per version, so the next turn starts from the memory of this one.

My part was smaller and, I think, the part that mattered. I said which theory to test first. When I saw a failure the numbers missed, I asked for a specification for it, which is how four rules became seven. I set the budget: 8 by 8 grids for the bulk of the seeds, and only three 16 by 16 maps, because a room needs space to exist but a large map costs eight times a small one. And I said when to stop: when the agent could no longer name a change it was confident would move a number, the loop had done its work for now.

The agent also broke things. It once committed a version with a failing test, because a shell pipe hid the exit code. It once ran two evaluations against modules it was editing at the same time. Both were caught, one by the continuous integration check and one by the maps coming out wrong, and both became a written rule for the next turn. The loop is not a promise that every version is better. It is a promise that you will know.

What is still wrong

The corridors are stubs: a route between two doors is a shape, and shapes are the plan's job, so the next version should draw the paths the way it draws the rooms. Rooms are often empty, because the state says what the world lacks but not what a kitchen holds. Free-standing walls still split some maps in two. And rooms never share a wall, which real streets do all the time. Each of those has a number now, which is the point.

You can try the generator with your own OpenRouter key, read the code and the roadmap, or look at every map of every version, which are committed to the repository next to their scores.

The numbers

Mean score per specification over the same 38 seeds, with every metric as it stands today. A cell is highlighted when it moved by 0.05 or more against the previous version. The judge column is GPT-5.2 answering "how much does this map read as the setting?" on every map, from 0 to 1.

AI World Gen, versions 1 to 11, mean score per specification (0 to 1)
VersionBarriers form structuresPaths form routesReachable and playableA place reads as a placeThe vocabulary's rules holdRoutes lead to doorsLandmarks and storyJudge
v10.180.740.690.480.831.000.070.09
v20.390.530.750.430.580.770.610.24
v30.680.660.820.500.490.620.790.45
v40.820.800.770.500.620.660.660.48
v50.930.820.810.530.490.700.760.54
v60.950.810.810.510.610.600.680.48
v70.950.800.820.520.990.610.780.48
v80.990.750.840.890.990.780.770.65
v90.980.800.870.870.980.870.840.69
v100.980.780.870.880.990.860.830.71
v110.980.770.880.880.990.820.830.69

v1's routes score is a mean over few maps, because v1 drew almost no doors, so there was little to judge. The judge was run on the older versions after the fact, at the time of writing, so that this column is complete.