Skip to content

Query and Change a Tile Map in ZX Spectrum BASIC

Keep room data separate from its picture, with checked reads, edits, properties and world-to-cell conversion.

A wall is a rule as well as a picture. Keep that rule in a map and you can ask what is under a player without trying to recognise the picture on screen.

The caller prints an eight-by-six room, reads tile 2 at column 3, row 2, changes it to tile 3, then prints the changed room. Sinclair BASIC arrays start at one. Logical columns and rows still start at zero, so the final access adds one. Build198x can package this listing as a tape; the execution check loads that tape through the stock 48K ROM.

The maintained caller running through the machine’s stock BASIC ROM.
The maintained caller running through the machine’s stock BASIC ROM. Emu198x 0.44.1 capture.

Put it in a project

Copy routines.bas into unused line numbers, then add the initialisation from demo.bas before your game loop. If you renumber a routine, update every call to it. The arrays and variables belong to your program; none are local.

Complete room, query and edit example

demo.bas
   5 BORDER 0: PAPER 0: INK 7: BRIGHT 0: FLASH 0: CLS
  10 LET W=8: LET H=6: LET S=8: LET L=3
  20 DIM M(W*H): DIM P(L+1)
  30 FOR I=0 TO 47: LET M(I+1)=0: NEXT I
  40 FOR I=0 TO 7: LET M(I+1)=1: LET M(40+I+1)=1: NEXT I
  50 FOR I=0 TO 5: LET M(I*8+1)=1: LET M(I*8+7+1)=1: NEXT I
  60 LET M(19+1)=2: LET M(28+1)=3
  70 LET P(1)=0: LET P(2)=1: LET P(3)=2: LET P(4)=4
  80 LET Q=0: LET U=0: LET A=8: LET B=6
  90 PRINT "BEFORE": GO SUB 1400
 100 LET C=3: LET R=2: GO SUB 1000: GO SUB 1200
 110 PRINT "TILE";T;" FLAGS";F
 120 LET V=3: GO SUB 1100
 130 PRINT "AFTER": GO SUB 1400
 140 PRINT "DONE"
 150 STOP
1000 LET E=0: LET T=-1
1010 IF C<0 THEN RETURN
1020 IF R<0 THEN RETURN
1030 IF C>=W THEN RETURN
1040 IF R>=H THEN RETURN
1050 IF C<>INT (C) THEN RETURN
1060 IF R<>INT (R) THEN RETURN
1070 LET D=R*W+C: LET T=M(D+1): LET E=1
1080 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 GO SUB 1000
1150 IF E=0 THEN RETURN
1160 LET M(D+1)=V: LET T=V
1170 RETURN
1200 LET E=0: 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): LET E=1
1250 RETURN
1300 LET E=0: LET C=-1: LET R=-1: LET T=-1
1310 IF S<=0 THEN RETURN
1320 IF X<0 THEN RETURN
1330 IF Y<0 THEN RETURN
1340 LET C=INT (X/S): LET R=INT (Y/S)
1350 GO SUB 1000
1360 RETURN
1400 LET E=0
1410 IF Q<0 THEN RETURN
1420 IF U<0 THEN RETURN
1430 IF A<1 THEN RETURN
1440 IF B<1 THEN RETURN
1450 IF Q+A>W THEN RETURN
1460 IF U+B>H THEN RETURN
1470 IF Q<>INT (Q) THEN RETURN
1480 IF U<>INT (U) THEN RETURN
1490 IF A<>INT (A) THEN RETURN
1500 IF B<>INT (B) THEN RETURN
1510 FOR K=U TO U+B-1
1520 FOR J=Q TO Q+A-1
1530 LET C=J: LET R=K: GO SUB 1000
1540 PRINT CHR$ (48+T);
1550 NEXT J
1560 PRINT
1570 NEXT K
1580 LET E=1: RETURN

Set W/H to positive integer map dimensions, S to a positive integer tile size in world units and L to the highest permitted tile ID. Allocate and populate M()/P() before calling. Numeric array elements use interpreter storage, rather than one byte per tile. On this interpreter the array base is one: the routine accesses M(D+1) for logical offset D.

The calls

Call Supply Result
1000: read C column, R row E=1, T=tile; invalid coordinates return E=0, T=-1
1100: change C/R, V tile ID Writes only if coordinates and ID are valid; E=1, T=V, otherwise E=0
1200: classify T tile ID E=1, F=property; invalid ID returns E=0, F=-1
1300: world position X/Y, S Floors X/S and Y/S to C/R, then performs the checked lookup
1400: show Q/U start, A/B size Prints a bounded text region at the current cursor

Use GOSUB (Spectrum: GO SUB) followed by the entry number. Read E immediately: every later call can replace it. Reads reject fractional cell coordinates; writes also reject fractional or out-of-range IDs. Non-negative fractional world positions are allowed. A negative position or non-positive S gives C=R=T=-1; a positive position beyond the map retains calculated C/R but returns E=0, T=-1.

Reusable map access and display routines

routines.bas
1000 LET E=0: LET T=-1
1010 IF C<0 THEN RETURN
1020 IF R<0 THEN RETURN
1030 IF C>=W THEN RETURN
1040 IF R>=H THEN RETURN
1050 IF C<>INT (C) THEN RETURN
1060 IF R<>INT (R) THEN RETURN
1070 LET D=R*W+C: LET T=M(D+1): LET E=1
1080 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 GO SUB 1000
1150 IF E=0 THEN RETURN
1160 LET M(D+1)=V: LET T=V
1170 RETURN
1200 LET E=0: 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): LET E=1
1250 RETURN
1300 LET E=0: LET C=-1: LET R=-1: LET T=-1
1310 IF S<=0 THEN RETURN
1320 IF X<0 THEN RETURN
1330 IF Y<0 THEN RETURN
1340 LET C=INT (X/S): LET R=INT (Y/S)
1350 GO SUB 1000
1360 RETURN
1400 LET E=0
1410 IF Q<0 THEN RETURN
1420 IF U<0 THEN RETURN
1430 IF A<1 THEN RETURN
1440 IF B<1 THEN RETURN
1450 IF Q+A>W THEN RETURN
1460 IF U+B>H THEN RETURN
1470 IF Q<>INT (Q) THEN RETURN
1480 IF U<>INT (U) THEN RETURN
1490 IF A<>INT (A) THEN RETURN
1500 IF B<>INT (B) THEN RETURN
1510 FOR K=U TO U+B-1
1520 FOR J=Q TO Q+A-1
1530 LET C=J: LET R=K: GO SUB 1000
1540 PRINT CHR$ (48+T);
1550 NEXT J
1560 PRINT
1570 NEXT K
1580 LET E=1: RETURN

Reserve M()/P(), W/H/S/L, C/R/V/T/E/F/X/Y and scratch D/J/K/Q/U/A/B. Calls are synchronous and not re-entrant. Keep W/H consistent with the allocation; increasing them does not enlarge an array. Reading a stored tile does not certify its ID: property lookup checks that separately.

Why the arithmetic matters

Each complete row occupies W cells. That puts a cell at R*W+C. At column 3, row 2 in an eight-column map, the offset is 19. The array-base adjustment happens only after that calculation. Using H as the stride can look correct on a square map and fail when a level changes shape.

The reverse operation divides a world coordinate by the tile size and rounds down. World positions, tile positions and array subscripts are three different things. This is the sort of representation change explored in Maths for Games.

Change it and spot mistakes

Change the property table to make a new tile block movement, hurt the player or mark an exit. Enlarge P() and L together. Keep actors separate if their movement must leave the underlying floor intact. A map edit changes data; it does not redraw the screen.

A wrong stride shifts the room’s internal features. The retained wrong-stride caller demonstrates that symptom. A rejected write should leave every map cell unchanged; the checks read the whole map afterwards. Fix the index or bounds test instead of hiding the symptom by making the level square.

The text adapter uses IDs 0–3 as printable digits. For a separately includable renderer with a complete ID preflight, use bounded text regions. A text region is useful for debugging or a text-based room; graphical artwork needs a display adapter.

Deliberately wrong stride: the same logical request selects different source cells.
Deliberately wrong stride: the same logical request selects different source cells. Emu198x 0.44.1 capture.

Code and verification

Take the source and contract. Retained execution records identify the emulator, firmware, source and observers. Tests use a non-square map, bad coordinates, bad writes, world-to-cell boundaries and a deliberately wrong stride. Whole decoded captures are compared with independently specified outputs. This establishes those calls in the recorded configuration, not a complete game’s speed or original-hardware behaviour.

Array conventions: Sinclair BASIC Programming, chapter 12, arrays.