Dani Mélich.

AI & agentic systems · Gameplay and systems programming

I build multi-agent LLM pipelines for production use: typed contracts between stages, human approval gates, retrieval measured against a gold set, and cost telemetry on every stage.

I work as a gameplay and systems programmer as well: three years of production experience across a live title on Google Play and the App Store, a game I shipped alone, and a browser game at a for-hire studio.

Shipped

Stepland

Godot · iOS + Android · live

Habit-tracking lifestyle game, live on Google Play and the App Store. 128 files in the shipped game are mine, and 7 of its 16 services.

Dungeon Heroes

Unity · Android · closed testing

Real-time action roguelike. ~34,300 lines of C#, signed release builds, a Remote Config update gate, and balance telemetry recorded per run.

Degen Crawler

C# · browser · degendungeons.com

Browser roguelike at a for-hire studio. The Seashore biome, ~20 relics, and the agent used to balance them.

Case studies

Every figure traces to a file, a commit, or a public listing.

UI Harness — a 20-node agent pipeline that ships production UI code

An agent pipeline that takes a written brief to built, validated Unity code, behind five human approval gates. It has shipped screens into a commercial mobile game.

Scale20-node LangGraph pipeline · 10 role-specialised agents · 5 human gates
Code~34,200 lines of Python 3.12 · 1,018 tests
WorkersClaude · Codex · OpenHands · Unity MCP · deterministic
OutputScreens running in a released Unity game
Cost$67–98 per accepted screen, attributed by cause

What it does

  • Turns a written brief into a typed contract, then into working Unity C# and art.
  • Runs every artifact through 14 validator modules before a human sees it.
  • Scores the result with a six-axis visual judge, then asks a human to approve or reject.
  • Routes a rejection back to the stage that caused it, not to the start.
  • Records token, tool-call, duration, cache, retry and cost data for every stage.
  • Exposes the whole thing as an MCP server and a native desktop app.

Results

Three screens, taken end to end and accepted by a human reviewer. Every figure below is read from the pipeline's own kpi.json, written per run.

ScreenCostAttemptsTool callsCached tokensJudge score
hero-collection-v1$67.281754558.4M0.908
hero-collection-v2$97.732759162.7M0.913
library-screen-v3$78.643459746.9M0.642

These shipped. Both screen families landed in the game on 2026-08-18.

Where the money goes, on the run that measured it:

  • First attempt: $34.14
  • Reruns: $63.59 — 65% of the total
  • Of that: $47.76 validator, $10.68 human, $5.15 crash

Knowing that reruns cost twice the first pass, and that three quarters of rerun spend is validator-driven, is what makes the next optimisation obvious instead of a guess.

What the stages produce

Structural greybox generated by the technical-design stage
Structure. Greybox rendered from the layout contract, with absolute geometry. Gated by a human before anything downstream runs.
Art mockup generated by the art-direction stage
Aesthetics. The art stage may restyle, but inherits the approved geometry. Also gated.

Both are artifacts of one recorded run, combat-screen-6643dafd0c.


How it works

Typed contracts are the only source of truth

Pydantic models are canonical. Markdown and HTML are generated views. A stage cannot hand the next stage prose it invented — it hands over a validated object or it fails.

Human gates, with rejection routed to the causal stage

Five gates: readiness, sketch, layout, art, final. A rejection at the art gate returns to art direction, not to the top of the graph. Rejections can be element-anchored:

  • reject --annotate "HeroCard: portrait is cropped"
  • The annotation is applied as a minimal JSON-Patch on the presented contract.
  • Everything not annotated is preserved.
  • The re-raised gate lists the exact delta.

An evaluation harness, built before the features

  • 14 validator modules — geometry, components, compilation, naming, pixel scale, asset presence, policy, code rules, live Unity state, mockup comparison.
  • Deterministic checks run before the visual model is called, so a cheap failure never costs an expensive review.
  • Validator findings get a bounded JSON-Patch repair cycle before they cost a human turn.
  • The visual judge scores six axes: composition, readability, style conformity, asset completeness, mockup alignment, defect-free.

Judge-vs-human agreement is measured

The pipeline records whether its automated judge and the human reached the same verdict:

RunJudgeHumanAgreement
hero-collection-v1passapproved
hero-collection-v2pass 0.913rejected
library-screen-v3fail 0.642approved

An evaluator you have not checked against a human is an assumption. This one is checked every run, and the disagreements are the input to the next round of prompt and rubric work.

Retrieval, scored against a gold set

  • Hybrid dense + lexical over a local sqlite-vec index, fused by reciprocal rank.
  • Embeddings run locally through ONNX; the index is built incrementally, so unchanged chunks are never re-embedded.
  • Scored against a 32-pair gold set built from real failure history, with queries deliberately not phrased in the target entries' wording.
  • recall@10 0.97, MRR 0.66.

Cost control that never throws away paid work

  • Per-stage model and reasoning-effort routing, with provider fallback chains.
  • Budgets are advisory: exceeding one raises a warning, and completed valid output is kept. A budget that discards work you already paid for costs more than it saves.
  • Usage is append-only per attempt, attributed to provider, requested model, effort, and the provider's own reported model breakdown.

Durability

  • SQLite checkpointing; completed stages resume after interruption.
  • Exclusive leases — one run writes to the Unity Editor at a time, one writer touches the harness's own source at a time.
  • Stages are bounded on silence: five-second heartbeats, streamed provider events and process identity, so a hung worker cannot hold a run open indefinitely.
  • Artifacts are immutable and content-addressed within a run.

The pipeline improves itself, and never applies its own changes

  • Every accepted run generates a refinement proposal grounded in that run's KPIs.
  • Proposals have a visible lifecycle: proposed → approved / rejected → applied.
  • Applying is a human act. The harness records where the human landed the change and applies nothing itself.

replay — regression testing for prompts and validators

Re-judges every stored contract with the current validators against the human's recorded gate decisions. Reports regressions on previously approved contracts and new catches on rejected ones.

No Unity. No model calls. Prompt and rubric changes become checkable for free.


Problems I found and fixed

The instrumentation exists to catch things. These are three it caught.

Budgets set from intent are not controls

Found: every one of the 15 stage executions on the ledger had breached its output budget. readiness was set to 4,000 against observed 4,115 → 8,344 with a 17,284 outlier. art_direction was set to 10,000 against 20,593.

Why it mattered: a limit that everything always breaks is noise. It hides the one genuine blow-up you needed to see, and it trains the operator to ignore the only channel that would report it.

Fixed: re-tuned from the ledger at roughly p100 × 1.2. Every breach since is a real signal.

A fallback chain that had never been observed failing over

Found: the config advertised three providers. When Claude failed, the recorded fallthrough read codex: Executable probe failed with exit code 1; openhands: … — one provider failure was terminal, while the message implied three had been tried.

Root cause: the probed executable was the npm .cmd shim, which resolves node from PATH. The operator service's environment has no node, so cmd.exe exits 1 — and the identical command succeeds in an interactive shell, which is why it passed every manual test.

Fixed, in three parts:

  • Discovery prefers the vendored native binary, which needs neither node nor cmd.exe.
  • Probe failures carry the process's stderr tail instead of a bare exit code.
  • Intake writes a provider_availability event, so every run states up front which fallbacks it actually has.

The fix that mattered was not the discovery glob. It was making availability an event on the ledger.

The system's memory worked against it

Found: archiving a resolved defect removed it from the pipeline's retrieved context — so the record of a solved problem stopped being available as precedent at the moment it became most useful.

Fixed: the feedback log is retrieved rather than pasted, chunked one entry at a time, and triaged rather than tidied. Resolved entries stay reachable. This is why retrieval exists in the system at all.


What the stages report about themselves

Every participant — orchestrator, readiness, technical designer, art director, code adapter, Unity implementer, asset creator, visual reviewer — appends to a shared feedback log whenever an input, handoff or tool gets in its way.

236 entries so far. They are retrieved into later runs, so the pipeline's own observations about its weak points become context for the work that follows.


Runs remember

Gate rejections, design-owned blocking findings and recovery instructions from earlier runs of the same screen family reach the design and review stages as bounded screen_memory context, projected deterministically from the durable ledgers.

--no-memory runs a control, so the feature's effect can be measured rather than assumed.


The operator — a native desktop app

The pipeline is driven from a desktop application I built for it: Tauri 2 + Rust (~34,900 lines) with a React/TypeScript renderer (~10,000 lines).

  • Rust owns the window, tray, single-instance behaviour, project selection, sidecar lifecycle and an allowlisted bridge. Python owns durable project and task state.
  • Creates harness runs, inspects stages, workers, telemetry and context, previews image evidence, approves or rejects the human gates, routes repairs, cancels and recovers runs.
  • Monitors pipeline state with no browser, no localhost page, no terminal and no model calls — state comes from the durable ledgers, not from asking a model what happened.

Stack

Orchestration LangGraph · Pydantic · SQLite checkpointing · MCP server · asyncio Providers Anthropic · OpenAI · OpenHands · per-stage model and effort routing · fallback chains Retrieval hybrid dense + lexical · ONNX embeddings · sqlite-vec · recall@k · MRR Evaluation 14 validator modules · six-axis visual judge · JSON-Patch repair · replay Operator Tauri 2 + Rust desktop app · React/TypeScript · Unity MCP Language Python 3.12 · 1,018 tests · pytest


Evidence

Every figure on this page is read from the repository or from the pipeline's own generated reports, re-measured 2026-08-23. Source access can be arranged during a hiring process.

Stepland — eight months on a live commercial mobile game, on two stores

One of three core programmers inside a larger team. Every system named below is one I owned, with its line counts.


Summary

Stepland is a habit-tracking lifestyle game: real-world walking, hydration, meditation and exercise feed a Tamagotchi-like virtual pet. It is live on Google Play and the Apple App Store, free-to-play with ads and a subscription, localised in English, Catalan and Spanish. Anyone can install it and check every screen described here.

July 2025 to March 2026, as a technical designer and gameplay programmer in Godot 4.6, one of three core programmers inside a larger team.

  • 128 files in the shipped game were created by me.
  • 7 of its 16 services are mine.
  • Represented the studio on-stand at four industry events: Gamescom 2025, Godot Fest, BCN Game Fest and 4YFN 2026.

Three parts of this are worth reading first:

  1. I replaced a third-party dependency that was costing us players during onboarding with an in-house Android plugin, written in Kotlin — a language I had not used — in a week. StepSensorManager.kt is 211 of its 212 lines mine. It has been stable since.
  2. I rebuilt the game's tutorial as 82 declarative steps behind a sequence runner, by driving coding-agent sessions in a production repository, and it landed on the opening day of a trade-show showcase.
  3. I inserted a structural-mockup stage between product intent and final art, because art was being produced before requirements existed. The screens below show my blockout next to the shipped result.

The product

Google Playcom.somnigamestudios.steplandStepland: Self-Care Steps
App Storeid 6749793290, seller Somni Game Studios
ModelFree-to-play, ads plus subscription
LocalisationEnglish, Catalan, Spanish — 194 of the 690 translation rows are mine
EngineGodot 4.6 Mobile, iOS and Android both shipped
Repository494 commits, Jan 2025 – Mar 2026 · 26,356 GDScript lines · 193 scenes · 16 services · 15 autoloads · 72 gdUnit4 tests · 19 release tags
Stack around itPlayFab cloud save with schema migration, AdMob rewarded video, ByteBrew and Facebook analytics

What "my work" means here

Across all surviving GDScript, line-level blame attributes 4,230 of 24,945 lines to me — about 17%, third of the three core programmers. Every figure below should be read against that one.

Line share understates ownership here. My role was the high-uncertainty end: prototypes, greenfield services, the first working version of a system nobody had built yet. That work gets polished — and sometimes rewritten — by whoever hardens it afterwards, and line-level blame credits the rewriter.

The tasks system is the clearest example. I created tasks_service.gd from nothing at +376/−0, plus all 25 task definitions, five resource classes and the whole task UI. Three months later a colleague rewrote it (+364/−333). Blame today reads as theirs. Both facts are true, and the second does not cancel the first: the system shipped, and it existed because I built it.

The evidence table gives the number for each system.


Replacing the step-counting dependency

Stepland's core mechanic is counting the player's real steps. Everything else in the game — the pet, the currency, the streaks — is downstream of that number being correct.

Our implementation read steps through Google Health Connect, a third-party hub. That had three consequences at the point where they are most expensive, first launch:

  • A large share of players did not have Health Connect installed at all.
  • Health Connect is only a hub; it needs its own separate step-recording connector, so onboarding became a two-app setup unrelated to Stepland.
  • Misconfiguration failed silently — Stepland displayed "connected" while receiving zero steps. To the player, that is not a misconfigured hub. That is Stepland being broken.

The cost was retention, before players ever reached the game.

I proposed replacing it with an in-house plugin reading the Android pedometer directly. It was a significant architectural change and the team wanted to exhaust the existing approach first, which took about four months. When it was agreed, I built the replacement in a week, in Kotlin, which I had never written — working from references and pattern recognition.

What shipped: no third-party dependency, no second app, no silent zero-step state. One "allow pedometer access" prompt at first launch. Steps continue to accrue with the app closed, via a WorkManager snapshot scheduler and worker. It has been stable since.

Line-level blame, file by file:

FileMine / totalWhat it is
StepSensorManager.kt211 / 212The in-house pedometer reader
StepSnapshotScheduler.kt~100%WorkManager scheduling
StepSnapshotWorker.kt~100%Background snapshotting with the app closed
GodotAndroidPlugin.kt72 / 157The Godot ↔ Android bridge
PermissionsManager.kt36 / 106Runtime permissions
HealthConnectManager.kt2 / 185⬅ the third-party path this replaced

Two lines in the path I argued against, essentially all of the path I proposed. 375 of the plugin's 769 Kotlin lines are mine.

The judgement, separate from the delivery: the argument for owning this was never "third-party code is bad". It was that this failure mode was silent and attributed to us: invisible in our own telemetry, and unambiguous to the player.


Rebuilding the tutorial, against a showcase date

The original tutorial was built by a colleague earlier in the project and had been outgrown. I built its replacement: 82 declarative steps across 32 polymorphic step classes, behind a sequence runner with resume-from-index and per-step telemetry. It accounts for about 76% of the tutorial code surviving in the shipped game.

Two things make this more than a rewrite.

It was declarative on purpose. A tutorial that is a hard-coded sequence of special cases cannot be reordered, A/B-ed or resumed. Making each step a class behind a runner means the tutorial becomes data — and resume-from-index means a player who drops out of onboarding does not restart it, which is the point at which tutorials usually lose people.

I delivered it by driving coding-agent sessions, in a production repository, under a fixed date. The push ran 2026-02-17 → 2026-03-02 and landed on 2 March 2026 — the opening day of the 4YFN showcase (2–5 March 2026), immediately before the release candidate was cut.

I drove agent-assisted implementation and owned the result, in a shipping commercial codebase, against a deadline that did not move. It is the one piece of agentic engineering I have done where the product was someone else's.

The tradeoff: the tutorial ships without a service boundary of its own. Every other system I built here has one. This one was scoped against the showcase date, and I chose the deadline. I know what the debt is and where it is.


Structure before aesthetics

Product intent was reaching the concept artist directly, so full-fidelity art was being produced before requirements were settled — then reworked weeks later when a missing button or a missing state turned up. The art was not the problem. The order was.

I made myself the stage in between. For each new screen or feature I produced a structural mockup without final art — either in Figma, or as plain Panel blockouts built directly in Godot with basic functionality — which the artist then replaced with final art. Requirements got argued while they were still cheap to change.

This is greyboxing, and it worked. Below is the before and after, from the live game.

Memory level select

Structural mockup of the Memory level-select screen
My structural mockup
The same screen live in Stepland
Shipped, with final art

What survived: the title-plus-tagline header, the scrolling level list with per-row star ratings and reward chips, the locked-row treatment, the daily-rewards-remaining line, and the close affordance. What did not: the level-details panel (cards, essence, attempts, memories) was dropped, and the explicit start button became row selection.

Memory level complete

Structural mockup of the level-complete screen
My structural mockup
The same screen live in Stepland
Shipped, with final art

What survived: the completion banner, the rewards block, and the two-button footer with the second button as the rewarded-ad double ("Bonus de Recompensa!" → "DOUBLE REWARD"). What did not: the star trio and the run-summary block (pairs found, attempts spent and remaining) were both cut.

I also mocked the buildings menu the same way — rows carrying icon, name, level, progress bar, and GO! / UNLOCK! / locked-with-requirement states.

The structure is mine; the art and the final refinement are the artist's and the team's. I specified what had to be on each screen and how it was arranged, before anything was drawn.

The one that is not a comparison

The streak screen, live in Stepland

The streak screen is not a mockup that was replaced. I blocked it out directly in Godot, and it is still live, essentially unchanged, in a commercial product. The streak feature behind it is mine end to end — streak_service at 164/175 lines, streak_data at 116/118, the whole UI, and a 204-line test suite covering 14 cases.


The design half of the job

I was not only implementing features handed to me. The design work is documented, and it reasons about retention rather than describing screens:

  • A diagnosed onboarding problem, not a complaint about it: the first-time experience "feels like following a check-list" and runs out in 5–10 minutes, before the player is hooked — with a stated target of holding a player for at least 30 minutes.
  • Return timing treated as a design problem: the player has no way of knowing when to come back, so the design prescribes an explicit time horizon, in-app step feedback as a proxy for idle progress, and a notification at the point the resource fills.
  • A two-tier task system with a stated purpose per tier — progress tasks give direction, daily tasks give a reason to return — staged with escalating rewards, tied to both real-world actions and in-game ones, with premium currency deliberately placed on the behaviours the game wants to reinforce.
  • Economy analysis in the working vocabulary: revising sources and sinks, and the specific diagnosis that the game had many one-time sources, many recurring sources and many one-time sinks — but lacked recurring sinks.
  • A design principle I hold and can defend: never pure RNG. Give NPCs recurrent, discoverable patterns — players who detect randomness disengage; players who detect a pattern keep playing.

The design→build loop closes on the task system: I specified the two-tier design, argued for it, and then created tasks_service.gd and all 25 task definitions from nothing.


Other systems I own

SystemMine / totalWhy it is interesting
Exploration~1,092 / 1,257 feature lines (87%)Created greenfield. Constructor-injected dependencies, externalised .tres configuration, two mode controllers behind one service interface, 16 unit tests. The best-architected feature I built here
App boot (init_manager.gd)175 / 258Save load, service construction ordering, version gating, auth — on a shipped mobile title
Cloud sync (sync_manager.gd)108 / 112PlayFab sync orchestration
Remote config (remote_config_manager.gd)77 / 77Server-driven configuration, sole-authored
Streaks~100%, plus 204 lines of testsA complete retention feature, end to end
Blob creature behaviour294 / 788State machine, wander, focus and interaction, feed zones, squash-and-stretch reactions, evolve sequence
Habitats105 / 197 + 7 files createdService plus its resource and entity definitions
NPC interaction / proximity124 / 140, 57 / 59Reusable, component-based
press_to_spend.gd145 / 195Press-and-hold spending with an accelerating rate — small, and exactly the kind of game-feel component that either feels right or does not
Localisation pipeline194 / 690 rowsSet up the CSV → .translation flow

Evidence

ClaimWhere it comes from
Live on both stores, model, localisation, versionGoogle Play and App Store listings, checked 2026-07-30
494 commits, Jan 2025 – Mar 2026, Godot 4.6stepland-proto-v3 git history
4,230 / 24,945 surviving GDScript linesLine-level blame across game/**/*.gd
128 files created; 7 of 16 servicesFile-creation authorship audit
Kotlin plugin blame, file by fileBlame on addons_source_code/androidPlugin/
Plugin timeline (proposal → Dani/step tracker plugin #162, 2025-10-13 → #249, 2025-10-23)PR history
tasks_service.gd +376/−0, 25 task definitions, later rewritePR Dani/tasks #75, 2025-09-02, and the December rewrite
Exploration 87%, greenfield service, 16 testsPR authorship plus blame, service created 2025-08-04
Tutorial: 82 steps, 32 step classes, ~76% of surviving code, landed 2026-03-02PR #635, co-authorship on every sub-commit
4YFN 2026 ran 2–5 March 2026The event's own attendee card, in a public post from March 2026
Streaks, boot, sync, remote config, blob, habitats, NPC, press-to-spendLine-level blame per file
Mockup → shipped comparisonsMy Figma and Godot mockups; screenshots from the live build
Design reasoning: onboarding, return timing, task tiers, sources and sinksProject design notes and design backlog

Verification: the game is public on both stores, so every shipped screen on this page can be checked by installing it. I can walk through any of the internal numbers.

Dungeon Heroes — a real-time action roguelike, designed, built and shipped alone

In closed testing on Google Play, with the 12 testers required for production access and the 14-day clock running.


Summary

Dungeon Heroes is a real-time action roguelike whose abilities are delivered as cycling cards. It is not a turn-based card game — the cards sit inside a live fight with attack tells, interrupts and a parry window, so most card-game intuitions do not transfer.

I built it alone: design, programming, art, UI, release engineering and store compliance. 156 C# scripts, ~34,300 lines, Unity 6000.3.19f1, Android arm64 and armv7. Signed release AABs, a Remote Config update gate, and per-run balance telemetry into a Unity Gaming Services sink.

The prototype came first and came fast — about a week, in June 2025, before the repository existed. That prototype is also why a studio contacted me a month later. When citing 319 commits, that first week is not in them.

Dungeon Heroes combat: four cycling card slots, energy meter, floor and currency counters, an enemy mid-tell
Combat. Four auto-refilling card slots along the bottom with the energy meter beneath them, the parry shield on the hero, health bars on both units, floor and currency in the header. Captured from a development build in July 2026.

The design problem

A card game gives you time. You read the board, you count mana, you commit. Every intuition that comes with the genre — from Slay the Spire, Magic, Hearthstone — is built on that pause, and none of it survives contact with a real-time fight.

Dungeon Heroes has no pause. The rules that create the experience:

FactValue
Hand4 slots, auto-refilling ~0.5s after a card leaves
Energymax 4, +0.5/s passive, starts at 0, +1 per successful parry
Castinglocks the hero for 0.42–1.5s — no block, no parry — and the effect lands at cast end
Enemiesone acts at a time, FIFO by readiness, 1.5s global gap (0.5s after an interrupt)
Attack tell0.8–1.2s wind-up with a white flash 0.5–0.7s before impact
Per-enemy cooldown5–9s between that enemy's own actions
Blockhalves damage while held; armor shatters on any hit unless the run has Bastion

The consequence those numbers create is the actual game. With two or three enemies alive, a tell arrives every 2.5–4 seconds, and that gap is the only place card play can happen.

Casting inside it is a bet:

  • The cast lock removes your defence for up to a second and a half.
  • The payoff lands at the end of the animation, not the start.
  • Against a lone enemy — most tier-1 fights, and every elite and boss — the window stretches to that enemy's 5–9 second cooldown, and the fight becomes a slower duel.

So the card is never judged by its stat line. It is judged by how it lands in a ten-second loop where the player is also watching for a white flash.

One rule I set: cards are not designed from other cards. Card text is the space that has already been mined, and designing from it produces recombinations of existing effects. The mechanics files are where the unexploited design space lives — the interrupt rule, the readiness queue, the formation table — because each is a lever no card may have pulled yet. That is written into the project's own design instructions, not just into my habits.


What I built

A staged damage-resolution pipeline

Damage is not a subtraction. It runs through an ordered pipeline of stages — IDamageStage implementations split into base, hero and monster sets — with a DamageContext carrying the in-flight values and a DamageResult exposing the outcome to whatever asked.

The alternative I rejected: the usual TakeDamage(int) with a growing pile of special cases inside it. That function becomes unreadable at about the fifth mechanic and untestable immediately, and every new status effect risks reordering something invisible. Staging makes the order explicit and the interactions inspectable — which matters when 24 status effects, 13 keywords and 33 relics can all touch the same hit.

It also means a spell handler asks the result what happened rather than recomputing it. Crit is a flat 2× multiplier applied in the pipeline; elements, armor and block all resolve in known positions.

102 cards, 33 relics, and the constraint that keeps them honest

The catalogue is large: 102 spells, 33 relics, 24 status effects, 13 keywords, 9 encounter types, 8 enemy units — all enumerated in code rather than in a spreadsheet that drifts.

No design ships without two things:

  • A play walkthrough — the card reasoned through the second-by-second experience of the fight it lands in.
  • An unlock-path placement — a point in the progression where a player actually meets it.

A card that reads well and has nowhere to live is not finished.

A design bible that cites its own source code

The reasoning behind the systems lives in design-bible/ — combat feel, reference scenarios, and a systems-rationale log recording why each rule exists. Every number in it carries the file and constant it came from, so it can be re-verified when tuning changes. When a rationale is established during a working session, it gets appended before the session ends.

Design documents rot because they restate values that later move in code. Citing the source turns the document into something checkable, and a rule whose rationale is recorded cannot be silently reintroduced after being engineered out.

The pacing targets it protects are explicit: regular fights 20s–1:30, elites 40s–2min, bosses 1–3min, a biome 5–10min, a full three-biome run 15–30 minutes — chosen for mobile sessions. Deck size runs 8–20 cards, with trimming treated as good play.

Balance is designed for the real distribution, not the ideal one. Most players land 10–20% of available parries and good players 30–40%, so parry payoffs are priced for that band rather than for expert play.

  • Perfect play is meant to zero out easy and normal fights.
  • Elites and bosses are supposed to land damage on everyone.
  • Some attacks deliberately resist perfect armor play, so the second defensive layers have a reason to exist.

The unglamorous half: shipping it

Most of the work between a prototype that runs and a product that ships is here.

  • Versioned save-schema migrationsMetaProfile.CurrentSchemaVersion plus a ProfileMigrator. A roguelike accumulates meta-progression, and an update that eats a player's unlocks is unrecoverable trust damage.
  • Google Play identity and cloud syncGooglePlayIdentity, CloudSyncService.
  • Live-ops SDKs integrated: Cloud Save, Leaderboards, Analytics, Push, IAP and LevelPlay ads.
  • Store compliance authored, not skipped — privacy policy and data-deletion pages, which is a public repository of its own.
  • Play App Signing configured through an upload keystore, and signed release AABs cut at v0.4.4, v0.5.0 and v0.5.1.
  • ~24 UI screen scripts, 4 game scenes, 26 prefabs, 431 sprites.

The tooling around it

Building this game alone is also what produced the AI work. The UI Harness — a gated multi-agent pipeline that turns a written brief into a built, validated Unity screen — exists because authoring screens for this game was the bottleneck, and it writes into this repository. It has its own case study, which covers the engineering.

Two things are worth saying here rather than there:

  • The tooling is judged by whether the game shipped, not by whether the pipeline is interesting. It writes into a repository I release from, which is why it has human gates and validators instead of autonomy.
  • Where determinism was correct, I used determinism. chrome-gen, the sprite generator in the same toolchain, is deterministic with pixel-exact diff verification. Knowing when not to reach for a model is part of building with them.

Evidence

ClaimWhere it comes from
Unity 6000.3.19f1, Android arm64/armv7, min SDK 25 / target 36ProjectSettings/
Package com.sunforge.dungeonheroes, bundleVersion 0.5.1ProjectSettings.asset
319 commits, 2025-07-13 → 2026-07-27, sologit log
156 C# scripts, ~34,300 linesfile count plus wc -l, includes blanks and comments
102 spells, 33 relics, 24 statuses, 13 keywords, 9 encounter types, 8 enemy unitsenum members in SpellHelper.cs, Relic.cs, Status.cs, KeywordHelper.cs, EncounterHelper.cs, UnitHelper.cs
Staged damage pipelineAssets/Scripts/Damage/IDamageStage, DamageContext, DamageResult, Stages/
Combat constants (hand, energy, cast lock, gaps, tells, cooldowns)HeroDeck, UnitHelper, AnimationHelper, Hero.cs, EnemyActionQueue, each cited in design-bible/combat-feel.md
Save-schema migrationsMeta/MetaProfile.cs, Meta/ProfileMigrator.cs
Cloud identity and syncMeta/GooglePlayIdentity.cs, Meta/CloudSyncService.cs
Live-ops SDKsPackages/manifest.json
Store-compliance pagesDocs/legal/, published from a separate public repository
Signed AABs v0.4.4 / v0.5.0 / v0.5.1Builds/
Pacing and parry-rate targetsdesign-bible/combat-feel.md, recorded 2026-08-03
Combat screenshotin-editor run capture, Assets/Screenshots/CombatScreen/

Source availability: the game repository is private. The build is installable through closed testing, and access to the code can be arranged during a hiring process.

Purple Pwny — a game-playing agent, a biome, and a browser game's Unity client

The agent below is an expert system — a hand-authored heuristic policy, rules and utility scoring, encoding how I play the game.


Summary

Purple Pwny Studios is an independent for-hire studio whose products include Degen Dungeons. I worked there August 2023 to October 2024 — starting on concept art, then committing code from September 2023 — as a gameplay programmer, shipping features into an established, self-hosted 2,710-commit production codebase.

Three pieces of that work are worth showing:

  1. DunAI — a 552-line decision agent that plays the dungeon crawler competently, written in a single commit and essentially untouched since. It exists so the game could be measured against skilled play instead of random play.
  2. The Seashore biome — its enemy roster, its boss, its signature status mechanic, and the balance pass — plus ~20 relics I designed and implemented, with the engine changes needed to support them.
  3. The Degen Drafts Unity client, built end to end — about 86% of its ~2,680 lines, including a generator that inverse-simulates a race from a predetermined winner.

The game is public at degendungeons.com.


The agent

DunAI.cs is 552 lines. I wrote it in one commit in April 2024, and line-level blame is 427 mine to 124 — the 124 being a later formatting pass by someone else, not a change of behaviour.

The problem it solves. A roguelike's balance question is "how does this item perform in the hands of someone who knows what they are doing?" Simulating with random play answers a different and much easier question. You need a player, at volume — and a human cannot play ten thousand dungeons.

What it is. A rule tree branching per landmark type with explicit thresholds, sitting on closed-form utility scoring. Weapon evaluation is damage × speed × (accuracy / 100) with multiplicative bonus adjustments over hand-tuned per-stat coefficient tables. Shop policy is a value lookup — buy when cost <= relicValue, ranked by relic_value / basePrice(rarity) — against DunAIDB, a 356-line table of per-class relic gold values code-generated from a spreadsheet the designers actually maintained.

Why hand-authored was the right call. The goal was not to discover optimal play. It was to encode known competent play, so item performance could be measured against a stable, inspectable baseline.

Why rules rather than a learned policy:

  • A learned policy is a second thing to debug.
  • When a balance number comes out strange, you cannot tell whether the item is wrong or the policy has drifted.
  • Rules can be read. When this agent does something surprising, the reason is in the file.

The source carries a note I wrote on the agent's own behaviour:

// 90% optimized. I could test for specific curses…

Written at the time, to myself, in a file nobody was auditing. It says the agent is good enough for its purpose and names the gap that remains — which is the calibration I would want from anyone shipping a heuristic.

Attribution. The same repository contains a forward-search solver, a batch balance harness that runs ten thousand dungeons per item, and a genetic algorithm fitting a target death distribution. All three are the other engineer's work. My agent is invoked by a runner flag, and the repository keeps no report artifacts, so what shipped from its output is not recorded there.


The Seashore biome, and ~20 relics

The biome. I authored the Seashore row — a fast, evasive, ability-damage-leaning coast with the boss Chef Scampi — its six enemy definitions, and the TENDER ("Tenderize") status mechanic threaded through the whole biome so the enemies, the boss and the player's counterplay all referenced one idea rather than each having their own.

The boss ability, StirFry.cs, is entirely mine — a stateful two-mode ability:

  • Self-buffs by composing three effect instances, or deals 200% damage if already Heated.
  • An OnReceiveDamage override strips the buff on a critical hit.

That last clause is the design. It gives the player a specific, learnable answer to the boss's escalation instead of a damage race.

Scope: my contribution is the mechanical and balance layer. The biome's tileset is not mine; its map builder is a stub inheriting the forest one.

The relics. Around 20 implementations, 13 of them files with no one else's commits: BargainingChip, CursedClaymore, CursedGrimoire, DoubleEdgedSword, GoldenChalice, LuckyCharm, OldRevolver, PiggyBank, RustyShield, SacredMace, TrinketOfBrutality, UnityStone, VIPPass, and more.

These were designed, not ticketed. The brief was a list of relic names and existing icon art: design an effect that suits the name and the art and makes sense inside the game's existing ecosystem. So the mechanic was mine to invent, constrained by a name and a picture — which is a harder and more interesting constraint than a spec, because the fantasy is fixed before the mechanic exists.

And they came with engine work. Several effects had no support in the engine, so I extended it:

  • A FLAT_PRICES cell for shop discounts.
  • WalletChangeReason threaded through currency actions, so a spend could be attributed.
  • GetSellPrice.

Content is only as good as the systems willing to express it. If you cannot go down a layer, you end up designing what the engine already permits.


Degen Drafts — the Unity client, end to end

Degen Drafts is a betting game around a race. I built its Unity client end to end — about 2,319 of its ~2,680 lines, with the round kernel, the odds table, the season kernel and the EV simulator being separate libraries.

The interesting piece is BehaviourGenerator.cs — 339 lines, 338 mine. The race outcome is decided before the race is shown, which is a provably-fair requirement, not a design choice. So the generator runs the problem backwards: from a predetermined winner it derives drink times, builds per-contestant behaviour timelines — drink stages, cough distractions — and computes progress curves that arrive in the right order.

Its own comment states the goal: to create tension and story — was it close, was it a brutal victory. That is the constraint that makes it difficult: the outcome is fixed before the race is drawn, and it still has to play as a race. A correct result that reads as a foregone conclusion is a failed feature.

Also mine:

  • BaseState.cs — a state machine over a game-event enum (ROUND_START → BETTING → DRINKING → CELEBRATION → PRIZE → DONE). 290 of 306 lines.
  • DraftsRNG.cs — seeded deterministic RNG composing a server season seed, a round seed and a monotonic counter. 100% mine, including its own written caveats about bias I had not yet tested.
  • The UI, text, coin, bet, banner, progress-bar, scheduler and scene-loading managers.
  • Contributions to the WebGL↔JS bridge and the Drafts web page.

On the bet multipliers: the committed code has 2×, 3×, 4× and 5× across two arrangements. I have said "up to 7×" from memory in the past; the repository does not support it, so 2×–5× is what I use.


What I learned from the codebase, as distinct from what I built

Worth naming because it shaped how I work, and because it is context rather than authorship: the project was server-authoritative, with a SERVER compile flag gating oracle code out of the client entirely; outcomes were provably fair, decoded from a hash chain; and determinism was a first-class concern, with DNA snapshot/restore across five aggregates and a desync fuzzer to catch drift.

That was my first exposure to a codebase where "it works on my machine" is not a defect report but a category error. None of that architecture is mine. Working inside it for a year is why the systems I have built since are deterministic where they can be and instrumented where they cannot.


Evidence

ClaimWhere it comes from
Employment Aug 2023 – Oct 2024; first commit 2023-09-19, last 2024-10-03dd_repo git history; the August start was concept art, before code
78 commits; repo 2,710 commits across three contributorsgit log; line-level blame used wherever attribution mattered
DunAI.cs 552 lines, 427 mine, single commit 2024-04-30crawl/crawl_core/ai/DunAI.cs; the other 124 lines are a 2024-07-18 formatting pass
Rule + utility policy, no learning componentRule tree per LandmarkID, closed-form CalculateWeaponScore, hand-tuned coefficient tables
DunAIDB 356 lines, code-generated from a spreadsheet; shop policycrawl/crawl_core/generated/DunAIDB.cs
The self-assessing commentsame file
Seashore biome row, 6 enemies, TENDER mechaniccrawl_generator/data/biomes.tsv, enemies.tsv, EnemyDefStorePartial.cs; commits 2024-08-02 and 2024-08-08
StirFry.cs 67 lines, 100% minecrawl/crawl_core/abilities/impls/StirFry.cs
~20 relics, 13 solo filescrawl/crawl_core/effects/impls/relics/
Engine extensions (FLAT_PRICES, WalletChangeReason, GetSellPrice)commits 2024-09-26 and 2024-10-03; Fighter.cs, Wallet.cs, CellID.cs, DungeonRunner.cs, ParamHelpers.cs
Drafts client ≈2,319 of ~2,680 linesblame across drafts/drafts_client/Assets/Scripts
BehaviourGenerator.cs 339 lines, 338 minesame directory
BaseState.cs 290/306; DraftsRNG.cs 100%same directory

Source availability: the repository is the studio's, self-hosted and private. The game is public at degendungeons.com.