| ← 8888 | Map | 8D9C → |
8BA8: Program entry and the main per-frame loop
|
Execution starts here, and the game never hands back. Setup runs once, then the front end, then a level's screen, then the frame loop.
A cleared level, a spent last life or a confirmed quit sends control back around that ring.
|
|||
| game_entry | 8BA8 | LD H,L | A leftover source marker: these three bytes spell "end" as opcodes and run harmlessly before the real start. |
| 8BA9 | LD L,(HL) | ||
| 8BAA | LD H,H | ||
| 8BAB | DI | Interrupts stay off, except for the front end's one-frame wait. The stack sits at the top of RAM. The main loop is not HALT-synced. | |
| 8BAC | LD SP,STACK_TOP | ||
| 8BAF | LD HL,spr_visor_1 | Copy the source bank – the canonical sprite bitmaps – into the sprite bank in the system-variable area. Play draws from the sprite bank, and the source page is reused as the back buffer. | |
| 8BB2 | LD DE,slot_visor_1 | ||
| 8BB5 | LD BC,$0B20 | ||
| 8BB8 | LDIR | ||
| 8BBA | LD A,$09 | Seed the pool entry count that bounds the per-frame object walk. | |
| 8BBC | LD (object_count),A | ||
|
New game / restart entry. Runs the front end, then the full new-game initialization, then falls into per-level setup.
|
|||
| new_game | 8BBF | CALL title_screen | |
| 8BC2 | CALL init_game_vars | ||
| 8BC5 | CALL clear_screen | ||
| 8BC8 | LD A,$47 | Paint every attribute cell white-on-black. | |
| 8BCA | CALL fill_attrs | ||
| 8BCD | XOR A | Black border | |
| 8BCE | OUT ($FE),A | ||
|
Per-level screen setup. It clears the work buffers and play state, draws the playfield border and composes the level's brick field, and renders the tile map. It then draws the lives, level-number, and score readouts. Falls into the per-frame loop.
|
|||
| setup_level | 8BD0 | CALL clear_buffers | |
| 8BD3 | LD A,$FF | Plant the terminator that ends the work-buffer scans of the cell recomposer and the dirty-cell blitter, in the first cell of the bottom character row. That cell is drawable, and an object that reaches it stamps the terminator away – see Scramble. | |
| 8BD5 | LD (WORKBUF_FB+$2E0),A | ||
| 8BD8 | CALL clear_state_buffers | ||
| 8BDB | CALL draw_playfield_border | ||
| 8BDE | CALL compose_brick_field | ||
| 8BE1 | CALL render_tilemap | ||
| 8BE4 | CALL draw_lives | ||
| 8BE7 | CALL draw_level_number | ||
| 8BEA | CALL draw_score_readout | ||
|
Top of the per-frame loop (also the loop-back target). While the expand prize is in effect, the loop stages the expanded bat for this frame in the object pool. A grow or shrink animation holds that off, so the grow flag and the shrink flag must both be zero. This stage activates the extension and limits how low the expanded bat may sit. It seeds the extension's position from the bat's. It aims the bat's sprite at the top half of the expanded-bat sprites, and the extension's at the bottom half.
|
|||
| frame_loop | 8BED | LD A,(expand_active) | Stage the expanded bat only while the bat is expanded. |
| 8BF0 | OR A | ||
| 8BF1 | JR Z,idle_track | ||
| 8BF3 | LD A,(fx_grow_arm) | The grow animation holds it off while it runs. | |
| 8BF6 | OR A | ||
| 8BF7 | JR NZ,idle_track | ||
| 8BF9 | LD A,(fx_shrink_arm) | So does the shrink animation. | |
| 8BFC | OR A | ||
| 8BFD | JR NZ,idle_track | ||
| 8BFF | LD A,$01 | ||
| 8C01 | LD (bat_ext_active),A | ||
| 8C04 | LD A,(bat_y) | Clamp the bat's Y to the bat's lowest expanded position. | |
| 8C07 | CP $8A | ||
| 8C09 | JR C,seed_bat_extension | ||
| 8C0B | LD A,$8A | At or below that line on screen, pin it there. | |
| 8C0D | LD (bat_y),A | ||
| seed_bat_extension | 8C10 | LD HL,(object_table) | Seed the extension's position from the bat's and aim both at their sprites. |
| 8C13 | LD A,H | ||
| 8C14 | ADD A,$10 | ||
| 8C16 | LD H,A | ||
| 8C17 | LD (bat_ext_object),HL | ||
| 8C1A | LD HL,slot_bat_exp_top | ||
| 8C1D | LD (bat_sprite_ptr),HL | ||
| 8C20 | LD HL,slot_bat_exp_bottom | ||
| 8C23 | LD (bat_ext_sprite_ptr),HL | ||
|
Anti-stuck nudge. Monitors ball A's on-screen position. If ball A stays within 16 pixels of the saved position on both axes for 30 consecutive frames, the nudge fires. The box reaches 15 pixels on one side and 16 on the other. It turns ball A's heading half a turn around to break the stuck rally. When the ball moves out of the box, or the nudge fires, the routine saves the new position and resets the idle counter.
|
|||
| idle_track | 8C26 | LD HL,(ball_a_object) | Take ball A's position and how far its X moved from the saved position. |
| 8C29 | LD A,(idle_ball_x) | ||
| 8C2C | SUB L | ||
| 8C2D | JP P,idle_check_dx | ||
| 8C30 | CPL | Fold a negative difference. CPL leaves one less than its magnitude, so the box reaches a pixel further on that side. | |
| idle_check_dx | 8C31 | CP $10 | Moved far enough across, so the rally is not stuck. |
| 8C33 | JR NC,idle_save | ||
| 8C35 | LD A,(idle_ball_y) | Measure the Y difference the same way. | |
| 8C38 | SUB H | ||
| 8C39 | JP P,idle_eval_dy | ||
| 8C3C | CPL | Fold the Y difference the same way, one short again. | |
| idle_eval_dy | 8C3D | CP $10 | Moved far enough on Y either way, so the rally is not stuck. |
| 8C3F | JR NC,idle_save | ||
| 8C41 | LD A,(idle_frame_count) | Inside the box on both axes: count one more idle frame, and act once the count reaches its limit. | |
| 8C44 | INC A | ||
| 8C45 | LD (idle_frame_count),A | ||
| 8C48 | CP $1E | ||
| 8C4A | JR NZ,frame_pipeline | ||
| 8C4C | LD A,(ball_a_heading) | Flip ball A's heading by a half-turn to break the rally. | |
| 8C4F | SUB $24 | ||
| 8C51 | CP $48 | ||
| 8C53 | JR NC,idle_wrap_dir | ||
| 8C55 | ADD A,$48 | A heading that fell below the range wraps back up. | |
| idle_wrap_dir | 8C57 | LD (ball_a_heading),A | Store the flipped heading. |
| idle_save | 8C5A | LD (idle_ball_x),HL | Remember where the ball is and clear the idle count. The ball moved, or the nudge fired. |
| 8C5D | XOR A | ||
| 8C5E | LD (idle_frame_count),A | ||
|
Per-frame subsystem pipeline. Runs each update in the fixed order of the stages below. The end-of-frame state checks come last.
|
|||
| frame_pipeline | 8C61 | CALL read_ctrl | Stage 1: sample the player's controls into the control bitfield. |
| 8C64 | CALL update_balls | Stage 2: advance every ball – motion, wall and bat bounce, brick hits. | |
| 8C67 | CALL manage_objects | Stage 3: animate the prize / effect objects. | |
| 8C6A | CALL move_bat | Stage 4: move the bat from the sampled control. | |
| 8C6D | CALL update_inhabitants | Stage 5: run the inhabitants' behaviors. | |
| 8C70 | CALL scan_brick_field | Stage 6: sweep the brick field, play queued break animations and detect a cleared level. | |
| 8C73 | CALL remove_lost_ball | Stage 7: retire a ball that fell past the bat's losing edge (drives life loss). | |
| 8C76 | CALL process_wave_script | Stage 8: spawn objects scheduled by the current wave script. | |
| 8C79 | CALL spawn_wheel_swarm | Stage 9: spawn the wheel swarm into the free inhabitant slots. | |
| 8C7C | CALL run_prize_safety_net | Stage 10: animate the safety net barrier's color flash. | |
| 8C7F | CALL add_pending_score | Stage 11: fold pending points into the displayed score. | |
| 8C82 | CALL award_score_life | Stage 12: grant a bonus life when the score passes a milestone. | |
| 8C85 | CALL handle_pause | Stage 13: poll the pause key; a press freezes the loop until the next press. | |
| 8C88 | CALL update_objects | Stage 14: update and redraw all active objects. | |
| 8C8B | CALL sfx_net_flash | Stage 15: advance the seven sound-effect players. | |
| 8C8E | CALL sfx_bomb_detonate | ||
| 8C91 | CALL sfx_bat_grows | ||
| 8C94 | CALL sfx_bat_shrinks | ||
| 8C97 | CALL sfx_death_burst | ||
| 8C9A | CALL sfx_spawn | ||
| 8C9D | CALL sfx_chew | ||
| 8CA0 | LD A,(orb_spit_timer) | Stage 16: tick the orb-spit sound delay and play the orb-spit sound on the tick before it runs out. | |
| 8CA3 | OR A | ||
| 8CA4 | JR Z,poll_quit | ||
| 8CA6 | DEC A | ||
| 8CA7 | LD (orb_spit_timer),A | ||
| 8CAA | CP $01 | ||
| 8CAC | CALL Z,sfx_orb_spit | ||
| poll_quit | 8CAF | CALL handle_quit_confirm | Stage 17: poll the quit key (Q opens the are-you-sure dialog). |
|
Stage 18: prize-reveal tune sequencer. Once armed it plays the prize-reveal tune one note per frame through the tune player, and stops at the tune's terminator.
|
|||
| 8CB2 | LD A,(prize_reveal_tune_gate) | ||
| 8CB5 | OR A | ||
| 8CB6 | JR Z,frame_end_checks | ||
| 8CB8 | CP $01 | On the tune's first active frame, aim the tune pointer at it. | |
| 8CBA | JR NZ,tune_play | ||
| 8CBC | LD HL,prize_reveal_tune | ||
| 8CBF | LD (tune_ptr),HL | ||
| tune_play | 8CC2 | CALL play_tune_note | Emit this frame's note; the terminator clears the state and stops the tune. |
| 8CC5 | CP $0D | ||
| 8CC7 | JR NZ,tune_advance | ||
| 8CC9 | XOR A | ||
| 8CCA | LD (prize_reveal_tune_gate),A | ||
| 8CCD | JR frame_end_checks | ||
| tune_advance | 8CCF | LD HL,prize_reveal_tune_gate | Otherwise bump the state, so the tune pointer is aimed only once. |
| 8CD2 | INC (HL) | ||
|
End-of-frame state checks. Once the death pause reaches 60 the life is over. With no lives left, play goes to game over. Otherwise play returns to the top of the frame loop, unless an active warp or the level-cleared flag diverts it to the level-advance handler.
|
|||
| frame_end_checks | 8CD3 | LD A,(death_pause_timer) | The death pause ran its course, so the life is over. With no lives left, the game ends. |
| 8CD6 | CP $3C | ||
| 8CD8 | JR C,check_warp_countdown | ||
| 8CDA | LD A,(lives) | ||
| 8CDD | OR A | ||
| 8CDE | JR Z,game_over | ||
|
While a warp runs, the main loop advances one level per frame until the warp countdown runs out, then clears the warp flag. A warp thus jumps the player forward 1 to 4 levels.
|
|||
| check_warp_countdown | 8CE0 | LD A,(warp_active_flag) | When no warp runs, there is nothing to advance. |
| 8CE3 | OR A | ||
| 8CE4 | JR Z,check_level_cleared | ||
| 8CE6 | LD HL,warp_countdown | Count the warp down, and divert to the level-advance handler until it runs out. | |
| 8CE9 | DEC (HL) | ||
| 8CEA | LD A,(HL) | ||
| 8CEB | OR A | ||
| 8CEC | JR NZ,level_complete | ||
| 8CEE | XOR A | Countdown spent: clear the warp and fall through. | |
| 8CEF | LD (HL),A | ||
| 8CF0 | LD (warp_active_flag),A | ||
| check_level_cleared | 8CF3 | LD A,(level_cleared) | No level cleared: loop back to the frame top for the next frame. |
| 8CF6 | OR A | ||
| 8CF7 | JP Z,frame_loop | ||
|
Level cleared. After a pause it awards a level-clear bonus of 100 points per step of the level index level, capped at 2500, then advances the index. An index that reaches 50 or 100 speeds up the rally: it lowers rally_accel_interval by 3, with a floor of 2. At 100 the index also wraps back to 0 at wrap_level_index. Re-enters setup_level for the next level.
|
|||
| level_complete | 8CFA | LD BC,$0000 | |
| 8CFD | CALL delay | ||
| 8D00 | LD A,(level) | Clamp the level index before the bonus calculation. | |
| 8D03 | CP $19 | ||
| 8D05 | JR C,level_bonus_calc | ||
| 8D07 | LD A,$19 | Above that, the bonus grows no more. | |
| level_bonus_calc | 8D09 | LD DE,$000A | Multiply the clamped index by ten, ready for the score's own ten-times commit. |
| 8D0C | LD HL,$0000 | ||
| 8D0F | LD B,A | ||
| level_bonus_loop | 8D10 | ADD HL,DE | |
| 8D11 | DJNZ level_bonus_loop | ||
| 8D13 | LD A,L | ||
| 8D14 | LD (pending_score),A | ||
| 8D17 | CALL add_pending_score | ||
| 8D1A | LD HL,level | ||
| 8D1D | INC (HL) | ||
| 8D1E | LD A,(HL) | ||
| 8D1F | CP $32 | ||
| 8D21 | JR Z,speed_up_rally | ||
| 8D23 | CP $64 | ||
| 8D25 | JR NZ,next_level | ||
|
Wrap the level index back to 0. The brick-field layouts, spawn cells and wave scripts hold 100 records each, one per level. They therefore repeat from here. The displayed level in the level caption does not restart with it.
|
|||
| wrap_level_index | 8D27 | LD (HL),$00 | The hundredth clear wraps the index back to the first layout. |
| speed_up_rally | 8D29 | LD A,(rally_accel_interval) | Shorten the rally interval a step, down to a floor of two. |
| 8D2C | CP $02 | ||
| 8D2E | JR Z,next_level | ||
| 8D30 | SUB $03 | ||
| 8D32 | LD (rally_accel_interval),A | ||
| next_level | 8D35 | JP setup_level | Set up the next level's screen. |
|
Game over. Draws the GAME OVER banner with the double-height printer and plays the game-over tune to its $0D terminator. It then runs the high-score check to test and enter the score. Plays the title tune while it waits for fire, any key or the tune's end, then restarts at the new-game entry.
|
|||
| game_over | 8D38 | LD HL,SCREEN_BASE+$84C | Print the two GAME OVER lines and aim the tune pointer at the game-over tune. |
| 8D3B | LD DE,msg_game_over | ||
| 8D3E | LD C,$4E | ||
| 8D40 | CALL print_string_2x | ||
| 8D43 | LD HL,SCREEN_BASE+$88C | ||
| 8D46 | LD DE,msg_over | ||
| 8D49 | LD C,$4E | ||
| 8D4B | CALL print_string_2x | ||
| 8D4E | LD HL,game_over_tune | ||
| 8D51 | LD (tune_ptr),HL | ||
| game_over_tune_play | 8D54 | CALL play_tune_note | Play the tune a note per pass until its terminator. |
| 8D57 | CP $0D | ||
| 8D59 | JR Z,game_over_pause | ||
| 8D5B | LD BC,$0BB8 | Pause between notes. | |
| 8D5E | CALL delay | ||
| 8D61 | JR game_over_tune_play | ||
| game_over_pause | 8D63 | LD BC,$0000 | The tune reached its terminator. Two full-length delays run, then the high-score check offers the score to the table and the tune pointer takes the title tune. |
| 8D66 | CALL delay | ||
| 8D69 | CALL delay | ||
| 8D6C | CALL check_highscore | ||
| 8D6F | LD HL,title_tune | ||
| 8D72 | LD (tune_ptr),HL | ||
|
Wait for restart. Loop the title tune until its terminator, any key, or fire. Any of the three restarts a new game at the new-game entry.
|
|||
| await_restart | 8D75 | CALL play_tune_note | When the title tune reaches its terminator, the game restarts on its own. |
| 8D78 | CP $0D | ||
| 8D7A | JP Z,new_game | ||
| 8D7D | LD BC,$0BB8 | ||
| 8D80 | CALL delay | ||
| 8D83 | LD BC,$00FE | Any key restarts the game, because the port read selects every keyboard half-row. | |
| 8D86 | IN A,(C) | ||
| 8D88 | AND $1F | ||
| 8D8A | XOR $1F | ||
| 8D8C | JP NZ,new_game | ||
| 8D8F | CALL read_ctrl | Fire restarts the game. | |
| 8D92 | LD HL,controls | ||
| 8D95 | BIT 4,(HL) | ||
| 8D97 | JR Z,await_restart | ||
| 8D99 | JP new_game | ||
| ← 8888 | Map | 8D9C → |
