by Marco's Retrobits English language blog Italian language blog YouTube channel BooMfire video
BooMfire is my entry to the 13th edition (2024) of the BASIC 10 Liner contest, SCHAU category.
It is an implementation of the DOOM FIRE effect for the Sinclair ZX Spectrum, inspired by Roberto Capuano's MSX entry to the 11th edition of the contest.
My first attempt was coded using Sinclair BASIC, but to achieve smoother animation I relied on the ugBASIC compiler by Marco Spedaletti. BooMfire does not (yet?) exploit isomorphism (the main feature of ugBASIC, which allows you to generate executables for dozens of 8-bit systems from a single source), as the program uses some specific features of the ZX Spectrum and so can only be built for this platform.
Although the program runs on a regular ZX Spectrum, in order to enjoy(!) the music and sound effects, a model with the AY-3-8912 chip (128K, +2, Next, etc...) is required.
When the program starts, the BooM writing, which mimics a simplified version of the DOOM videogame logo, is displayed. In the meanwhile, the AY-3-8912 chip will play an annoying music inspired by the DOOM theme. After the logo has been painted on screen, you'll hear an explosion (powered by the aforementioned AY-3-8912) and then the fire will start to burn. You can change the fire effect by pressing one of the following keys: 1: fire above the BooM logo, 2: fire across the logo (for colour clash enthusiasts), 3: fire behind the logo, 0: extinguish fire.
Note: BooMfire has been coded by the fireplace.
The BooM logo drawing algorithm is similar to the one used on the Amiga to display the Kickstart floppy image.
The flood fill routine is an implementation in ugBASIC of the recursive flood fill algorithm described in the A Fast Well-Behaved Pattern Flood Fill article by Alvin Albrecht.
ugBASIC offers a nice flood fill routine by means of the PAINT statement; however, a custom subroutine has been implemented in order to play music while painting.
The fire animation is my own implementation of the well-known DOOM Fire effect. Instead of plotting coloured pixels, the flame is drawn by updating the attributes of the character cells.
Here is the full code, expanded for better readability and extensively commented:
x0 ' Define AY-3-8912 control (65533) and data (49149) output port numbers.
CONST c=65533
CONST a=49149
' "boom" procedure definition.
' This procedure plays an explosion sound through the AY-3-8912 chip, while the border colour is set to yellow.
PROCEDURE boom
' AY-3-8912 initialization: noise on channels A, B, C.
OUT c,7 :' Select the Mixer register.
OUT a,7 :' Enable noise only on channels A, B, C.
OUT c,8 :' Select the Channel A volume register.
OUT a,31 :' Set Channel A volume to the max.
OUT c,9 :' Select the Channel B volume register.
OUT a,31 :' Set Channel B volume to the max.
OUT c,10 :' Select the Channel C volume register.
OUT a,31 :' Set Channel C volume to the max.
OUT c,12 :' Select the Envelope coarse duration register.
OUT a,56 :' Set envelope duration.
OUT c,13 :' Select the Envelope shape register.
OUT a,9 :' Set the envelope shape: single decay then off.
' Yellow border colour.
COLOR BORDER 6
' Volume Fade out.
i=15 :' Initial volume value.
k=TIMER :' Get current frame counter.
WHILE i>0 :' Loop while volume is > 0.
OUT c,8 :' Select the Channel A volume register.
OUT a,i :' Update Channel A volume.
OUT c,9 :' Select the Channel B volume register.
OUT a,i :' Update Channel B volume.
OUT c,10 :' Select the Channel C volume register.
OUT a,i :' Update Channel C volume.
' Decerement volume every 5 frames.
IF TIMER-k>5 THEN :' If 5 frames elapsed...
DEC i :' ...then decrement volume by 1
k=TIMER
ENDIF
WEND
' Black border colour.
COLOR BORDER 0
END PROC
1 ' Musical notes, used by the routine at line 9.
' For each note, there are 2 values, representing fine and coarse pitch.
DIM n AS BYTE(90) = #{255,255, 168,2, 255,255, 251,2, 255,255, 90,3, 255,255, 195,3, 255,255, 181,3, 90,3, 255,255, 168,2, 255,255, 251,2, 255,255, 90,3, 255,255, 195,3, 195,3, 195,3, 195,3, 255,255, 168,2, 255,255, 251,2, 255,255, 90,3, 255,255, 195,3, 255,255, 181,3, 90,3, 255,255, 168,2, 255,255, 251,2, 255,255, 236,0, 169,0, 30,1, 169,0, 142,0, 118,0, 0,0 }
' Initialize the screen (black border and paper, green ink) and clear it.
BITMAP ENABLE:COLOR BORDER 0:PAPER 0:INK 4:CLS
' Enable AY-3-8912 tone on Channel A only
OUT c,7
OUT a,%00111110
' Set Channel A volume
OUT c,8
OUT a,15
' Draw the "BOOM" logo using this simple algorithm:
' Read the coordinates x and y from the DATA statements.
' If y=255, then x is interpreted as the drawing mode and stored in m;
' otherwise draw based on current mode and then repeat the process.
' The drawing modes are:
' 1: plot a pixel at the next (x, y) coordinates;
' 2: draw a line from current point to the next (x, y) coordinates;
' 3: flood fill starting at the next (x, y) coordinates, by calling the subroutine at line 9;
' 0: stop drawing.
' The algorithm is similar to the one used on the Amiga to display the Kickstart floppy image.
REPEAT
READ x,y
IF y==255 THEN:m=x:READ x,y:ENDIF
IF m==1 THEN:PLOT x,y :' Mode 1: plot.
ELSEIF m==2 THEN:DRAW TO x,y:ENDIF :' Mode 2: draw.
IF m==3 THEN:i=0:k=TIMER:GOSUB 7:ENDIF :' Mode 3: flood fill and music play.
:' i is used as index to the n array, containings the notes to play.
:' The TIMER is saved to k for music playing synchronization.
UNTIL m==0
' Call the "boom" procedure
boom[]
2 ' Data for drawing the "B" outline
DATA 1,255,7,18,2,255,50,18,62,25,62,62,44,72,62,82,62,109,11,151,11,32,7,18
DATA 1,255,31,39,2,255,45,39,45,53,31,61,31,39,1,255,31,82,2,255,45,89,45,96,31,107,31,82
' Data for drawing the first "O" outline
DATA 1,255,76,18,2,255,113,18,120,25,120,104,92,125,68,106,68,26,76,18
DATA 1,255,88,39,2,255,99,39,99,102,88,92,88,39
3 ' Data for drawing the second "O" outline
DATA 1,255,134,18,2,255,170,18,178,26,178,104,150,125,126,106,126,25,133,18
DATA 1,255,146,39,2,255,157,39,157,92,146,102,146,39
' Data for drawing the "M" outline
DATA 1,255,181,18,2,255,204,18,214,46,224,18,247,18,245,22,245,156,227,142,227,90,215,126,202,80,202,124,184,110,184,23,181,18
4 ' "B", "O", "O", "M" flood fill coordinates
DATA 3,255,32,72,73,22,173,22,215,72
' Stop
DATA 0,255
' Fire algorithm
' There are 3 flame types: fire in front of the "BOOM" logo, fire across the logo and fire behind the logo.
' These are actually the same, the only difference is the "palette" of display attributes used to display the flame.
' The f array stores the display attribute values corresponding to the different temperatures (8) for each of the flame types (3), so 24 items;
' for each type, the higher the index, the higher the temperature. You can change the fire type by pressing the "1", "2" or "3" key,
' which affect the offset o used when indexing the f array.
' The b array is the framebuffer, which represents the fire area (the lower 8 lines of the screen).
' Each item in the b array contains an index to the f array and the b array is initialized to zeroes except for the bottom line,
' which is the generator of fire.
' At each step, for each of the character cells (except for the bottom line, which is never updated), the heat is propagated upwards.
' Some randomness has been added in order to change how fast a heat point decays.
' To improve the illusion further, the heat can be randomly propagated to go not only above but also left and right.
' If the user presses 0, the fire is extinguished by cooling a temperature in the f array at each loop.
DIM f(24)=#{4,4,4,18,82,54,118,127, 4,4,4,84,92,116,124,124, 4,4,4,20,28,52,60,60}
DIM b(32*8)
m=0 :' m=1 is the loop exit condition, so initialize it to 0
x=-1 :' x is used for extinguishing the fire
FOR i=0 TO 32*7-1:b(i)=0:NEXT :' Initialize the "framebuffer" to 0: cool...
FOR i=32*7 TO 32*8-1:b(i)=7:NEXT :' ...except for the bottom line, which is 7: hot!
FOR i=23200 TO 23231: POKE i,127:NEXT :' Bottom display character line attributes: bright white ink and paper
5 o=0 :' Offset o is set to 0, so we use items 0..7 of the f array
REPEAT :' Loop
' Set the offset o depending on the key (1, 2 or 3) pressed. If 0 is pressed, we are about to extinguish the fire
' Fire is extinguished starting at the base of the flame (x = 7).
d=INKEY$:IF d=="1" THEN:o=0:ELSE IF d=="2" THEN:o=8:ELSE IF d=="3" THEN:o=16:ELSE IF d=="0" AND x==-1 THEN:m=1:x=7:ENDIF
IF x>=0 THEN:f(o+x)=4:DEC x:ENDIF :' Fire is extinguished by setting the colour attributes to green ink and black paper (4)
' If a key is pressed, set the bottom display character line attributes to the coolest attributes from the f array, displaced by the offset o
IF d<>"" THEN:FOR i = 23200 TO 23231: POKE i,f(o+7):NEXT:ENDIF
i=32*8-1 :' Start from the character in the bottom right position and loop backwards...
6 WHILE i>=32 :' ...until we reach the 2nd framebuffer line
v=b(i) :' Read the heat value at current character position
r=RND(10) :' Use some randomness...
v=v+(r<8)+(r<2) :' ...to propagate fire up (true condition evaluates to -1)
IF v<0 THEN:v=0:ENDIF :' (without going outside the lower bound (0) of the f array)
t=i-32+(r==0)-(r==9) :' ...and to propagate fire left or right
b(t)=v :' Update the propagated position in the framebuffer...
POKE i+22944,f(v+o) :' ...and the corresponding character cell on the screen
DEC i :' Loop backwards to the previous character position
WEND :' Keep looping...
UNTIL m==1 AND x==-1 :' Until the fire is completely extinguished.
' Fades out the "BOOM" logo, by setting the ink colour of the whole screen (display attributes in memory start at address 22528)
' from green (4) to black (0), passing through magenta (3), red (2) and blue (1).
i=4:WHILE i>=0:FOR y=0 TO 23:FOR x=0 TO 31:POKE 22528+y*32+x,i:NEXT:NEXT:DEC i:WEND
' Stop
HALT
7 ' Flood fill (while playing music) routine.
' Note: ugBASIC already implements an effective fill routine by means of the PAINT statement.
' This routine has been implemented in order to add simultaneous music playback.
' Music play routine.
' Every 9 frames, a note is played. Notes are stored in the n array as couples: fine pitch, coarse pitch.
' The flashing border adds some drama.
IF ((TIMER-k)>8 AND i<90) THEN :' If 9 frames elapsed since the last time a note was sent to the AY chip:
COLOR BORDER (i AND 2) :' Change the border colour, alternating between black (0) and red (2)
OUT c,0 :' Select the AY Channel A fine pitch register
OUT a,n(i) :' Write the next note fine pitch to Channel A
INC i :' Increment the notes array iterator i
OUT c,1 :' Select the AY Channel A coarse pitch register
OUT a,n(i) :' Write the next note coarse pitch to Channel A
INC i :' Increment the notes array iterator i
k=TIMER :' Store current frame counter value in k for next note synchronization.
ENDIF
' Actual flood fill routine, adapted from the recursive flood fill algorithm described in the
' "A Fast Well-Behaved Pattern Flood Fill" article by Alvin Albrecht.
IF (POINT(x,y)==0) THEN :' If the point at (x, y) is black, then:
PLOT x,y :' plot a pixel using current ink colour (green) at coordinates (x, y)
INC x :' recursively call the routine for the point at coordinates (x+1, y)
GOSUB 7 :' making sure that, when the subroutine returns, the original value
DEC x :' of x is restored by subtracting 1 from the previously incremented value
DEC x :' do the same for (x-1, y)
GOSUB 7
INC x
INC y :' and then for (x, y+1)
GOSUB 7
DEC y
DEC y :' and finally for (x, y-1)
GOSUB 7
INC y
ENDIF
RETURN
9 ' BooMfire ugBASIC 10Liner demo for the ZX Spectrum by Marco's Retrobits https://retrobits.itch.io/. Original algorithm: https://fabiensanglard.net/doom_fire_psx/. Inspired by Roberto Capuano's 2022 MSX entry. Other credits on the docs!
The program has been compressed using abbreviations and by joining multiple statements per line. The resulting program is 10 (actually 9, since last line is a comment) lines long. Each line contains less than 256 characters, so BOOMfire is a suitable entry to the SCHAU category.
xxxxxxxxxx
0 C# c=65533:C# a=49149:Prcd boom:Ou c,7:Ou a,7:Ou c,8:Ou a,31:Ou c,9:Ou a,31:Ou c,10:Ou a,31:Ou c,12:Ou a,56:Ou c,13:Ou a,9:Cr Bo 6:i=15:k=Tmr:Wh i>0:Ou c,8:Ou a,i:Ou c,9:Ou a,i:Ou c,10:Ou a,i:If Tmr-k>5 Th:DEC i:k=Tmr:Ei:We:Cr Bo 0:Ee Prb
1 Di n As By(90)=#{255,255,168,2,255,255,251,2,255,255,90,3,255,255,195,3,255,255,181,3,90,3,255,255,168,2,255,255,251,2,255,255,90,3,255,255,195,3,195,3,195,3,195,3,255,255,168,2,255,255,251,2,255,255,90,3,255,255,195,3,255,255,181,3,90,3,255,255,168,_
2,255,255,251,2,255,255,236,0,169,0,30,1,169,0,142,0,118,0,0,0}:Bm En:Cr Bo 0:Pa 0:Ik 4:Cl:Ou c,7:Ou a,62:Ou c,8:Ou a,15:Rpt:R# x,y:If y==255 Th:m=x:R# x,y:Ei:If m==1 Th:Pl x,y:Eif m==2 Th:Dr To x,y:Ei:If m==3 Th:i=0:k=Tmr:Gs 7:Ei:Un m==0:boom[]
2 Da 1,255,7,18,2,255,50,18,62,25,62,62,44,72,62,82,62,109,11,151,11,32,7,18,1,255,31,39,2,255,45,39,45,53,31,61,31,39,1,255,31,82,2,255,45,89,45,96,31,107,31,82,1,255,76,18,2,255,113,18,120,25,120,104,92,125,68,106,68,26,76,18,1,255,88,39,2,255,99,39
3 Da 99,102,88,92,88,39,1,255,134,18,2,255,170,18,178,26,178,104,150,125,126,106,126,25,133,18,1,255,146,39,2,255,157,39,157,92,146,102,146,39,1,255,181,18,2,255,204,18,214,46,224,18,247,18,245,22,245,156,227,142,227,90,215,126,202,80,202,124,184,110
4 Da 184,23,181,18,3,255,32,72,73,22,173,22,215,72,0,255:Di f(24)=#{4,4,4,18,82,54,118,127,4,4,4,84,92,116,124,124,4,4,4,20,28,52,60,60}:Di b(32*8):m=0:x=-1:Fo i=0 To 32*8-1:b(i)=0:Nx:Fo i=32*7 To 32*8-1:b(i)=7:Nx:Fo i=23200 To 23231: Po i,127:Nx
5 o=0:Rpt:d=INKEY$:If d=="1" Th:o=0:El If d=="2" Th:o=8:El If d=="3" Th:o=16:El If d=="0" An x==-1 Th:m=1:x=7:Ei:If x>=0 Th:f(o+x)=4:DEC x:Ei:If d<>"" Th:Fo i = 23200 To 23231: Po i,f(o+7):Nx:Ei:i=32*8-1
6 Wh i>=32:v=b(i):r=RND(10):v=v+(r<8)+(r<2):If v<0 Th:v=0:Ei:t=i-32+(r==0)-(r==9):b(t)=v:Po i+22944,f(v+o):DEC i:We:Un m==1 An x==-1:i=4:Wh i>=0:Fo y=0 To 23:Fo x=0 To 31:Po 22528+y*32+x,i:Nx:Nx:DEC i:We:Ht
7 If((Tmr-k)>8Ani<90)Th:Cr Bo (i An 2):Ou c,0:Ou a,n(i):INCi:Ou c,1:Ou a,n(i):INCi:k=Tmr:Ei:If(Pt(x,y)==0)Th:Pl x,y:INCx:Gs 7:DECx:DECx:Gs 7:INCx:INCy:Gs 7:DECy:DECy:Gs 7:INCy:Ei:Rt
9 ' BooMfire ugBASIC 10Liner demo for the ZX Spectrum by Marco's Retrobits https://retrobits.itch.io/. Original algorithm: https://fabiensanglard.net/doom_fire_psx/. Inspired by Roberto Capuano's 2022 MSX entry. Other credits on the docs!
The following image shows the "compressed" program in Notepad++. The cyan marker on the right indicates the 256th column.
BooMfire is provided in TAP tape image format, which can be easily loaded in most ZX Spectrum emulators and on the real machines, either equipped with devices such as the DivMMC or by playing it through the MIC port using tools like PlayTZX or WinTZX.
The following instructions apply to the Fuse open source emulator, which is available for Unix, Linux, Windows, macOS and many other platforms. For other emulators or devices, please refer to their specific documentation for loading TAP files.
Start the Fuse emulator and select a Spectrum 128K model in "Machine" -> "Select..."
Make sure that automatic loading of tape image files is enabled, by checking the corresponding options in "Options" -> "Media..."
Open the BooMfire.tap file, either by selecting it in "File"->"Open..." or by dragging and dropping it on the emulator window.
The ugBASIC runtime will prompt you for a command:
To start the program, type run
and tap Enter:
To view the listing, type list
and tap Enter:
If your emulator of choice does not support automatic tape loading, after mounting the tape image you must make sure that "Tape Loader" is highlighted (you can change the selection using the cursor up/down keys) and press Enter.
You might also have to press "PLAY" on the virtual tape emulator.
Alternatively, if your web browser supports Javascript, you can play directly in your browser (powered by JSSpeccy - A ZX Spectrum emulator in Javascript). Just surf to: https://retrobits.itch.io/boomfire. Mobile devices are not currently supported.