Short answers to what readers ask most about this topic.
01What is The Archipelago?
It is a 3D game version of this portfolio at matthewswong.com/play. You arrive by boat at dawn and walk four islands — the Harbor, Workshop Island, Product Cove and the Lighthouse — where every stall, building and lamp is a real project, product or certification from the site. The goal is to light the lighthouse before dark, which takes about seven minutes.
02How long did it take ChatGPT Astra to build the game?
The playable loop — route, world, controller, HUD, cards, gates and tests — landed in one session on 12 September 2026 from a 1,015-line PRD. A second pass on 18 September rebuilt the world's look, fixed the movement bugs, made it work on phones and added bloom. Writing the brief and reviewing the output took me longer than either session.
03Does the game run on phones?
Yes. Phones get a battery-saver tier by default with a floating touch stick, a look surface and jump and interact buttons, and the dialogs are capped to the viewport so they scroll on a landscape phone. The whole loop is tested on an emulated Pixel 7 with real touch events, but a pass on physical devices is still on the list.
04What did the agent need from me to build it?
A brief it could verify: a product requirements document with goals in order, non-goals, budgets written as CI gates, and the data rules the game must respect. After that it needed me to look at screenshots and say what felt wrong, and it still needs the three human-only steps — the selfie for the real avatar, a Blender art pass, and a test on real phones.
05How do you keep an AI-built game honest?
By turning every rule into something that runs. A prebuild gate fails the build if a project has no placement in the world; an import guard keeps three.js inside the game's folders; a smoke test and a memory soak run on every pull request; and three Playwright scripts replay movement, touch input and the full game on desktop and phone. The agent also screenshots the real game after each change and reads the image back.
How ChatGPT Astra Built My 3D Portfolio Game, Step by Step
A 1,015-line PRD, six gated phases and a headless screenshot loop: how ChatGPT Astra built The Archipelago, a React Three Fiber portfolio game, and the bugs only a test could see.
The Archipelago is a 3D portfolio game built by ChatGPT Astra from a 1,015-line product brief: six gated phases, a headless screenshot loop so the agent could look at its own output, and three human-only steps. This post walks through the ideation, the framework, the build, and the movement and colour bugs only tests could see.
I kept opening my own portfolio page and seeing the same thing: a grid of cards, twelve projects, and no reason to click any of them. The work below the fold — the ERP that runs a factory floor, two products with real users, six certificates — might as well have been in a drawer. In September 2026 I decided the portfolio should be a place you walk through rather than a page you scroll, and I decided not to build it by hand.
This is the record of how that page became The Archipelago, a walkable 3D game at /play, and how ChatGPT Astra built it: from a brief I wrote, through six gated phases, to the second pass that fixed what only a test could see. I have the commits, the test output and the screenshots, so every number here has a source.
This is what a correct implementation of the brief looked like on 12 September: every gate green, every exhibit reachable, and nothing worth looking at, which is the gap the second pass had to close. Matthews Wong, The Archipelago (own render)
The idea: a portfolio you walk instead of scroll
The concept came before any code, and it came out of one rule: a game needs one goal in the first thirty seconds. Ours is light the lighthouse before dark. You arrive by boat at dawn, playing as me, and the daylight is the progress bar: dawn, morning, afternoon, dusk, one step per island finished. Each island is a section of the site, so the metaphor also decides the engineering: an island is a chunk that streams in when you walk toward it.
Four islands in the first release: the Harbor, Workshop Island with the twelve projects, Product Cove with the two products, and the Lighthouse with the six certifications — 22 exhibits in all.
Commits and merges as collectibles: opening an exhibit stamps a commit in your logbook, finishing an island is a merge, and the sky moves on.
Three acts, about seven minutes end to end, with the finale card turning into the contact call to action.
Narrative lines in first person, present tense, at most 22 words, written natively in English and Indonesian rather than translated.
The one design rule I insisted on is the parity rule: exhibits are derived from the same modules the pages render from — lib/projects.ts, lib/certifications.ts and lib/products.ts — and never retyped. A prebuild gate fails the build if a project exists without a placement in the world, or a placement without a project. That single rule is why the game cannot drift from the site.
The brief is the prompt: a 1,015-line PRD
I did not prompt the agent with 'build me a 3D portfolio game'. I had it research first, then wrote a product requirements document with it: 1,015 lines, sixteen sections plus appendices. Problem, goals ranked in order, non-goals, references it had actually checked (library versions from the npm registry, browser support from caniuse, avatar vendors whose domains resolved), the concept, the world, the player experience, art direction, architecture, performance, reliability, accessibility, SEO, privacy, metrics, the delivery plan, risks, and the open questions for me. The PRD is the prompt; everything after it is execution.
The framework in one picture; the band that mattered most in practice was the fourth, because an agent that cannot look at its own output ships boxes.
Two things in the brief did more work than the rest. Non-goals kept the first release to four islands with three more visible on the horizon, so scope had nowhere to creep. And every budget was written as a CI gate rather than a paragraph: an import guard that only lets three.js be imported under the game's own folders, so the rest of the site cannot grow a 3D dependency by accident; the placement parity check; a Playwright smoke test and a memory soak test that run on every pull request; and a notice gate that tells the visitor the download size, computed from the build manifest, before a single byte of engine ships.
Write the brief so a machine can verify it. A sentence like 'the game must stay light' does nothing; 'three may only be imported under app/components/game and lib/game, checked at prebuild' becomes a script the agent runs after every change.
Six phases, each with a gate
The PRD's delivery plan split the work into six phases, each ending in something checkable rather than a feeling. The estimates assumed the agent would do the code phases and that three steps were human-only: the selfie for the avatar, the art pass, and looking at the output.
Phase
Outcome
Gate
0 — Foundation
Chrome-free route, dynamic import, capability probe, notice gate, Map fallback, import guard, CI smoke test
Every existing page's built HTML unchanged; smoke test green
1 — Vertical slice
Harbor, stand-in avatar, capsule controller on a BVH, camera, HUD, one real exhibit card, deep links, saved state
The whole loop playable
2 — Avatar
Avaturn selfie to GLB, Mixamo clips, Blender clean-up, at most 3 MB
Four of five people who know me name me from a still
3 — Workshop + Cove
Island art, 14 exhibits, placement parity, screenshot textures, merges, day advance
Budgets green; first visual review
4 — Lighthouse
Six lamps, the climb, dusk, the beam, finale card, share
Full playthrough under eight minutes, no dead ends
5 — Launch
Touch controls, accessibility, Indonesian copy, OG stills, SEO, device matrix, analytics, docs
Every performance and reliability row green
Phases 0 and 1, plus the code side of 3 and 4, landed in one session on 12 September 2026 with a procedural world and a mannequin standing in for the avatar. Two deviations from the brief are worth naming, because both were the agent pushing back with evidence: it kept the header and footer off the game route with a pathname switch instead of the route-group split the PRD asked for, because 32 scripts and documents hardcode the pages folder; and it pinned React to 19.2.7 because React Three Fiber 9.7 declares a range that excludes 19.3.
From boxes to a place
What the gates could not measure was the thing I saw first on 18 September: the game was correct and ugly. Islands were poker chips, palms were five cones on a stick, every building was a box with a slab roof, and I was a black capsule. So the second pass got a brief of its own, and the agent rebuilt the world without downloading a single model: island ground as one organic geometry that the renderer and the collider both read, so the shore you see is the shore you bump into; palms with curved, ringed trunks and drooping fronds; grass, bushes and rocks scattered by a seeded generator that avoids every footprint and path; textures drawn on a canvas at runtime — planks, thatch, corrugated sheet; water that knows where every shore is, with shallows and a foam line; and seven buildings rebuilt as a thatched warung, hip-roofed stalls, a corrugated ERP shed, a glass gym, a carwash bay, lamp posts and a banded lighthouse.
Same camera, same spot, six days later; draw calls rose from 84 to about 130 while triangles went from 26.6 thousand to 71 thousand, which is what instancing buys you. Matthews Wong, The Archipelago (own render)
Instancing is why the cost stayed sane: every static thing on an island — hundreds of grass tufts, every frond, every rock — is one instanced draw whose matrices are written once on mount, instead of the per-frame re-decomposition the helper library does by default. The memory soak test after the pass cycled the islands three times: heap 21.2 MB at baseline, 21.9 MB after the cycles, 19.1 MB after leaving and returning, with geometry and texture counts back at baseline. That test exists because the brief demanded it; it caught nothing this time, which is the point.
The loop: read, build, screenshot, look, fix
Every change in the second pass went through the same loop. The agent read the file it was about to change and everything that imported it, planned, implemented, ran the typechecker, and then did the step that made the difference: it rendered the real game headlessly and looked at the picture. Chromium in headless mode can run WebGL2 through SwiftShader with no GPU, and the game already exposed a small debug bridge in development for its soak test — teleport, go to an island, read the renderer's counters. Twenty lines of Playwright turned that into eyes.
// scratch/shoot.mjs — the agent's eyes. Headless Chromium renders WebGL2
// through SwiftShader, so no GPU is needed; the debug bridge the game exposes
// in development (window.__archipelago) moves the player without walking.
const browser = await chromium.launch({
headless: true,
args: ["--use-gl=angle", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"],
});
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto("http://localhost:3000/en/play/", { waitUntil: "networkidle" });
await page.getByRole("button", { name: /set sail/i }).click();
await page.locator("canvas").first().waitFor({ state: "visible" });
for (const island of ["workshop", "cove", "lighthouse"]) {
await page.evaluate((id) => window.__archipelago.goto(id), island);
// The follow camera is critically damped: give it two seconds to settle,
// or every still is a smear of the previous position.
await page.waitForTimeout(2000);
await page.screenshot({ path: "shots/" + island + ".png" });
}
// stats() returns draw calls, triangles, geometry and texture counts —
// the numbers the soak test watches, read back after every change.
console.log(await page.evaluate(() => window.__archipelago.stats()));
Under a software renderer the game runs at about three frames a second, so the script waits for the damped camera to settle before each still, and the agent read every image back before deciding the next change. That is how it noticed a pale band at the horizon (the fog colour did not match the sky), grass tufts that read as spikes, and foam blobbing across the shallows — none of which any gate would have reported.
A still from the loop rather than a marketing render; the orange rings over the stalls are the not-yet-opened markers, and the framed screens carry the real project screenshots served by the site's own image optimiser. Matthews Wong, The Archipelago (own render)
The bugs only a test could see
Movement felt wrong and I could not say why, so instead of describing it I asked for a reproduction. The agent wrote a Playwright script that holds keys for six seconds and samples the player's position and the camera's yaw through the bridge. The baseline run put numbers on three feelings at once.
Backing up spun the camera 7.4 radians in six seconds and the player never came back, because the camera auto-aligned behind the avatar while reversing: press back, the avatar turns to face the camera, the camera swings round, forward flips, and you walk a circle. A straight walk to Workshop Island stalled at the harbor's edge for 55 seconds because acceleration was clamped per axis, so the heading drifted eighteen degrees in the first half second and the path curved off the causeway into water. And the island's lip and the causeway edge acted as walls, because the collision capsule started at the feet.
// Wrong: each axis is clamped on its own, so a diagonal target ramps the
// shorter axis to full speed first. For half a second the velocity points
// along the diagonal, the avatar turns to face it, the camera aligns behind
// the avatar, and "forward" has quietly rotated. Over 50 m the path curves
// off a 4 m causeway onto the beach, where the water blocks both axes.
velocity.x += clamp(wanted.x - velocity.x, -maxChange, maxChange);
velocity.z += clamp(wanted.z - velocity.z, -maxChange, maxChange);
// Right: clamp the change *vector*, so the velocity ramps along the wanted
// direction from the first step and the heading never drifts.
let dx = wanted.x - velocity.x;
let dz = wanted.z - velocity.z;
const change = Math.hypot(dx, dz);
if (change > maxChange) {
dx *= maxChange / change;
dz *= maxChange / change;
}
velocity.x += dx;
velocity.z += dz;
The fixes were small once the numbers existed: align the camera only while the input pushes forward, clamp the change vector, start the collision capsule 0.4 metres above the feet so anything lower is a step the ground snap lifts you over, and widen the causeways from four metres to five. The same afternoon the phone run found two more that no desktop test would have: the logbook panel sat exactly where the left thumb lands and swallowed every touch, so the stick never appeared; and the pause menu was taller than a landscape phone and could not scroll, so Resume was off-screen with no way to reach it. Three scripts now stand guard:
move-test: back-up retraces with zero camera spin, a shore slide, a retreat, a causeway crossing that ends on the other island, and a jump that peaks near 0.9 metres and lands.
touch-test: on an emulated Pixel 7, the stick from the bottom-left corner, a walk, a look drag, and the jump and interact buttons, all through real touch events.
flow-test: the whole game from the notice to the finale and back — the menu, the logbook, all 22 exhibits with their cards, every merge and day change, the door, the ascension, reset and the deep link — on desktop with keys and on a phone with taps.
The phone HUD after the fixes; before them the logbook panel covered the bottom-left thumb zone and the interact button, which is why the emulated-phone run of the flow test exists. Matthews Wong, The Archipelago (own render)
Bloom, one colour grade, and what the agent could not do
The last graphics step was bloom on the high tier so lamps, lanterns and the sun glow. It exposed a real bug rather than adding polish: with the post-processing composer in the chain, every still came back pale and washed out. The bloom threshold was the first suspect and the wrong one. The sky and water shaders had never included the renderer's output transform, so without the composer they displayed too dark and saturated — and the palette had been tuned against that — while through the composer they were converted correctly and looked pale. Two tiers were showing two different games. The fix was to route both shaders through the same tone mapping and colour-space conversion as every other material and re-express the palette once; the battery-saver tier and the desktop tier now render the same picture.
Three things stayed human, exactly as the brief said they would. The agent cannot take the selfie the real avatar needs, so the traveller with the backpack is a stand-in. It cannot do the Blender art pass that would replace the procedural islands with authored ones. And it cannot stand in a room with a real phone; every mobile result here is an emulated Pixel 7. Beyond those, one lesson from an earlier project held: gates verify budgets and cannot see dull, so the visual review is a scheduled step with a date, not something I hoped would happen.
Reproduce before you fix: a six-second key hold with sampled positions turned three vague feelings into three numbers, and every fix is now a check that runs again.
Make the agent look: a headless screenshot after every change caught more than the typechecker, the linter and the gates combined.
Push back with evidence: the two deviations from the brief were both right, and both came with the count that justified them.
Dusk on the high tier with bloom: the six lit lamps are the six certifications, and the scene finally looks the same on a phone as it does here, which took a colour-pipeline fix rather than a bloom setting. Matthews Wong, The Archipelago (own render)
The game is live at matthewswong.com/play, in English and Indonesian, on desktop, touch and gamepad. Every exhibit in it is a real project, product or certificate from this site, and the mailbox at the harbor is the contact page.
The rule I carry out of this: write the brief so a machine can verify it, make the machine look at its own output, and keep the three human-only steps human. An agent given a paragraph builds a paragraph. Given a PRD with gates, a screenshot loop and a place to push back, it built a world I would not have finished alone — and told me, with numbers, where it was wrong.