|
| 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