30

MS-DOS versions 5 and 6 come bundled with the QBasic interpreter and a handful of example programs written in that BASIC dialect. One of the programs is Nibbles, a variant of the well-known Snake game.

However, whenever I attempt to start it up on a relatively modern machine or an emulator, it inevitably stops with a ‘division by zero’ error at the end of this fragment:

    startTime# = TIMER                          ' Calculate speed of system
    FOR i# = 1 TO 1000: NEXT i#                 ' and do some compensation
    stopTime# = TIMER
    speed = speed * .5 / (stopTime# - startTime#)

Why does this happen? What is the best way to fix it?

4
  • 1
    I wonder if moslo could be used to fix the problem. – cyberixae Mar 12 at 9:31
  • I love how one of the two answers addresses the why but gives a seriously overcomplicated fix, and the other addresses the best way to fix but omits the why. :-) – R.. GitHub STOP HELPING ICE Mar 13 at 4:25
  • 1
    @R..GitHubSTOPHELPINGICE This is Retrocomputing, practicality is not what we’re after – user3840170 Mar 13 at 11:13
  • 1
    the why's obvious: a short loop timed by a low-resolution timer running on a processor several hundred times faster is going to give you zero elapsed time. Since the what was solved with zero effort (load up on dosbox, works straight off) why go further? – scruss Mar 13 at 22:36
39

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 16
+        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 &hCB
+
 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 is 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                   ; DATA &hCB

The 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 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). With the patch, 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.

4
  • 6
    QuickBasic has CALL INTERRUPT, which was omitted from QBASIC, but it’s easy to work around as you demonstrate ;-). – Stephen Kitt Mar 11 at 10:30
  • 5
    Or if startTime equals stopTime, multiply the number of iterations (and the .5 value) by 10 and try again. Keep going until (stopTime# - startTime#) is nonzero. – snips-n-snails Mar 11 at 17:37
  • Does QBASIC not have some sort of Timer based function linked to a real-world time? A lot of 8-bit home computers had something like that, counting 1/300th or 1/50th of a second it similar. – Caleb Fuller Mar 12 at 17:37
  • Back in the day, I used to use WAIT &3da, 8 WAIT &3da, 8, 8 to use the display refresh rate as a timer (and also to avoid tearing). That method hasn't aged quite as badly as spin-loops, but could still be off by ~4x on modern monitors... – Mark K Cowan Mar 13 at 20:35
36

DOSBox, with the default CPU speed of 3000 cycles on this Linux box, runs nibbles.bas without problems.

1
1

On the modern CPU the FOR loop executes so quickly that the difference in the TIMER before and after is zero. Hence the line:

speed = speed * .5 / (stopTime# - startTime#)

gives a divide by zero error, because (stopTime# - startTime#) equals zero.

The simplest solution would simply be to set the speed variable manually, until you find something that gives a playable game. I mean, it only has to work on your computer anyway.

According to the formula, the faster the computer, the higher the speed number, which I assume then slows the game down somewhere.

So start with:

speed = 1000

or something, and if the game is too fast, increase it, if too slow decrease it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.