--- title: "20 Tons" id: 69484 type: "computer_media" slug: "20-tons" url: "http://localhost/computer_media/20-tons/" markdown_url: "http://localhost/computer_media/20-tons.md" published_at: "2026-04-13T09:01:57+00:00" modified_at: "2026-04-13T17:54:24+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2026/04/20-tons-3.png" excerpt: "Collect rings across twenty puzzle screens while dodging crushing weights and hungry eaters — complete with a built-in level designer for creating your own challenges." 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: "ZX Spectrum" slug: "zx-spectrum" taxonomy: "post_tag" url: "http://localhost/tag/zx-spectrum/" model: - name: "Timex/Sinclair 2068" slug: "ts-2068" taxonomy: "model" url: "http://localhost/model/ts-2068/" genre: - name: "Game" slug: "game" taxonomy: "genre" url: "http://localhost/type/game/" media_type: "Program" download_url: "https://archive.org/download/timex-sinclair-software-archive/20%20Tons%20%281984%29%28Cooke%2C%20P%29%28ZX%20Spectrum%29%28UK%29%28Program%29.zip" spectrum_computing: "https://spectrumcomputing.co.uk/entry/24/ZX-Spectrum/20_Tons/" mediadate: "198x" images: - url: "http://localhost/wp-content/uploads/2026/04/20-tons-3.png" - url: "http://localhost/wp-content/uploads/2026/04/20-tons-1.png" - url: "http://localhost/wp-content/uploads/2026/04/20-tons-2.png" media_type_tags: "Game" --- # 20 Tons Twenty Tons is a maze-style puzzle game in which the player collects rings across twenty different screens while avoiding falling weights and enemies called “eaters.” The game uses machine code routines loaded separately at address 33000 (15,000 bytes), called via USR at addresses 33000, 33003, 33006, 33009, 33012, and 33018 to handle sprite movement, collision detection, and display updates. Screen data is stored in a custom packed format beginning at address 37000, with each screen occupying 512 bytes plus four metadata bytes encoding shape counts, target score, and player start coordinates. A built-in screen designer allows players to create, edit, and save up to 20 custom levels, with ATTR-based collision detection used to distinguish walls, rings, exits, and hazards. The program uses DEF FN helpers to split 16-bit integers into high and low bytes for POKE operations, and scrolls the title screen text by rotating a string one character at a time in a loop. *** ## Program Analysis ### Program Structure The program is divided into clearly commented sections, with line numbers grouping functional areas: - Lines 1–99: Initialization, UDG setup, and title screen - Lines 100–250: Main menu handling - Lines 1000–1210: Game loop (play game) - Lines 5000–5630: Screen designer - Lines 6000–6100: Screen save routine - Lines 7000–7010: Screen load routine (machine code call) - Lines 8000–8050: Screen viewer - Lines 9000–9020: Border/HUD drawing subroutine - Lines 9100–9130: Title animation - Lines 9910–9999: UDG setup, loader stub, and SAVE commands ### Two-File Loading System The program is designed to be loaded in two parts. Line 9998 acts as a loader stub: it displays a “Loading Code” message and then executes `LOAD "20tons C" CODE` to bring in the machine code binary at address 33000, followed by `RUN` to restart the BASIC. Line 9999 contains the corresponding `SAVE` commands that write both the BASIC program and the code block to tape. ### Machine Code Interface All performance-critical operations are delegated to a 15,000-byte machine code block loaded at address 33000. BASIC calls it through multiple entry points via `USR`: | Address | Apparent Function | | --- | --- | | `USR 33000` | Main sprite/game update; returns status in `PEEK 32996` | | `USR 33003` | Load screen `ls` from packed data at 37000+512*(ls-1); sets start x/y at 32990/32991, towin at 32995 | | `USR 33006` | Sound/effect routine (called after collision events) | | `USR 33009` | Called before attribute read for movement; likely erases player sprite | | `USR 33012` | Called during death animation sequence | | `USR 33018` | Enemy/weight movement update | Communication between BASIC and machine code uses a small scratchpad region around address 32990–32996. `POKE 32992,(LS-1)` passes the screen number before calling `USR 33003`, and `PEEK 32996` reads a collision/status flag back. ### Screen Data Format Each of the 20 levels is stored in a 512-byte region starting at address 37000, with consecutive screens at 512-byte intervals (37000, 37512, 38024, …). The save routine in lines 6000–6090 iterates over all 25×20 = 500 cell positions, writing `ATTR(y,x)-64` per cell, followed by four metadata bytes: low and high bytes of `NOOFSHAPES`, then low and high bytes of `towin` (the score needed to unlock the exit). The `DEF FN h` and `DEF FN l` functions defined at line 20 split 16-bit values into byte pairs for these POKEs. ### ATTR-Based Collision Detection The game uses the Spectrum’s color attribute system as a lightweight collision map. In the game loop, `LET q=ATTR(oy,ox)` at line 1030 reads the paper/ink combination at the target cell, and specific values determine the outcome: - `q=70` or `q=66`: Wall — movement blocked - `q=68`: Ring — collected, score incremented - `q=71`: Exit — level complete if `thisscore>=towin` - `q=67`: Presumably a hazard cell, treated specially in the timer check at line 1110 ### Screen Designer The designer (lines 5000–5630) implements a full tile-based editor. The cursor is shown using `PAPER 9; INK 8; "+"`, and the cell beneath is restored using the `o$` string which maps tile indices to UDG characters. Number keys 1–5 select tile types, space clears, and `q` ends editing. After design, the player must separately place a start position (`s`) and an exit position (`e`) by navigating the cursor — the routine tracks completion of each with the flags `sdun` and `edun`. A subtle bug appears at line 5200: `LET y=y+x` should almost certainly be `LET y=y+1` — after wrapping `x` back to 1, adding `x` (now 1) is coincidentally correct at that moment, but the intent is clearly row advancement by one. The logic works only because `x` has just been reset to 1. ### Title Screen Animation The scrolling marquee at lines 9110–9130 rotates a long string one character at a time: `LET a$=a$(2 TO )+a$(1)`, printing the first 32 characters each iteration with `PAUSE 5` for pacing. `OUT 254,0` and `OUT 254,16` toggle the border color (black then blue) to create a simple flash effect. The loop exits as soon as any key is pressed. ### UDG Relocation Line 9940 dynamically computes the UDG base address from system variables at addresses 23637–23638 (the RAMTOP pointer area), adding 6, then writes the result into system variables 23675–23676 to redirect the UDG table pointer. This is a standard technique for relocating UDGs relative to a `CLEAR` boundary rather than using fixed addresses. ### Key BASIC Idioms - Boolean arithmetic for directional movement: `LET ox=x+(z$="p")-(z$="o")` (lines 1010, 5120, 5270) - `VAL "number"` in line 9940 used as a memory-saving technique for embedding a numeric expression in a string - The lower display area (stream `#1`) is used for status messages and game-over text, keeping it separate from the play area - Tape save/load of the screen data block (`CODE 37000,11000`) covers all 20 screens (20 × 512 = 10240 bytes, rounded up to 11000) in a single operation ### Anomalies and Notes - Line 230 handles both save (key `3`) and load (key `4`) with the same `SAVE` command — the load option appears to be unimplemented or mislabeled; a `LOAD` command is absent from this branch. - Line 1002’s `PRINT AT y,x` references `y` and `x` before they are initialized in that context; they are set by `GO SUB 7000` at line 7010 via `PEEK 32990/32991`. - Line 9950 contains a long `REM` with embedded binary data (UDG pixel definitions and possibly machine code fragments) disguised within keyword tokens — a common technique for embedding data in REM lines. - The `FLASH` attribute on the exit indicator at line 1040 activates only when `thisscore>=towin`, giving the player a visual cue that the exit is now open. ## Source Code ``` 1 REM Oct 1984 20 TONS \* P Cooke 10 CLEAR 32000: LET hi=0 20 DEF FN h(x)=INT (x/256): DEF FN l(x)=x-256*FN h(x) 30 PAPER 0: INK 7: BRIGHT 1: BORDER 0: CLS 40 POKE 23658,0: GO SUB 9910 50 GO SUB 9100 99 REM main menu 100 PAPER 0: INK 7: BORDER 0: CLS : PRINT AT 1,11;"\i\j \k\l\m\n";AT 4,7;"** MAIN MENU **" 110 PRINT AT 7,5;"1. PLAY GAME"''" 2. DESIGN SCREEN"''" 3. SAVE SCREENS > TAPE"''" 4. LOAD SCREENS < TAPE"''" 5. VIEW SCREENS" 120 PRINT AT 21,9;"PRESS 1 - 5" 200 LET Z$=INKEY$: IF Z$<"1" OR Z$>"5" THEN GO TO 200 210 IF Z$="2" THEN GO SUB 5000: GO TO 100 220 IF Z$="1" THEN GO SUB 1000: GO TO 100 230 IF z$="3" OR z$="4" THEN SAVE "20TONSSCREEN$ "CODE 37000,11000: GO TO 100 250 IF z$="5" THEN GO SUB 8000: GO TO 100 1000 LET score=0: CLS : INPUT "start on screen (1-20) ";ls: IF ls<1 OR ls>20 THEN GO TO 1000: 1001 GO SUB 7000: LET q=0: LET ti=500: LET thisscore=q 1002 FOR n=1 TO 10: LET V=USR 33000: PRINT AT y,x; INK 5;"\::\::\::\::\::\::\s\''\t\b"(n): IF n>5 THEN LET v=USR 33018 1003 IF PEEK 32996<>0 THEN LET W=USR 33006 1004 FOR m=1 TO 10: NEXT m: NEXT n 1010 LET z$=INKEY$: LET ox=x+(z$="p")-(z$="o"): LET oy=y+(z$="z")-(z$="a"): IF ox=x AND oy=y THEN FOR n=1 TO 6: NEXT n: GO TO 1100 1030 LET v=USR 33009: LET q=ATTR (oy,ox): IF q=70 OR q=66 THEN GO TO 1100 1040 IF q=68 THEN BEEP .02,36: LET thisscore=thisscore+1: PRINT AT 7,27; FLASH (thisscore>=towin);"\e"; FLASH 0;" ";score+thisscore 1045 IF thisscore0 THEN LET W=USR 33006 1110 LET ti=ti-1: IF V=0 AND Q<>67 AND ti>0 THEN GO TO 1010 1112 FOR n=1 TO 2: LET v=USR 33012: LET v=USR 33006: NEXT n: FOR n=1 TO 5: PRINT AT y,x;"\o\p\q\r "(n): LET v=USR 33006: FOR m=1 TO 10: NEXT m: NEXT n 1115 IF ti=0 THEN PRINT #1;AT 1,0; FLASH 1;" OUT OF TIME",: GO TO 1130 1120 IF ti<>0 THEN PRINT #1;AT 1,0; PAPER 7; INK 2; FLASH 1;" CRUSHED", 1130 BEEP 1,-5: BEEP 1,-5: PRINT #1;AT 0,0; PAPER 0; INK 7;" GAME OVER ", 1135 FOR n=1 TO 200: NEXT n: PAUSE 200: RETURN 1140 LET score=score+thisscore: IF score>hi THEN LET hi=score 1150 RETURN 1200 PRINT AT y,x; INK 5; FLASH 1;"\d": FOR n=1 TO 16: LET v=USR 33018: NEXT n: PAUSE 0: LET ls=ls+1: IF ls=21 THEN LET ls=1 1210 GO TO 1001 4000 STOP 4999 REM DESIGN SCREEN$ 5000 LET ls=0: PAPER ls: INK ls: BORDER ls: CLS : GO SUB 9000: REM BORDER 5010 INPUT "Load screen ?(y/n) ";y$: IF y$<>"y" AND y$<>"n" THEN GO TO 5010 5020 IF y$="n" THEN GO TO 5070 5030 INPUT "load screen (1-20)";ls: IF ls<1 OR ls>20 THEN GO TO 5030 5040 GO SUB 7000: REM LOAD ls. 5050 REM 5070 PRINT #1;"design screen keys a/z o/p space 1 "; INK 1;"\h "; INK 7;"2 "; INK 2;"\g "; INK 7;"3 "; INK PI;"\f "; INK 7;"4 "; INK 4;"\e "; INK 7;"5 "; INK 6;"\a"; INK 7;" q end" 5080 LET x=1: LET y=x: LET ox=x: LET oy=y: LET o=x: LET man=0: LET exit=0 5090 LET o$=" \h\g\f\e\b\a\::" 5100 PRINT AT oy,ox; PAPER 0; INK o;o$(o+1): LET o=ATTR (y,x)-64: PRINT AT y,x; PAPER 9; INK 8;;"+": LET ox=x: LET oy=y 5110 LET z$=INKEY$: IF z$<>" " AND z$<>"a" AND z$<>"z" AND z$<>"p" AND z$<>"o" AND z$<>"q" AND (z$<"0" OR z$>"5") THEN GO TO 5110 5120 IF z$="p" OR z$="o" OR z$="a" OR z$="z" THEN LET x=x+(z$="p" AND x<25)-(z$="o" AND x>1): LET y=y+(z$="z" AND y<20)-(z$="a" AND y>1): GO TO 5100 5130 IF z$="q" THEN GO TO 5220 5140 IF z$=" " THEN LET z$="0" 5150 LET o=VAL z$: IF o=5 THEN LET o=6 5200 LET x=x+1: IF x=26 THEN LET x=1: LET y=y+x: IF y=21 THEN LET y=1 5210 GO TO 5100 5220 LET sdun=0: LET edun=0: INPUT "": PRINT #1;"Please place start pos (s) and end pos (e)." 5250 PRINT AT oy,ox; PAPER 0; INK o;o$(o+1): LET o=ATTR (y,x)-64: PRINT AT y,x; PAPER 9; INK 8;;"+": LET ox=x: LET oy=y 5260 LET z$=INKEY$: IF z$<>"s" AND z$<>"e" AND z$<>"a" AND z$<>"z" AND z$<>"p" AND z$<>"o" THEN GO TO 5260 5270 IF z$="p" OR z$="o" OR z$="a" OR z$="z" THEN LET x=x+(z$="p" AND x<25)-(z$="o" AND x>1): LET y=y+(z$="z" AND y<20)-(z$="a" AND y>1): GO TO 5250 5280 IF z$="e" AND NOT edun THEN LET o=7: LET edun=1 5290 IF z$="s" AND NOT sdun THEN LET o=5: LET sdun=1: LET xstart=x: LET ystart=y 5295 IF NOT sdun OR NOT edun THEN GO TO 5250 5296 PRINT AT oy,ox; PAPER 0; INK o;o$(o+1) 5300 INPUT " OK to save? (Y/N) ";Y$: IF y$<>"y" AND y$<>"n" THEN GO TO 5300 5310 IF y$="n" THEN GO TO 5600 5400 INPUT "save screen no(1-20) ";screen: IF screen<1 OR screen>20 THEN GO TO 5400 5500 GO SUB 6000 5510 RETURN 5600 INPUT "Press C continue or Q quit ";y$ 5610 IF y$<>"c" AND y$<>"q" THEN GO TO 5610 5620 IF y$="q" THEN RETURN 5630 GO TO 5070 5999 REM SAVE SCREEN$ 6000 PRINT #1; FLASH 1;" ** Saving ** Please wait.": LET ADD=37000+512*(SCREEN-1) 6010 LET X=1: LET Y=x: LET MAXSCORE=0: LET NOOFSHAPES=0: LET towin=0 6020 LET A=ATTR (Y,X)-64: IF A=4 THEN LET MAXSCORE=MAXSCORE+1: PRINT AT 7,29;maxscore;AT 9,27;"(max)" 6030 POKE ADD,A: LET ADD=ADD+1: IF A>2 AND A<>5 AND A<>7 THEN LET NOOFSHAPES=NOOFSHAPES+1 6040: LET X=X+1: IF X=26 THEN LET X=1: LET Y=Y+x 6050 IF Y<21 THEN GO TO 6020 6060 INPUT "SCORE TO FINISH (<= max) ";towin 6070 IF towin>MAXSCORE THEN GO TO 6060 6080 POKE ADD,FN L(NOOFSHAPES): POKE ADD+1,FN H(NOOFSHApES): POKE ADD+2,FN L(towin): POKE ADD+3,FN H(towin) 6090 INPUT "": PRINT #1;" ALL OK." 6100 PAUSE 0: RETURN 6999 REM LOAD SCREEN$ 7000 REM LOAD SCREEN$ ls 7010 GO SUB 9000: POKE 32992,(LS-1): LET V=USR 33003: LET X=PEEK 32990: LET Y=PEEK 32991: LET TOWIN=PEEK 32995: RETURN 8000 CLS : LET ls=1: GO SUB 9000 8005 POKE 32992,(LS-1): LET V=USR 33003 8010 PRINT AT 11,29;ls;" ";#1;AT 1,10;"Screen ";ls;" "; 8020 FOR n=1 TO 100: IF INKEY$<>"" THEN RETURN 8025 NEXT n: FOR n=1 TO 2: LET v=USR 33009: LET v=USR 33012: NEXT n 8030 LET ls=ls+1: IF ls=21 THEN LET ls=1 8040 IF INKEY$="" THEN GO TO 8005 8050 RETURN 9000 BRIGHT 1: PAPER 0: INK 7: PRINT AT 0,0; INK 2;"\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g": FOR n=1 TO 20: PRINT INK 2;"\g"; INK 1;"\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h\h"; INK 2;"\g": NEXT n: PRINT INK 2;"\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g\g"; 9010 PRINT AT 2,28;"\i\j";AT PI,27;"\k\l\m\n";AT 5,27;"Score";AT 7,27;"\e 0";AT 9,27;"Sheet";AT 11,29;ls;AT 13,27;"High";AT 15,29;hi;AT 17,27;"Time" 9020 RETURN 9100 PRINT AT 13,9;"20 tons": PLOT 30,30: DRAW 30,80: DRAW 80,0: DRAW 30,-80: DRAW -140,0: PLOT 80,110: DRAW 0,5: DRAW 40,0: DRAW 0,-5: CIRCLE 100,125,10 9110 LET a$=" Twenty tons. Collect the rings through twenty screens , but avoid the 20 ton weights and the 'eaters'...................... Press a key to start. " 9120 PRINT AT 21,0; PAPER 1;a$( TO 32): OUT 254,0: OUT 254,16: LET a$=a$(2 TO )+a$(1): PAUSE 5: IF INKEY$="" THEN GO TO 9120 9130 RETURN 9910 REM UDGs 9930: 9940 LET x=VAL "(PEEK 23637+PEEK 23638*256)+6": POKE 23675,FN l(x): POKE 23676,FN h(x): RETURN 9950 REM $~~ COPY COPY COPY P($<*p|r""~RND\ 'ATTR COPY MERGE \ '~ COPY COPY " COPY COPY \. COPY COPY TO 3 TO 3 TO 3 TO 3`<|\' \' AD COPY COPY COPY COPY COPY COPY COPY COPY \ LN FN ATTR SCREEN$ VAL$ \q LPRINT 9998 PAPER 0: INK 7: BORDER 0: CLEAR 32000: PRINT AT 5,12;"20 tons.";AT 7,10;"Loading Code";AT 9,10;"Please wait.": LOAD "20tons C"CODE : RUN 9999 SAVE "20tons" LINE 9998: SAVE "20tons C"CODE 33000,15000 ```