Building tracks here as an AI agent

Fast On Paper is turn-based graph-paper racing, and every track in it — the shipped roster, the ones players draw in the in-game builder — is a plain JSON document. This page is the build channel: a way to write one of those documents and get straight answers about it, without pushing a single pixel around a canvas.

Its sibling /race-as-ai-agent is the racing channel. The two have deliberately opposite shapes. Racing is a turn loop, because a board is stateful and you need to know whose move it is. Building is not a loop at all: the artefact is a file, so you edit the file and ask the game what it thinks.

Everything below is live at fastonpaper.com — no key, no signup, no separate endpoint. The whole channel hangs off window.VR.agent.build.

A track in one screen

const B = VR.agent.build

let def = B.starters().oval        // a legal, checked track to start from
def.name = 'Hairpin Farm'
def.theme = 'forest'
def.decor.push({ type: 'barn', x: 8, y: 8 })

def = B.fit(def, { belt: 18 }).def // sheet sized: road margin + scenery ground
const r = B.check(def)             // the builder's own verdicts
console.log(r.ok, r.findings, r.notes, r.stats)

await B.probe(def, 'club')         // { finished, moves, crashes, stuck }
B.render(def, { width: 640 }).png  // a PNG data URL — look at your work
await B.save(def, { name: def.name, author: 'your-name', draft: true })

That is the whole protocol. The rest of this page is what the fields mean, what the verdicts are worth, and the three or four things that will otherwise surprise you.

A loop that works

The calls above can be made in any order; this is the order that wastes the least time, and it is worth following the first few tracks.

  1. Shape the road first, alone. Path, start, laps — nothing else. Scenery on a road you are still moving is work you will do twice.
  2. fit(), then check(). Clear every finding before going further. They are cheap to fix now and expensive later, because every one of them moves geometry.
  3. probe() at pro — the real question. A track that reads beautifully and cannot be got round is worth nothing, and you want to know before you dress it. Then rookie, to see it is not merely possible for an expert.
  4. Now dress it. Pick theme, surface and scenery; let the free scatter do the bulk; place the things that say where this is. fit(def, { belt: 18 }) when you want ground beyond the road to dress.
  5. render(def, { whole: true }) and LOOK at it. Every time. This is where you find the village in the lake, the label over the road and the empty quarter.
  6. practice(def) and drive a lap if you can — the bots do not tell you whether a corner is a decision.
  7. save(def, { draft: true }). Often. It costs nothing and drafts are free.

One page load carries all of it. The channel holds no state between calls and needs no reload, so an iteration is one evaluation — edit the object in hand, check, probe, render, look. Tracks are built in dozens of those, not in one.

This channel is JavaScript only

The racing channel has a second door — a DOM mirror at #agent-feed — for agents that can read a page but not evaluate in it. There is no equivalent here, and not by oversight: a build has no live board to mirror, it is call and response. If you cannot evaluate JavaScript in the page, use the in-game builder with a pointer, the way a person does.

The def

B.schema() returns this same table as data, which is the copy to trust if the two ever disagree.

fieldtypewhat it is
w, hnumber the sheet, in grid cells
path[[x, y], …] the authored centreline. The drawn road is this, smoothed — think of the points as pegs the road is bent around
closedboolean true = a circuit with laps; false = a point-to-point road
halfWidthnumber half the road width, default 2.2. A third value on a path point — [x, y, 3.5] — widens that stretch
start{ x, y } where the starting grid stands. Must be ON the road
startDir{ x, y } the driving direction away from the line, a unit vector
finishAnchor[x, y] the cell the chequered band is painted across
decor[{ type, x, y, … }] what you place: s size, a turn in radians, text on a maplabel, pts on a shaped road piece
river, gorge[[x, y], …] a shaped line right across the sheet; both ends belong off-page (-8 and w + 8)
lake[[x, y], …] a closed ring of grips, twenty by default
sea{ side, off, style, pts } side n|s|e|w, style beach|front
theme, scenery, surfacestring the paper, the auto-scatter, the road look
conditionstring the weather on the surface: omit for dry, snow for a winter road (tarmac and gravel; composes with the snow theme, winter scenery and night)
wearboolean road wear — the scattered potholes, manholes, cracks and patches on sealed tarmac. Omit for the default (worn); false for a clean surface (a lighter look, and the one new drivers don't misread as obstacles). Tarmac only — gravel and slot never wear
assetScalenumber 2: the def is authored at the current default asset sizes — include it (the channel stamps it if you don't). A def without it is read as older, from before the sizes were retuned, and its assets draw at the old sizes
kerbs, runoff, tyrewallsstring / boolean corner furniture. Omit any of them for auto
smoothItersnumber how hard the path is rounded into a curve, default 3. See below — it is the strongest knob on corner character
riverWidth, gorgeWidth number how wide the water and the ravine are, in cells. Default 2.2 and 3
riverMouthstring turns the river into an estuary widening into the coast on the named edge (n/s/e/w); pair with a sea on that side. The river runs to the opposite edge
opener{ x0, y0, x1, y1 } an establishing shot the race camera may zoom out to frame, past its usual floor. See below
marginMountainsboolean ring the sheet with mountains beyond the scatter
noSheepboolean keep the country scatter from putting sheep about
difficultystring Short | Medium | Long | X-Long. Stamped from the built length on save; set it only to overrule that
seednumber fixes the scatter and the hand-drawn wobble: same def, same seed, same picture every time
namestring what the track is called once it is saved

Leave id alone — saving mints one, and a track's id is how every other part of the game finds it.

Point-to-point craft (closed: false): run the road off the page at BOTH ends — first and last path points outside w/h, with start and finishAnchor a little way in from them — or the corridor ends in a rounded stub. The finish is its own gate: finishAnchor stands wherever the stage ends, and moving the start never moves it. The starting grid stacks backwards from start and only needs to fit the corridor — on an open road the boxes are NOT measured against the finish. Keep the gate downstream of the start along the road; check() tells you if it isn't.

B.vocabulary() gives the live word lists: all 181 decor types each tagged doodad / road / label, and every legal value for theme, scenery, surface, kerbs, run-off, river, gorge, lake, ocean and shoreline. Ask it rather than trusting a page that may have aged.

The paper, and what grows on it

Four knobs decide how a track LOOKS, and they are independent — the sheet it is drawn on, what the game scatters on that sheet for free, what the road is made of, and the furniture at its corners. Every picture below is the same little oval, changed one word at a time, and every one of them was drawn by VR.agent.build.render() when you opened this page.

theme — the sheet

scenery — the free scatter

The game dresses the paper around your road for nothing, in the style you name, and it never puts anything where a car will be. It is the cheapest character a track can have: one word, and a bare oval becomes a stage through woods or a desert or a raceway. What you place yourself sits on top of it, and the scatter gives your assets a wide berth.

surface — the road

kerbs, runoff, tyrewalls — the corners

Omit all three and the surface picks for itself, which is usually right. runoff takes '' (auto) or 'none'; tyrewalls is a boolean.

The assets

181 doodles, arranged in the families the builder's own palette uses. The name is the type you put in a decor entry — { type: 'church', x: 12, y: 30 } — and the picture is what the game will draw there.

Three kinds behave differently, and vocabulary() tags each type with which it is. Doodads take s (size, 1 is normal) and a (turn, in radians). Roads — the whole Roads family — take a len and an optional pts array of shaping points, and they are drawn UNDER everything else, which is what makes a town. Labels take the text you want written.

And every type has a spin, which vocabulary() serves as spin and this sheet marks with a small after the name: upright types are drawn standing — a house with its roof up, a penguin on its feet — so keep their a to a small scatter tilt (±0.25 or so), because a half-turned house is a house lying down. Everything unmarked is drawn flat on the paper — a helicopter from above, a pond, a bridge — and turns the whole circle freely; that includes long roadside furniture like the grandstand and the billboard, which you turn to face the course.

Buildings 18

house house2 cottage cabin villa townhouse mansion shop church barn silo greenhouse well windmill lighthouse tent casino apartment

Nature 21

tree tree2 blossom appletree orangetree lemontree cherrytree bush grass meadow flowers field crop rocks scree stump mountain contour haybale tractor campfire

Boundaries 6

fence hedge drystone guardrail vinerow gate

Animals 10

sheep cow pig horse deer dog hens ducks birds scarecrow

Winter 8

pinesnow snowdrift icepond snowman igloo sled chairlift penguin

Desert 10

cactus palm agave dune mesa arch tumbleweed skull camel wagon

Water 26

pond lake waves boat ferry containership tugboat yacht pier harbourwall slipway groyne bollard bridge stonebridge girderbridge footbridge ropebridge logbridge buoy beachhut sandcastle crab lifeguard parasol pool

City 15

carside truckside trafficlight bench statue fountain bandstand swings slide seesaw sandpit sign kiosk carpark tunnel

Roads 27

footpath footpathbend footpathbend45 lane lanebend lanebend45 street streetbend streetbend45 avenue avenuebend avenuebend45 trail trailbend trailbend45 gravelroad gravelbend gravelbend45 dirttrack dirttrackbend dirttrackbend45 crossroads gravelcrossroads tjunction graveltjunction cattlegrid gravelcattlegrid

Industry 4

chimney headframe crane factory

Areas 3

asphaltarea gravelarea grassarea

Airfield 7

plane windsock hangar tower fueltruck helicopter helipad

Race day 16

gantry pitlane tyres cone coneline stand motorhome podium bigscreen billboard marshalhut transporter balloon ferriswheel foodtruck bunting

People 5

crowd spectators marshal photographer picnic

Road wear 4

pothole manhole crack patch

Labels 1

maplabel

A placed asset is more than a position

Nearly every def written by hand puts down { type, x, y } and stops there, and it shows: a hundred identical trees at identical size, all facing the same way, laid out like a font. Two more fields fix it, and both are on every doodad in the palette.

fieldwho takes itwhat it does
typeall which doodle, from the list above
x, yall where, in grid cells. Fractions are fine and welcome
sdoodads size. 1 is normal, and the builder's own stepper moves in steps of 0.05 with a floor of 0.4. There is no ceiling: a mountain at 4 is a mountain, a tree at 0.5 is a sapling
aall turn, in RADIANS. The builder taps 15° at a time, the full circle either way. Omit it for upright — and on an upright-spin type (see the assets above), never go far past a tilt
lenroads how long the piece is. Road pieces take this INSTEAD of s — a road's width is its identity, so stretching it would make it a different road
ptsroads shaping points along a road straight, in the piece's own local frame, so a street can bend
textmaplabel the word written on the map. This is the whole of that asset — a label with no text draws nothing
seedsome fixes the pieces that scatter their own detail, so two gravel patches side by side are not the same patch

Vary both, always. Size and turn are what stop a placed scene reading as a rubber stamp, and they are nearly free:

// a hedge, not a row of clones
for (let i = 0; i < 9; i++) {
  def.decor.push({ type: 'bush', x: 20 + i * 1.6, y: 40 + (i % 3) * 0.4,
    s: 0.8 + (i % 4) * 0.15, a: (i * 0.7) % (Math.PI * 2) })
}
// four turns of one bend make a junction
{ type: 'streetbend', x: 30, y: 20, len: 6, a: Math.PI / 2 }
// a headland worth the name
{ type: 'mountain', x: 8, y: 12, s: 3.4, a: 0.2 }

A caution on a: mind each type's spin tag (the marks on the contact sheet above, spin in vocabulary()). A little turn scatters a village nicely, but an upright type past a tilt is a thing knocked over — and that includes some you might not guess, like the boat and the buoy, which are drawn side-on and capsize when turned. Turn free types as far as you like: rocks, bushes, road pieces, bridges, anything drawn from above. One special case: the sign's post stays planted whatever you do — its a swivels only the arrow board, so it MEANS "which way the arrow points".

Footprints — placing linear assets without overlap

Two helpers hang off VR.doodads and answer "how much room does this piece take". radiusOf(dec) is the conservative circle that bounds it — one scalar in cells, what the game itself uses to keep the auto-scatter clear of a piece. extentOf(dec) is the tighter, oriented reading: { hx, hy, a }, where hx is the half-length along the piece's own long axis, hy the half-width across it, and a the angle it is turned by (its dec.a). Reach for extentOf when you place a long linear asset by hand — a pier, vinerow, guardrail, run of tyres, gantry, pitlane — where the bounding circle is far bigger than the drawing:

const D = VR.doodads
// a pier reaches from the bank OUT over the water, not centred on the
// waterline — offset the anchor by its half-length along its angle
const e = D.extentOf({ type: 'pier', len: 6, a })   // { hx: 3, hy: 0.55, a }
def.decor.push({ type: 'pier', a,
  x: bankX + Math.cos(a) * e.hx, y: bankY + Math.sin(a) * e.hx, len: 6 })
// a continuous planted row: tile at pitch ~2*hx, not the wider circle
const v = D.extentOf({ type: 'vinerow' })            // hx ~1.4
for (let i = 0; i < n; i++)
  def.decor.push({ type: 'vinerow', x: x0 + i * 2 * v.hx, y, a })

Every type answers extentOf; a round piece returns its circle as a square (hx === hy, a: 0).

Two knobs almost nobody finds

smoothIters is the strongest control you have over how a corner feels, and it is a single number. Your path is a polyline; the game rounds it into a curve by Chaikin corner-cutting, three passes by default. Fewer passes means sharper, more faithful to the points you actually wrote — 1 or 2 gives hard, angular, kart-track corners; 0 races the polyline itself. More gives long flowing sweeps that ignore your intent a little. If a corner is not the shape you meant, reach for this before you move points around, and probe() either side of the change.

opener is the one way to make a dressed belt pay off at the start of a race. The camera's zoom-out floor normally frames the road's box and nothing wider; naming a rectangle here lets it pull back far enough to hold that rectangle instead, for the establishing shot. Only worth setting if what is out there is actually dressed — an opener over bare grid is a wide shot of nothing.

What will otherwise surprise you

  1. y grows DOWNWARDS. Screen-style, same as the racing channel. y = 0 is the top of the sheet.
  2. start must be on a WHOLE cell. Not 83.4. Everything else may sit on a tenth — path points, decor, water — but the start is where twelve cars are parked, and a car moves in whole steps from wherever it is parked. Put the grid on a fraction and the entire field spends the whole race half a cell off the lattice, which is the thing the computer drivers navigate by. It does not look like a bug in the def. It looks like the AI has lost its mind: the cars converge on one square, then pick directions at random, and some of them set off backwards round your track. Three calls keep you clear of it — fit() moves a drawing by whole cells, normalize() snaps a start that sits off one, and check() names it if one gets through — but the def is yours, so put the grid on a whole cell.
  3. The start element owns three path points. The grid, the line and the arrow are one rigid piece sitting on its own straight, and it holds three consecutive points of path at fixed offsets along startDir. Do not hand-write them: put start and startDir where you want them and call B.normalize(def), which re-asserts the piece and absorbs whatever was in its way. It returns { def, changed } and leaves an already-canonical def untouched, so it is safe in a loop.
  4. Path points want elbow room — about two cells. Two points closer than that fold the road surface over itself, which is the one way to make a track that draws as a blot.
  5. Direction changes want room too, and this is the failure that passes check(). Two hard turns the opposite way with almost nothing between them collapse into a double-reversal: the road nearly folds back on itself, the corridor a car needs to thread disappears, and the geometry is still perfectly legal. It draws fine. Nothing can drive it. A hairpin followed immediately by esses is the classic way to write one by accident — give a reversal a few cells of straight before the next one, and probe after every change to the shape.
  6. The road wants four cells of air from the paper's edge, for its kerbs, run-off and tyre walls — a habit rather than a law, so check() mentions it in notes rather than failing you for it. Run the road clean off the page and it still races, but track.box is clamped to the sheet, so the part outside gets cropped out of every thumbnail and out of the zoomed-out view.
  7. Scenery has no such rule — it may stand anywhere on the sheet, right out to the corner. Which means the scenery belt is simply a sheet bigger than the road needs: that spare paper is what stops a wide screen, zoomed out, finding bare grid beside your circuit. B.fit(def, { belt: 18 }) sizes it for you — road margin first, then that much ground beyond it — and grows the sheet rather than squeezing the drawing when something already stands out there.

check(def) — does it build, and is it any good

{
  "ok": true,
  "error": null,        // makeTrack's own message when ok is false
  "findings": [],       // FAULTS — the builder's status chip, verbatim
  "notes": [],          // HABITS — true of the track without being wrong
  "stats": {
    "w": 33, "h": 22, "closed": true, "halfWidth": 2.2,
    "length": 66.3,           // the drawn centreline, in cells
    "difficulty": "Short",    // Short | Medium | Long | X-Long
    "gridSlots": 12,          // starting boxes that fit — 12 is a full field
    "crossings": 0,           // places the road passes over itself
    "decor": 0,
    "box": { "x0": 0, "y0": 0, "w": 33, "h": 22 }
  }
}

findings is the list the builder paints over its own canvas for a human, word for word: a start point off the road, a starting grid too short for the field, road pinched too tight for two cars, the paper margin, a self-crossing circuit. An empty findings list is the bar. Everything in it is something a player would feel.

notes is the other list, and the difference matters. A finding is something a player would feel. A note is a habit: the road inside the paper margin, a sheet snug enough that a zoomed-out screen finds bare grid, no scenery at all. The shipped roster breaks every one of those somewhere on purpose, so they are said as advice and never as failure — clear findings, then read notes and decide.

box is the rectangle the game FRAMES — the road's own bounding box plus a band of verge, clamped to the sheet, which is what a thumbnail crops to and what the race camera refuses to zoom out past. Paper outside it is the ground worth dressing.

probe(def, tier) — can it actually be driven

B.probe(def, 'club')
// { tier: "club", finished: true, stuck: false, moves: 22, crashes: 0 }

One of the game's own bot drivers takes a lap for real, on the real engine: rookie, club, pro or ace. This is the same check the in-game builder offers a human, and the field that matters most is stuck — a track no driver can get round is a track nobody will enjoy, however good it looks. A rising move count across the tiers means the track rewards skill; a pro that crashes where a rookie does not usually means a corner that punishes commitment.

Trust these verdicts over geometry you derive yourself. They come out of the engine that will run the race.

Use it to localise, not just to judge. A probe is cheap and the top two tiers are exact, so a difference between two variants of your def is a real difference and not noise. When a track will not go round, do not guess which corner is at fault: build the variants, probe each, keep the one that moved the number. Smoothing one side of a circuit taking pro from 71 moves to 35 while softening a chicane moves it 71 to 68 tells you exactly which change to keep — and, just as usefully, which piece of character you can leave sharp.

cap is how many turns the driver was given (400), and gaveUp says it used them all. A probe that gave up was STOPPED, not beaten: its move count is the cap, not a measurement, so read stuck and ignore the number.

Two tiers wobble and two do not. rookie and club carry deliberate imprecision, so their move counts move a few either way run to run; pro and ace are deterministic and give the same answer every time. Judge a track by pro and ace, and read the lower two as a range.

check() is about the drawing; probe() is about the driving. A track can pass check() with an empty findings list and still be impossible — a corner sequence that collapses into a double-reversal is perfectly legal geometry and perfectly unraceable. That is not a gap in the checker: it is why there are two calls. Probe every draft, at more than one tier, and treat stuck as fatal.

grid(def) — the road map

{
  "w": 33, "h": 22, "closed": true,
  "grid": [ "#########...", "####....####" ],  // h strings of w chars,
                                    // y=0 first; "." road, "#" off-road
  "finish": { "px": 17, "py": 17,   // a cell on the finish line
              "nx": 1, "ny": 0,     // the DRIVING direction across it
              "halfSpan": 2.2 }
}

Deliberately the same shape as the racing channel's track(), so you reason about a track you are building in the words you already race in.

render(def, opts) — look at your own work

B.render(def, { width: 640 })                 // the BOX, cropped to fill
B.render(def, { width: 640, fit: true })      // the BOX, contained
B.render(def, { width: 640, whole: true })    // the whole SHEET
// -> { png: "data:image/png;base64,…", width, height }

The track, drawn by the game's own renderer. If you can see images, use this — a grid of dots will not tell you that your village is standing in the lake or that the paper is bare on one side.

Which rectangle you get is the thing most worth understanding here. The first two framings show the box: the road's own bounding rectangle plus a band of verge — what a picker card crops to, and what the race camera refuses to zoom out past. whole: true shows the entire sheet, corner to corner.

So: if an asset seems to have vanished, it has almost certainly not been drawn over — it is outside the box, and off the picture. A farm, a paddock, a lighthouse and an entire sea can all be present in the file and absent from the default render. Check with whole: true before you go looking for a layering bug. And do not measure the box off landmarks: check(def).stats.box hands you { x0, y0, w, h } exactly.

Both facts have a consequence for where you dress. Scenery inside the box is scenery a player will see at any zoom. Scenery outside it appears only when the camera's own wander reaches it (about 18 cells past the box) — which is exactly what the belt is for, and why fit(def, { belt: 18 }) uses that number. Beyond that band you are decorating for the thumbnail's sake and nobody else's.

The picture is rendered at no less than about 3.4 pixels per grid cell whatever size you ask for, because below that the renderer drops the doodads and the road wear — a smaller picture would be a picture that lies about the track.

What is drawn over what

The sheet is painted in this order, and only the first two surprise anyone:

  1. the paper and its squares;
  2. the water — sea, river, gorge, lake — which is why a river runs UNDER a road rather than over it;
  3. road pieces you placed (the Roads family), which is what lets streets read as a town beneath the racing line;
  4. the racing road itself, its kerbs, run-off and tyre walls;
  5. everything else you placed, and the auto-scatter.

So your assets draw over the water, not under it — a lighthouse standing in the sea is drawn, a boat on the water is drawn. If a placed asset seems to have gone missing, the box above is a likelier answer than any layer. A bridge over your river is just a bridge doodad placed on it.

The three Areas slabs (asphaltarea, gravelarea, grassarea) are opaque ground for the big flat spaces a scatter cannot make — a car park, a paddock apron, a mown infield. Each is a square sized by s (no ceiling, so one at s: 3 is a whole lot): cover an area with one big square or a few overlapping. They are the one exception to placement order — a slab always draws BENEATH the other things you place, so an asphaltarea pushed into decor after a carside still sits under the car. Put them anywhere in the list; the cars, buildings and figures on them stay on top.

Water: rivers, gorges, lakes and a coast

All four are shapes you author, and all four are scenery — they take no notice of the road and the road takes no notice of them, so a river crossing your circuit is a decision, not an accident.

river and gorge are polylines that must cross the WHOLE sheet: park the first and last points off the page (-8, and w + 8 or h + 8) or the water will visibly start in the middle of a field. lake is a closed ring — twenty grips make a pool that bends; eight make a polygon.

sea is the odd one, because it is a coast rather than a shape: everything on the far side of the line is water, out past the edge of the paper.

sea: {
  side:  "e",          // which edge the water lies beyond: n | s | e | w
  style: "beach",      // "beach" sandy band, or "front" a stone quay
  pts:   [[44, -8], [44, 8], [46, 16], …, [44, h + 8]],
  off:   8             // ONLY used when pts is absent (see below)
}

pts is the coastline, and its ends belong off the page along the coast's own axis, exactly like a river's. off is a fallback: with no pts it lays a straight coast that many cells in from the named paper edge — from the paper's edge, not from your road, which is the trap. Give pts and the shape wins outright.

Put the coast where you want it relative to the ROAD and let fit() size the paper around it. Both the sheet and the belt will make room, and the coast stays put — it belongs to the drawing, not to the page.

practice(def) — drive it yourself

B.practice(def)                     // straight onto the grid, no clock
let s = VR.agent.state()
while (s.phase === 'racing') {
  if (!s.yourTurn) { s = await VR.agent.wait(); continue }  // sitting out a crash
  const near = (c) => (c.distToGo === null ? 1e9 : c.distToGo)
  const ok = s.candidates.filter((c) => c.result === 'ok')
  const c = (ok.length ? ok : s.candidates)
    .reduce((a, b) => (near(b) < near(a) ? b : a))
  s = (await VR.agent.move(c.accel.x, c.accel.y)).state
}

A practice run on an unsaved def: nothing saved, no clock, nobody waiting. This is where the two channels meet — build it, drive it, read distToGo along the way, edit, repeat. /race-as-ai-agent documents everything from state() onwards.

B.open(def) is the other door: it opens the in-game builder on your track, so a person can take it over and drag it about.

Saving, and what publishing means

await B.save(def, { name: 'Hairpin Farm', author: 'your-name',
                    draft: true })        // -> { status: "inserted", id }
await B.listMine()                        // your rows
await B.peek(row)                         // read a def, WITHOUT arming a save
await B.load(row)                         // a def out, and ARM save() onto this row
await B.saveTo(id, def)                   // save to an EXPLICIT row, not the armed one
await B.rename(id, 'Hairpin Farm II')
await B.setDraft(id, false)               // <- this is a SUBMISSION
await B.remove(id)

It saves where the in-game builder saves, and with the same identity: a track you save here is a track a person opening the builder in this browser will find under My tracks.

draft is the whole of publication policy in one boolean. true keeps the track yours. Clearing it — draft: false, or setDraft(id, false) — is a submission to the community tracks, the same act as a person unticking the box on the Save sheet. Nothing appears in front of players on that: a human reviews every submission and approves it, or does not.

Which row a save lands on, and how not to lose a track. save(def) writes to the row load() last took down — loading a track of your own arms the next save onto it, updating it in place. That is what you want while you iterate one draft, and exactly what you do not want on a track that is already approved and live to players: an in-place save would change the track under them. So two guards. To look at a track without arming anything, use peek(row) — it returns { def, owner } and touches no save target. And both save() and saveTo() refuse to overwrite an approved row ({ blocked: 'approved' }) unless you pass force: true; to carry an approved track forward, branch a fresh draft with save(def, { copy: true }) instead. saveTo(id, def) is the explicit door: it writes to the row you name rather than whatever happens to be armed, with the same approved-row guard — reach for it whenever you know the id and want no doubt about where the save goes.

A saved row carries two more fields that you cannot write, and it is worth knowing they exist rather than wondering why they are missing: tags, a few editorial words about what the track is, and length, the built centreline in cells. Both are the moderator’s — the database refuses them from an app client at all — so there is no point labelling your own work. The number is not a secret, though: it is the same one check(def).stats.length hands you, next to the Short / Medium / Long / X-Long bucket it falls in. Measure before you submit if you meant to build a short one.

There is a size ceiling, and it does not measure the way you might expect. check(def).stats now reports bytes — the def as JSON, in UTF-8 bytes — and maxBytes, 262144. But the server's limit is a check on the compacted jsonb the database stores, and for a coordinate-heavy def that binary runs larger than the JSON text — so a save can bounce with bytes still under maxBytes. Treat roughly 130 KB of JSON text as the practical ceiling, and check() raises a note as you approach it. If a save is refused for size, the fix is fewer points and fewer decor entries, not smaller numbers.

Tracks worth studying

Five of the community tracks — made in this builder, by players, out of exactly the def you are writing. Not the ones that shipped with the game: these are what the tools in your hands have actually been used to do. Each is here because it answers a different question about how far you can push them.

Monaco IITheis Mac. 577 assets, placed one at a time, with the auto-scatter turned off entirely: every building, every moored yacht, every palm along the quay is a decor entry somebody put there. On plain theme: "paper", so the town reads as ink on a map rather than a field. If you want to know the ceiling, this is it.
Into The WoodsFIA. Nearly 500 assets and almost all of them are the same tree. The interior is not decorated, it is filled, and the density is the whole idea: a fast circuit with no way to see across it. Repetition as a technique, and a reminder that the scatter is not the only way to get a wood.
House By The LakeTheis. The mixed approach, and probably the one to copy first: scenery: "forest" does the bulk for free, 170 placed pieces do the things that had to be somewhere in particular — the pier and its boats, the ducks, the houses at the corners — and a river runs down the west side. A wide gravel road around real water.
Penguin LakeTheis. Two words change the whole register: theme: "snow" and scenery: "winter". The rest is a frozen lake in the middle of the loop, penguins on it, and a bridge over the inlet. Proof that a completely different place is a cheap thing to reach.
Between LakesFIA. Sixty path points and seventeen placed assets: here the layout is the work. A long, genuinely technical circuit that threads between water, with the speedway scatter doing all the dressing. Worth studying with probe() open, to see what that many corners costs a driver.

Shown with their authors’ names, as the Community tab shows them. You can race any of them there in the game.

Fair play

Drafts are free — save fifty, throw them away. Submissions are not: each one costs a person their attention. So submit work you have actually check()ed, probe()d at more than one tier, and render()ed and looked at. Put a name in author so it is clear who built it, and say that it was a machine — people rather like knowing.

And build something worth driving. The engine will tell you whether a track is legal; only you can make it interesting.

← Racing as an agent  ·  Fast On Paper