The problem is simple. At initialisation, Nibbles measures the time it takes to perform 1000 empty iterations of a FOR
loop with a DOUBLE
counter in order to determine how many such iterations are required to produce a ½ ms delay. Back when this code was written, CPUs were pretty slow (and FPUs even more so, if they were available at all), so it was reasonable to expect this loop would produce a non-negligible delay. On a modern machine, the time to complete the loop is so small that it cannot actually be measured by the built-in TIMER
function, and to the program it appears as if it finished instantly.
You can of course address it by multiplying the number of iterations (and the factor in the formula) by 20 or so, but that would just be kicking the can down the alley that would require fixing again when moving to even faster systems. My preferred solution is to replace the delay mechanism with an invocation of interrupt 0x15 function 0x86, provided by the BIOS. QBasic doesn’t provide an inline assembler or any facility to link in external code, but it does provide a way to call raw machine code via CALL ABSOLUTE
, which is what I am going to use.
Here are the modifications required in the form of a unified diff:
--- nibbles.bas
+++ nibbles.bas
@@ -69,6 +69,7 @@
DECLARE SUB Level (WhatToDO, sammy() AS snaketype)
DECLARE SUB InitColors ()
DECLARE SUB EraseSnake (snake() AS ANY, snakeBod() AS ANY, snakeNum%)
+DECLARE SUB Delay (speed)
DECLARE FUNCTION StillWantsToPlay ()
DECLARE FUNCTION PointIsThere (row, col, backColor)
@@ -83,8 +84,10 @@
'Global Variables
DIM SHARED arena(1 TO 50, 1 TO 80) AS arenaType
DIM SHARED curLevel, colorTable(10)
+DIM SHARED DelayProc%(10)
RANDOMIZE TIMER
+ GOSUB InitDelayProc
GOSUB ClearKeyLocks
Intro
GetInputs NumPlayers, speed, diff$, monitor$
@@ -113,6 +116,27 @@
DEF SEG
RETURN
+InitDelayProc:
+ DEF SEG = VARSEG(DelayProc%)
+ RESTORE DelayProcData
+ FOR a% = 0 TO 18
+ READ b%
+ POKE VARPTR(DelayProc%(0)) + a%, b%
+ NEXT a%
+ DEF SEG
+ RETURN
+
+DelayProcData:
+ DATA &h55
+ DATA &h89, &hE5
+ DATA &h8B, &h5E, &h06
+ DATA &h8B, &h17
+ DATA &h8B, &h4F, &h02
+ DATA &hB4, &h86
+ DATA &hCD, &h15
+ DATA &h5D
+ DATA &hCA, &h02, &h00
+
SetColors:
IF monitor$ = "M" THEN
RESTORE mono
@@ -137,6 +161,13 @@
PRINT text$;
END SUB
+SUB Delay (speed)
+ DEF SEG = VARSEG(DelayProc%)
+ t& = speed * 500&
+ CALL ABSOLUTE(t&, VARPTR(DelayProc%(0)))
+ DEF SEG
+END SUB
+
'DrawScreen:
' Draws playing field
SUB DrawScreen
@@ -190,7 +221,6 @@
LOCATE 9, 22: PRINT "1 = Novice"
LOCATE 10, 22: PRINT "90 = Expert"
LOCATE 11, 22: PRINT "100 = Twiddle Fingers"
- LOCATE 12, 15: PRINT "(Computer speed may affect your skill level)"
DO
LOCATE 8, 44: PRINT SPACE$(35);
LOCATE 8, 43
@@ -214,11 +244,6 @@
monitor$ = UCASE$(monitor$)
LOOP UNTIL monitor$ = "M" OR monitor$ = "C"
- startTime# = TIMER ' Calculate speed of system
- FOR i# = 1 TO 1000: NEXT i# ' and do some compensation
- stopTime# = TIMER
- speed = speed * .5 / (stopTime# - startTime#)
-
END SUB
'InitColors:
@@ -463,7 +488,7 @@
END IF
'Delay game
- FOR a# = 1 TO curSpeed: NEXT a#
+ Delay (curSpeed)
'Get keyboard input & Change direction accordingly
kbd$ = INKEY$
What the above does is replace the delay busy-loop with a short machine code routine stored in the DelayProcData
section. Here it is in assembly form (NASM syntax):
push bp ; DATA &h55
mov bp, sp ; DATA &h89, &hE5
mov bx, [bp+0x6] ; DATA &h8B, &h5E, &h06
mov dx, [bx] ; DATA &h8B, &h17
mov cx, [bx+0x2] ; DATA &h8B, &h4F, &h02
mov ah, 0x86 ; DATA &hB4, &h86
int 0x15 ; DATA &hCD, &h15
pop bp ; DATA &h5D
retf 2 ; DATA &hCA, &h02, &h00
This BIOS call appeared in the PC/AT, so it should definitely be available on machines recent enough to trigger problems with the delay calibration loop. Plus, using a dedicated call instead of a busy-loop can help free up CPU time, avoid needlessly heating up the processor and thus decrease power consumption both by the CPU itself and by the cooling fan.
The fix alters one game mechanic slightly: if you answer ‘Y’ at the ‘Increase game speed during play?’ prompt, the game will decrease the curSpeed
variable by 10 at the end of each level. Without the patch, this variable is simply the number of iterations performed by the delay loop and thus the game speed increase will depend on the overall speed of the system (the slower the system, the more noticeable the speed-up). After patching, curSpeed
holds the actual delay in ½-ms units, which means a delay decrease will be always 5 ms per level, irrespective of the system speed. I don’t know how this compares to machines contemporaneous to the game’s creation.