NIM 2K implements the classic mathematical strategy game Nim for two human players, fitting entirely within a 2K RAM machine. The board displays seven rows, each initially containing seven objects, and players take turns removing one to seven objects from a single row; the player left with the last object loses. A notable technical constraint is that the program exploits the compressed display file of the 2K machine: it calculates display-file addresses directly using PEEK 16396 and 16397 to obtain the base address, then computes individual cell locations via the formula DF+38*H+2*I+1, POKEing character codes from an animation string to erase tokens. Variable names like N0–N2, N7, O7, and P1 cache frequently used numeric constants, a common 2K RAM conservation technique. The delay routine at line 250 spins on an empty INKEY$ loop rather than using PAUSE, and VAL “number” is used in GO TO and GO SUB targets to save memory.
Demonstrates what’s possible in just 2K of RAM, using only standard Sinclair BASIC. Because it uses the characteristics of the compressed display file, if you want to run it in a 16K ZX81 or emulator, you first have to POKE 16389, 72 then NEW before loading.
Program Analysis
Program Structure
The program is organized into a compact set of functional blocks, exploiting line-number ranges as a rough substitute for named procedures:
- Lines 10–110: Initialization — set SLOW mode, cache numeric constants, probe display file base address, and display the title banner.
- Lines 120–170: Player name entry loop (two players).
- Lines 200–270: Subroutines — address calculator (200), validated numeric input 1–7 (220), and a keypress/delay loop (250).
- Lines 300–390: Board setup — CLS, initialize the row-state string
X$and object countN, draw the seven rows of tokens. - Lines 500–810: Main game loop — prompt for row, validate, prompt for count, validate, animate removal via POKE, update state, test win/lose conditions.
- Lines 820–940: End-of-game messages, play-again prompt, and optional side-swap.
- Lines 1000–1010: SAVE and RUN.
Constant Caching
A hallmark 2K technique is pre-computing frequently used literals into variables at startup, avoiding repeated tokenization of numeric literals throughout the program:
| Variable | Value | Used for |
|---|---|---|
N0 | 0 | Zero comparisons, loop initialization, AT column |
N1 | 1 | Loop starts, STEP, index arithmetic |
N2 | 2 | Player count, DIM, address formula |
N7 | 7 | Row count, object count, loop bounds |
O7 | 17 | AT row for status messages |
P1 | 21 | AT row for input prompts |
Using VAL "number" on the right-hand side of LET (lines 20–70) is itself a space-saving trick, as the stored string representation can be shorter than a full numeric literal in some BASIC implementations. The same idiom is used in GO TO VAL "..." and GO SUB VAL "..." throughout.
Display File Address Calculation
The 2K compressed display file does not use the standard 6912-byte layout; instead each print position maps more compactly. Line 80 reads the display file base address from system variables:
LET DF=PEEK 16396+256*PEEK 16397
The subroutine at line 200 then computes the exact byte address of a token in row H, column I:
LET L=DF+38*H+N2*I+N1
which expands to DF + 38*H + 2*I + 1. This reflects the 2K display file’s 38-byte-per-line stride and 2-byte-per-character encoding. The game then uses POKE L, CODE B$(A) to cycle through the animation string B$="▞▚▞*. ", replacing the token graphic with successive characters ending in a space, giving a rudimentary erasure animation.
Game Logic
Row occupancy is tracked by the seven-character string X$="1111111" (line 340); a row’s character is set to "0" when all its objects are removed (line 730). The total object count N starts at 49 (N7*N7) and is decremented by the number taken each turn (line 740).
- If
N=0after a move, the current player took the last object and loses (line 820 — “YOU’RE A FOOL”). - If
N=1before a move, the current player is forced to take the last object, so the other player wins (line 840).
Finding the topmost remaining object in a row (lines 600–620) iterates I from 7 down to 1, calling subroutine 200 and checking PEEK L=N0 to skip already-empty cells. The removal loop (lines 640–720) then steps back from I by S positions, POKEing each cell.
Input Validation
The subroutine at line 220 uses INPUT A and loops back to itself if the entered value is outside 1–7:
IF A<N1 OR A>N7 THEN GOTO VAL "220"
A separate check at line 630 catches the case where the player requests more objects than remain in the chosen row, jumping to an error message and re-prompting. Row-empty validation (lines 520–550) checks the X$ flag string rather than re-scanning the display.
Delay and Keypress Routines
The subroutine at line 250 implements a combined pause-and-keypress-skip: it counts up to N7*O7 (119) iterations, returning early if any key is held. This is used after error messages and the title screen to give the player time to read before continuing.
The play-again prompt (lines 860–930) polls INKEY$ in a tight loop, branching on “Y” or “N”. The side-swap prompt at line 910 similarly polls until a valid key is seen, then sets the turn variable T using the boolean expression (INKEY$="Y"), which evaluates to 1 (true) or 0 (false).
Notable Techniques and Anomalies
N$(T+N1)selects the current player’s name by adding 1 to the 0/1 turn flag, indexing into the two-row string array. This avoids an IF/THEN branch.- Line 170 has no line 160 — a harmless gap.
- The removal animation string
B$="▞▚▞*. "has six characters; iterating over all of them means each cell gets POKEd six times before settling on a space, producing a brief flicker effect visible in SLOW mode. NOT Tat line 770 cleanly toggles the turn between 0 and 1 without arithmetic.- The use of
GO TO VAL "900"at line 900 (targeting itself) combined with an INKEY$ check creates a wait-for-keypress loop that stalls until any key is released before proceeding — a standard INKEY$ debounce pattern.
Content
Image Gallery
Source Code
10 SLOW
20 LET N0=VAL "0"
30 LET N1=VAL "1"
40 LET N2=VAL "2"
50 LET N7=VAL "7"
60 LET O7=VAL "17"
70 LET P1=VAL "21"
80 LET DF=PEEK 16396+256*PEEK 16397
90 LET T=N0
100 DIM N$(N2,N7)
110 PRINT AT N7,N7;"▌▌▌[*] [N][I][M] [*]▐▐▐",,,,"BY F.NACHBAUR",,,
120 GOSUB VAL "250"
130 FOR A=N1 TO N2
140 PRINT "NAME-PLAYER ";A;"?"
150 INPUT N$(A)
170 NEXT A
180 LET B$="▞▚▞*. "
190 GOTO VAL "300"
200 LET L=DF+38*H+N2*I+N1
210 RETURN
220 INPUT A
230 IF A<N1 OR A>N7 THEN GOTO VAL "220"
240 RETURN
250 FOR A=N0 TO N7*O7
255 IF INKEY$ <>"" THEN RETURN
260 NEXT A
270 RETURN
300 CLS
340 LET X$="1111111"
350 LET N=N7*N7
360 PRINT "[R][O][W]",
370 FOR A=N1 TO N7
380 PRINT ,,," ";A;" ";"[O] [O] [O] [O] [O] [O] [O] ";
390 NEXT A
500 PRINT AT O7,N0;"YOUR TURN,";N$(T+N1);AT P1,N0;"WHICH ROW? "
510 GOSUB 220
520 IF X$(A)="1" THEN GOTO 560
530 PRINT AT P1,N0;"█[R][O][W]█[E][M][P][T][Y][.]"
540 GOSUB VAL "250"
550 GOTO VAL "500"
560 LET H=A
570 PRINT AT P1,N0;"HOW MANY? "
580 GOSUB 220
590 LET S=A
600 FOR I=N7 TO N1 STEP -N1
610 GOSUB 200
620 IF PEEK L=N0 THEN NEXT I
630 IF S>I THEN GOTO 790
640 FOR I=I TO I-S+N1 STEP -N1
650 GOSUB 200
660 FOR A=N1 TO LEN B$
670 POKE L,CODE B$(A)
680 NEXT A
720 NEXT I
730 IF I=N0 THEN LET X$(H)="0"
740 LET N=N-S
750 IF N=N0 THEN GOTO 820
760 IF N=N1 THEN GOTO 840
770 LET T=NOT T
780 GOTO 500
790 PRINT AT P1,N0;"[T][O][O]█[M][A][N][Y][.] "
800 GOSUB VAL "250"
810 GOTO 500
820 PRINT AT O7,N0;"YOU""RE A FOOL,";N$(T+N1)
830 GOTO VAL "850"
840 PRINT AT O7,N0;"YOU WIN, ";N$(N1+T)
850 PRINT AT P1,N0;"AGAIN? Y/N"
860 IF INKEY$ ="Y" THEN GOTO 890
870 IF INKEY$ ="N" THEN STOP
880 GOTO 860
890 PRINT AT P1,N0;N$(N2);" STARTS?"
900 IF INKEY$ <>"" THEN GOTO VAL "900"
910 LET T=(INKEY$ ="Y")
930 IF INKEY$ <>"Y" AND INKEY$ <>"N" THEN GOTO 910
940 GOTO VAL "300"
1000 SAVE "NI[M]"
1010 RUN
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.