Skip to content

Commit 37d8924

Browse files
committed
eggs: a check band per group, so a far read verifies
A zoom that resolves two bands has to be able to VERIFY two bands. With one check digit at the end of the stack every partial read was unverifiable, and progressive disclosure came down to trusting whatever the coarse read said. Now the check travels with each group, over everything above it. Three legal stopping points, each a complete answer at its own resolution: kind at two bands, version and id at six, value at nine. A read that stops mid-group drops back to the last check band and reports what that vouches for. Version left the far group to make room for the check. A verified kind is worth more than an unverified grammar number: misreading the kind acts on the wrong egg, whereas not having read the version yet means zooming in. Six pixels a band still carries the whole nine-band stack through every degradation in the suite. Also: tools/verify-frame.ts, which checks a real screenshot and refuses in specifics, and fixtures/README.md saying which captures to take and what each one proves.
1 parent 3b61849 commit 37d8924

11 files changed

Lines changed: 395 additions & 59 deletions

File tree

‎.gitignore‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ temp/
8989
# Generated by scripts/write-fixtures.ts. Regenerate rather than commit.
9090
packages/optical-codec/fixtures/
9191

92-
apps/screen-angel/modules/soul-angel/bridges/sims1/fixtures/
92+
# Generated panels, and any screenshot dropped in for tools/verify-frame.ts. The instructions for
93+
# taking one are not generated, so they are tracked where the person taking it will look.
94+
apps/screen-angel/modules/soul-angel/bridges/sims1/fixtures/*
95+
!apps/screen-angel/modules/soul-angel/bridges/sims1/fixtures/README.md
9396
# Font contact sheets and metrics dumps: regenerate with export-fonts, the pack is the asset.
9497
apps/screen-angel/modules/soul-angel/bridges/sims1/exports/

‎apps/screen-angel/EGGS.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ the_blip_editor:
178178
the_rule: "Most significant first, top down. Kind, then version, then identity, then magnitude, then the fine parameters."
179179
why_it_is_the_whole_trick: "A truncated read is a valid PREFIX, not a corrupt code. What the far zoom shows is exactly what the near zoom shows first, so nothing has to be re-read when the camera moves in and nothing is wrong when it does not. Same property as an address prefix or an interlaced image: you always have a correct coarse answer and the refinement is additive."
180180
the_check_travels_with_each_group: "Every group that can legally be the last visible one ends with its own check band. Otherwise a single trailing check digit would make every partial read unverifiable, which is the failure mode that turns progressive disclosure into guessing."
181+
the_three_groups_as_built:
182+
far: "Kind, then a check band over it. Two bands: what this egg is, verified."
183+
mid: "Version and id, then a check over everything above. Six bands: which egg, and which grammar to read it with."
184+
near: "Value, then a check over the whole stack. Nine bands: the complete code."
185+
version_is_not_in_the_far_group: "It was, when the far group was two unchecked bands. A verified KIND is worth more than an unverified grammar number, because a reader that misreads the kind acts on the wrong egg, while a reader that has not read the version yet simply zooms in. The cost is two extra bands on a full stack, paid once (packages/optical-codec/src/egg-code.ts, GROUPS)."
186+
a_stack_that_stops_mid_group: "Authored art never draws one, and a resample certainly produces one. The reader drops back to the last check band and reports what that vouches for; the bands hanging below it are returned in the digits and believed by nobody."
181187
authored_not_merely_downsampled:
182188
what: "The far-zoom art draws FEWER bands, taller. It is not the near-zoom art scaled down until the stripes alias into mud."
183189
why: "The game already ships a separate sprite per zoom (DGRP), so choosing what a zoom level says costs nothing extra and is decided by the author instead of by resampling. Legibility at the smallest zoom becomes a drawing decision, which is the kind of problem that gets solved."

‎apps/screen-angel/RECOGNIZER.yml‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,17 @@ the_egg_reader:
139139
how_it_finds_one: "Column by column: the brightest and darkest runs in a column are candidate caps, and a candidate survives only if the distance between their centres divides evenly by a band height that also explains the colour changes between them. Then adjacent columns that agree are grouped, and a group narrower than a band is tall is thrown away."
140140
the_caps_do_three_jobs: "They delimit the stack, they set the scale — a cap is exactly one band tall, so the metric comes out of the picture rather than out of a config file — and they are the white and black reference for an affine colour correction. A sunset shifts the bands and the caps by the same amount, and correcting by the caps removes it before a single digit is decided."
141141
it_refuses_rather_than_guesses: "Every band reports the distance to the runner-up colour. The smallest of those is the reading's confidence, and a reading below the floor is returned as refused WITH its digits, so a caller can see what was nearly there. A failed check digit is refused the same way."
142-
the_measured_floor: "SIX PIXELS A BAND reads twelve colours through every degradation in the suite. Five reads twelve through all but a window at a third scale. Below four, only a clean or evenly-dimmed grab reads at all."
142+
a_short_read_is_an_answer_not_a_failure: "The stack is three groups and each one ends with a check band over everything above it — 2 bands, 6, 9. So a two-band read at the far zoom VERIFIES, and reports the kind with version, id and value as null. This is what one trailing check digit could not do: every partial read was unverifiable, so progressive disclosure came down to trusting whatever the coarse read said (EGGS.yml, the_three_groups_as_built)."
143+
the_measured_floor: "SIX PIXELS A BAND reads twelve colours through every degradation in the suite, for the full nine-band stack. Five reads twelve through all but a window at a third scale and a noisy blurred phone grab, where it drops to eight colours. Below four, only a clean or evenly-dimmed grab reads at all."
143144
the_grid:
144145
clean_screen_grab: "2px and up, twelve colours."
145146
evening_light: "2px and up. A global dim costs nothing, because the caps dim with the bands and the correction is affine."
146147
window_at_50_percent: "4px and up."
147148
window_at_33_percent: "6px and up."
148149
soft_focus: "5px and up."
149150
phone_noise_and_warm_cast: "2px and up, once the frame is median-filtered across the width."
150-
phone_noisy_and_soft: "5px and up."
151-
phone_tilted: "4px and up."
151+
phone_noisy_and_soft: "5px for eight colours, 6px for twelve."
152+
phone_tilted: "5px and up."
152153
two_findings_worth_keeping:
153154
noise_invents_boundaries: "Finding the band count from colour changes works and then stops working the moment there is grain, because every row becomes a change. A band is one colour all the way across, so a median of five neighbours across the width removes it and cannot move a horizontal boundary. Without that filter every noisy cell in the sweep read nothing at all."
154155
a_band_can_be_mistaken_for_a_cap: "The reader looks for the caps as the extremes of a column's range and claims the top and bottom 15% of it, so a dark band inside that claim becomes a black cap and the whole egg vanishes — not misread, not refused, invisible. Seven of sixteen codes were being lost this way. The fix is a bound on the palette, not on the reader: bands live between luma 72 and 188 (src/egg-code.ts)."

‎apps/screen-angel/modules/soul-angel/bridges/sims1/FONT-RECOGNITION.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ how_it_is_tested_without_the_game:
127127
the_synthetic_font: "packages/optical-codec/test builds its own awkward five-pixel font — one letter a strict subset of another, one a single stroke that fits inside them all, one with a descender — so the matcher's claims, the container's round trip and the metrics can all be tested by anyone, with no installation and no licence question."
128128
what_was_retired: "The colour matcher, the hand-measured tables in source, the JSON font, the BMP and the eleven separate .gzf files. One format, one matcher, one file. The captured face is now coverage like everything else, its screenshot colours divided back out into the coverage they always were."
129129
the_fixture: "tools/render-panel-fixture.ts draws the panel we claim to read and reads it back, and writes fixtures/panel.png so a person can see what the machine is matching."
130+
the_verifier: "tools/verify-frame.ts takes a PNG the game produced and reports which claims it confirms: both window finders run every time and their agreement is the result worth having, the strip's geometry is printed, the text comes back with the fraction of pixels the glyphs explain, and a stretched capture is named as stretched rather than quietly decimated. fixtures/README.md says exactly which captures to take and what each one proves. Run against the synthetic fixture it correctly reports that the ART finder does not answer — the fixture draws a stand-in strip, not the game's panel background — which is the tool refusing in specifics rather than passing itself."
130131
the_resolution_tests: "The same synthetic panel at 800x600 and at 1024x768, inset into a 1280x800 desktop where the corner path correctly finds nothing, and stretched, where the match reports the scale and the reader decimates before reading. Each asserts the text, not just that something was found."
131132
what_it_cannot_cover: "Whether the shipped art still matches in the Legacy re-release, and whether the description really is drawn at the offsets measured in 2004. Both need one real frame. Everything above is measured against pixels this repository drew."
132133

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# One real frame
2+
3+
Everything this bridge claims about reading The Sims 1 was measured on pixels this repository drew.
4+
The font came out of the game and is blended the way the game blends it, the panel geometry came out
5+
of the game's own source, and `panel.png` is a stand-in panel drawn by `tools/render-panel-fixture.ts`.
6+
That makes the reader testable. It does not make it verified.
7+
8+
One screenshot the game itself produced settles it:
9+
10+
```
11+
pnpm --filter @screen-angel/soul-bridge-sims1 verify fixtures/sims-<whatever>.png
12+
```
13+
14+
## What to capture
15+
16+
Any of these is useful. The first is the one that matters.
17+
18+
1. **Windowed, at the default size, with something selected.** Click an object so the description
19+
strip has text in it — an object with a name, a price and a couple of lines of description is
20+
ideal, since a long line exercises the wrapping and the strip's right edge.
21+
2. **The other resolution.** The game offers 800x600 and 1024x768. A capture of each is what proves
22+
the edge-relative geometry rather than merely asserting it, because a top-relative measurement is
23+
right at one of them and wrong at the other.
24+
3. **Full screen.** Same reading, no window frame, and it tells us whether the anchors survive
25+
whatever the fullscreen path does to the panel art.
26+
4. **A live conversation.** Sims talking, balloons up, the panel showing a relationship line. That is
27+
the frame an actual reader will be looking at.
28+
29+
## What matters about the file
30+
31+
- **PNG, not JPEG.** JPEG rings on exactly the hard edges the glyph masks are made of.
32+
- **Native pixels.** No scaling by the capture tool, no Retina doubling, no window-manager zoom. If
33+
the capture is scaled the tool will find the window anyway and say so — a smooth upscale is located,
34+
decimated, and reported at low confidence, which is a diagnosis rather than a rescue.
35+
- **The whole frame is fine.** The window does not have to fill it; finding the window inside a larger
36+
frame is one of the things being tested.
37+
38+
## Reading the output
39+
40+
`the window` is the only line that matters at first. Both finders run every time:
41+
42+
- **art** correlates the game's own panel background against the bottom of the frame, and reports the
43+
window's edges, the capture scale, and which anchor answered.
44+
- **colours** is the 2004 signature — four corner pixels of the description strip.
45+
46+
If both answer and they agree, the geometry is confirmed. If they disagree, one is wrong and the tests
47+
cannot say which, so look at the frame. If art fails and colours succeeds, the anchors need rebuilding
48+
from this game's own art (`pnpm --filter @screen-angel/soul-bridge-sims1 import-anchors`).
49+
50+
`pixels explained` below about 90% means the reading is a guess wearing a string's clothes. The usual
51+
cause is the wrong font size rather than odd text; `tools/identify-font.ts` compares a crop against
52+
every size the game ships.
53+
54+
`eggs: none` is the correct answer until there is an egg object in the game. The egg numbers in
55+
`../../../../EGGS.yml` are measured on eggs this code drew, and no screenshot of the game as it stands
56+
can confirm them — that needs an egg the game rendered.
57+
58+
## What is here
59+
60+
- `panel.png` — the stand-in panel, drawn and read back by `tools/render-panel-fixture.ts`. Committed
61+
so a person can see what the machine is matching.

‎apps/screen-angel/modules/soul-angel/bridges/sims1/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"export-fonts": "tsx tools/export-fonts.ts",
2121
"identify": "tsx tools/identify-font.ts",
2222
"fixture": "tsx tools/render-panel-fixture.ts",
23-
"test": "vitest run"
23+
"test": "vitest run",
24+
"verify": "tsx tools/verify-frame.ts"
2425
},
2526
"devDependencies": {
2627
"tsx": "^4.19.2",
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Check a real frame, and say plainly which claims it confirms.
3+
*
4+
* pnpm --filter @screen-angel/soul-bridge-sims1 verify path/to/screenshot.png
5+
*
6+
* Every number this repository publishes about reading The Sims 1 was measured on pixels this
7+
* repository drew: the font came out of the game, the panel geometry came out of the game's source, and
8+
* the eggs were rendered by the same code that reads them. That is a floor, not a fact. One capture the
9+
* game itself produced turns the whole set of claims from plausible into checked, and this is the thing
10+
* that does the turning.
11+
*
12+
* It refuses in specifics. "Panel not found" is useless; "found the window at scale 1.5, which means
13+
* the capture was stretched, and decimating it left the description strip 2 rows short" is a lead.
14+
*/
15+
16+
import { readFileSync } from 'node:fs';
17+
import { basename } from 'node:path';
18+
19+
import { decodePNG } from '@micropolis/optical-codec/node/png';
20+
import { readEggs, type EggReading } from '@micropolis/optical-codec';
21+
22+
import { sims1Anchors, sims1PanelFont } from '../src/text/font';
23+
import { findGameWindowByArt, findGameWindowByColours, findPanel, readPanelText } from '../src/text/panel';
24+
25+
const path = process.argv[2];
26+
if (path === undefined) {
27+
console.error('usage: verify-frame <screenshot.png>\n\nSee fixtures/README.md for what capture to take.');
28+
process.exit(2);
29+
}
30+
31+
const frame = decodePNG(readFileSync(path));
32+
console.log(`${basename(path)}: ${frame.width}x${frame.height}\n`);
33+
34+
// FINDING THE WINDOW
35+
//
36+
// Both ways, always, even when the first one answers — a disagreement between the art and the four
37+
// corner colours is the most informative thing this tool can find, and it is silent if only one runs.
38+
39+
const anchors = sims1Anchors();
40+
if (anchors.length === 0) {
41+
console.log('No anchors built in this checkout, so only the four corner colours can find the window.');
42+
console.log('Build them with: pnpm --filter @screen-angel/soul-bridge-sims1 import-anchors\n');
43+
}
44+
const byArt = findGameWindowByArt(frame, anchors);
45+
const byColours = findGameWindowByColours(frame);
46+
47+
console.log('the window');
48+
if (byArt === null) {
49+
console.log(' art not found. Either this is not The Sims, or the panel art has been reskinned.');
50+
} else {
51+
console.log(` art left ${byArt.left}, right ${byArt.right}, bottom row ${byArt.bottomRow}, scale ${byArt.scale.toFixed(3)} (correlation ${byArt.score.toFixed(3)}, anchor ${byArt.anchor ?? 'unnamed'})`);
52+
if (Math.abs(byArt.scale - 1) > 0.01) {
53+
console.log(` STRETCHED. Decimated back to native pixels before reading; a smooth upscale cannot be undone, so expect lower text confidence.`);
54+
}
55+
}
56+
console.log(byColours === null ? ' colours not found.' : ` colours left ${byColours.left}, right ${byColours.right}, bottom row ${byColours.bottomRow}`);
57+
58+
if (byArt !== null && byColours !== null) {
59+
const agree = byArt.left === byColours.left && byArt.bottomRow === byColours.bottomRow;
60+
console.log(agree ? ' BOTH AGREE, which is the result worth having.' : ' THEY DISAGREE. One of the two is wrong and the tests cannot tell you which; look at the frame.');
61+
}
62+
63+
const window = byArt ?? byColours;
64+
if (window === null) {
65+
console.log('\nNothing further can be checked without a window. Not a failed read — a refused one.');
66+
process.exit(1);
67+
}
68+
69+
// THE PANEL AND ITS TEXT
70+
71+
const region = findPanel(frame, window);
72+
console.log(`\nthe description strip\n ${region === null ? 'not found inside the window' : `x ${region.x}, y ${region.y}, ${region.width}x${region.height}`}`);
73+
74+
const font = sims1PanelFont();
75+
const text = readPanelText(frame, font, anchors);
76+
if (text === null) {
77+
console.log(' no text read.');
78+
} else {
79+
console.log(` pixels explained: ${(text.confidence * 100).toFixed(1)}%`);
80+
console.log(` found by: ${text.window.found}`);
81+
console.log(' read back:');
82+
for (const line of text.description.split('\n')) console.log(` | ${line}`);
83+
if (text.confidence < 0.9) {
84+
console.log(' LOW. Below about 90% the reading is a guess dressed as a string: check the font size against');
85+
console.log(' the sizes the game ships (tools/identify-font.ts) before believing any of it.');
86+
}
87+
}
88+
89+
// EGGS
90+
//
91+
// There are none in the game yet, so finding none is the expected result and finding one is news.
92+
93+
const eggs = readEggs(frame);
94+
console.log(`\neggs\n ${eggs.length === 0 ? 'none, which is correct until an egg object exists' : `${eggs.length} found`}`);
95+
for (const egg of eggs) describe(egg);
96+
97+
function describe(egg: EggReading): void {
98+
const where = `(${egg.box.x},${egg.box.y}) ${egg.box.width}x${egg.box.height}`;
99+
const bands = `band ${egg.bandHeight.toFixed(1)}px, digits [${egg.digits.join(' ')}]`;
100+
const verdict =
101+
egg.code === null
102+
? `refused: ${egg.refused}`
103+
: `kind ${egg.code.kind}, ${egg.code.groups} of 3 groups verified${egg.code.id === null ? '' : `, id ${egg.code.id}`}${egg.code.value === null ? '' : `, value ${egg.code.value}`}`;
104+
console.log(` ${where} ${bands} margin ${egg.margin.toFixed(0)} ${verdict}`);
105+
}
106+
107+
// WHAT A PASS MEANS
108+
109+
console.log(`
110+
what this run settles
111+
The window was found on real pixels, or it was not. Everything else in RECOGNIZER.yml and
112+
FONT-RECOGNITION.yml stands on that one question, and it is the only one a synthetic frame cannot
113+
answer. A pass here does not make the egg numbers real: those need an egg the game drew.`);

0 commit comments

Comments
 (0)