PETRI DISH GAMES

Judging

The public surface at /rorschach, where a visitor sees two unlabelled renders and casts one verdict on the pair.

Judging is what a visitor does at petridish.games/rorschach: two shaders draw side by side and one verdict is cast on the pair. Each verdict is an Elo game on the public ladder, which selection reads. Nothing on the page names either side.

The pair

The page is a pure client. It holds no shaders of its own, and fetches every pair from the lab across the public API.

A pair ships as two entries, each carrying an organism id and its shader source and nothing else. No rating, game count, title or prompt gene crosses to the browser, and the ids are used to address the verdict rather than drawn. The pair stacks vertically in portrait orientation. Which of the two is dealt first is a coin flip.

Only an organism that is alive, has been proven to render, and carries no recorded error is eligible, and a pool-scoped request narrows that to one phylum. When no unproven organism is waiting, the pair is built by the public half of pair selection: the first slot splits between coverage of the least-voted-on and resolution at the top of the ladder, and the opponent is drawn from the nearest by rating that is not a near-copy of it (see diversity).

Each side is compiled and drawn in its own WebGL2 context. A side that fails to compile or link is reported as a failure and the pair is abandoned for a fresh one, up to three times in a row.

The page also handles three states in which no pair is drawn:

  • The lab is unreachable: the page says so and retries after 20 seconds.
  • The lab has no pair to offer: the page says so and retries after 15 seconds.
  • The configuration fetch failed at load: the page reloads itself after 30 seconds.

Hiding the tab tears down both renders; returning to it draws a fresh pair.

Controls

Every verdict is one of two things: a pick, which names a winner, and a rejection, which names a loser. Both close the pair and draw the next one.

InputEffect
A click on a render, or its this one buttonPicks that organism.
Its not this one button, a right-click on the render, or a press held on it for 550 msRejects that organism.
or APicks the first organism of the pair. Held with shift, rejects it.
or DPicks the second. Held with shift, rejects it.
Space, or the skip this pair buttonDraws a new pair with no verdict.
The cross these two buttonBreeds a child from the pair. See crossing.
EscapeCloses the cross overlay.
The sound buttonStarts or stops the voice.
The header selectChanges the pool.
The gallery linkLeaves for the gallery.

The long press sets a flag that swallows the click following its release, so one press does not also register as a pick.

While the cross overlay is open the pair’s keys are inert and Escape is the only one that acts. No second verdict is accepted while one is in flight, and the cross button refuses for the same window.

Each verdict flashes the whole page, tinted red for a rejection. The header counts the verdicts that reached the lab this page-load; a verdict lost to a failed request is not counted and not retried.

Effects of a verdict

A verdict posts the pair’s two ids and exactly one verdict id, named winner or loser. Naming both, naming neither, or naming an id outside the pair is rejected. A visitor’s verdict writes the pub_* columns and nothing else, which is what keeps it off the owner’s ladder.

VerdictEffect on the public ladder
PickBoth organisms move, each on its own K by its own public game count.
RejectionThe rejected organism takes an Elo loss. The other side gains nothing.

The K factor falls as an organism accumulates public games, and each side is updated with its own, on the schedule selection sets out.

When the two organisms carry different prompt genes, those genes take the same result on their own public ladder at a smaller step. Identical prompt genes on both sides move nothing.

Every accepted verdict also inserts a duel row recording the pair, the outcome, the timestamp, public as the judge, and the client key as the voter.

The browser keeps its own record. Every verdict is written to local storage under rorschach_picks before the request leaves, leaving a verdict the lab never received still held locally. Each entry holds:

  • the two ids of the pair
  • the verdict id, keyed winner or loser
  • a millisecond timestamp

The list is capped at 500 entries and the oldest are dropped past that. Nothing is sent from it: the vote request is the only thing that leaves the page. Storage that is full or blocked is a silent no-op, and the verdict still counts at the lab.

Two other keys live beside it: rorschach_sound, which is the voice toggle, and rorschach_pool, which is the pool choice and is shared with the gallery.

Rate limiting

Every public request is charged against a token bucket held per client key, the key being Cloudflare’s forwarded client address, then the first x-forwarded-for entry, then the socket address, truncated to 45 characters. The bucket is a budget rather than an access control: an address is cheap to change, and the bucket exists to bound how many rows one script can write.

ValueSetting
Burst60 tokens
Refill1 token per second
Cross8 tokens
Vote1 token
Render report0.25 tokens
Everything else (pair, pools, gallery, config)0.5 tokens

An empty bucket answers 429 with a retry-after of 30 seconds and refuses the request. The lab logs a throttle event at most once a minute. The bucket table is capped at 5000 keys, and the least recently seen half is dropped when it fills.

The render report

An organism is unproven until a browser reports that it drew. A pair carries at most one unproven organism, the canary, against a proven partner, whenever a backlog of unproven ones exists in the pool.

The browser reports on every render it attempts. A render that survives 300 milliseconds of drawing is reported as a success; one that fails to compile or link is reported as a failure. The report carries the organism id and a boolean and nothing else, and a report naming an organism the lab did not deal to this client does nothing, which is why reporting every render is harmless. The protocol and what each report is worth are set out under the contract.

A verdict on a canary is accepted while its promotion report is still in flight, on the same dealt-recently record.

The pool switcher

A pool is the population a visit judges inside. The header select lists what the lab reports, each entry labelled with its member count: the ecosystem, then the phyla, which are a reading of ancestry rather than anything configured and are composed under taxonomy.

The choice is stored in the browser and shared with the gallery. A stored pool that no longer exists resolves to the ecosystem. A pool list that fails to load leaves the stored choice untouched rather than reassigning the visit.

Changing the selection stores the new pool, closes the cross overlay, and draws a pair from the new population. Every request the page makes afterwards carries the pool: a pair is drawn inside it, and a cross is refused by the lab if both parents are not in it. The ecosystem is the absence of that filter rather than a pool of its own.

Bound to

The source this page's claims were read against, named by symbol and pinned by the digest of the file it lives in.

SymbolDigest
pool.ts 1035b8ea9c67
publicPair f14fffef049d
rorschach.astro b439b3c1f84b
server.js 2ee5143085a9

Near this subject

These are the subjects this one is joined to in the field. Step out of this card and they are the wells around the one you are standing in.