A readable map is inviting to edit. It also makes it easy to lose a character or place two players. We will check the format before the game uses it.
Continue from the map loader. Keep a copy of the eight original DATA lines so each experiment can be restored.
Stop at the first useful error
Replace line 4110.
Add lines 21, 4005, 4015, 4095, 4102, 4104, 4106, 4120, 4130, 4140, 4150, 4160, 4500, 4510, 4520, 4530, 4540, 4550.
21 IF e$<>"" THEN GO TO 4500
4005 LET people=0: LET crates=0: LET goals=0: LET e$=""
4015 IF LEN m$<>8 THEN LET e$="Use eight symbols per row.": RETURN
4095 IF v=-1 THEN LET e$="Unknown map symbol.": RETURN
4102 IF b$="P" OR b$="+" THEN LET people=people+1
4104 IF v=3 OR v=4 THEN LET crates=crates+1
4106 IF v=2 OR v=4 THEN LET goals=goals+1
4110 NEXT c: NEXT r
4120 IF people<>1 THEN LET e$="Use exactly one player.": RETURN
4130 IF crates=0 THEN LET e$="Add at least one crate.": RETURN
4140 IF crates<>goals THEN LET e$="Match crates and targets.": RETURN
4150 IF goals=0 THEN LET e$="Add at least one target."
4160 RETURN
4500 BORDER 0: PAPER 0: INK 7: CLS
4510 PRINT AT 4,2;"Check room ";room
4520 PRINT AT 6,2;e$
4530 IF r<9 THEN PRINT AT 8,2;"Row ";r
4540 PRINT AT 11,2;"Edit DATA, then RUN again."
4550 STOPComplete program for this step
10 GO SUB 7000: GO SUB 5000
15 LET room=1
20 DIM g(8,8): LET moves=0: LET won=0: GO SUB 4000
21 IF e$<>"" THEN GO TO 4500
85 GO SUB 6000
86 IF left=0 THEN LET won=1
100 GO SUB 1000
110 GO SUB 3000
120 IF k$="q" OR k$="Q" THEN GO TO 9000
130 IF k$="r" OR k$="R" THEN GO TO 20
140 IF won=1 THEN GO TO 110
150 LET dr=0: LET dc=0
160 IF k$="i" OR k$="I" THEN LET dr=-1
170 IF k$="k" OR k$="K" THEN LET dr=1
180 IF k$="j" OR k$="J" THEN LET dc=-1
190 IF k$="l" OR k$="L" THEN LET dc=1
200 IF dr=0 AND dc=0 THEN GO TO 110
210 LET nr=pr+dr: LET nc=pc+dc
220 IF nr<1 OR nr>8 OR nc<1 OR nc>8 THEN GO TO 700
230 LET v=g(nr,nc)
240 IF v=1 THEN GO TO 700
250 IF v=3 OR v=4 THEN GO TO 400
260 GO TO 500
400 LET br=nr+dr: LET bc=nc+dc
410 IF br<1 OR br>8 OR bc<1 OR bc>8 THEN GO TO 700
420 LET bv=g(br,bc)
430 IF bv<>0 AND bv<>2 THEN GO TO 700
440 LET g(nr,nc)=0
450 IF v=4 THEN LET g(nr,nc)=2
460 LET g(br,bc)=3
470 IF bv=2 THEN LET g(br,bc)=4
475 IF v=4 THEN LET left=left+1
476 IF bv=2 THEN LET left=left-1
480 LET r=br: LET c=bc: GO SUB 2000
500 LET oldr=pr: LET oldc=pc
510 LET pr=nr: LET pc=nc: LET moves=moves+1
520 LET r=oldr: LET c=oldc: GO SUB 2000
530 LET r=pr: LET c=pc: GO SUB 2000
590 IF left=0 THEN LET won=1
600 GO SUB 2500
610 GO TO 110
700 GO TO 110
1000 BORDER 0: PAPER 0: INK 7: CLS
1010 PRINT AT 0,2; INK 5;"CRATES";AT 0,24; INK 7;"ROOM 0";room
1020 PRINT AT 20,2; INK 7;"I up J left K down L right"
1030 PRINT AT 21,6; INK 5;"R restart Q quit";
1040 FOR r=1 TO 8: FOR c=1 TO 8
1050 GO SUB 2000
1060 NEXT c: NEXT r
1070 GO SUB 2500
1080 RETURN
2000 LET y=3 +2 *(r-1): LET x=8 +2 *(c-1)
2010 LET z=g(r,c)+1
2020 IF r=pr AND c=pc THEN LET z=6
2025 IF z=6 AND g(r,c)=2 THEN LET z=7
2030 PRINT AT y,x; INK a(z);t$(z,1 TO 2);AT y+1,x;t$(z,3 TO 4)
2040 RETURN
2500 PRINT AT 1,2; INK 7;"STEPS ";moves;" "
2505 PRINT AT 1,18; INK 5;"GOALS ";total-left;"/";total
2510 IF won=1 THEN PRINT AT 19,2; INK 4;"Delivered! R replay / Q quit"
2540 RETURN
3000 LET k$=INKEY$
3010 IF k$=h$ THEN GO TO 3000
3020 LET h$=k$
3025 IF k$="" THEN GO TO 3000
3030 RETURN
4000 RESTORE 8000
4005 LET people=0: LET crates=0: LET goals=0: LET e$=""
4010 FOR r=1 TO 8: READ m$
4015 IF LEN m$<>8 THEN LET e$="Use eight symbols per row.": RETURN
4020 FOR c=1 TO 8: LET b$=m$(c): LET v=-1
4030 IF b$="-" THEN LET v=0
4040 IF b$="#" THEN LET v=1
4050 IF b$="." THEN LET v=2
4060 IF b$="C" THEN LET v=3
4070 IF b$="*" THEN LET v=4
4080 IF b$="P" OR b$="+" THEN LET pr=r: LET pc=c: LET v=0
4090 IF b$="+" THEN LET v=2
4095 IF v=-1 THEN LET e$="Unknown map symbol.": RETURN
4100 LET g(r,c)=v
4102 IF b$="P" OR b$="+" THEN LET people=people+1
4104 IF v=3 OR v=4 THEN LET crates=crates+1
4106 IF v=2 OR v=4 THEN LET goals=goals+1
4110 NEXT c: NEXT r
4120 IF people<>1 THEN LET e$="Use exactly one player.": RETURN
4130 IF crates=0 THEN LET e$="Add at least one crate.": RETURN
4140 IF crates<>goals THEN LET e$="Match crates and targets.": RETURN
4150 IF goals=0 THEN LET e$="Add at least one target."
4160 RETURN
4500 BORDER 0: PAPER 0: INK 7: CLS
4510 PRINT AT 4,2;"Check room ";room
4520 PRINT AT 6,2;e$
4530 IF r<9 THEN PRINT AT 8,2;"Row ";r
4540 PRINT AT 11,2;"Edit DATA, then RUN again."
4550 STOP
5000 BORDER 0: PAPER 0: INK 7: CLS : LET h$=""
5010 PRINT AT 2,12; INK 5;"CRATES"
5020 PRINT AT 5,2; INK 7;"A small warehouse puzzle."
5030 PRINT AT 7,1;"I up, J left, K down, L right."
5040 PRINT AT 9,1;"Push crates onto the targets."
5050 PRINT AT 11,1;"You can push, but never pull."
5060 PRINT AT 13,1;"R restarts the room. Q quits."
5070 PRINT AT 15,1;"One step per press. Take time."
5080 PRINT AT 18,1;"S starts. Q quits."
5090 GO SUB 3000
5100 IF k$="q" OR k$="Q" THEN GO TO 9000
5110 IF k$<>"s" AND k$<>"S" THEN GO TO 5090
5120 RETURN
6000 LET left=0: LET total=0: FOR r=1 TO 8: FOR c=1 TO 8
6010 IF g(r,c)=2 THEN LET left=left+1
6015 IF g(r,c)=2 OR g(r,c)=4 THEN LET total=total+1
6020 NEXT c: NEXT r: RETURN
7000 RESTORE 7200: FOR n=0 TO 159: READ b: POKE USR "a"+n,b: NEXT n
7010 DIM t$(7,4): DIM a(7)
7020 LET t$(1)=" ": LET t$(2)=CHR$ 144 +CHR$ 145 +CHR$ 146 +CHR$ 147
7030 LET t$(3)=CHR$ 148 +CHR$ 149 +CHR$ 150 +CHR$ 151
7040 LET t$(4)=CHR$ 152 +CHR$ 153 +CHR$ 154 +CHR$ 155: LET t$(5)=t$(4)
7050 LET t$(6)=CHR$ 156 +CHR$ 157 +CHR$ 158 +CHR$ 159
7055 LET t$(7)=CHR$ 160 +CHR$ 161 +CHR$ 162 +CHR$ 163
7060 LET a(1)=0: LET a(2)=1: LET a(3)=5: LET a(4)=6: LET a(5)=4: LET a(6)=7: LET a(7)=5: RETURN
7200 DATA 0,254,129,129,129,129,129,0
7210 DATA 0,254,2,2,2,2,2,0
7220 DATA 127,64,64,64,64,64,127,0
7230 DATA 127,129,129,129,129,129,127,0
7240 DATA 0,0,0,3,12,8,16,17
7250 DATA 0,0,0,192,48,16,8,136
7260 DATA 17,16,8,12,3,0,0,0
7270 DATA 136,8,16,48,192,0,0,0
7280 DATA 0,127,64,95,88,84,82,81
7290 DATA 0,254,2,250,26,42,74,138
7300 DATA 81,82,84,88,95,64,127,0
7310 DATA 138,74,42,26,250,2,254,0
7320 DATA 3,7,7,3,1,15,31,27
7330 DATA 192,224,224,192,128,240,248,216
7340 DATA 27,3,3,7,6,6,14,0
7350 DATA 216,192,192,224,96,96,112,0
7360 DATA 3,7,7,3,13,15,31,27
7370 DATA 192,224,224,192,176,240,248,216
7380 DATA 27,19,11,15,7,6,14,0
7390 DATA 216,200,208,240,224,96,112,0
8000 DATA "########"
8010 DATA "#------#"
8020 DATA "#--#.--#"
8030 DATA "#--#---#"
8040 DATA "#--C---#"
8050 DATA "#-P----#"
8060 DATA "#------#"
8070 DATA "########"
9000 PAPER 0: INK 7
9010 PRINT AT 21,0;"Finished. RUN to try again. ";
9020 STOPLine 4005 resets three counts and clears e$, the error message. Line 4015 checks LEN m$ before the column loop indexes the string. A short or long row returns immediately with an explanation. Line 4095 does the same if no symbol matched and v is still -1.
RETURN here leaves the loader, not the entire program. Line 21 checks the message before counting targets or drawing a playable room. An error goes to 4500, which displays the room, the message and a row number when the failure occurred within a row, then stops. Although part of the array may have been filled, input cannot reach it.
The cell loop counts players, crates and goals. P and + each count as a player. Codes 3 and 4 each count as a crate; codes 2 and 4 each count as a target. After all rows, the loader requires exactly one player, at least one crate and equal crate and target totals. The final goal check states that requirement too; with positive crates and equal totals, it is already satisfied.
Investigate one row
Keep the complete working program above as our recovery copy. We will change only line 8010, leaving the loader and drawing code alone. If the game is running, press Q to return to BASIC before editing.
First establish the working case:
8010 DATA "#------#"Enter RUN, then press S at the title. Check that the room appears with one player, one crate and one target. Make one legal move, then press R: the original positions and zero steps should return. Press Q before the next edit.
Now predict each result before running it. Count the symbols inside the quotes; the quotes themselves are not map cells.
| Case | Line to enter | Our prediction | Observed message and row |
|---|---|---|---|
| Valid control | 8010 DATA "#------#" |
Room plays | Record the working result |
| Short row | 8010 DATA "#-----#" |
Write this before RUN | Record what actually appears |
| Unknown symbol | 8010 DATA "#--X---#" |
Write this before RUN | Record what actually appears |
For each faulty case, enter its line, RUN, and press S. Does the game draw a playable room, or stop with a message? Record the exact message and row number. The error screen stops in BASIC, so we can edit the line there.
Restore the valid line and run it successfully between the two faults, and once more after the last fault. That gives us a working comparison each time, rather than letting one broken edit influence the next investigation.
Before opening the explanation, choose a hypothesis: is the loader rejecting the row’s length, rejecting a symbol, or accepting data that the drawing routine cannot display? Which observed message would support or contradict that explanation?
Hint: find the first check that can reject this row
Read line 4015 before following the symbol tests. Does it reach m$(c) for both faulty rows? For the eight-symbol row, follow what happens to v when b$ is X.
Compare our evidence and repair
The short row has seven symbols. Line 4015 sets e$ to Use eight symbols per row. and returns before indexing its characters. The screen reports Row 2.
The X row has eight symbols, so it passes the length check. At column 4, X matches none of the supported symbols, leaving v at -1. Line 4095 sets Unknown map symbol. and returns. The screen again reports Row 2; this version reports a row, not the column. We locate column 4 by inspecting the DATA ourselves.
Neither case reaches room drawing or movement: line 21 directs the returned error to the diagnostic screen. Some earlier cells may already be in g(), but the program does not offer that partial room for play. We have evidence of a data-format fault, so changing colours or movement code would not repair its cause.
For each case, restore 8010 DATA "#------#", then RUN and S. Check the initial room, make a legal move and press R again. A message disappearing is useful evidence; a working move and restart also check that the repaired data still reaches the game correctly.
Finish by explaining why the two bad rows reach different checks even though they report the same row number. We have described the expected behaviour, reproduced a fault, tested a specific explanation, changed one relevant value and checked the repair.
More mistakes to explore
These are optional extensions. Keep changing one line at a time, predict first, and restore the original DATA after each trial.
Other edits and their expected results
| Temporary edit | Expected result |
|---|---|
| Line 8010: add one dash | Eight-symbol row error, row 2 |
| Line 8050: replace P with a dash | Exactly-one-player error |
| Line 8010: replace one dash with P | Exactly-one-player error |
| Line 8040: replace C with a dash | At-least-one-crate error |
| Line 8010: replace one dash with a dot | Crate/target count mismatch |
A missing DATA statement is different. READ can run out of values before our checks can run, and the ROM reports Out of DATA. This validator is not a general BASIC error handler. Check that all eight map lines exist and that RESTORE selects the right section.
A map has eight rows of eight recognised symbols, one player and equal positive numbers of crates and targets. Must it be solvable?
Show the explanation
No. Those checks establish a usable structure, not a route through the puzzle. A crate could be trapped in a corner or a target cut off. We still need to solve and play an edited room.
Combined symbols matter to that distinction too. * contributes both a crate and a target; + contributes a player and a target. A structurally valid map with every crate already delivered starts completed because line 86 sees zero uncovered targets. A malformed map never reaches that line.
If the program enters play after an error, check line 21. If a bad row produces a subscript error instead of our message, check that the length test precedes m$(c). Good diagnostics happen before the operation that would fail.
Optional: which room did we build?
Once our handwritten DATA works, we can try a small tool on our development computer. This needs Python 3.10 or later; it is optional, and the Spectrum game still uses the same BASIC loader.
Open the converter and its instructions. Keep the original unit-08 program and room.txt as recovery copies. Work on a copy named trial-room.txt, in the tool’s folder. Run:
python3 map_to_data.py trial-room.txt -o trial-room.basThe output is eight DATA lines, not a complete game or a tape. Compare all eight with lines 8000–8070 above: they should match. In particular, authoring row 6 becomes 8050 DATA "#-P----#". When the loader reads it, P becomes pr=6, pc=3 and floor in g(6,3).
Press Q to leave the game before editing. Replace only lines 8000–8070 in our working program with the generated lines, then RUN and press S. Move left once and press R. Check that the player returns to row 6, column 3 and the steps return to zero.
Leave an old output behind
In trial-room.txt, change row 2 to #--X---#. Before rerunning the command, predict: will it replace the output, remove it, or leave the previous output alone?
Run the command and read its diagnostic. Compare trial-room.bas with its earlier contents. For this controlled experiment only, put those remaining DATA lines into BASIC and run the game again. Why can it show a playable room when our authoring file contains X?
Hint: follow the files before following the loader
Does the generated file contain X? Which file does our command read, and which DATA does BASIC actually consume? A working game cannot tell us whether a separate conversion succeeded.
Explain the evidence, then repair the chain
The converter reports row 2, column 4 and fails. It preserves the last successful output, so that file still describes the original room. BASIC accepts the DATA we gave it; it never reads trial-room.txt. The output is valid but stale.
Restore row 2 to #------#. Now change row 6 to #--P---#, moving P one column right. Regenerate successfully and inspect line 8050. Before replacing it in BASIC, predict whether RUN alone will show our change. It will not: generating a file does not edit the program already in the Spectrum.
Replace the eight DATA lines, RUN and press S. The player should start at row 6, column 4. Move left and restart: column 4 should return. Finally restore the original authoring map, regenerate successfully, replace the DATA and repeat the movement/restart check at column 3.
We have finished when we can point to the authoring row, generated line and runtime position, and explain which step was missing. Keep conversion errors visible; an output file’s existence is not evidence that the latest edit reached it.
This puts unit 07’s three representations into practice. Later, Night Patrol prepares changes to the guard’s view: that preparation happens inside the game, whereas this converter runs before play. In both cases, we must know which prepared data the next operation actually uses.
Sources
Steven Vickers, edited by Robin Bradbeer, ZX Spectrum BASIC Programming, second edition (Sinclair Research, 1983), chapters 6 (DATA), 8 (LEN and slicing), 12 (arrays), and Appendix B (BASIC reports).
Run it here
This unit's program, built for you.
- Program
- step-01.tap
- Machine
- ZX Spectrum 48K (PAL)