This is a submission for the DEV Weekend Challenge: Dog Days Edition.
Upload a photo of your dog. An AI accuses them of a crime. You're their defense attorney.
Paw & Order is a browser game where your own dog is the defendant.
You upload one photo and a few seconds later your dog has been arrested:
The People vs. Biscuit
Docket #PAW-042
DEFENDANT: Biscuit
CHARGE: Grand Theft Sausage
COUNSEL: You
STATUS: Extremely suspicious
Then the trial starts. The prosecutor puts a question to you, you pick a response, and the case branches from there. Three exhibits go into evidence: generated images of your dog, at the scene, with the frosting still on their muzzle. Two witnesses give statements, and at least one of them is usually lying.
A trial runs a few minutes. At the end you get one of four verdicts:
Plus a scoreline that isn't the same thing as winning:
VERDICT
NOT GUILTY
Biscuit is free to commit additional crimes.
Defense Performance: 94/100
You can lose the case and still score 96. You can win it badly.
Every case has a hidden truth, generated before the trial begins. Sometimes the dog really did it, sometimes they're innocent, sometimes the evidence just lies. The client never sees any of it, so you're not hunting for a correct answer. You're building the strongest defense the facts allow.
Choices decide the outcome. Replay the same case, answer differently, and the verdict and the score change with you.
Live: https://paw-order.pages.dev
Bring a dog photo, or don't. The home page has a public docket of cases other players entered into the public record, and you can play any of them without up anything.
Justice for every good boy.
Upload a photo of your dog. AI generates a fictional criminal case around that dog. You defend them in court.
Live: https://paw-order.pages.dev
Built for the DEV Weekend Challenge: Dog Days Edition, start to finish inside the challenge window. Everything up to the last commit before the deadline of 2026-08-17 06:59 UTC is the submitted entry; anything after that timestamp is post-deadline work.
Submission post: SUBMISSION.md
.
nvm use
npm ci
cp packages/api/.env.example packages/api/.env # GEMINI_API_KEY is the only one needed
npm run dev:api # :4270
npm run dev:app # :5173, proxies /api
npm run all
(format, lint, typecheck, test) must be green before a commit.
One rule sits under everything else:
The AI creates the world. The engine runs the game.
Nothing calls a model during the trial. Generation runs once, up front, and produces the whole case: crime, hidden truth, evidence, witnesses, the branching question tree, and the verdict thresholds. After that the game is deterministic code reading structured data.
Six model calls per case, all Gemini:
gemini-3.7-flash
call against the photo before any of the rest runs.gemini-3.1-flash-lite-image
, rendered concurrently, each with the player's photo passed inline as a reference so the same dog turns up in every exhibit. Without that likeness you're looking at a stock dog and the joke dies.When the prosecutor says:
"Then perhaps you can explain the frosting on your client's muzzle."
the exhibit had better show frosting on your dog's muzzle. Otherwise it's AI noise with a caption.
So each exhibit carries visualFacts
, one to four things visibly present in the rendered image, and the tree model gets those and nothing else to write from. Three levels stay separate:
| Level | Example |
|---|---|
| Narrative fact (what happened) | Biscuit ate the cake |
| Evidence fact (what can be proven) | Biscuit has frosting on his muzzle |
| Visual fact (what is actually in the picture) | White frosting is visible around Biscuit's mouth |
The trial can cite the third column, and can't invent a fourth.
A response schema is a request, not a guarantee. Every generated payload goes through a hand-written validator before anything is stored. It checks what a JSON schema structurally can't:
When validation fails, the reasons go back into the retry prompt. tree node N4 points at unknown node N9
is something a model can act on, where a bare retry just rolls the dice again. One retry only.
Asked to set its own thresholds, the model has to guess blind, before it knows what totals its own choice effects add up to. Measured over a batch of generated cases that came out at roughly 80% acquittals, with the tainted-acquittal ending unreachable in every case sampled.
So the engine works them out instead. It walks every run the finished tree can be played to and places the lines by quantile inside that real spread: the top 30% of endings acquit, the bottom 30% convict outright, the middle band convicts with reasonable doubt, and half the acquittals come out tainted. The same quantiles hold whatever numeric scale the model wrote its effects on.
Two edge cases needed handling. A plain quantile over endings like 0, 0, 10, 20, 30
lands on the minimum, so every run sits above the line and the verdict below it becomes unreachable. And two independent quantiles over one list can land on the same value, emptying the middle band while both lines still look correctly placed. Both are now drawn from the values strictly above the floor. A tree where every run ends on the same doubt total is rejected: if no choice decides anything, it's a cutscene rather than a trial.
The score is calculated the same way, weighted towards doubt and normalised against what that particular tree made possible. That's what lets a loss read as an excellent defense. The ceiling is usually below 100, because the run that maximises doubt and the run that argues best are rarely the same run.
The api serves the case minus truth
, minus each exhibit's image prompt, minus whether a witness is reliable. Those stripping functions list every field by hand instead of spreading and deleting, so a field added to the type later can't ride out onto the wire without someone deciding it should. The verdict function never reads truth
at all, only player state and the derived thresholds.
An anonymous upload endpoint in front of image generation needs bounds:
isDog
and closed on safeForPublic
. A model outage shouldn't tell someone holding a real dog that it isn't a dog, but an unscreened photo must never reach the public docket.Vue 3 + Vite as a static SPA on Cloudflare Pages, Express + TypeORM on Railway with Postgres, images in Cloudflare R2. The shared types and the trial engine live in one workspace package, so the api is the authority on the verdict while the app renders a score from the exact same code. 167 tests, plus a CI job that dumps the production schema into a throwaway Postgres to prove the committed migrations still describe it.
Best use of Google AI. Gemini does four separate jobs here:
gemini-3.7-flash
writes the case as schema-constrained JSON, so what comes back is a structured world rather than prosegemini-3.1-flash-lite-image
renders the exhibits with the player's own photo as a reference, which keeps one specific dog recognisable across three separately generated imagesThen it stops. Gemini builds the world, and the verdict belongs to code that never saw the truth.
Justice for every good boy.