DEV Community

Tyson Cung
Tyson Cung

Posted on

The engineering behind an AI agent that ships real Roblox games

My AI agent has now shipped three Roblox games: a pet daycare, an outback
tycoon, and a hidden-object island hunt. Two are public, all three are
monetized, and every one of them survived contact with a real device in a
kid's hands. The interesting part is not that an LLM wrote Luau. It is the
engineering around the LLM that made the output shippable. That is what this
post is about.

The stack is a Roblox Studio plugin (Luau) talking over a local WebSocket to
a Node companion, which runs the agent loop against an LLM. The agent builds
inside your own Studio place using a small set of tools: create instances,
write scripts, snapshot, playtest, read errors.

1. The library is the product: 136 kits, all testable without Roblox

The agent does not freestyle a game from tokens. It composes from a library
of hand-crafted kits: 136 of them across 17 families (obby, tycoon,
simulator, tower defense, racing, arena, pet daycare, treasure hunt, and so
on, plus cross-genre families for polish, monetization, and retention). A kit
is a themed, tested system, not a grey box: a rebirth crystal with a
hold-to-confirm prompt, a conveyor that actually carries ore, a daily quest
board with rotating quests.

The rule that makes the library maintainable: a kit module may not touch a
Roblox global at require time.
Services are fetched lazily inside
functions, color palettes are built lazily, and every kit exports a pure
core:

-- CrossPromoSignManager.Core, from the newest kit
function Core.featuredOrder(count: number, utcDay: number, maxShown: number): { number }
    local order: { number } = {}
    if count < 1 or maxShown < 1 then
        return order
    end
    local start = (utcDay % count) + 1
    for offset = 0, math.min(count, maxShown) - 1 do
        table.insert(order, ((start - 1 + offset) % count) + 1)
    end
    return order
end
Enter fullscreen mode Exit fullscreen mode

Because nothing Roblox-specific runs at require, lune
can load the module on CI and drive the pure core plus the whole manager loop
against fakes: a fake DataStore implementing UpdateAsync, an injected
clock, an injected teleporter. The quest system's daily reset, the streak
math in the rewards calendar, the teleport gate on the cross-promo sign: all
of it is asserted headless, no Studio in the loop.

Effects that must touch the platform go behind injected seams. A kit never
pays a player directly; it calls an injected grantReward callback. It never
teleports directly; it calls an injected teleporter. The game wires those
seams to its real economy once, and the kit stays testable forever.

2. Three type gates, because LLM code lies confidently

Every kit passes luau-lsp analyze in strict mode on CI, with a
Rojo-generated sourcemap so cross-kit requires resolve. The first time we
turned that gate on it found 103 latent type errors across 30 kits,
including 13 guaranteed runtime crashes. LLM-authored code compiles in your
head; it does not always survive a type checker.

Two things a checker catches that review does not: hallucinated APIs (an
early kit called CFrame.fromComponents, which does not exist) and quiet
literal widening (strict mode wants to know that a function returning
"ok" | "unknown" | "current" | "failed" cannot return a plain string).

3. The agent playtests its own build

The newest piece is a self-repair loop. After the build phase, the companion
snapshots the place, runs the game with RunService:Run() for 30 seconds,
collects every error since the run started, classifies them, and hands them
back to the LLM to fix with a script update. Then it playtests again. Three
strikes and it restores the snapshot instead of shipping a broken build.

The nuance that took a day to learn: a Studio plugin cannot enter Play Solo.
RunService:Run() gives you server scripts only, no player, and (in the
headless harness) no physics: unanchored parts do not fall and Touched
never fires. So the repair loop catches script errors and wiring mistakes,
and a separate probe pattern covers structure ("did the composition build 45
treasures and both retention kiosks"), but touch-driven gameplay still needs
a human on a device. Knowing exactly where the automation boundary sits is
worth more than pretending it is not there.

That boundary is where the best fixes came from. A player ran across a
treasure and did not collect it: touch events can miss a fast run-through.
The fix went into the kit, not the game: every pickup now also runs a
server-side proximity sweep. Every future hunt game inherits it.

4. Deploys are a curl, not a dialog

Studio's publish dialog broke permanently on my machine (it hangs on
"Upload in progress" forever; a known-weird client state). The workaround
became the pipeline, and it is better than the dialog ever was:

rojo build showcase/treasure-cove/default.project.json -o TreasureCove.rbxlx

curl -X POST "https://apis.roblox.com/universes/v1/$UNIVERSE/places/$PLACE/versions?versionType=Published" \
  -H "x-api-key: $ROBLOX_KEY" \
  -H "Content-Type: application/xml" \
  --data-binary @TreasureCove.rbxlx
# -> {"versionNumber":8}
Enter fullscreen mode Exit fullscreen mode

The whole game lives in the repo as code (Rojo project + Luau), so a deploy
is: merge PR, build, POST, done. The API key needs the universe-places:write
scope. Things the API will not do, and the dashboard must: icons,
thumbnails, names, descriptions, and creating the experience in the first
place. Budget human time for those.

Operational surprises worth knowing before you ship a "kid-friendly" game:
new Roblox experiences default to a 16+ audience regardless of how clean
your content is, and unlocking younger players is an engagement-gated review
(a 1,000 Robux fee plus 500 engaged players within 60 days). Players first,
rating second.

5. Retention and distribution are kits too

The newest family treats the boring growth mechanics as composable systems
like everything else: a daily login-streak rewards kiosk, a quest board
whose three daily quests rotate deterministically per UTC day (same lineup
on every server, pure function of the date), and a cross-promo sign that
advertises the developer's other games with walk-up teleport pads. One
shared promotion table ships to every game; each sign drops the current
place from its own lineup automatically.

All three follow the same persistence discipline: state saves through an
atomic UpdateAsync with a merge that never rolls a newer day back and
never shrinks same-day progress, so a player who hops servers cannot lose a
claim or get paid twice.

What I would tell you to steal

  1. Give your agent a library of tested components and a hard vocabulary of ids, not free rein. Composition beats generation for shippable output.
  2. Make every module loadable outside the engine. Pure cores plus injected seams turn "game code" into ordinary unit-testable software.
  3. Put a type checker between the LLM and the merge. It catches the exact class of error LLMs make most.
  4. Automate the playtest you can (script errors, structure probes) and be precise about the boundary you cannot (physics, touch, fun).
  5. Deploy through the platform API from CI-shaped builds. Clicking a dialog is not a pipeline.

The games, if you want to poke at the output: Paws and Bubbles Daycare
and Treasure Cove. Find a bug
and tell me; the fix will land in a kit, and every game after that inherits
it.

Top comments (0)