PETRI DISH GAMES

The outcome record

One scored visit kept whole: the interaction that was measured, the fitness it produced, the genome behind it, and the frame it was measured in.

An outcome record is one scored visit kept whole: the interaction that was measured, the fitness that interaction produced, the genome that was on screen, and the frame the measurement was taken in. A fitness is one number and states only how good a structure was, never how it was used, so two structures used in completely different ways become the same point the moment they score the same. The record is what the scorer would otherwise discard, written down instead, stamped with the schema version it was written under.

Record contents

Two populations on the site produce records. Both are emergence loops in which the measurement is kept rather than discarded. The exotic regions breed a structure genome and score one visit to it. The instrument breeds a voice and scores one generation of six genomes against what the player is doing.

OutcomeRecord in src/exotic/types.ts is the first of the two. It carries no storage key: where a record was written is a property of the log rather than of the record.

FieldWhat it holds
vThe outcome schema in force when the record was written.
writtenAtEpoch milliseconds, supplied by the writer. No score reads it.
genomeIdThe pool’s own id, unique only inside the pool that minted it.
genomeHashA hash of the serialised genome: the identity that survives a pool reset.
genomeThe serialised genome with code forced to null, so a dead structure can be rebuilt from the record through parseGenome.
bodyHashA hash of the bred shader body, or 0 for a genome that has none.
diagonalPxThe region’s own diagonal at score time.
regionPxThe region’s width and height in CSS pixels.
cellIdsEvery cell the region offered, in the order the recorder was given them.
interactionThe visit itself, as scored.
fitnessThe number the scorer produced from that visit.

The interaction is what createRecorder collected while the region was mounted: milliseconds mounted and visible, pointer path length in CSS pixels, a count of pointer move events, the list of acquisitions, milliseconds hovered per cell without committing, the visitor’s explicit verdict when one was given, the worst contrast ratio any cell rendered at, the largest number of cells that ever overlapped, and whether the region was left with no interaction at all. One acquisition holds the cell taken, milliseconds from mount, milliseconds from that cell’s first hover, how many times the pointer entered and left before committing, and where the pointer was when the click landed, in 0..1 of the region’s own width and height. A keyboard activation and a tap with no prior pointer movement carry a null position rather than borrowing the target’s centre.

A trial is recorded when it clears one of three bars: an acquisition happened, a verdict was stated, or the region was mounted at least 1200 ms. Each side of a duel writes its own record and both are appended in one call, because a duel closes two sides at once and a log this size must not be parsed and re-serialised twice for one trial. The record is assembled from a single outcome() reading, which takes one snapshot and scores it, rather than from a snapshot and a separate score: the clock runs between two readings and an open dwell grows with it, so a record built from the first reading would not have produced the number in the second.

The stored frame

Two of the record’s fields cannot be recovered from the interaction and are therefore kept beside it. The first is cellIds, which says what was on offer and in what order, and without which a cell id in an acquisition is a name with nothing to compare it against. The second is diagonalPx.

Path length enters the score only as a ratio against the region’s own diagonal, so a reader checking a stored record needs that exact number. Recomputing it from regionPx is not equivalent: Math.hypot is not required to be correctly rounded, so a recomputed diagonal can differ from the one the scorer used in the last bit and move the score it was supposed to be verifying. A reader with no diagonal at all falls back to the module’s DEFAULT_DIAGONAL_PX of 1200 and lands somewhere else entirely.

The property that follows is that a stored record reproduces its own score. scripts/test-exotic.mjs asserts it directly, over a defective visit and a clean one, by serialising a record, parsing it back and requiring the score of the parsed interaction against the parsed diagonal to be identical to the fitness the record carries. The same file carries the negative control: scoring the same visit without the stored diagonal produces a different number. The instrument holds the same property by the same rule. scoreOutcome in src/instrument/evolve.ts rebuilds the champion’s genome from the record and replays it through the fitness function that produced it, and src/instrument/archive.ts rounds only the four columns that never enter the score, because a rounded genome cannot reproduce the score it was given.

Schema versions

Every record carries its own version. A bump does not corrupt the records already written, it declares that they are no longer comparable with the new ones, because a vector derived under one schema is not a coordinate in the same space as a vector derived under the next.

  • The exotic outcome schema stands at 1. It versions one record’s field meanings, and is not the population’s storage version, which versions the pool’s payload; the two move for different reasons.
  • The instrument outcome schema stands at 2. Version 1 records were a shorter drift vector scored by a four-term function, and the fifth term makes them incomparable rather than merely older.
  • The container versions itself separately. The exotic log’s envelope and the instrument’s store file each carry a version describing the file’s layout and nothing about the records inside it.

A version a reader does not recognise is never reinterpreted. The exotic reader looks the version up in a registry of upgrade functions, which ships empty, and skips any record it finds no lift for. Reinterpreting a field under a new meaning is silent and produces coordinates that look valid, which is exactly the failure a schema version exists to make impossible. A record from a newer schema is skipped and left in place rather than deleted, and the only thing that ever removes an entry is the oldest-first trim. The instrument is stricter about the container and equally lenient per record: a store file under an unknown layout is dropped whole, while a single record under an unknown schema is dropped alone.

The two logs

Both logs live in the visitor’s own browser, in localStorage, and go nowhere else. There is no server in either loop, and an archive is whatever one person’s use has put in it. Every bound below is a share of one origin’s storage budget.

Exotic regionsInstrument
KeyThe population’s storage key plus .recglia_records_v1
Written bycloseTrial, once per side per trialremember, once per scored generation
Held300 records, oldest first out200 records, oldest first out
Second boundA payload budget of 700000 characters, trimmed by proportion in at most three passesA write every 16 records, so a killed tab costs at most sixteen
On a quota throwOne retry with the newer half of the logOne retry with the newer half of the archive

The exotic log is a second key rather than a share of the population’s own. A full record log must never cost a visitor the structures they bred: the pool sheds shader bodies to survive a quota throw and would have to shed records instead, a payload whose envelope cannot be read is abandoned whole, and anything reading records later should not have to parse a population to find them. The log is wiped with the pool it belongs to, because ids are reissued from scratch after a reset and leftovers would collide on identity.

The instrument declines to record a generation whose population showed no spread, since a generation in which the player distinguished nothing would fill the archive with one point repeated.

The outcome vector

outcomeVector reads one record and returns 20 numbers, every one finite and in 0..1. OUTCOME_KEYS names them in the order they are returned, and that order is part of the schema: a vector is a coordinate only if every record agrees which axis is which. This is the input to behaviour space, and the descriptor derived from it is what gives a genome a measured position.

EntryWhat it reads
dwellMilliseconds mounted and visible.
presencePointer move events.
pathDiagonalsPointer path length in region diagonals.
acquisitionsHow many clicks landed.
acquireValueMean worth of an acquisition: the two clocks, divided down by reentries.
decideMean decay of the hover-to-click clock.
findMean decay of the mount-to-click clock.
reentriesMean entries and exits before a commit.
firstAcquireDecay of the time to the first click, or of the whole visit when there was none.
directnessPath travelled per acquisition, against the direct and the hunting bounds.
idleFractionDwell on cells never clicked, over the time mounted.
hoverBreadthHow many of the offered cells were hovered.
acquireBreadthHow many of the offered cells were clicked.
acquireX, acquireYMean click position in the region’s own frame, 0.5 when no click had one.
orderDriftDistance between the order cells were offered in and the order they were taken in.
contrastThe worst contrast ratio rendered, over the ceiling of 21.
overlapThe largest number of cells that ever overlapped.
verdict1 for kept, 0 for killed, 0.5 for no verdict.
confidenceThe visit’s evidence total, as a fraction of itself plus a half-weight.

Several of these quantities are naturally milliseconds or counts. Fed raw to an encoder they would spend most of its capacity on a dwell-time axis with the rest as noise on it, so each is saturated through a named constant instead. The raw milliseconds stay in the record, which is where a later re-derivation reads them from. Every empty case has a stated fallback rather than a NaN or a drifting sentinel, because a visit with no acquisitions must land in the same place every time it is vectorised or it is not a coordinate at all.

The saturation constants are declared in records.ts and are not the scorer’s. Several are numerically the same as a constant inside fitness.ts, which is a coincidence of what the two clocks measure rather than a shared definition. They are declared again for two reasons: the scorer’s are module-private, and importing them would tie a record’s coordinates to a fitness weight the site is free to retune, which would move the whole archive whenever somebody adjusted how much a reentry costs.

What runs today

Writing is live. Every exotic trial that clears the bar appends a record, and every scored instrument generation with any spread in it appends one.

Reading has one live consumer. src/exotic/population.ts walks the log once per session, keeps the best record per genome hash, derives a descriptor from it and places that individual in a niche, which is what the elite archive ranks its overflow tier by. The walk is lazy and memoised, and reachable only from culling and breeding, because a parse of the whole log on the click path beside a shader compile is the cost that would otherwise be paid every trial.

Three things are shipped and inert.

  • A first visit has an empty log. Nothing has been recorded, so no individual has a record, the descriptor of every founder is null, and the pool ranks by genotype exactly as it did before descriptors existed. The first descriptor appears after the first recorded trial.
  • No encoder is trained. The committed weights artifact ships with an empty key list, so the trained branch never runs and a descriptor is the fixed seeded projection described under behaviour space.
  • The upgrade registry is empty. No older schema exists to lift from yet, so the branch that would lift one is reachable and does nothing.

All three wait on records, which accumulate one visit at a time in one browser at a time.

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
closeTrial unbound
descriptorOf unbound
hydrate unbound
OutcomeRecord unbound
outcomeVector unbound
Recorder unbound
remember unbound
scoreOutcome unbound
test-exotic.mjs unbound

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.