DDS Vibe Academy · Masterclass
From Prototype to Published
It didn't compile. It shipped its own API key. It had zero tests. Then it became Ashaveth — a tested, published AAA browser game. This is the exact, verified method, with the receipts.
A worked, end-to-end case study · Every number re-derived from a build · The game is free at ashaveth.ddsboston.com (18+).
Quick answer
This is a free, advanced masterclass that reconstructs — with verified evidence — how the browser RPG Ashaveth went from a prototype that did not compile to a tested, published game. You learn the 2026 web-game stack (React 19, TypeScript, Vite, a Cloudflare Worker in front of Gemini, Cloud Run) and a repeatable prototype-to-published pipeline: audit honestly, make it testable, ship AI without the key, and verify a release that can actually fail.
Key takeaways
- The prototype did not compile, shipped its API key in the browser bundle, had 0 tests, and packed 13 screens plus all logic into a single 1,543-line file.
- The v1.0 release compiles with 0 TypeScript errors, keeps the key server-side, and ships 154 unit tests covering the game logic — with that same file cut to 181 lines.
- A 12-step repair roadmap, drawn from an honest audit, was executed to 12/12 complete.
- The entry bundle is 327 kB, and the game ships 103 hand-generated illustrations at zero AI cost per play.
- The API key never reaches the browser — a build-time scan fails the build if it ever does, and the guard was proven by poisoning a build until it blocked.
- Publishing is 8 steps; the production build is step 3, and not the one most likely to break.
- It was built with a multi-agent stack — Cowork, Opus 4.8, and Antigravity — under a Vibe Coding loop of intent, constraints, acceptance criteria, and an audit gate per phase.
The toolchain
The 2026 stack
Ashaveth is built on the current web-game stack: React 19 and TypeScript 5.8 in strict mode, bundled by Vite and tested with Vitest. AI dialogue runs through the Google Gemini API behind a Cloudflare Worker; the game ships as a static bundle in a multi-stage Docker image served by nginx on Google Cloud Run. None of it is exotic — the discipline is in how it fits together.
React
View layer, lazy-loaded one screen per chunk — 20 code-split bundles.
TypeScript
The gate: tsc --noEmit at 0 errors before anything ships.
Vite
Build and code-split. A 327 kB entry bundle, built in about two seconds.
Vitest
154 unit tests over pure game logic, green in roughly a second.
Gemini + Cloudflare Worker
AI dialogue, with the key held server-side as a Worker secret.
Cloud Run + nginx
Multi-stage Docker: build with Node, serve static with nginx. No source in the runtime image.
Pre-generated art
Illustrations generated offline and committed — zero AI calls at play time.
Multi-agent Vibe Coding
Intent and constraints defined; agents execute; an audit gate closes each phase.
The delta
Prototype vs v1.0
Ten days separated a prototype audit from a shipped game. The prototype demoed convincingly for weeks — and did not compile, shipped its API key, and had no tests. The gap between "feels finished" and "is finished" is the entire subject of this class, and it is measurable.
| Measure | Prototype (Jul 10) | v1.0.0 (measured) |
|---|---|---|
| Compiles? | No — 4 raw > in JSX | Yes, exit 0, ~2.3 s |
| Source files | 14 | 67 |
| Lines of code | ~6,062 | 11,938 |
| App.tsx | 1,543 lines | 181 lines |
| Tests | 0 | 154 across 7 files |
| TypeScript | non-strict, 16 any at the AI edge | strict, 0 errors |
| API key in bundle | Yes — present in dist | No — proxied-by-worker |
| Confirmed defects | ~45 broken behaviors | pinned by regression tests |
| Architecture grade (audit's own) | C− | shipped |
1 Establish ground truth
Audit the prototype honestly
Before you scale a prototype, measure what you actually have. The Ashaveth build began with a report-only audit — no code changed — that catalogued every defect with a file-and-line citation and a total count. That document became the plan: a 12-step roadmap, later verified complete.
The audit's rule was strict: find out the real state of the code before improving it. It counted roughly 45 broken behaviors, recorded that the prototype did not compile (four raw > characters in JSX, fatal to the compiler), and graded the architecture C− — a single 1,543-line file holding thirteen screens, all game logic, and persistence. None of that was visible in a demo. The prototype ran under a tolerant sandbox and looked finished for weeks.
Two ground-truth checks come first, and both caught real failures on this project. Verify your substrate: copy the repo out and byte-compare it, then run a structural check that never references the source — parse every JSON file, confirm every block closes. A byte-comparison alone is blind to a corrupt original; only the structural pass found a truncated file that a checksum had declared clean. And initialize version control on day zero: here, git init happened at v1.0, so the entire prototype-to-release journey is unrecoverable from history. Your story is a business asset — do not lose it.
A prototype that demos is not a prototype that ships. Measure it — with citations and a count — before you scale it.
2 Rebrand and clearance
The IP sweep you must finish
The prototype carried a pre-rebrand title — a decades-old dungeon-RPG name with obvious trademark exposure — which is exactly why it became Ashaveth. But a rename is a sweep, not a switch, and this one stopped early. The unfinished hits became permanent constraints on what the studio is allowed to say about its own game.
The rebrand changed the marketing and the metadata and then stopped before the boss object, the save-file keys, and several monster names. The consequence is concrete: the final boss still renders on screen under its old name. So the studio cannot name that boss in marketing without shipping a contradiction — and the antagonist is referred to everywhere, including on the game's own page, simply as the Red, the first fire.
There is a trap inside the fix. Each monster's image cache key is its name — so renaming the off-tone holdovers deletes their portraits. A one-line find-and-replace becomes an art-regeneration project. This is why the sweep must be planned and finished up front, before a single asset depends on a name you intend to change.
The durable output of all this is a banned-claims list shipped beside the safe-claims list. Every public asset — this masterclass included — is diffed against it before it goes out, so the code and the marketing can never contradict each other.
| Instead of | Say this — and why |
|---|---|
| Naming the final boss on screen | "the Red" — the boss object still renders its old name |
| Overstating the game's test coverage | "154 unit tests covering the game logic" — components aren't machine-tested |
| Rounding the bundle down to a rounder number | "327 kB" — the measured value; never round down past a measurement |
| Saying the AI dialogue is "always" live | "AI-driven dialogue with an offline fallback" — it degrades silently |
| An unqualified "0.05% relic drop" | "0.05% when the AI is live" — the fallback path can't drop them |
Finish the IP sweep before you write a word of marketing. Every hit you leave behind becomes a banned claim — and banned claims outlive the launch, the page, and the person who remembers why.
3 Make it correct
Architecture that's testable by design
Testability is not something you add later; it is an architecture choice you make first. The prototype's 1,543-line file couldn't be tested because logic, view, and I/O were fused. v1.0 separates code by what a thing is, with dependencies pointing exactly one way — so the logic becomes pure, and pure logic can be checked.
components → state → services → data. Nothing points back up. That single rule is what makes the logic testable.
Two decisions carry the whole design. First, the core is pure: the combat engine takes its randomness as an injected rng parameter and calls no Date.now and no Math.random. That is the only reason 35 combat tests can exist and run in milliseconds. Second, the reducer distrusts its caller: a stale render is a hostile caller, so every state transition re-checks its own rules — advancing the forge re-runs the gate rather than trusting the button that dispatched it. The prototype's worst exploits — a cooldown wipe, a gold-minting bank, an infinite daily reset — were all the same bug: an invariant enforced only in a component.
The finale earns its own mention here: its prose was pulled out of the view into a pure function of the save file, precisely so it could be tested. That refactor is the difference between an ending that can be verified and one that contradicts itself in front of players.
No clock, no random, no I/O in the core. It becomes a pure function; a pure function can be tested; and correctness becomes checkable instead of hoped for.
4 Make it feel expensive
Systems that feel AAA
"AAA feel" here does not mean a big engine. It means hand-made density and a few well-tested rules: a wall of original art that costs nothing at play time, a boss ladder that never cheats, five distinct biomes, relationships tracked on six axes, and an ending that reads the save you actually earned.
The art pipeline: density at zero runtime cost
Ashaveth ships 103 original illustrations — 72 monsters, 26 characters, 5 locations — generated offline once and committed to the repository. At play time the game makes zero AI calls for art; it serves static files. Each image is cached on a stable key, which is where the earlier IP lesson bites: because a monster's key is its name, the sweep and the art are coupled forever.












The bestiary climbs — one honest stat block each
Five bosses gate the five biomes. The escalation is in the numbers, not in hidden tricks: each boss is one unbroken stat block with no surprise second phase. The design proves that pacing can come from tuning alone.

Arachne, the Broodmother
The Whispering Woods1,200 HP
Golgoth, the Rot Giant
The Drowned Host4,000 HP
High Cultist Malakor
The Obsidian Peaks9,000 HP
Xel'nath, the Void Walker
The Void Edge20,000 HP
the Red — the first fire
The Dragon's Domain · final boss25,000 HPFive biomes, five moods





Relationships on six axes — and an AI that remembers
Companions are not a favor bar. Each is tracked on six independent axes, and their portrait and their written journal both change as the bond deepens. The dialogue is AI-driven, with an offline fallback; the journal entries are stored per character, and the ending will read them back to you.







Osric
A warrior who can travel with you, with a journal that fills in as trust grows.

Wren
A second bond on the same six axes — a different voice, a different record.

Vosk
Keeper of the forge, where the blade that matters against the Red is made.
The forge, and an ending that reads your save
One mechanic is coupled tightly to the theme. At Vosk's forge you raise Godsbane through four stages; fully forged, it strikes for triple damage against the Red, and its quench heals exactly 10% of max HP — once, enforced by the reducer, not the button. Then the finale reads the save you actually built: three endings, up to three companion journals quoted back verbatim, and no score — because being recorded is not the same as being bonded.
Depth is felt at the end. Couple one mechanic tightly to your theme, make the finale a pure function of the save, and let a few well-tested rules do the work an engine can't.
5 Make it safe
Ship AI without shipping the key
The prototype's most expensive defect was a single string: its API key, sitting in the browser bundle for anyone to lift. The fix is architectural — the key never touches the client — and, crucially, the fix is proven, not assumed.
Every model call routes through a Cloudflare Worker that holds the key as a Worker secret. The browser bundle ships the literal apiKey:"proxied-by-worker" and no real credential. But "we moved it" is a claim, and claims decay — so a postbuild step scans the built bundle for credential patterns and fails the build if one appears. Then the team did the thing most teams skip: they poisoned a build on purpose to confirm the guard actually blocks it.
# deliberately poison: inline a fake key, leave the proxy URL unset $ npm run build vite v6.2.0 building for production... ✓ 214 modules transformed → dist/assets/index-9f2a.js 327.46 kB $ node scripts/check-secrets.mjs scanning dist/ for credential patterns... ✗ FAIL credential pattern found in dist/assets/index-9f2a.js build aborted — a key must never reach the client $ echo $? 1
That experiment exposed a subtle truth the class calls "conditionally dead, not dead." The build tool inlines the key only when the proxy URL is unset — so the dangerous path isn't deleted, it's dormant. One empty environment variable re-enables it. A guard that can be switched off by a config mistake is exactly the guard you must keep testing.
Spend is bounded in five layers: an origin allowlist, a model allowlist, a per-player quota, a per-IP quota, and — the only one that actually bounds the bill — a global ceiling of 5,000 requests per day. A subtlety worth teaching: there are two independent clocks. The Worker's quota resets at 00:00 UTC; the in-game daily reset is 12:01 local time. Treating them as one clock is a bug in waiting.
Get the key out of the client, then prove the guard fires by trying to defeat it. A security check is worth exactly as much as the last time you watched it fail on purpose.
6 Prove it works
Tests named after the bugs
The 154 unit tests are not a coverage number to brag about; they are a memory of every way the code was once wrong. Many test blocks are literally named (bug 1), (bug 2), and so on — each one pins a specific prototype failure so it can never return quietly.
The cautionary tale is the ending. It shipped in the prototype with a clean compiler and 96 passing tests: the victory screen printed three companion journal quotes and then declared that no one had written the hero down. Both could be true at once because the dev fixture seeded an empty journal for every character — so the test harness could only ever exercise the empty path. The contradiction was invisible to the tests and perfectly visible to players.
A fixture that cannot reach a state will certify that state as working — right up until a player sees it.The lesson the ending bug taught
The fix was architectural, not cosmetic: the ending prose was pulled out of the view into a pure function of the save file, and 36 tests now pin the invariant that being recorded is not being bonded. The forge got the same treatment — a hostile-caller suite proves that skipping a stage, spamming the button, replaying an old action, or passing an out-of-range value are all no-ops, and that the quench heal is applied exactly once.
Write the regression test before the fix, then interrogate your fixtures: for every invariant, ask what state would make this check fail — and confirm your fixture can actually reach it. A check that cannot fail is not a test.
7 Publish and verify
Publishing is 8 steps; the build is step 3
"It builds" is a small part of "it's live." Shipping Ashaveth means a proxy Worker with a hand-set secret, a quota store, a container image, a Cloud Run service, DNS, a TLS certificate, a storefront page — and a smoke test that can actually fail. The production build is one step of eight, and not the riskiest.
Set the Worker secret
Deploy the proxy + quota store
npm run build
Build the Docker image
Deploy to Cloud Run
Point DNS at the service
Wait for the TLS cert
Smoke-test the real origin
The topology is a multi-stage Docker build — node:22-alpine compiles, nginx:alpine serves — deployed to Cloud Run. DNS is a CNAME; the TLS certificate is issued by Google Trust Services and can take fifteen minutes to a few hours to go live. The storefront lives on Shopify.
Now the deepest trap in the whole project. The AI proxy rejects the temporary *.run.app origin by design (it's not on the allowlist), and the game falls back to pre-written dialogue that is deliberately indistinguishable from live AI. So a developer who tests on the Cloud Run URL sees a beautiful, fully playable game whose signature feature is completely dead — and ships it. The only proof the AI is live is a 200 from the proxy in the network tab, on the real origin.
That is why the deploy gate is a sequence of falsifiable checks that hard-stop on failure. Before the image is even built, the gate verifies the proxy returns 400 with no player id, 403 from a bad origin, and 200 from the real one — in that order. And a near-miss to remember: the .dockerignore once excluded scripts/, which would have stripped the secret scanner out of the build environment entirely. A guard that doesn't exist where the build runs is not a guard.
Make every deploy step a command with a pass condition, and hard-stop on the first failure. Your smoke test must be a specific observable on the real origin — a 200 from the proxy — not "the game works."
8 The method behind it
The multi-agent Vibe Coding loop
All of the above was produced with a small multi-agent stack — Cowork, Opus 4.8, and Antigravity — run under one repeating loop. The loop, not the tools, is the transferable part.
Each phase runs the same four beats: define intent (what "done" means), set constraints (the rules the result must obey), write acceptance criteria (a check that can fail), then let the agents execute — and close the phase at an audit gate that can send the work back. The gate is the load-bearing part. It caught the ending bug's root cause, and it is what turned "we moved the key" into a poisoned-build proof.
One honesty note the class insists on: the repository records a single git author with no per-commit trailers, so which agent wrote which line is unrecoverable. This masterclass therefore teaches the method and never claims that a specific model wrote a specific file. Attribution you cannot verify is not a fact you get to publish.
Define intent, constraints, and acceptance criteria; let agents execute; then run an audit gate that can actually stop the next phase. The loop is the product — the model roster is an implementation detail.
The reproducible pipeline
From your prototype to published
Tool-agnostic, and every step is a scar. Each names the decision, the acceptance test — a command with a pass condition, because if it cannot fail it is not a test — and the failure mode it exists to prevent. Run it in order; then keep the checks running on every build.
Phase I · Establish ground truth
Initialize version control today
Every prototype gets
git initon day zero.Accept: git rev-list --count HEAD ≥ 1 before the first feature.
Failure: Ashaveth's entire prototype-to-v1.0 history is unrecoverable — init happened at v1.0.
Verify your substrate
Never trust a disk or a mount.
Accept: copy out, byte-compare, then a structural check that never reads the source.
Failure: a checksum that "passes" because it compared a copy to a corrupt original.
Audit honestly — report only
Find out what you have before you scale it.
Accept: a document with file:line citations for every finding and a defect count.
Failure: scaling a codebase whose real state you have never measured.
Phase II · Make it buildable and safe
Build with the production toolchain
The playground is not the toolchain.
Accept: npm run build → exit 0; tsc --noEmit → 0 errors.
Failure: Ashaveth's prototype demoed for weeks and never compiled.
Get the key out — and prove it
All model calls go through a proxy you control.
Accept: 0 credential matches in dist as a build-failing step, then poison a build and watch it block.
Failure: a key in the bundle; worst-case spend, unbounded.
Cap spend in layers
Origin, model, per-user, per-IP, and a global daily ceiling.
Accept: the proxy returns 400 → 403 → 200, in that order.
Failure: per-user limits don't bound your bill; only the global ceiling does.
Phase III · Make it correct
Extract the pure core
Game math has no clock, no RNG, no I/O. Randomness is injected.
Accept: the engine imports nothing stateful; its tests run in milliseconds.
Failure: an engine that calls Math.random can't be tested, so it won't be, so its bugs ship.
Put every transition behind a re-validating reducer
Assume the dispatch site is hostile. A stale render is hostile.
Accept: a test per invariant, named after the exploit — "skipping a stage is a no-op."
Failure: an invariant enforced only in a component is an exploit.
Test the bug you have; interrogate fixtures
Your first test is a regression test, not a happy path.
Accept: confirm your fixture can actually reach the failing state.
Failure: the one that got Ashaveth — journal:[] hid the ending bug from 96 green tests.
Phase IV · Make it feel expensive
Generate offline; ship it static
Anything expensive and non-interactive is built, not run.
Accept: per-session AI cost is zero; the assets are committed.
Failure: paying per session for content that never changes.
Key caches on stable IDs, never display names
Display names change; keys must not.
Accept: rename an entity and confirm no asset is orphaned.
Failure: a find-and-replace becomes an art-regeneration project.
Let the ending read the save
Depth is felt at the end; make the finale read the real save.
Accept: the finale is a pure function of the save file, and therefore unit-testable.
Failure: an ending assembled inline in a view that contradicts itself.
Phase V · Clear, publish, verify
Finish the IP sweep before any marketing exists
Grep the old name across everything — data, keys, asset filenames, deploy config.
Accept: zero player-visible hits.
Failure: a final boss you cannot legally name in your own marketing.
Ship the runtime, not the toolchain — keep your guards
Multi-stage build; the final image has no source and no env files.
Accept: inspect the final layer, and confirm the build-time guards survive into it.
Failure: a .dockerignore that quietly deletes your secret scanner.
Falsifiable deploy steps; hard-stop on failure
Each step is a command with a pass condition. Nothing proceeds on a warning.
Accept: a specific observable — a 200 from the proxy in the network tab, on the real origin.
Failure: shipping a game whose signature feature is silently, invisibly dead.
Re-derive every public number at release
No number reaches a marketing asset without a command behind it.
Accept: each claim carries a command and its output.
Failure: a bundle quoted at a rounder number than the 327 kB it actually is.
Publish the banned-claims list
The constraints are a deliverable, not a footnote.
Accept: every marketing asset is diffed against the banned list before it ships.
Failure: re-publishing a claim the code contradicts.
Audit before you build; get the key out and prove it; make the core pure and the reducer authoritative; generate the expensive things offline; and never trust a green signal you haven't tried to break.The pipeline in one sentence
Receipts
Every number, from a build
No claim on this page is decorative. Each one has a command behind it — this is the discipline the class asks you to adopt.
- 154 unit tests, 0 failures —
vitest run - 0 TypeScript errors, strict —
tsc --noEmit - 327 kB entry bundle —
stat dist/assets/index-*.js→ 327,462 bytes - App.tsx is 181 lines —
wc -l src/App.tsx - No key in the bundle — credential scan of
dist/→ 0 matches - 103 art assets, 0 runtime calls —
ls public/pregen | wc -l - Global spend ceiling 5,000/day —
proxy/cloudflare-worker.js - 12 of 12 roadmap steps complete — verified against current code
Questions
Frequently asked
What does this masterclass teach?
It reconstructs, with verified evidence, how the browser RPG Ashaveth went from a prototype that did not compile to a tested, published game. You learn the 2026 web-game stack and a repeatable prototype-to-published pipeline: honest auditing, testable architecture, safe AI integration, and a falsifiable release process.
Who is this masterclass for?
Developers who can already build a prototype and want to ship one. It assumes comfort with React, TypeScript, and the command line, and it is written at an advanced level. It is free and self-paced at the DDS Vibe Academy.
What is the 2026 stack used to build Ashaveth?
React 19.2 and TypeScript 5.8 in strict mode, built with Vite 6.2 and tested with Vitest. AI dialogue runs through the Google Gemini API behind a Cloudflare Worker. The game ships as a static bundle in a multi-stage Docker image served by nginx on Google Cloud Run, with a Shopify storefront page.
How different was the prototype from the shipped game?
The prototype did not compile, shipped its API key in the browser bundle, had zero tests, and packed 13 screens plus all logic into a single 1,543-line file. The v1.0 release compiles with zero TypeScript errors, keeps the key server-side, ships 154 unit tests covering the game logic, and reduces that file to 181 lines.
How do you ship AI features without exposing the API key?
All model calls route through a Cloudflare Worker that holds the key as a Worker secret, so the browser bundle ships the string apiKey:proxied-by-worker and no real credential. A build-time scan fails the build if a key is ever inlined, and it was proven by deliberately poisoning a build until the guard blocked it.
How is a text-forward game tested?
The game logic is written as pure functions — the combat engine has no clock and no random source; randomness is injected. That purity is what makes 154 unit tests possible. Many tests are named after specific prototype bugs, so each one pins a way the code was once wrong.
Why does the masterclass say to test the bug first?
Because happy-path tests certify happy paths. A shipped bug in Ashaveth's ending survived a clean compiler and 96 passing tests, because the test fixture seeded an empty journal and could only ever exercise the empty path. The lesson: a fixture that cannot reach a state certifies that state as working.
How much of publishing is actually the build step?
The production build is one of eight publishing steps, and not the one most likely to break. Publishing also means a proxy Worker with a hand-set secret, quota storage, a container image, a Cloud Run service, DNS, a TLS certificate, a storefront page, and a smoke test on the real origin that can actually fail.
What is the invisible-fallback trap in deployment?
The AI proxy rejects requests from the temporary Cloud Run URL by design, and the game silently falls back to pre-written dialogue that is deliberately indistinguishable from live AI. A developer can test the temporary URL, see a perfect game, and ship with the AI completely dead. The only proof is a 200 from the proxy in the browser network tab on the real origin.
How was the game's art made without high runtime cost?
The 103 illustrations were generated offline once and committed to the repository, so art costs zero AI calls at play time. Each image is cached by a stable key. The lesson is to generate expensive, non-interactive content at build time and ship it static.
What multi-agent workflow built Ashaveth?
It was built with a multi-agent stack — Cowork, Opus 4.8, and Antigravity — under a Vibe Coding loop: define intent, constraints, and acceptance criteria; let agents execute; then pass an audit gate before the next phase. The repository does not record which agent wrote which code, so the class teaches the method rather than assigning authorship.
Is Ashaveth free, and who can play it?
Ashaveth is free to play in the browser with no download and no signup, at ashaveth.ddsboston.com. It is a grimdark RPG intended for adults and is self-rated 18+ by Design Delight Studio; it carries no ESRB or PEGI rating.
Why publish a banned-claims list?
Because the constraints are a deliverable, not a footnote. An unfinished rename left the final boss rendering under its old name, which permanently limits what the studio can claim. Every public asset is diffed against a banned-claims list before it ships, so the code and the marketing never contradict each other.
Where can I take this masterclass and see the game?
The masterclass is free at the DDS Vibe Academy. You can play the finished game free at ashaveth.ddsboston.com and read its details on the Ashaveth page at ddsboston.com.
Bottom line
The distance between a prototype and a product is not more features — it is signals that can fail, and that you have watched fail. Audit honestly, make the core pure, get the key out and prove it, generate the expensive things offline, and verify the release on the real origin. Ashaveth is the proof that the method works end to end; running it on your own prototype is the assignment.
Play the proof. Then build yours.
Ashaveth is free in the browser — no download, no signup. Then take the method into your own build at the DDS Vibe Academy.
Free to play · 18+ · Built and published by Design Delight Studio, Boston.
