; ===================================================================== ; PAC-MAN for the Commodore 64 — clean-room 6502 implementation ; Same game logic as the browser version: real maze, four ghosts with ; per-character target AI (Blinky/Pinky/Inky/Clyde), scatter/chase waves, ; frightened mode, dots/energizers, fruit-less scoring, lives and SID SFX. ; Build: acme -f cbm -o pacman.prg pacman.asm ; ===================================================================== !cpu 6510 ; ---- hardware ---- VIC = $d000 SPR_X = $d000 SPR_Y = $d001 SPR_MSB = $d010 SPR_ENA = $d015 SPR_COL = $d027 RASTER = $d012 D011 = $d011 SCROLY = $d011 VICCTRL2 = $d016 VICMEM = $d018 BORDER = $d020 BGCOL = $d021 CIA1_PRA = $dc00 ; joystick port 2 SID = $d400 SCREEN = $0400 COLRAM = $d800 SPRPTR = $07f8 CHARSET = $2000 TILEMAP = $c000 ; 28x21 runtime tile grid MAZE_W = 28 MAZE_H = 21 MAZE_X0 = 6 MAZE_Y0 = 2 ; tile types T_EMPTY = 0 T_DOT = 1 T_POWER = 2 T_WALL = 3 T_DOOR = 4 ; screen codes for custom glyphs CH_WALL = 91 CH_DOT = 92 CH_POWER = 93 CH_DOOR = 94 ; directions: 0=up 1=left 2=down 3=right ; 5=none D_UP=0 : D_LEFT=1 : D_DOWN=2 : D_RIGHT=3 : D_NONE=5 ; ghost modes M_HOUSE=0 : M_LEAVE=1 : M_HUNT=2 : M_FRIGHT=3 : M_EYES=4 ; zero page ptr1 = $fb ptr2 = $fd zt0 = $02 zt1 = $03 zt2 = $04 zt3 = $05 zt4 = $06 zt5 = $07 ; ===================================================================== ; BASIC stub: 10 SYS 2064 ; ===================================================================== * = $0801 !byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$34,$00,$00,$00 * = $0810 jmp main ; ===================================================================== ; main ; ===================================================================== main sei jsr copy_charset jsr build_glyphs jsr vic_setup jsr init_sound jsr new_game mainloop jsr wait_frame inc frame lda state cmp #ST_PLAY bne ml_other jsr read_joy jsr update_pac jsr eat_check jsr update_waves jsr release_ghosts jsr update_ghosts jsr collide jsr place_sprites jsr sfx_update lda dots beq + jmp mainloop + ; level cleared lda #ST_CLEAR sta state lda #90 sta sttimer jmp mainloop ml_other cmp #ST_READY bne ml_n2 jsr place_sprites dec sttimer beq + jmp mainloop + lda #ST_PLAY sta state jmp mainloop ml_n2 cmp #ST_DEAD bne ml_n3 jsr death_anim jmp mainloop ml_n3 cmp #ST_CLEAR bne ml_n4 dec sttimer beq + jmp mainloop + jsr next_level jmp mainloop ml_n4 ; ST_OVER jsr read_joy_fire bcs + jmp mainloop + jsr new_game jmp mainloop ; ===================================================================== ; wait for raster line 251 (frame sync) ; ===================================================================== wait_frame lda #251 - cmp RASTER bne - rts ; ===================================================================== ; VIC setup: screen/charset bank, sprites on, colors ; ===================================================================== vic_setup lda #$1b sta SCROLY ; default, screen on lda #$18 ; screen $0400, charset $2000 sta VICMEM lda #0 sta BORDER sta BGCOL lda #%00011111 ; sprites 0-4 enabled sta SPR_ENA lda #0 sta SPR_MSB sta VICCTRL2+0 ; (placeholder; single-color sprites) lda #0 sta $d01c ; all sprites hi-res (not multicolor) sta $d017 ; no Y expand sta $d01d ; no X expand ; sprite colors lda #7 : sta SPR_COL+0 ; pac yellow lda #2 : sta SPR_COL+1 ; blinky red lda #10 : sta SPR_COL+2 ; pinky light-red/pink lda #3 : sta SPR_COL+3 ; inky cyan lda #8 : sta SPR_COL+4 ; clyde orange rts ; ===================================================================== ; copy char ROM (uppercase set) to RAM at $2000 ; ===================================================================== copy_charset sei lda #$33 sta $01 ; char ROM visible at $d000 lda #<$d000 : sta ptr1 lda #>$d000 : sta ptr1+1 lda #CHARSET : sta ptr2+1 ldx #8 ; 8 pages = 2KB ldy #0 ccs_l lda (ptr1),y sta (ptr2),y iny bne ccs_l inc ptr1+1 inc ptr2+1 dex bne ccs_l lda #$37 sta $01 cli rts ; install custom glyphs into the copied charset build_glyphs ldx #0 bg_l lda glyph_wall,x : sta CHARSET + CH_WALL*8,x lda glyph_dot,x : sta CHARSET + CH_DOT*8,x lda glyph_power,x : sta CHARSET + CH_POWER*8,x lda glyph_door,x : sta CHARSET + CH_DOOR*8,x inx cpx #8 bne bg_l rts glyph_wall !byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff glyph_dot !byte $00,$00,$00,$18,$18,$00,$00,$00 glyph_power !byte $00,$3c,$7e,$7e,$7e,$7e,$3c,$00 glyph_door !byte $00,$00,$00,$ff,$ff,$00,$00,$00 ; ===================================================================== ; new game / level ; ===================================================================== new_game lda #0 sta score+0 : sta score+1 : sta score+2 lda #3 sta lives lda #1 sta level jsr setup_level rts next_level inc level jsr setup_level rts setup_level jsr copy_sprites jsr clear_screen jsr draw_maze ; resets tilemap, dots, screen jsr init_actors jsr draw_score jsr draw_lives lda #ST_READY sta state lda #80 sta sttimer ; reset wave schedule lda #0 sta waveidx sta frightt lda #M_HUNT sta globmode ; start scatter actually lda #0 sta globmode ; 0=scatter 1=chase used below lda wave_secs+0 sta wavetimer+0 lda wave_secs+1 sta wavetimer+1 rts ; copy sprite bitmaps into VIC bank (they already live at $2800 in the ; loaded image, so nothing to copy — kept for clarity / future banking) copy_sprites rts ; clear the 1000-byte screen to spaces and colour to black clear_screen ldx #0 lda #32 cs_l sta SCREEN,x sta SCREEN+250,x sta SCREEN+500,x sta SCREEN+750,x inx cpx #250 bne cs_l ldx #0 lda #0 cc_l sta COLRAM,x sta COLRAM+250,x sta COLRAM+500,x sta COLRAM+750,x inx cpx #250 bne cc_l rts ; ===================================================================== ; draw maze from mazetext -> screen, color, tilemap; count dots ; ===================================================================== draw_maze lda #0 sta dots sta dots+1 ldx #0 ; row dm_row ; set source pointer into mazetext = mazetext + row*28 txa sta zt0 lda #mazetext : sta ptr1+1 ; add row*28 ldy zt0 beq dm_nooff dm_addrow clc lda ptr1 : adc #MAZE_W : sta ptr1 bcc + inc ptr1+1 + dey bne dm_addrow dm_nooff ; screen/color/tile row pointers lda scrlo,x : sta ptr2 lda scrhi,x : sta ptr2+1 lda collo,x : sta zt3 lda colhi,x : sta zt4 lda tilelo,x : sta zt1 lda tilehi,x : sta zt2 ldy #0 dm_col lda (ptr1),y ; ascii char ; map char -> (A=screen code, tile in zt5, color in X-temp) jsr map_tile ; returns scr code in A, tile in zt5, color in zt0 sta (ptr2),y ; screen pha lda zt0 sta (zt3),y ; color (zt3/zt4 pointer) ; store tile type lda zt5 sta (zt1),y ; count dot/power cmp #T_DOT beq dm_count cmp #T_POWER bne dm_nocount dm_count inc dots bne + inc dots+1 + dm_nocount pla iny cpy #MAZE_W bne dm_col inx cpx #MAZE_H bne dm_row rts ; map ascii (A) -> screen code A, tile zt5, color zt0 map_tile cmp #'#' bne mt_1 lda #CH_WALL : sta zt6scr lda #T_WALL : sta zt5 lda #6 : sta zt0 ; blue lda zt6scr : rts mt_1 cmp #'.' bne mt_2 lda #CH_DOT : sta zt6scr lda #T_DOT : sta zt5 lda #1 : sta zt0 ; white-ish lda zt6scr : rts mt_2 cmp #'o' bne mt_3 lda #CH_POWER : sta zt6scr lda #T_POWER : sta zt5 lda #1 : sta zt0 lda zt6scr : rts mt_3 cmp #'-' bne mt_4 lda #CH_DOOR : sta zt6scr lda #T_DOOR : sta zt5 lda #10 : sta zt0 ; pink lda zt6scr : rts mt_4 ; space / anything else lda #32 : sta zt6scr lda #T_EMPTY : sta zt5 lda #0 : sta zt0 lda zt6scr : rts ; ===================================================================== ; init actors (positions in maze pixels: tile*8) ; ===================================================================== init_actors ; pac at col 13.. start tile (13,15) facing left lda #13*8 : sta ax+0 lda #15*8 : sta ay+0 lda #D_LEFT : sta adir+0 sta awant+0 ; blinky outside above house (col13,row7) hunting lda #13*8 : sta ax+1 lda #7*8 : sta ay+1 lda #D_LEFT : sta adir+1 sta awant+1 lda #M_HUNT : sta amode+1 ; pinky inside (col13,row9) lda #13*8 : sta ax+2 lda #9*8 : sta ay+2 lda #D_UP : sta adir+2 sta awant+2 lda #M_HOUSE : sta amode+2 ; inky inside (col11,row9) lda #11*8 : sta ax+3 lda #9*8 : sta ay+3 lda #D_UP : sta adir+3 sta awant+3 lda #M_HOUSE : sta amode+3 ; clyde inside (col15,row9) lda #15*8 : sta ax+4 lda #9*8 : sta ay+4 lda #D_UP : sta adir+4 sta awant+4 lda #M_HOUSE : sta amode+4 ; combo + release timers lda #0 sta combo sta relcnt sta relcnt+1 rts ; ===================================================================== ; joystick read (port 2 @ $dc00). bits: 0 up,1 down,2 left,3 right,4 fire ; sets awant+0 ; ===================================================================== read_joy lda CIA1_PRA tax and #%00000001 bne rj_nup lda #D_UP : sta awant+0 : rts rj_nup txa and #%00000010 bne rj_ndn lda #D_DOWN : sta awant+0 : rts rj_ndn txa and #%00000100 bne rj_nlf lda #D_LEFT : sta awant+0 : rts rj_nlf txa and #%00001000 bne rj_nrt lda #D_RIGHT : sta awant+0 : rts rj_nrt rts read_joy_fire lda CIA1_PRA and #%00010000 bne rjf_no sec rts rjf_no clc rts ; ===================================================================== ; PAC-MAN update ; ===================================================================== update_pac ; only decide when tile-aligned lda ax+0 and #7 bne up_move lda ay+0 and #7 bne up_move ; aligned: try want dir ldx #0 lda awant+0 jsr can_go_pac ; carry set if passable in A dir from actor X bcc up_keep lda awant+0 sta adir+0 jmp up_move up_keep ; can we keep current dir? ldx #0 lda adir+0 jsr can_go_pac bcc up_stop jmp up_move up_stop rts ; blocked, stay put this frame up_move ldx #0 jsr step_actor rts ; ===================================================================== ; can_go_pac: A=dir, X=actor index. Returns C=1 if the neighbour tile in ; that direction is walkable for Pac-Man (not wall/door). ; uses actor tile (ax/ay of X) ; ===================================================================== can_go_pac sta zt0 ; dir jsr actor_tile ; -> zt1=tc, zt2=tr lda zt0 jsr neighbour ; -> zt1=ntc, zt2=ntr (signed-ish) jsr tile_type_xy ; -> A=type (handles tunnel) cmp #T_WALL beq cg_no cmp #T_DOOR beq cg_no sec rts cg_no clc rts ; can_go_ghost: A=dir, X=actor index, considers door passable if leaving/eyes can_go_ghost sta zt0 jsr actor_tile lda zt0 jsr neighbour jsr tile_type_xy cmp #T_WALL beq cgg_no cmp #T_DOOR bne cgg_yes ; door: allowed only if mode leave or eyes lda amode,x cmp #M_LEAVE beq cgg_yes cmp #M_EYES beq cgg_yes cmp #M_HOUSE beq cgg_yes bne cgg_no cgg_yes sec rts cgg_no clc rts ; actor_tile: X=index -> zt1=tc (ax/8), zt2=tr (ay/8) actor_tile lda ax,x lsr : lsr : lsr sta zt1 lda ay,x lsr : lsr : lsr sta zt2 rts ; neighbour: A=dir, input tile zt1/zt2 -> zt1/zt2 moved one tile (signed wrap) neighbour cmp #D_UP bne nb_1 dec zt2 rts nb_1 cmp #D_DOWN bne nb_2 inc zt2 rts nb_2 cmp #D_LEFT bne nb_3 dec zt1 rts nb_3 inc zt1 ; right rts ; tile_type_xy: tile coords in zt1(tc),zt2(tr) -> A=type ; handles tunnel row (tr=10) off-grid as EMPTY (walkable) tile_type_xy lda zt2 cmp #MAZE_H bcs tt_wallret ; tr>=21 -> wall lda zt1 cmp #MAZE_W bcc tt_inrange ; tc>=28 (incl wrap of $ff). tunnel only on row 10 lda zt2 cmp #10 beq tt_empty bne tt_wallret tt_inrange ; ptr = tilelo[tr]/tilehi[tr]; y=tc ldy zt2 lda tilelo,y : sta ptr1 lda tilehi,y : sta ptr1+1 ldy zt1 lda (ptr1),y rts tt_empty lda #T_EMPTY rts tt_wallret lda #T_WALL rts ; ===================================================================== ; step_actor: move actor X one pixel in adir,x with tunnel wrap ; speed gating handled by caller (skip frames) ; ===================================================================== step_actor lda adir,x cmp #D_UP bne sa_1 dec ay,x rts sa_1 cmp #D_DOWN bne sa_2 inc ay,x rts sa_2 cmp #D_LEFT bne sa_3 dec ax,x ; tunnel wrap left: if went below 0 (->$ff) lda ax,x cmp #$ff bne sa_done lda #(MAZE_W-1)*8 sta ax,x sa_done rts sa_3 cmp #D_RIGHT bne sa_4 inc ax,x lda ax,x cmp #MAZE_W*8 ; 224 -> wrap to 0 bne sa_done2 lda #0 sta ax,x sa_done2 rts sa_4 rts ; none ; ===================================================================== ; eat_check: if Pac tile-aligned and on a dot/power, consume it ; ===================================================================== eat_check lda ax+0 and #7 bne ec_ret lda ay+0 and #7 bne ec_ret ldx #0 jsr actor_tile ; zt1=tc zt2=tr ; pointer to tile ldy zt2 lda tilelo,y : sta ptr1 lda tilehi,y : sta ptr1+1 ldy zt1 lda (ptr1),y cmp #T_DOT beq ec_dot cmp #T_POWER beq ec_power ec_ret rts ec_dot lda #T_EMPTY sta (ptr1),y jsr clear_tile_char jsr dec_dots lda #10 jsr add_score jsr draw_score jsr sfx_chomp rts ec_power lda #T_EMPTY sta (ptr1),y jsr clear_tile_char jsr dec_dots lda #50 jsr add_score jsr draw_score jsr enter_fright jsr sfx_power rts ; clear the screen char at Pac tile (zt1/zt2) clear_tile_char ldy zt2 lda scrlo,y : sta ptr1 lda scrhi,y : sta ptr1+1 ldy zt1 lda #32 sta (ptr1),y rts dec_dots lda dots sec sbc #1 sta dots lda dots+1 sbc #0 sta dots+1 rts ; ===================================================================== ; enter_fright: ghosts hunt->fright, reverse, reset combo, set timer ; ===================================================================== enter_fright lda #150 ; ~3s @ 50Hz sta frightt lda #0 sta combo ldx #1 ef_l lda amode,x cmp #M_HUNT bne ef_n lda #M_FRIGHT sta amode,x ; reverse dir lda adir,x eor #2 and #3 sta adir,x sta awant,x ef_n inx cpx #5 bne ef_l rts ; ===================================================================== ; scatter/chase wave timer ; globmode 0=scatter 1=chase ; ===================================================================== update_waves lda frightt beq uw_run ; fright active: countdown, revert when 0 dec frightt bne uw_ret ; revert fright ghosts to hunt ldx #1 uw_rv lda amode,x cmp #M_FRIGHT bne uw_rv2 lda #M_HUNT sta amode,x uw_rv2 inx cpx #5 bne uw_rv uw_ret rts uw_run ; decrement 16-bit wavetimer (counts frames) lda wavetimer ora wavetimer+1 beq uw_ret ; 0 means "infinite chase" lda wavetimer sec sbc #1 sta wavetimer lda wavetimer+1 sbc #0 sta wavetimer+1 lda wavetimer ora wavetimer+1 bne uw_ret ; advance wave inc waveidx lda waveidx cmp #wave_count bcc uw_set ; clamp at last (infinite chase): leave timer 0, mode chase lda #1 sta globmode rts uw_set tax lda wave_mode,x sta globmode ; set timer = wave_secs[idx] (already in frames, 16-bit table) txa asl tax lda wave_secs,x sta wavetimer lda wave_secs+1,x sta wavetimer+1 ; reverse all hunting ghosts ldx #1 uw_rev lda amode,x cmp #M_HUNT beq uw_rdo cmp #M_FRIGHT bne uw_rnx uw_rdo lda adir,x eor #2 and #3 sta adir,x uw_rnx inx cpx #5 bne uw_rev rts ; ===================================================================== ; release ghosts from the house on timers ; ===================================================================== release_ghosts ; increment 16-bit release counter inc relcnt bne + inc relcnt+1 + ; pinky leaves immediately ldx #2 lda amode,x cmp #M_HOUSE bne rg_inky lda #M_LEAVE sta amode,x rg_inky ; inky after ~150 frames lda relcnt+1 bne rg_i_ok lda relcnt cmp #150 bcc rg_clyde rg_i_ok ldx #3 lda amode,x cmp #M_HOUSE bne rg_clyde lda #M_LEAVE sta amode,x rg_clyde ; clyde after ~320 frames lda relcnt+1 beq rg_done ; <256 -> not yet (need >=320) lda relcnt cmp #64 ; 256+64=320 bcc rg_done ldx #4 lda amode,x cmp #M_HOUSE bne rg_done lda #M_LEAVE sta amode,x rg_done rts ; ===================================================================== ; update all ghosts ; ===================================================================== update_ghosts ldx #1 ug_loop stx ghidx lda amode,x cmp #M_HOUSE beq ug_house cmp #M_LEAVE beq ug_leave cmp #M_EYES beq ug_eyes ; hunt or fright -> speed gating then AI move jsr ghost_speed_ok bcc ug_next ldx ghidx jsr ghost_ai_move jmp ug_next ug_house ldx ghidx jsr ghost_bob jmp ug_next ug_leave ldx ghidx jsr ghost_leave jmp ug_next ug_eyes ldx ghidx jsr ghost_eyes_move jmp ug_next ug_next ldx ghidx inx cpx #5 bne ug_loop rts ; ghost_speed_ok: returns C=1 if this ghost should move this frame. ; fright -> move every other frame (half). tunnel -> every other. else ; move 7/8 frames. ghost_speed_ok ldx ghidx lda amode,x cmp #M_FRIGHT bne gs_norm lda frame and #1 beq gs_yes clc rts gs_norm ; tunnel row? tr==10 jsr actor_tile lda zt2 cmp #10 bne gs_full lda frame and #1 beq gs_yes clc rts gs_full lda frame and #7 beq gs_skip ; skip 1 in 8 -> ~87% sec rts gs_skip clc rts gs_yes sec rts ; ghost_bob: gentle vertical bob inside the house ghost_bob lda frame and #$10 beq gb_up inc ay,x rts gb_up dec ay,x rts ; ghost_leave: move toward door col (13*8) then up & out to row 7 ghost_leave ; align x to col 13 (104) first lda ax,x cmp #13*8 beq gl_vert bcc gl_right dec ax,x rts gl_right inc ax,x rts gl_vert ; move up until row 7 (56) lda ay,x cmp #7*8 beq gl_out dec ay,x rts gl_out lda #M_HUNT sta amode,x lda #D_LEFT sta adir,x rts ; ghost_eyes_move: fast path back to the door-top tile (13,7), allow door ghost_eyes_move ; move twice (fast) jsr eyes_one jsr eyes_one ; reached door top? ldx ghidx lda ax,x cmp #13*8 bne gem_ret lda ay,x cmp #7*8 bne gem_ret lda #M_LEAVE ; re-enter via leave path (will come back out) sta amode,x ; drop into house first lda #9*8 sta ay,x gem_ret rts eyes_one ldx ghidx lda ax,x and #7 bne eo_move lda ay,x and #7 bne eo_move ; aligned: choose dir toward (13,7) with door allowed lda #13 sta tgt_c lda #7 sta tgt_r jsr choose_dir_ghost eo_move ldx ghidx jsr step_actor rts ; ===================================================================== ; ghost_ai_move: hunt/fright AI then move ; ===================================================================== ghost_ai_move stx ghidx lda ax,x and #7 bne gam_move lda ay,x and #7 bne gam_move ; aligned -> decide lda amode,x cmp #M_FRIGHT beq gam_fright jsr compute_target ; sets tgt_c/tgt_r for ghidx jsr choose_dir_ghost jmp gam_move gam_fright jsr choose_dir_random gam_move ldx ghidx jsr step_actor rts ; ===================================================================== ; choose_dir_ghost: pick non-reverse passable dir minimising distance to ; (tgt_c,tgt_r). preference order up,left,down,right. sets adir,x ; ===================================================================== choose_dir_ghost ldx ghidx lda #$ff sta best_d lda #$ff sta best_lo sta best_hi lda adir,x eor #2 and #3 sta rev_d ; forbidden reverse lda #0 sta cur_d cdg_loop lda cur_d cmp rev_d beq cdg_skip ; no-up restriction: skip up on no-up tiles when hunting cmp #D_UP bne cdg_try jsr on_noup_tile bcc cdg_try ; only restrict when hunting (not fright/eyes) ldx ghidx lda amode,x cmp #M_HUNT beq cdg_skip cdg_try ldx ghidx lda cur_d jsr can_go_ghost bcc cdg_skip ; compute candidate tile distance to target ldx ghidx jsr actor_tile ; zt1,zt2 lda cur_d jsr neighbour ; zt1,zt2 -> candidate jsr dist_to_target ; -> zt3(lo) zt4(hi) ; compare with best lda zt4 cmp best_hi bcc cdg_better bne cdg_skip lda zt3 cmp best_lo bcs cdg_skip cdg_better lda zt3 : sta best_lo lda zt4 : sta best_hi lda cur_d : sta best_d cdg_skip inc cur_d lda cur_d cmp #4 bne cdg_loop ; apply ldx ghidx lda best_d cmp #$ff bne cdg_set ; dead end: reverse lda rev_d cdg_set sta adir,x rts ; dist_to_target: candidate tile zt1(c),zt2(r); target tgt_c/tgt_r. ; result squared euclidean (clamped) in zt3(lo)/zt4(hi) dist_to_target ; dc = |zt1 - tgt_c| lda zt1 sec sbc tgt_c bcs dt_dcpos eor #$ff clc adc #1 dt_dcpos cmp #32 bcc + lda #31 + sta zt0 ; dc ; dr = |zt2 - tgt_r| lda zt2 sec sbc tgt_r bcs dt_drpos eor #$ff clc adc #1 dt_drpos cmp #32 bcc + lda #31 + sta zt5 ; dr ; sum = sq[dc] + sq[dr] (16-bit) ldx zt0 lda sqlo,x sta zt3 lda sqhi,x sta zt4 ldx zt5 clc lda sqlo,x adc zt3 sta zt3 lda sqhi,x adc zt4 sta zt4 rts ; on_noup_tile: C=1 if ghost X tile is a "no up" tile (above the house) on_noup_tile ldx ghidx jsr actor_tile lda zt2 cmp #7 bne nu_no lda zt1 cmp #11 beq nu_yes cmp #16 beq nu_yes nu_no clc rts nu_yes sec rts ; choose_dir_random: frightened ghosts pick a random passable non-reverse dir choose_dir_random ldx ghidx lda adir,x eor #2 and #3 sta rev_d ; build list of legal dirs in tmpdirs, count in zt0 lda #0 sta zt0 lda #0 sta cur_d cdr_l lda cur_d cmp rev_d beq cdr_skip ldx ghidx lda cur_d jsr can_go_ghost bcc cdr_skip ldy zt0 lda cur_d sta tmpdirs,y inc zt0 cdr_skip inc cur_d lda cur_d cmp #4 bne cdr_l lda zt0 beq cdr_rev ; none -> reverse ; pick rnd % count jsr rnd ldx zt0 jsr mod_x ; A = A mod zt0 tay lda tmpdirs,y ldx ghidx sta adir,x rts cdr_rev ldx ghidx lda rev_d sta adir,x rts ; A mod X (X in 1..4) simple mod_x mx_l cmp zt0 bcc mx_done sec sbc zt0 jmp mx_l mx_done rts ; simple 8-bit LFSR random in A rnd lda rndseed beq rnd_z asl bcc rnd_n eor #$1d rnd_n sta rndseed rts rnd_z lda #$a5 sta rndseed rts ; ===================================================================== ; compute_target for ghidx -> tgt_c/tgt_r ; globmode 0=scatter -> corner ; 1=chase -> per character ; ===================================================================== compute_target lda globmode bne ct_chase ; scatter corners by ghost index ldx ghidx lda scat_c-1,x sta tgt_c lda scat_r-1,x sta tgt_r rts ct_chase ldx ghidx cpx #1 beq ct_blinky cpx #2 beq ct_pinky cpx #3 beq ct_inky ; clyde jmp ct_clyde ct_blinky ; target = pac tile lda ax+0 : lsr:lsr:lsr : sta tgt_c lda ay+0 : lsr:lsr:lsr : sta tgt_r rts ct_pinky ; pac tile + 4 * pacdir (with up-bug: up also -4 col) lda ax+0 : lsr:lsr:lsr : sta tgt_c lda ay+0 : lsr:lsr:lsr : sta tgt_r ldx #4 jsr add_ahead ; uses pac dir, adds X tiles ahead to tgt rts ct_inky ; blinky tile -> temp lda ax+1 : lsr:lsr:lsr : sta bl_tc_tmp_calc lda ay+1 : lsr:lsr:lsr : sta bl_tr_tmp_calc ; p2 = pac + 2 ahead ; target = 2*p2 - blinky lda ax+0 : lsr:lsr:lsr : sta tgt_c lda ay+0 : lsr:lsr:lsr : sta tgt_r ldx #2 jsr add_ahead ; tgt = 2*tgt - blinkytile lda tgt_c asl sec sbc bl_tc_tmp_calc ; need blinky tile sta tgt_c lda tgt_r asl sec sbc bl_tr_tmp_calc sta tgt_r rts ct_clyde ; if dist(clyde,pac) > 8 tiles -> pac ; else corner ldx #4 jsr actor_tile ; zt1,zt2 = clyde tile lda ax+0 : lsr:lsr:lsr : sta tgt_c lda ay+0 : lsr:lsr:lsr : sta tgt_r jsr dist_to_target ; dist clyde->pac in zt3/zt4 ; compare > 64 (8^2) lda zt4 bne ct_clyde_far ; hi>0 -> definitely >64 lda zt3 cmp #65 bcs ct_clyde_far ; near -> corner lda scat_c-1+4 : sta tgt_c ; clyde corner (index 4) lda scat_r-1+4 : sta tgt_r rts ct_clyde_far lda ax+0 : lsr:lsr:lsr : sta tgt_c lda ay+0 : lsr:lsr:lsr : sta tgt_r rts ; add_ahead: add X tiles in pac's facing direction to tgt_c/tgt_r ; includes the classic up overflow (up also shifts left by X) add_ahead lda adir+0 cmp #D_UP bne aa_1 ; up: tr -= X ; tc -= X (bug) txa sta zt0 lda tgt_r : sec : sbc zt0 : sta tgt_r lda tgt_c : sec : sbc zt0 : sta tgt_c rts aa_1 cmp #D_DOWN bne aa_2 txa : sta zt0 lda tgt_r : clc : adc zt0 : sta tgt_r rts aa_2 cmp #D_LEFT bne aa_3 txa : sta zt0 lda tgt_c : sec : sbc zt0 : sta tgt_c rts aa_3 ; right txa : sta zt0 lda tgt_c : clc : adc zt0 : sta tgt_c rts ; ===================================================================== ; collisions: pac vs ghosts ; ===================================================================== collide ldx #1 co_l stx ghidx lda amode,x cmp #M_EYES beq co_next cmp #M_HOUSE beq co_next ; |ax-ax0|<6 and |ay-ay0|<6 ? lda ax,x sec sbc ax+0 jsr absa cmp #5 bcs co_next lda ay,x sec sbc ay+0 jsr absa cmp #5 bcs co_next ; collision lda amode,x cmp #M_FRIGHT beq co_eat ; pac dies jsr pac_dies rts co_eat ; score 200< use decimal add) jsr add_score16 jsr draw_score lda combo cmp #3 beq + inc combo + rts pac_dies lda #ST_DEAD sta state lda #0 sta frightt lda #100 sta sttimer rts ; ===================================================================== ; death animation (simple): flash + fall through lives, then respawn ; ===================================================================== death_anim ; sweep a falling tone lda sttimer and #3 bne da_skip jsr sfx_death_step da_skip ; spin pac sprite frames lda frame and #4 beq + lda #pacClosed sta SPRPTR+0 jmp da_t + lda #pacOpenU sta SPRPTR+0 da_t dec sttimer bne da_ret ; lose a life dec lives bpl da_resp ; game over lda #ST_OVER sta state jsr draw_gameover rts da_resp jsr draw_lives jsr init_actors lda #ST_READY sta state lda #70 sta sttimer ; reset waves lda #0 sta waveidx sta frightt sta globmode lda wave_secs+0 : sta wavetimer+0 lda wave_secs+1 : sta wavetimer+1 da_ret rts ; ===================================================================== ; place sprites: convert actor maze-pixel pos -> VIC sprite coords, ; set pointers (animation), colours, MSB. ; ===================================================================== place_sprites lda #0 sta SPR_MSB sta msbacc ; ---- pac (sprite 0) ---- ldx #0 jsr set_sprite_xy ; pac frame: animate open/closed by direction lda state cmp #ST_DEAD beq ps_ghosts ; death anim sets pac frame itself lda frame and #4 beq ps_pac_closed lda adir+0 cmp #D_RIGHT beq + cmp #D_LEFT beq ++ cmp #D_UP beq +++ lda #pacOpenD : jmp ps_pac_set + lda #pacOpenR : jmp ps_pac_set ++ lda #pacOpenL : jmp ps_pac_set +++ lda #pacOpenU : jmp ps_pac_set ps_pac_closed lda #pacClosed ps_pac_set sta SPRPTR+0 ps_ghosts ; ---- ghosts sprites 1..4 ---- ldx #1 ps_g_l stx ghidx jsr set_sprite_xy ldx ghidx lda amode,x cmp #M_EYES beq ps_eyes cmp #M_FRIGHT beq ps_fr ; normal ghost frame + per-ghost colour lda #ghostSpr sta SPRPTR,x lda ghcolor-1,x sta SPR_COL,x jmp ps_g_next ps_fr lda #ghostSpr sta SPRPTR,x ; flash near end of fright lda frightt cmp #50 bcs + lda frame and #8 beq + lda #1 ; white flash sta SPR_COL,x jmp ps_g_next + lda #6 ; blue sta SPR_COL,x jmp ps_g_next ps_eyes lda #eyesSpr sta SPRPTR,x lda #1 sta SPR_COL,x ps_g_next ldx ghidx inx cpx #5 bne ps_g_l ; write accumulated MSB lda msbacc sta SPR_MSB rts ; set_sprite_xy: X=sprite index. sprite_x16 = ax+64 ; sprite_y = ay+60 set_sprite_xy txa asl ; *2 for SPR_X/Y register pairs tay ; X low + carry to msb lda ax,x clc adc #64 sta SPR_X,y bcc + ; set msb bit for this sprite lda msbacc ora msbtab,x sta msbacc + lda ay,x clc adc #60 sta SPR_Y,y rts msbtab !byte 1,2,4,8,16,32,64,128 ; ===================================================================== ; scoring (BCD, 3 bytes little-endian; display 6 digits) ; ===================================================================== add_score ; A = points (binary, multiple of 10, <=255) -> convert simple: ; we keep score in BCD; add A as BCD by repeated... simpler: store ; score as binary 24-bit and convert at draw time. clc adc score+0 sta score+0 bcc + inc score+1 bne + inc score+2 + jsr check_extra rts add_score16 ; zt0/zt1 = 16-bit binary points clc lda score+0 adc zt0 sta score+0 lda score+1 adc zt1 sta score+1 bcc + inc score+2 + jsr check_extra rts check_extra ; extra life at 10000 (once). 10000 = $2710 lda extra_done bne ce_ret lda score+2 bne ce_award ; >=65536 obviously past lda score+1 cmp #$27 bcc ce_ret bne ce_award lda score+0 cmp #$10 bcc ce_ret ce_award inc lives lda #1 sta extra_done jsr draw_lives ce_ret rts ; draw_score: 24-bit binary score -> 6 decimal digits at top of screen draw_score ; convert score (3 bytes) to decimal via repeated subtraction tables ; simple approach: binary-to-decimal using 16-bit (score rarely >65535 ; in normal play; if it does, high byte adds 65536 handled crudely) ; We'll render 6 digits from a 24-bit value. lda score+0 : sta bcd0 lda score+1 : sta bcd1 lda score+2 : sta bcd2 ldy #0 ; div10_24 clobbers X, so index with Y here ds_dig jsr div10_24 clc adc #48 ; screen code for the digit sta scorebuf,y iny cpy #6 bne ds_dig ; print reversed into screen row 0 at col 8 ldx #0 ldy #5 ds_pr lda scorebuf,y sta SCREEN+1,x lda #1 sta COLRAM+1,x dey inx cpx #6 bne ds_pr rts ; div10_24: bcd2:bcd1:bcd0 /= 10 ; remainder (digit 0..9) returned in A ; standard shift/restore long division div10_24 lda #0 sta drem ldx #24 d10_l asl bcd0 rol bcd1 rol bcd2 rol drem lda drem sec sbc #10 bcc d10_skip sta drem inc bcd0 ; set quotient bit 0 d10_skip dex bne d10_l lda drem rts draw_lives ; draw up to lives pac icons on row 24 (screen offset 24*40) ldx #0 dl_l cpx lives bcs dl_blank lda #pacClosed_ch ; we draw a yellow 'C'-like char; use CH for pac ; use the dot? simpler: print '*' char ; use screen code for filled lda #81 ; filled circle screen code (ROM) ~ ball sta SCREEN+24*40+1,x lda #7 sta COLRAM+24*40+1,x jmp dl_n dl_blank lda #32 sta SCREEN+24*40+1,x dl_n inx cpx #5 bne dl_l rts draw_gameover ldx #0 go_l lda txt_over,x beq go_d sta SCREEN+12*40+14,x lda #2 sta COLRAM+12*40+14,x inx bne go_l go_d rts ; ===================================================================== ; sound (SID) — minimal original effects ; ===================================================================== init_sound lda #0 ldx #0 is_l sta SID,x inx cpx #25 bne is_l lda #15 sta SID+24 ; volume max rts sfx_chomp ; short blip; alternate pitch lda chompf eor #1 sta chompf beq + lda #$20 : sta SID+1 lda #$40 : sta SID+0 jmp ++ + lda #$18 : sta SID+1 lda #$80 : sta SID+0 ++ lda #0 : sta SID+5 lda #$f0 : sta SID+6 lda #$21 : sta SID+4 ; sawtooth gate on lda #4 sta sndtimer rts sfx_power lda #$08 : sta SID+1 lda #0 : sta SID+0 lda #$0a : sta SID+5 lda #$aa : sta SID+6 lda #$11 : sta SID+4 ; triangle gate lda #20 sta sndtimer rts sfx_eatghost lda #$40 : sta SID+1 lda #0 : sta SID+0 lda #0 : sta SID+5 lda #$d0 : sta SID+6 lda #$11 : sta SID+4 lda #14 sta sndtimer rts sfx_death_step ; falling sweep using sttimer as pitch lda sttimer lsr sta SID+1 lda #0 : sta SID+0 lda #$00 : sta SID+5 lda #$f8 : sta SID+6 lda #$11 : sta SID+4 rts sfx_update lda sndtimer beq su_ret dec sndtimer bne su_ret ; gate off all voices lda #$20 : sta SID+4 su_ret rts ; ===================================================================== ; data tables ; ===================================================================== ; square table 0..31 sqlo !for i,0,31 { !byte <(i*i) } sqhi !for i,0,31 { !byte >(i*i) } ; screen/colour/tile row base pointers (per maze row 0..20) scrlo !for r,0,20 { !byte <(SCREEN + (MAZE_Y0+r)*40 + MAZE_X0) } scrhi !for r,0,20 { !byte >(SCREEN + (MAZE_Y0+r)*40 + MAZE_X0) } collo !for r,0,20 { !byte <(COLRAM + (MAZE_Y0+r)*40 + MAZE_X0) } colhi !for r,0,20 { !byte >(COLRAM + (MAZE_Y0+r)*40 + MAZE_X0) } tilelo !for r,0,20 { !byte <(TILEMAP + r*MAZE_W) } tilehi !for r,0,20 { !byte >(TILEMAP + r*MAZE_W) } ; scatter corners (index by ghost 1..4 via scat_c-1,x) scat_c !byte 26, 1, 26, 1 scat_r !byte 0, 0, 20, 20 ghcolor !byte 2,10,3,8 ; blinky,pinky,inky,clyde (index ghost-1) ; ghost-eaten scoring (binary): 200,400,800,1600 gscore_lo !byte <200,<400,<800,<1600 gscore_hi !byte >200,>400,>800,>1600 ; wave schedule (mode 0=scatter 1=chase) + duration in frames (50Hz) wave_count = 7 wave_mode !byte 0,1,0,1,0,1,1 wave_secs !word 350,1000,350,1000,250,1000,0 ; last 0 = infinite txt_over !scr "game over",0 ; pac life icon char placeholder pacClosed_ch = 81 ; -------- maze text (21 rows x 28 cols) -------- mazetext !text "############################" !text "#............##............#" !text "#.####.#####.##.#####.####.#" !text "#o####.#####.##.#####.####o#" !text "#..........................#" !text "#.####.##.########.##.####.#" !text "#......##....##....##......#" !text "######.##..........##.######" !text "######.##.###--###.##.######" !text "######.##.# #.##.######" !text " .##.# #.##. " !text "######.##.# #.##.######" !text "######.##.########.##.######" !text "#............##............#" !text "#.####.#####.##.#####.####.#" !text "#o..##....... .......##..o#" !text "###.##.##.########.##.##.###" !text "#......##....##....##......#" !text "#.##########.##.##########.#" !text "#..........................#" !text "############################" ; ===================================================================== ; sprite bitmaps (must be 64-byte aligned in VIC bank) ; ===================================================================== * = $2800 !source "sprites.inc" ; ===================================================================== ; state constants ; ===================================================================== ST_READY = 0 ST_PLAY = 1 ST_DEAD = 2 ST_CLEAR = 3 ST_OVER = 4 ; ===================================================================== ; variables (placed right after the sprite block, ~$29c0) ; ===================================================================== frame !byte 0 state !byte 0 sttimer !byte 0 level !byte 0 lives !byte 0 dots !word 0 score !byte 0,0,0 extra_done !byte 0 combo !byte 0 frightt !byte 0 globmode !byte 0 waveidx !byte 0 wavetimer !word 0 relcnt !word 0 ghidx !byte 0 cur_d !byte 0 rev_d !byte 0 best_d !byte 0 best_lo !byte 0 best_hi !byte 0 tgt_c !byte 0 tgt_r !byte 0 tmpdirs !byte 0,0,0,0 rndseed !byte $a5 chompf !byte 0 sndtimer !byte 0 msbacc !byte 0 bcd0 !byte 0 bcd1 !byte 0 bcd2 !byte 0 drem !byte 0 scorebuf !byte 0,0,0,0,0,0 zt6scr !byte 0 bl_tc_tmp_calc !byte 0 bl_tr_tmp_calc !byte 0 ; actor arrays (index 0=pac, 1..4 ghosts) ax !byte 0,0,0,0,0 ay !byte 0,0,0,0,0 adir !byte 0,0,0,0,0 awant !byte 0,0,0,0,0 amode !byte 0,0,0,0,0