--- title: "Tilt Board" id: 57533 type: "computer_media" slug: "tilt-board" url: "http://localhost/computer_media/tilt-board/" markdown_url: "http://localhost/computer_media/tilt-board.md" published_at: "2024-10-05T21:35:24+00:00" modified_at: "2026-03-30T21:17:21+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2024/10/288_TB.png" excerpt: "A fast-moving dodgem game that reads the display file directly for collision detection — can you weave through randomly generated barriers without losing your score?" category: - name: "Archived Media" slug: "archived-media" taxonomy: "category" url: "http://localhost/category/archived-media/" post_tag: - name: "Downloadable" slug: "downloadable" taxonomy: "post_tag" url: "http://localhost/tag/downloadable/" - name: "TS 1000" slug: "ts1000" taxonomy: "post_tag" url: "http://localhost/tag/ts1000/" model: - name: "Timex/Sinclair 1000" slug: "ts-1000" taxonomy: "model" url: "http://localhost/model/ts-1000/" genre: - name: "Game" slug: "game" taxonomy: "genre" url: "http://localhost/type/game/" media_contents: - id: 56737 title: "Timex Sinclair Public Domain Library Tape 1006" type: "computer_media" url: "http://localhost/computer_media/timex-sinclair-public-domain-library-tape-1006/" media_type: "Program" mediadate: "198x" images: - url: "http://localhost/wp-content/uploads/2024/10/288_TB.png" media_type_tags: "Game, Software" --- This program implements a single-player dodgem-style game in which the player steers a character (“$”) across a screen filled with randomly placed inverse-video obstacles, trying to avoid walls and obstacles while accumulating a score. The screen is set up in FAST mode: columns of inverse spaces are printed to form barriers, then one cell per column is blanked to create gaps, and a solid border of block-graphic characters forms the perimeter. Movement is controlled via Q (up), Z (down), and L (right), and the program reads the display file directly using PEEK on the system variable at address 16396/16397 to detect collisions with inverse-video cells (value 128). The score starts at 20,000 and decrements by 50 each move and by 673 on each obstacle hit; hitting a wall divides the score by 3. A high-score tracker persists across rounds in variable U, and the SAVE command at line 530 uses an inverse character in the filename as an auto-run flag. *** ## Program Analysis ### Program Structure The program is divided into clearly separated phases, each handled by a distinct block of lines: 1. **Initialisation and screen setup (lines 10–140):** Sets the high-score accumulator `U` to zero, enters `FAST` mode, draws random column obstacles, and paints a solid border. 2. **Round initialisation subroutine (lines 440–510):** Called via `GOSUB 440` at line 150; sets starting position (`A=10`, `B=1`), initial score `Z=20000`, and last-direction `A$="Z"` (downward), then switches to `SLOW` mode. 3. **Main game loop (lines 160–340):** Reads `INKEY$`, applies movement, erases the old sprite, performs collision detection, redraws the sprite, and loops back. 4. **End-of-round handling (lines 350–430):** Displays score, updates high score, flashes the player sprite, clears the screen, and restarts from line 20. 5. **Obstacle-hit subroutine (lines 170–200):** Replaces the hit cell with an inverse “$”, penalises the score by 673, updates the score display, and returns. 6. **Utility lines (520–540):**`CLEAR`, `SAVE` with auto-run filename, and `RUN` — executed only when preparing the tape image. ### Screen Setup Lines 30–80 build the obstacle field. The outer `FOR B` loop steps through even columns 2 to 28. For each column, every row 0–19 is filled with an inverse space (`"% "`, i.e. an inverse-video space character). Then line 70 blanks one random cell in that column (`RND*14+3`, giving rows 3–17 approximately) to create a navigable gap. This produces a series of near-solid vertical barriers with a single passable hole each. Lines 90–140 draw the border: line 100 prints the block-graphic character `\@@` (a solid block, character 128) along rows 0 and 19, and lines 120–140 print it down columns 0 and 30, forming a rectangular enclosure. ### Collision Detection via Display File PEEK The most technically notable feature is the collision routine at line 290: `IF PEEK (PEEK 16396+256*PEEK 16397+33*A+B+1)=128 THEN GOSUB 170` System variables at addresses 16396 and 16397 hold the low and high bytes of the display file start address (D_FILE). Adding `33*A+B+1` computes the offset into the display file for row `A`, column `B` (each row is 33 bytes wide: 32 character cells plus a NEWLINE). A PEEK value of 128 corresponds to an inverse space — the obstacle character. This technique reads the screen contents directly rather than maintaining a separate map array, saving RAM and eliminating synchronisation issues. ### Movement and Inertia Line 220 implements a simple inertia system: if no key is pressed (`INKEY$=""`), the last valid keypress stored in `A$` is reused. This means the player continues moving in the last chosen direction without holding a key. `A$` is initialised to `"Z"` (move down) in the setup subroutine, so the player immediately starts moving downward. Movement deltas are computed on lines 260–270: - `A=A+(Z$="Z")-(Z$="Q")` — Z moves down (increases row), Q moves up. - `B=B+(Z$="L")` — L moves right; there is no leftward control. The absence of a left-movement key is intentional design: the player must progress rightward across the screen, navigating gaps in each vertical barrier. ### Scoring | Event | Score change | | --- | --- | | Each move (every iteration of main loop) | −50 | | Hitting an obstacle cell | −673 (plus the −50 for the move) | | Hitting a wall (out of bounds) | Score divided by 3 (integer) | The score starts at 20,000. Hitting a wall also terminates the round (line 320 branches to 350 if out of bounds). Line 360 updates the persistent high score `U` if the current score exceeds it. ### End-of-Round Animation Lines 370–410 loop 6 times, flashing the player’s position between normal and inverse “$” while alternating the “BEST SO FAR” display between normal and inverse text (`"%B%E%S%T"` is “BEST” in inverse video). This provides simple visual feedback without any additional machinery. ### Key BASIC Idioms - Boolean arithmetic for movement (`Z$="Z"` evaluates to 1 or 0) avoids `IF`/`THEN` branching for direction calculation. - `FAST`/`SLOW` switching: the screen is built in `FAST` mode for speed, then `SLOW` is restored for gameplay so the display remains visible. - The coordinate variables `Y`/`X` store the previous position so the old sprite can be erased before the new position is drawn (lines 240–250, 280). ### Bugs and Anomalies - Line 310 applies the wall penalty (`Z=INT(Z/3)`) and line 320 then checks the same boundary condition again to branch to the end-of-round section. This duplication means the penalty is correctly applied before the branch, but the condition is evaluated twice unnecessarily. - There is no upward boundary column guard for `B` in the wall-penalty check at line 310 (`B<1` is checked but `B>29` is not), while line 320 does check `B>29`. A player reaching column 30 or beyond gets the end-of-round without the score-division penalty from line 310 — this may be intentional (reaching the right edge is a success state). - The display-file offset formula uses `+1` to skip the NEWLINE byte at the start of each row, which is correct for the ZX81 display file layout. ## Source Code ``` 10 LET U=0 20 FAST 30 FOR B=2 TO 28 STEP 2 40 FOR A=0 TO 19 50 PRINT AT A,B;"% " 60 NEXT A 70 PRINT AT RND*14+3,B;" " 80 NEXT B 90 FOR A=0 TO 30 100 PRINT AT 0,A;"\@@";AT 19,A;"\@@" 110 NEXT A 120 FOR B=1 TO 18 130 PRINT AT B,0;"\@@";AT B,30;"\@@" 140 NEXT B 150 GOSUB 440 160 GOTO 210 170 PRINT AT A,B;"%$" 180 LET Z=Z-673 190 PRINT AT 20,0;"SCORE: ";Z;" " 200 RETURN 210 LET Z$=INKEY$ 220 IF Z$="" THEN LET Z$=A$ 230 LET Z=Z-50 240 LET Y=A 250 LET X=B 260 LET A=A+(Z$="Z")-(Z$="Q") 270 LET B=B+(Z$="L") 280 PRINT AT Y,X;" " 290 IF PEEK (PEEK 16396+256*PEEK 16397+33*A+B+1)=128 THEN GOSUB 170 300 PRINT AT A,B;"$" 310 IF A>18 OR A<2 OR B<1 THEN LET Z=INT (Z/3) 320 IF A>18 OR A<2 OR B<1 OR B>29 THEN GOTO 350 330 LET A$=Z$ 340 GOTO 160 350 PRINT AT 20,0;"END OF ROUND SCORE: ";Z 360 IF Z>U THEN LET U=Z 370 FOR G=1 TO 6 380 PRINT AT 21,3;"BEST SO FAR: ";U 390 PRINT AT A,B;"%$";AT A,B;"$";AT A,B;"%$" 400 PRINT AT 21,3;"%B%E%S%T" 410 NEXT G 420 CLS 430 GOTO 20 440 LET A=10 450 LET B=1 460 LET Z=20000 470 LET Y=A 480 LET X=B 490 LET A$="Z" 500 SLOW 510 RETURN 520 CLEAR 530 SAVE "1028%8" 540 RUN ```