Before your game draws a wall, it needs to know where the wall is. A map can answer that question without touching the screen. Set column C to 0 and row R to 1, call line 1000, and this room returns tile 3 with success flag E=1.
These are reusable ZX81 BASIC routines. The unchanged readable public caller has now run on 4 KiB RAM with the original ROM. Keep it for its flexible descriptors and explicit contracts; a separately qualified stock 1 KiB option appears below. The assembly version has its own calling contract and execution record.
Put the pieces in one program
Enter the reusable routines, room data and short caller below into the same BASIC program, then RUN. They are listing fragments to merge, rather than three programs to RUN separately. Each line has one statement. The keyword positions differ between the ZX80 and ZX81; use this machine’s own keyboard layout when entering the listing.
Checked map routines — lines 1000–1380
1000 LET E=0
1005 LET T=-1
1007 LET D=0
1010 IF C<0 THEN RETURN
1020 IF R<0 THEN RETURN
1030 IF C>W-1 THEN RETURN
1040 IF R>H-1 THEN RETURN
1050 IF C-INT (C) THEN RETURN
1060 IF R-INT (R) THEN RETURN
1070 LET D=R*W+C
1075 LET T=M(D+1)
1080 LET E=1
1090 RETURN
1100 LET E=0
1110 IF V<0 THEN RETURN
1120 IF V>L THEN RETURN
1130 IF V-INT (V) THEN RETURN
1140 GOSUB 1000
1150 IF E=0 THEN RETURN
1160 LET M(D+1)=V
1170 LET T=V
1180 RETURN
1200 LET E=0
1205 LET F=-1
1210 IF T<0 THEN RETURN
1220 IF T>L THEN RETURN
1230 IF T-INT (T) THEN RETURN
1240 LET F=P(T+1)
1250 LET E=1
1260 RETURN
1300 LET E=0
1305 LET C=-1
1307 LET R=-1
1310 LET T=-1
1320 IF S<1 THEN RETURN
1330 IF X<0 THEN RETURN
1340 IF Y<0 THEN RETURN
1345 IF X>32767 THEN RETURN
1347 IF Y>32767 THEN RETURN
1348 IF S-INT (S) THEN RETURN
1350 LET C=INT (X/S)
1360 LET R=INT (Y/S)
1370 GOSUB 1000
1380 RETURNThe caller owns the map and its properties. This example allocates them in line 2000, fills a seven-column, five-row room, and returns. Call that initialiser once, before querying the map. Repeated DIM is not a portable way to select another room: in particular, the ZX80 can allocate another array while accesses still find the old one.
Room data and property table
2000 LET W=7
2010 LET H=5
2020 LET L=3
2030 LET S=8
2040 DIM M(36)
2050 DIM P(4)
2060 LET M(1)=0
2070 LET M(2)=1
2080 LET M(3)=2
2090 LET M(4)=3
2100 LET M(5)=0
2110 LET M(6)=1
2120 LET M(7)=2
2130 LET M(8)=3
2140 LET M(9)=0
2150 LET M(10)=1
2160 LET M(11)=2
2170 LET M(12)=3
2180 LET M(13)=0
2190 LET M(14)=1
2200 LET M(15)=2
2210 LET M(16)=3
2220 LET M(17)=0
2230 LET M(18)=1
2240 LET M(19)=2
2250 LET M(20)=3
2260 LET M(21)=0
2270 LET M(22)=1
2280 LET M(23)=2
2290 LET M(24)=3
2300 LET M(25)=0
2310 LET M(26)=1
2320 LET M(27)=2
2330 LET M(28)=3
2340 LET M(29)=0
2350 LET M(30)=3
2360 LET M(31)=2
2370 LET M(32)=1
2380 LET M(33)=0
2390 LET M(34)=3
2400 LET M(35)=2
2410 LET M(36)=9
2420 LET P(1)=0
2430 LET P(2)=1
2440 LET P(3)=2
2450 LET P(4)=4
2460 RETURNThe final map element is a diagnostic guard, 9, outside the 35 cells. It helps expose stray writes; a game can omit it once its capacity and checks are sound. Property values 0, 1, 2 and 4 mean floor, wall, hazard and goal in this example. Your game gives them meaning; they are not screen colours or character codes.
Read, classify, change and convert
10 GOSUB 2000
20 LET C=0
30 LET R=1
40 GOSUB 1000
50 CLS
60 PRINT "TILE ";T
70 GOSUB 1200
80 PRINT "FLAGS ";F
90 LET C=0
100 LET R=0
110 LET V=3
120 GOSUB 1100
130 PRINT "CHANGED ";T
140 LET X=55
150 LET Y=39
160 GOSUB 1300
170 PRINT "WORLD ";C;",";R
180 PRINT "TILE ";T
190 STOPRUN prints TILE 3, FLAGS 4, CHANGED 3, WORLD 6,4 and TILE 2. The first lookup reads (0,1), the write changes (0,0) to 3, and world position (55,39) selects the bottom-right cell. STOP leaves the results visible. The report 9/190 acknowledges STOP at line 190.

Reserve lines 1000–1380 for the routines and 2000–2460 for this initialiser. If your program already uses those numbers, renumber every branch and call together. Put your own caller where the demonstration sits, and keep an explicit STOP or branch before execution can fall into the subroutines.
Know what a call changes
| Call | Supply | Read back |
|---|---|---|
| GOSUB 1000 | C=column, R=row | E=1,T=tile,D=flat offset; invalid gives E=0,T=-1,D=0 |
| GOSUB 1100 | C,R,V=new tile | A valid write gives E=1,T=V; failure gives E=0 and leaves storage alone |
| GOSUB 1200 | T=tile ID | E=1,F=property; invalid gives E=0,F=-1 |
| GOSUB 1300 | X,Y=world position, S=units per cell | C,R=cell and E,T from the checked lookup |
Always test E before using a returned value. A rejected write with an invalid ID leaves T unchanged; a rejected position gives T=-1. A world input rejected before division gives C=R=T=-1. If division succeeds but selects an outside cell, C and R retain that calculated coordinate while E=0,T=-1.
W,H,L and S hold the width, height, largest tile ID and positive whole-number cell size. M is the map array; P is the property array. Allocate W×H cells and L+1 properties, then initialise every map ID within 0..L. Bounds checks cannot rescue a descriptor that claims more storage than you allocated. They do not revalidate your entire room on every lookup.
Reserve scalar C,R,V,X,Y for arguments and E,T,D,F for results and scratch. Reading changes E,T,D; writing also changes M; property lookup changes E,F; world conversion changes C,R and calls the reader. Other scratch may retain its old value after an early return. These global variables make the routines non-reentrant: save any result you need before another call overwrites it. The caller owns the arrays and their lifetime; the library does not reserve a fixed screen area or install an interrupt.
Keep coordinates separate from array subscripts
Both the first column and the first row are numbered zero. The row-major offset is R*W+C: moving down a row skips W cells. On this machine the BASIC array starts at 1, so the final access is M(D+1). The property lookup similarly uses P(T+1).
ZX81 BASIC stores numeric values in floating-point form. INT (X/S) rounds down deliberately. The routines also reject fractional cell coordinates, tile IDs and cell sizes before using them as subscripts. World positions may contain fractions: (7.9,8.1) still belongs to cell (0,1). A subscript that BASIC rounds for you would hide a mistake in a cell query.
With S=8, world positions 0–7 select cell 0 and 8–15 select cell 1. The same routine also works with S=3: position (6,3) selects cell (2,1). This is the grid, division and coordinate-space work from Maths for Games in a form you can use immediately. The units need not be screen pixels. Query a world map with world coordinates; subtracting the camera first asks about a different position.
The world contract accepts non-negative positions up to 32767 and a positive whole S. Signed worlds, wrapping, rectangular cells and very large coordinates need an explicit extension. A cell query also differs from box collision: an actor can touch several cells, so its centre alone may miss a wall.
Make it your own
Turn the goal into a closed exit by replacing line 2450 with this line, then RUN from a fresh program:
Goal property 4 becomes 5
2450 LET P(4)=5Querying tile 3 now returns F=5 instead of 4, with the same room IDs. You might interpret that as goal plus blocking. The convention does not add bitwise operators to BASIC; your game must interpret those values. The change adds no elements and no additional lookup work. More tile IDs require a larger P allocation and a matching L; a larger room requires dimensions, allocation and data to change together.
Diagnose two convincing mistakes
Deliberately broken: this alternative reader uses W−1 for the row stride:
Wrong: skip six cells per row in a seven-wide room
1070 LET D=R*(W-1)+C
1075 LET T=M(D+1)Reading (0,1) returns T=2,E=1, instead of 3. The bounds check passed, but the address calculation was wrong. A test confined to row zero would miss it. Restore the actual width:
Correct: use the configured map width
1070 LET D=R*W+C
1075 LET T=M(D+1)
The next fault removes the check after the reader returns. In the bounded reproduction, writing 1 at (7,0) is rejected as outside the room. But D has been reset to 0, and the unguarded assignment changes the first element from 0 to 1, even though E remains 0. A returned failure does not prove that storage stayed untouched.
Deliberately broken: write after a failed lookup
1140 GOSUB 1000
1160 LET M(D+1)=V
Keep the return guard before the assignment:
Correct: return before writing an invalid cell
1140 GOSUB 1000
1150 IF E=0 THEN RETURN
1160 LET M(D+1)=VCheck later rows, every room edge, valid and rejected writes, and unchanged data around the map. The retained diagnostic prints the complete map and trailing guard after every call, alongside an increasing case number to distinguish a completed call from stale output.
What the readable version costs
The 35 map elements, four properties and one guard use 200 bytes of numeric element payload, at 5 bytes each. Array headers, scalars, the program, display file and interpreter stacks/workspace need further RAM. This is element accounting, not a measured free-memory figure.
The diagnostic uses the normal display-serving mode. FAST can give computation more CPU time by blanking the display, but that is a separate caller choice; these records do not measure that speed trade-off.
Each successful read calculates a row offset. Precomputed row offsets exchange storage and setup work for a different query path. Byte or packed maps can reduce data RAM, but extraction and updates become more involved. The assembly entry offers a separately verified byte-map route. None of these alternatives is automatically faster for every workload.
How little RAM does it actually need?
The earlier 16 KiB setting was a diagnostic configuration, not a measured minimum. We entered the unchanged listings through the ROM and counted the stored, tokenised lines:
| Component | Bytes |
|---|---|
| Reusable routines | 659 |
| Explicit room initialiser | 1,086 |
| Public caller | 285 |
| Total program | 2,030 |
ZX81 BASIC keeps a marker and a five-byte numeric value alongside each number in the listing. These counts include that hidden storage, line headers and terminators; the size of the text file would give the wrong answer.
The complete public caller succeeds on 4 KiB. The ROM rejects entry of the unchanged listing on 2 KiB with error 4: out of memory. These are exercised configurations, rather than an exact byte-by-byte minimum.
On 4 KiB, the stopped caller has 291 bytes of variables, a 793-byte display file and a separate 125-byte system area. The smallest gap between calculator-stack end and CPU stack pointer observed at frame checkpoints is 796 bytes; at STOP it is 843 bytes.
Those gaps are sampled headroom, not an instruction-level peak or a promise that your game has that much spare space. Editing, INPUT, extra variables, nested calls and output all need workspace. The measurements retain the actual system pointers and CPU stack pointer alongside the program bytes.
Try the stock 1 KiB version
ZX81 benefits from a different representation. Keep a tile ID in each character of M$, instead of a five-byte numeric array element:
A 35-character room
10 LET M$="01230123012301230123012301230123012"Checked string-map read and write
100 LET T=-1
110 IF C<0 THEN RETURN
120 IF R<0 THEN RETURN
130 IF C>6 THEN RETURN
140 IF R>4 THEN RETURN
145 IF C-INT (C) THEN RETURN
147 IF R-INT (R) THEN RETURN
150 LET D=R*7+C+1
160 LET T=CODE M$(D)-28
170 RETURN
200 LET T=-1
210 IF V<0 THEN RETURN
220 IF V>3 THEN RETURN
223 IF V-INT (V) THEN RETURN
225 GOSUB 100
230 IF T<0 THEN RETURN
240 LET M$(D)=CHR$ (V+28)
250 LET T=V
260 RETURNFixed properties and world conversion
300 LET F=-1
310 IF T<0 THEN RETURN
320 IF T>3 THEN RETURN
325 IF T-INT (T) THEN RETURN
330 LET F=T
340 IF T=3 THEN LET F=4
350 RETURN
400 LET T=-1
410 IF X<0 THEN RETURN
420 IF Y<0 THEN RETURN
430 LET C=INT (X/8)
440 LET R=INT (Y/8)
450 GOSUB 100
460 RETURNStock caller: read, write, read back
50 LET C=0
55 LET R=1
60 GOSUB 100
65 PRINT T
70 LET V=2
75 GOSUB 200
80 PRINT T
85 GOSUB 100
90 PRINT T
95 STOPMerge all four fragments, then RUN. On 1 KiB, use CLS at the command prompt between entered lines when the automatic listing crowds the workspace: that listing shares the same RAM. In the verified fresh fault/repair entry, CLS reclaimed up to 407 display bytes without changing the stored program. The caller prints 3, 2, 2 and stops at line 95. Do not combine these fragments with the readable version: the contracts and line numbers differ.

| Call | Supply | Read back |
|---|---|---|
| GOSUB 100 | C,R | T=ID, or -1 on failure; D=string subscript on success |
| GOSUB 200 | C,R,V | T=V on success, -1 on failure; rejected writes leave M$ unchanged |
| GOSUB 300 | T | F=property, or -1 on failure |
| GOSUB 400 | X,Y | C=INT(X/8), R=INT(Y/8), then T from the reader; negative input returns T=-1 before conversion |
There is no E flag. Test T>=0 for read/write/world calls, F>=0 for properties. Coordinates and IDs must be whole numbers; the guards reject fractions. World positions may contain fractions. An early return leaves other scratch unchanged. The caller owns M$ and must initialise exactly 35 characters representing IDs 0–3. C/R/V/X/Y are arguments; T/D/F are results and scratch. These shared variables make calls non-reentrant.
The complete tokenised program is 683 bytes: room 46, caller 117, read/write 304 and extras 216. The room string occupies 38 variable bytes including its header, compared with 175 bytes for the readable numeric map’s elements alone. The public caller stops with an observed 101-byte stack-to-workspace gap.
The trade-off is visible in the code: width 7, height 5, four properties and cell size 8 are fixed. This room repeats 0,1,2,3 throughout; it omits the readable room’s irregular last row and guard. CODE/CHR$ perform conversion on access. Saving storage does not establish a speed improvement.
Replace room digits without changing the length or introducing IDs above 3. Changing dimensions requires bounds, row stride, string length and world extent to agree. To change the goal property from 4 to 5, replace line 340:
A compact closed exit
340 IF T=3 THEN LET F=5Use the readable version when configurable descriptors, arbitrary properties or a larger room matter more than the stock limit.
Mistakes still cost real data
The stock verification uses a small real BASIC INPUT/GOSUB caller, replacing the public demonstration. It checks every cell, room edges, writes and rejection, properties, world conversion, customisation and deliberately broken lines. ZX81 also rejects fractional cell coordinates and IDs. Immediate commands do not replace a program’s GOSUB return context. Each fault and repair starts in a fresh ROM session; an INPUT prompt is not the listing editor.
With a six-cell stride, reading (0,2) returns 0 instead of 2. A row-zero check would miss it. Removing the return guard after a failed lookup lets an outside write damage the first cell. Restoring the stride and guard fixes those symptoms; always check both returned status and unchanged storage after rejection.

Compact caller, contracts and retained memory records.
Sources and execution
The original ZX80 manual and ZX81 BASIC Programming manual explain the two interpreters’ arrays, arithmetic and memory arrangements. The maintained listings are also run through each named ROM in Emu198x 0.44.1, with 16 KiB RAM and real keyword-keyboard entry.
The execution record covers every cell in the non-square room, outside coordinates, rejected IDs and writes, properties, world conversion, customisation and both completed faults. It retains actual display bytes and source/tool/ROM identities. CI checks those records and rejects altered evidence; firmware stays local. This qualifies BASIC map queries on the named configuration, rather than native drawing, a frame deadline or original-hardware timing.