Targets
The player's plane can hit eight types of targets with its missile. Six are mobile enemies, one is a static facility, and one is a terrain structure:
Target Points Speed Behaviour
Ship 30 2 px/frame Patrols river, reverses at banks
Helicopter 60 2 px/frame Patrols river, reverses at banks
Balloon 60 2 px/frame Floats above river, bounces off banks
Fuel Depot 80 Static Refuels plane on flyover
Fighter 100 4 px/frame Flies across screen, wraps at edges
Helicopter+ 150 2 px/frame Patrols river, fires missiles at player
Tank 250 2 px/frame Moves along bank, fires parabolic shells
Bridge 500 Static Marks section boundary
Ships, helicopters, and balloons share basic patrol movement: they advance 2 pixels per frame toward the opposite river bank and reverse direction when they get within 16 pixels of the edge (see operate_ship_or_helicopter, ship_or_helicopter_right_advance, reverse_enemy_direction). Fighters are the fastest enemy at 4 pixels per frame and wrap around the screen edges instead of reversing (operate_fighter). Tanks on river banks check terrain ahead and fire shells when the path is clear (operate_tank_on_bank).
All displayed scores are multiples of 10. Internally, point values are stored as BCD divided by 10: each nibble encodes a decimal digit, with the low nibble representing tens and the high nibble representing hundreds. For example, POINTS_TANK is encoded as BCD 25 (two in the high nibble, five in the low nibble), which add_points adds as 250 displayed points. The trailing zero in the score display is purely cosmetic.
This "vanity zeroes" technique is a common arcade-era pattern that originated in pinball machines, where scores started at multiples of 10, then 100. Inflated numbers feel more rewarding to the player even though the trailing digit is always zero.
Bonus life every 10,000 points
A bonus life is awarded every 10,000 points. This is triggered when a carry propagates to the 10,000s digit (update_type=4) in update_score, which calls add_life to increment the life counter and trigger the bonus life sound. The jingle plays over 64 frames via do_bonus_life.
This is easy to miss during casual play since reaching 10,000 points requires surviving long enough to destroy a significant number of enemies.
Score stored as ASCII characters
The score is stored as 6 ASCII digit characters (e.g. "003250") rather than as a binary number. All arithmetic is done character-by-character with manual carry propagation — like pencil-and-paper long addition:
  • Load an ASCII digit character from the score buffer
  • Increment it (INC adds 1 to the character code)
  • Compare with '9'+1 to check for overflow
  • If overflow: store '0' and carry to the next position
  • If no overflow: store and print the updated digit
There is no binary score representation anywhere in the game. The tradeoff: updating a single digit is fast, but there is no efficient way to add arbitrary values — each point increment requires individual digit updates.
See inc_player_1_score_digit (player 1 digit increment) and inc_player_2_score_digit (player 2 digit increment).
Fuel mechanics
The fuel system governs how long the player can stay airborne:
Mechanic Rate Duration
Consumption 1 unit every 2 frames (~25 units/sec) Full tank lasts ~10 sec
Refueling 4 units per frame (~200 units/sec) Full refuel in ~1.3 sec
Key details:
  • Fuel consumption does NOT vary with scroll speed
  • The plane must be centered (not banking left/right) to refuel
  • Low fuel warning triggers when fuel drops below $40
  • A "tank full" beep (signal_fuel_level_excessive) plays when fuel reaches $FC
See consume_fuel (consumption), add_fuel (refueling), state_fuel (fuel state variable).
Three scroll speeds
The game has three speed settings that control how fast the terrain scrolls:
Speed Terrain scroll Fragments per frame
Slow 1 pixel/frame 1
Normal 2 pixels/frame 2
Fast 4 pixels/frame 4
Horizontal movement is always 2 pixels per input.
See state_speed (speed state), handle_up (accelerate), handle_down (decelerate), advance_scroll (scroll update).
Infinite loop after bridge 48
The game has 48 unique bridge sections, each containing 64 terrain fragments. After the player completes all 48, the game does not end — instead it wraps around using the formula:
new_bridge = ((progress - 48) mod 15) + 33
This creates an infinite loop through bridges 33-48 (the hardest 15 bridges), making the game theoretically endless. See init_current_bridge for the wraparound algorithm.
Enemy activation timing
Newly spawned objects start in an inactive state (bit 7 clear in the viewport objects array). They become active — able to move and shoot — only when (interrupt_counter AND activation_interval) equals zero, as checked in operate_viewport_slots. The activation interval stored at state_activation_interval controls the delay between spawn and activation.
Two-player alternating turns
In two-player mode, players alternate turns on death. Each player's state is independently tracked:
The switching logic is handled by multiple routines including switch_to_player_2 and switch_to_player_2_in_two_player_mode.
Ship color doubles as Player 2 color
Player 2's plane is rendered in cyan — the same color as enemy ships. This is not a coincidence: the plane rendering code (at handle_right, handle_left, render_plane) checks for Player 2 with CP PLAYER_2 and, if true, calls ld_attributes_ship — the same routine that loads ship attributes — to get the color.
With only 8 colors available on the ZX Spectrum, reusing the ship's cyan for Player 2 was a practical palette economy choice. It also provides a visual distinction between players without requiring additional sprite data.