Director’s commentary · Type-In
One wrong digit in a DATA line
Why a single mistyped number could sink a whole type-in program, and how Disk 1 models READ, DATA and a seven-year-old's typos so that Mum can fix them.
Type-in programs came in two halves. At the top was the program itself, a page of BASIC you could more or less follow. Underneath were the DATA lines: rows and rows of numbers holding the colours, the graphics and the sound, which nobody could follow and everybody had to copy exactly. At seven, with a library book propped next to an Amiga 500, I got the first half right and the second half wrong. In my own words: It didn't run. My mum had to fix all the DATA lines. Disk 1 is built on that sentence, so the listing, the mistakes and the way it fails all had to behave like the real thing.
What READ and DATA actually do
A DATA statement does nothing when the program reaches it. It's a shelf. READ is what takes things off the shelf, one value at a time, in the order the DATA lines appear, across every DATA line in the program as if they were one long list. READ doesn't know which line a value came from and doesn't care. It just takes the next one.
BRICKS.BAS, the listing written for the game, reads 130 values in four blocks: 24 for the palette, 40 for the ball's shape, 50 for the bricks and 16 for the tune. It's original, written in the spirit of the books rather than copied from one.
60 FOR I=0 TO 7 : READ R,G,B
70 PALETTE I,R/15,G/15,B/15 : NEXT
80 FOR I=1 TO 40 : READ V
90 S$=S$+CHR$(V) : NEXT : OBJECT.SHAPE 1,S$
100 FOR R=0 TO 4 : FOR C=0 TO 9 : READ W
110 LINE (C*31+2,R*9+20)-STEP(28,6),W,BF
120 NEXT C,R : FOR I=0 TO 15 : READ T(I) : NEXT
…
280 REM ---- COLOURS: RED,GREEN,BLUE ----
290 DATA 0,0,2,15,15,15,15,3,2,15,9,0
300 DATA 15,14,2,3,12,4,2,8,15,14,6,15
…
360 REM ---- THE WALL ----
370 DATA 2,2,3,3,4,4,3,3,2,2
380 DATA 3,4,4,5,5,5,5,4,4,3
390 DATA 4,5,6,6,7,7,6,6,5,4That shelf explains why one wrong digit was so cruel. There were three ways to get a value wrong, and only one of them told you anything.
- A wrong digit that's still a number. No error at all. A 4 becomes a 5 and a brick is the wrong colour, a pixel of the ball goes missing or a note in the tune goes sour. The program runs. It's just wrong, and nothing points at the line.
- A letter where a digit should be. An all-capitals listing means Caps Lock stays on, so a nought becomes a letter O and a one becomes a capital I. READ can't put a letter into a number, so the program stops with a Syntax error, and the error names the DATA line rather than the READ that asked for it.
- A value too many or too few. Every READ after it is out by one. Colours spill into the ball, the ball spills into the wall, and the program either runs out of DATA at the end or quietly draws nonsense.
It was worse on machines where the DATA was machine code, poked straight into memory, because one wrong byte could hang the computer without a message of any kind. That's why magazines ended up printing checksum programs for you to type in before the program you actually wanted.
Modelling RUN without a BASIC interpreter
The game doesn't run BASIC, and it doesn't need to. runListing walks the DATA values in READ order, block by block, and applies the same rules the interpreter would: anything that isn't a number stops the run with a Syntax error on the line it came from, and running off the end is Out of DATA.
export function runListing(lines: readonly string[]): RunResult {
const res: RunResult = { ok: true, palette: [], ball: [], wall: [], tune: [] };
const values: { v: string; line: number }[] = [];
lines.forEach((l, i) => {
if (isDataLine(l)) for (const v of splitData(l).values) values.push({ v, line: i });
});
let k = 0;
let rgb: number[] = [];
for (const block of READ_BLOCKS) {
for (let n = 0; n < block.count; n++, k++) {
const item = values[k];
if (!item) {
const last = DATA_LINES[DATA_LINES.length - 1];
res.ok = false;
res.error = { line: last, lineNo: lineNumber(lines[last]), message: "Out of DATA" };
return res;
}
if (!/^\d+$/.test(item.v.trim())) {
res.ok = false;
res.error = { line: item.line, lineNo: lineNumber(lines[item.line]), message: "Syntax error" };
return res;
}
// …
return res;
}The drama is in line 110. The program draws each brick as soon as it has read its colour, so a bad value in the fourth row means you watch three and a bit rows of wall appear before it stops. That's the moment the chapter wants: it nearly worked. So the fatal letter typo only ever goes where READ will reach it late, in the lower rows of the wall or in the tune.
/** Letters a seven-year-old types for digits (Caps Lock on, as it would be for an all-caps listing). */
const LOOKALIKE: Record<string, string> = { "0": "O", "1": "I" };
/** Lines where the fatal letter typo may go: the lower wall rows and the tune, so some bricks draw first. */
const LETTER_LINES: readonly number[] = LISTING.flatMap((l, i) => {
if (!isDataLine(l)) return [];
const block = blockOfDataLine(DATA_LINES.indexOf(i));
const hasLookalike = splitData(l).values.some((v) => /[01]/.test(v));
const late = block === "tune" || (block === "wall" && lineNumber(l) >= 400);
return late && hasLookalike ? [i] : [];
});Typos a seven-year-old would make
The mistakes are generated from a seed, so every playthrough gets its own set and a test can pin any one of them down. makeTypos picks six to nine wrong values, at most one per DATA line, and always at least one letter for a digit, because that's the one that stops RUN. It also adds one or two harmless slips in REM lines or strings, which is a small lesson of its own: a typo in a comment doesn't matter.
The wrong digits have to be plausible, so every candidate respects the range of the block it's in. A colour stays between 0 and 15 and a brick stays between 0 and 7, which means the program would still run, just wrongly. Plain wrong digits are weighted three to one against swapped, doubled and dropped ones, because that's the mistake you make most when your eyes are going back and forth between a page and a screen.
/** Every plausible digit-level mistake for one value, respecting the block's range. */
function candidates(value: string, block: BlockId): { kind: TypoKind; to: string }[] {
const out: { kind: TypoKind; to: string }[] = [];
const push = (kind: TypoKind, to: string) => {
if (to !== value && inRange(to, block) && !out.some((o) => o.to === to)) out.push({ kind, to });
};
for (let i = 0; i < value.length; i++) {
for (let d = 0; d <= 9; d++) push("digit", value.slice(0, i) + d + value.slice(i + 1));
}
for (let i = 0; i + 1 < value.length; i++) {
if (value[i] !== value[i + 1]) push("swap", value.slice(0, i) + value[i + 1] + value[i] + value.slice(i + 2));
}
for (let i = 0; i < value.length; i++) push("double", value.slice(0, i) + value[i] + value.slice(i));
if (value.length > 1) for (let i = 0; i < value.length; i++) push("drop", value.slice(0, i) + value.slice(i + 1));
return out;
}The seed carries through the chapter. The type-listing minigame, where you copy the program as me, hands its typos on to the data-lines minigame, where you fix them as Mum. The mistakes she finds are the ones you made.
Playing as Mum
This is the only puzzle in the game where you aren't me. Mum was a librarian, and she fixed it the way a librarian would: the book on one side, the screen on the other, every number checked against the page. You tap a value. A wrong one gets struck through and retyped, and a right one gets a nod. A magnifier highlights the matching line in the book, which is the built-in hint, and if you check six right values in a row without it, she suggests it. On a keyboard the arrow keys move between values, Home and End jump to either end of a line, and Page Up and Page Down move four lines at a time.
Her lines say what kind of mistake it was, which is what I'd have needed to hear at seven. They're placeholders in her voice until I can ask her what she actually said.
/** What Mum says when she finds a wrong value. */
export function mumFix(book: string, typed: string): string {
if (/[Oo]/.test(typed)) return "That's a letter O, love. It wants a nought.";
if (/[Il|]/.test(typed)) return "That's a capital I, love. The book wants a one.";
if (typed.length === book.length && [...typed].sort().join() === [...book].sort().join()) {
return `Two numbers the wrong way round. It's ${book}.`;
}
const i = [...typed].findIndex((c, k) => c !== book[k]);
const at = i < 0 ? Math.min(typed.length, book.length) : i;
if (typed.length === book.length + 1) return `One ${typed[at] ?? typed[typed.length - 1]} too many. It's ${book}.`;
if (typed.length + 1 === book.length) return `You've missed a ${book[at]} out. It's ${book}.`;
return `The book says ${book}. You've put ${typed}.`;
}When every line matches the book, she types RUN, and this time the wall, the bat and the ball all appear. It's the same runListing, fed the book's listing instead of mine.
What I'd do differently
Every generated typo changes a value but never the number of values, so Out of DATA exists in the code and never happens in play. A missing comma is the most realistic mistake of the lot and the most confusing one, because the error turns up lines away from the cause. It would make a better puzzle, and a harder one for Mum, and I left it out to keep her part to a couple of minutes. It's the first thing I'd add.
The bigger gap is the book. I don't remember its title. AmigaBASIC came on the Extras disk with the early versions of Workbench, which fits an original Amiga 500, and the likeliest candidates are three COMPUTE! collections from 1986 to 1988. Seeing the covers would settle it. Whichever it turns out to be, the book in the game stays an original design and the listing stays one written for the site.