I didn’t really know this until beginning this project but Sinclair BASIC is particularly slow and hyper-focused on saving memory over language features or performance. I knew it wasn’t quick or feature rich, but not that even at the time it was considered slow and, well, basic. I don’t know if performance was necessarily impacted by the space saving measures but it wasn’t a focus. The lack of certain language features probably was in part due to these measures, as commands are converted to tokens even as the user inputs them via key combinations, and the fewer possible tokens the less space required by the ROM. On a 48k ZX Spectrum the ROM is squeezed into 16k vs the 32k of the BBC Micro, which has an objectively better and faster BASIC implementation. But, ultimately, this is part of the reason they could be made and sold cheaply giving thousands of kids in the 80s access to their own home computer.

You can get around the lack of language features with carefully curated GO TO statements (I know), and create programs of reasonable size with the rest of the memory available. The programs are slow, but one technique is to use lookup tables where possible rather than rely on algorithms. This is a technique still used in modern software engineering to reduce “big O” complexity, preparing or caching data up front to reduce polynomial complexities to linear. That is, reduce the number of nested loops. It’s also how AI and machine learning algorithms operate, creating or training the models offline ready to be used when required to offer results in a timely manner.

This is all well and good but we are writing a ZX Spectrum 48k BASIC type-in and want to introduce AI to enable player vs computer space battles. I realised as soon as I saw someone else try to figure the game out that gameplay is a bit of a pig, and I intend to improve this, but for now perhaps having an AI opponent coming to destroy them will focus the player’s mind on figuring out the awkward controls. Creating two lookup tables for movement and attack based on relative positions, then resolving using some simple logic that converts x and y positions based on the player’s direction, provides a pretty straightforward solution. The lookup tables are populated up front meaning start-up time is impacted, but barely noticeable during gameplay and much quicker than waiting for a human opponent.

I have pulled out some of the code below to show how this has been implemented for movement, with attack being very similar. The full commit is available here. If an opponent is within 2 or 3 spaces then a particular movement is engaged, falling back to heading towards their general direction if further away. The values 1-14 correspond to how the movement is saved against a player when selected via the “helm computer” controls so it’s just a case of populating this value instead of requesting it.

210 REM movement ai behaviours
220 DIM v(6, 5)
...
500 REM load movement ai behaviours
510 RESTORE 9500
520 FOR x = 1 TO 6 STEP 1
530 FOR y = 1 TO 5 STEP 1
540 READ v(x, y)
550 NEXT y
560 NEXT x
...
8000 REM resolve AI movement
8010 LET d(p, 9) = 0
8020 FOR q = 1 TO t STEP 1
8030 IF q = p OR d(q, 1) < 1 OR d(p, 9) > 0 THEN GO TO 8100
8040 IF d(p, 3) = 0 THEN LET tx = d(q, 1) - d(p, 1) : LET ty = d(q, 2) - d(p, 2) : GO TO 8080
8050 IF d(p, 3) = 1 THEN LET tx = d(p, 2) - d(q, 2) : LET ty = d(q, 1) - d(p, 1) : GO TO 8080
8060 IF d(p, 3) = 2 THEN LET tx = d(p, 1) - d(q, 1) : LET ty = d(p, 2) - d(q, 2) : GO TO 8080
8070 LET tx = d(q, 2) - d(p, 2) : LET ty = d(p, 1) - d(q, 1)
8080 LET tx = tx + 4 : LET ty = ty + 3
8090 IF tx > 0 AND tx < 7 AND ty > 0 AND ty < 6 THEN LET d(p, 9) = v(tx, ty)
8100 NEXT q
8110 IF d(p, 9) <> 0 THEN RETURN
8120 IF ty > -11 AND ty < 0 THEN LET d(p, 9) = 1 : RETURN
8130 IF tx > 5 AND tx < 16 THEN LET d(p, 9) = 5 : RETURN
8140 LET d(p, 9) = 3
8150 RETURN
...
9490 REM movement ai behaviour data
9500 DATA 1, 2, 3, 4, 5
9510 DATA 6, 7, 8, 9, 10
9520 DATA 12, 12, 13, 14, 14
9530 DATA 12, 12, 13, 14, 14
9540 DATA 12, 12, 13, 14, 14
9550 DATA 12, 12, 13, 14, 14