Lazer Bars

Date: 198x
Type: Program
Platform(s): TS 2068
Tags: Arcade

Lazer Bars is a single-screen shooter where the player maneuvers a character across a fixed landscape, shooting upward at invaders while dodging laser bars that descend from above. The player uses six keys for movement (left, right, up, down) and firing in two horizontal directions plus a vertical shot. Four UDGs are defined: two invader sprites (UDGs B and D, loaded via POKE USR loops from DATA statements), a bullet character (UDG c, a thin vertical line via BIN 00011000), and a laser-bar character (UDG E, BIN 00010100). Collision detection relies on ATTR and SCREEN$ checks—ATTR value 106 flags a laser hit and 52 flags solid ground—triggering a life-loss subroutine at line 900 with a three-tone beep penalty. Scoring accumulates via variables j (a multiplier incremented every 16 sub-points) and s, displayed as (16×j)+s.


Program Structure

The program divides into clearly distinct phases:

  1. Introduction (lines 30–99): Title screen, gameplay explanation, controls screen, and a demonstration beep sequence for the life-loss sound.
  2. UDG Setup (lines 120–290): Four UDGs are defined using POKE USR loops and DATA/BIN literals.
  3. Initialization (lines 310–350): Variables, lives, score, and the static playfield are set up.
  4. Main Game Loop (lines 360–490): Draws laser bars, reads input, checks collisions, and loops back.
  5. Subroutines (lines 500–830): Firing (500), drop-down (600), jump-up (700), and laser-drop enemy (800).
  6. Life-loss and Game Over (lines 900–1100): Life penalty, game-over sequence, replay prompt, and farewell screen.

UDG Definitions

Four User-Defined Graphics are created before gameplay begins:

UDGUsageMethod
\b (UDG B)Invader sprite8-byte DATA: 126,90,60,126,90,66,66,129
\d (UDG D)Player sprite8-byte DATA: 36,66,153,153,90,126,189,255
\c (UDG c)Bullet/laser visualBIN 00011000 repeated 8 rows
\e (UDG E)Player’s upward shotBIN 00010100 repeated 8 rows

Note the mixed case: uppercase UDGs (B, D, E) are accessed via POKE USR "B"+a style indexing, while lowercase c uses POKE USR "c"+a, both valid on the Spectrum where UDGs span from ‘A’ through lowercase letters.

Main Game Loop

The loop begins at line 360 (targeted by GO TO 360 at line 451) and runs through line 490 before jumping back to line 420. Invader bars are drawn across row 7 at every even column (line 370–390). The player sprite UDG D is printed at position (m,l), where m is the row (vertical) and l is the column (horizontal). After processing input and subroutines, the player position is redrawn and the loop continues.

The loop actually has two entry points: line 360 (after score update) and line 420 (normal iteration). This means invader bars are only redrawn when the score milestone triggers the branch at line 451, not every frame—a performance optimization at the cost of some visual consistency.

Collision Detection

Collision is detected using two BASIC functions:

  • ATTR (m-1,l)=106 — checks the attribute of the cell above the player. Value 106 corresponds to INK 2 (red), PAPER 5 (cyan), BRIGHT 1, which matches the laser-drop character drawn at line 810.
  • ATTR (m-1,l)=52 — value 52 corresponds to PAPER 6, INK 4 (the ground/ceiling color), blocking upward movement.
  • SCREEN$ (u-1,l)<>" " — used in the firing subroutine (line 530) to detect when the bullet hits something non-blank, scoring a hit.

Scoring System

The score is stored as two variables: s (sub-score, 0–15) and j (a multiplier). Every time s exceeds 15, j is incremented and s resets (line 451). The displayed score is always (16*j)+s, effectively making the score a 16-based counter. This is an unusual but functional way to avoid a single large integer variable while showing a meaningful running total.

Subroutines

LineFunctionNotes
500Fire upwardLoops UDG E from player row up to row 6; uses OVER 1 at line 550 to erase; awards score on non-blank hit
600Drop downGuards against ATTR 52 below; calls subroutine 700 if the cell just vacated is non-blank (bumping head)
700Jump upGuards against ATTR 52 above and row limit (m<=10); moves player one row up
800Enemy laser dropTriggered randomly (1-in-50 chance per frame); drops UDG c (BRIGHT 1, INK 2) from row 8 to player row
900Life lossPlays three-tone penalty beep, decrements life, displays remaining lives; branches to 1000 on zero lives

Notable Techniques and Idioms

  • OVER 1 erasure (line 550): The bullet UDG is erased by reprinting it with OVER 1, which XORs the pixels back to zero without needing to explicitly blank the cell — a classic Spectrum graphics trick.
  • Horizontal blast (lines 462–463): Keys 3 and 8 print a space character to the left or right of the player respectively, with a short beep — a very minimal “blast” that clears a single adjacent character-cell. The left blast uses l-l (which equals 0) rather than the intended l-1, making it always print at column 0, which is a bug.
  • Random enemy trigger (line 470): IF INT(RND*50)=5 gives approximately a 2% chance per loop iteration of spawning a laser drop — simple and effective for difficulty scaling.
  • Line 815 LET t=t: This is a no-op assignment (t equals itself) and has no effect; it appears to be a vestigial placeholder, possibly intended for horizontal laser drift that was never implemented.
  • Line 801 RANDOMIZE without argument: Called at the start of the enemy subroutine to re-seed the random number generator each time an enemy drops, increasing variability in column placement.
  • Boundary clamping: Left boundary is clamped at line 425 (l<0 THEN LET l=0) and right at line 479 (l>=31 THEN LET l=31), but these checks are separated from the movement lines by other logic, meaning a single frame can show the player briefly out of bounds before correction.
  • Game-over flash (line 1010): The player sprite is reprinted with INK 2; FLASH 1 at death, providing a visual cue without additional animation code.

Bugs and Anomalies

  • Left blast column bug (line 462): PRINT AT m,l-l;" " computes l-l=0, so the blast always clears column 0 regardless of player position. The intended expression was almost certainly l-1.
  • Line 800 is never called (lines 470 vs 801): The random trigger at line 470 calls GO SUB 800, but the subroutine code begins at line 801, not 800. Since there is no line 800, this will cause a “Line does not exist” error at runtime — the game cannot spawn enemy lasers.
  • Double initialization of life: LET life=3 appears at both line 310 and line 320, making the line 310 assignment redundant.
  • Score display gap (line 435): The score header “SCORE= ” is printed every frame, but the score value itself is only updated inside subroutine 500, leading to a static label with a delayed value update.
  • Subroutine 900 entered as GO SUB 900 but coded from line 910: Line 900 does not exist; execution would fall through to line 910. This is a valid (if unusual) technique — BASIC finds the next line after 900, which is 910 — so this works correctly in practice.

Content

Appears On

Related Products

Related Articles

Related Content

Image Gallery

Source Code

   30 PRINT AT 10,10;"LAZER BARS"
   31 PAUSE 100: CLS : PRINT AT 1,0;"THE AIM OF THIS GAME IS TO SHOOT AS MANY INVADERS AS POSSIBLE.  UNFORTUNATELY,TO DO SO YOU MUST BURROW UNDER LAZERS WHICH THE   INVADERS DROP."
   32 PRINT " IF YOU ARE LOW ENOUGH YOU CAN  GAIN EXTRA POINTS BY SHOOTING A LASER."
   33 PRINT " YOU HAVE GOT THREE LIVES. PRESS ANY KEY": PAUSE 0: BEEP .1,40
   34 CLS : PRINT "  YOU WILL HEAR THIS NOISE WHEN YOU ARE LOSING A LIFE - press a key": PAUSE 0: BEEP .1,10: BEEP .1,20: BEEP .1,-10
   35 CLS 
   40 PRINT TAB (10);"CONTROLS"
   50 PRINT AT 1,1;"1 - to move left";AT 3,1;"0 - to move right";AT 5,1;"9 - to fire "
   60 PRINT AT 7,1;"2 - to drop down";AT 9,1;"m - to jump up"
   70 PRINT AT 11,1;"3 - to blast left";AT 13,1;"8 - to blast right"
   80 PRINT AT 15,2;"if you are sitting directly   beneath something, such as land, or a laser,you lose your lives."
   90 PRINT AT 19,0;"To gain a hole beneath,you must kill an invader above you"
   95 PRINT AT 20,25;". it   will become clearer as you play."
   99 PRINT AT 2,19; INK 1; PAPER 2;"Press any key": PAUSE 0
  120 FOR a=0 TO 7
  130 READ x
  131 POKE USR "B"+a,x
  135 NEXT a
  140 DATA 126,90,60,126,90,66,66,129
  142 FOR a=0 TO 7
  143 READ x
  144 POKE USR "D"+a,x
  145 NEXT a
  146 DATA 36,66,153,153,90,126,189,255
  160 FOR a=0 TO 7
  170 POKE USR "c"+a,BIN 00011000
  180 NEXT a
  270 FOR a=0 TO 7
  280 POKE USR "E"+a,BIN 00010100
  290 NEXT a
  310 LET j=0: LET l=10: LET m=18: LET life=3
  320 LET life=3: LET s=0: LET k=0
  330 PAPER 5: CLS : INK 0: CLS : BORDER 1: CLS 
  340 PRINT AT 19,0; INK 4; PAPER 6;"████████████████████████████████████████████████████████████████████████████████████████████████"
  350 PRINT AT 1,0; INK 7;"████████                     ██████████████      ███           ██████████       ██████             ██████      █████          ██████████         ████       ██"
  370 FOR a=0 TO 31 STEP 2
  380 PRINT AT 7,a;"\b"
  390 NEXT a
  410 PRINT AT m,l;"\d"
  420 IF INKEY$="1" THEN LET l=l-1
  425 IF l<0 THEN LET l=0
  430 IF INKEY$="0" THEN LET l=l+1
  435 PRINT AT 0,0; PAPER 0; INK 7; BRIGHT 1;"SCORE= "
  440 IF INKEY$="9" THEN GO SUB 500
  450 IF INKEY$="2" THEN GO SUB 600
  451 IF s>15 THEN LET j=j+1: LET s=0: PRINT AT 0,7;"   ": GO TO 360
  455 IF ATTR (m-1,l)=106 THEN GO SUB 900
  460 IF INKEY$="m" THEN GO SUB 700
  461 IF INKEY$="M" THEN GO SUB 700
  462 IF INKEY$="3" THEN PRINT AT m,l-l;" ": BEEP .01,0: BEEP .01,-10
  463 IF INKEY$="8" THEN PRINT AT m,l+1;" ": BEEP .01,0: BEEP .01,-10
  465 IF ATTR (m-1,l)=52 THEN GO SUB 900
  470 IF INT (RND*50)=5 THEN GO SUB 800
  479 IF l>=31 THEN LET l=31
  480 PRINT AT m,l;"\d"
  485 PRINT AT m,l; PAPER 5;" "
  490 GO TO 420
  505 IF ATTR (m-1,l)=106 THEN RETURN 
  510 FOR u=m TO 6 STEP -1
  520 PRINT AT u,l;"\e"
  530 IF SCREEN$ (u-1,l)<>" " THEN LET s=s+1: BEEP .005,4: BEEP .02,-2: PRINT AT 0,7;(16*j)+s: IF m<21 THEN PRINT AT m+1,l;" "
  540 PRINT AT m,l;"\d"
  550 PRINT AT u,l; OVER 1;"\e"
  560 NEXT u
  570 RETURN 
  605 IF ATTR (m+1,l)=52 THEN RETURN 
  610 PRINT AT m,l;" "
  620 IF m<21 THEN LET m=m+1
  630 PRINT AT m,l;"\d"
  640 IF SCREEN$ (m-1,l)<>" " THEN GO SUB 700
  645 RETURN 
  700 IF ATTR (m-1,l)=52 THEN RETURN 
  705 IF m<=10 THEN RETURN 
  710 PRINT AT m,l;" "
  720 LET m=m-1
  730 PRINT AT m,l;"\d"
  740 BEEP .01,-10
  750 RETURN 
  801 RANDOMIZE 
  805 LET t=INT (RND*31)
  808 FOR o=8 TO m
  809 BEEP .005,m: BEEP .005,0
  810 PRINT AT o,t; BRIGHT 1; INK 2;"\c"
  815 LET t=t
  820 NEXT o
  830 RETURN 
  910 BEEP .1,10: BEEP .1,20: BEEP .1,-10
  920 LET life=life-1
  930 PRINT AT 0,10; INK 0; PAPER 7; BRIGHT 1;"LIVES= ";life
  940 IF life=0 THEN GO TO 1000
  950 RETURN 
 1000 FOR a=1 TO 10: BEEP .01,a: BORDER RND*7: BORDER RND*7: NEXT a: BEEP 1,0: BEEP .1,-1: BEEP .1,-2: BEEP 1,-10
 1010 PRINT AT m,l; INK 2; FLASH 1;"\d"
 1015 PRINT AT 9,2;"YOUR SCORE  ";(j*16)+s
 1020 PRINT AT 1,1;"PRESS ANY KEY": PAUSE 0
 1030 INPUT "DO YOU WANT TO PLAY AGAIN? (y/n)";x$
 1040 IF x$="" THEN GO TO 1100
 1050 IF x$( TO 1)="y" THEN GO TO 300
 1100 CLS : PRINT AT 10,10;"THANK YOU ";AT 11,13;"FOR";AT 12,11;"PLAYING": BEEP .5,1: STOP 
 1200 SAVE "LASER" LINE 1

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.

People

No people associated with this content.

Scroll to Top