- Targets
- Bonus life every 10,000 points
- Score stored as ASCII characters
- Fuel mechanics
- Three scroll speeds
- Infinite loop after bridge 48
- Enemy activation timing
- Two-player alternating turns
- Ship color doubles as Player 2 color
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
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:
- Bridge progress: state_bridge_player_1 (player 1), state_bridge_player_2 (player 2)
- Score: state_score_player_1_low (player 1), state_score_player_2_low (player 2)
- Fuel level: saved and restored on switch
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.