_runtimePath_ "../../runtime" _runtimeStart_ &h7FFF _arraysStart_ &h7FFF _codeRomType_ ROMv5a 'size of your most complex expression, (temporary variables required)*2, defaults to 8 _tempVarSize_ 6 'free string work area, (better not use any of the string runtime!) free STRINGWORKAREA 'free scanline 0, 1, 118 and 119 for code and data free &h0800, 160 free &h0900, 160 free &h7E00, 160 free &h7F00, 160 'use this after _runtimeStart_ to specify maximum number of sprites if you have more than 48 _maxNumSprites_ 61 'defines the amount of contiguous RAM needed for sprite stripes, (in this case 15*6 + 1), also min address and search direction _spriteStripeChunks_ 15, &h3BA0, descending module "PucMonData_ROMv5a.m" 'livesDots <8:lives><8:dots> 'levelPain <8:level><8:pain> 'timeTicks <8:time><8:ticks> 'flags <8:puc flags><8:ghost flags>, puc flags XXXXXXXD, ghost flags XXXXXXSD : where S = slowed, D = dead 'define vars without intitialisation, (initialised in initVars) def livesDots, levelPain, timeTicks, flags, gmode, gx, gy, gxd, gyd, gdir, mz, pyd, oxd, oyd, puci, pucj, ti, tj, bi, bj, zi, zj, px, py, pxd, bonus, eatTicks, scaredTicks gosub initSystem 'call drawGrid 'call drawCells call drawMaze reset: gosub resetLevel start: gosub startLevel init: call initVars repeat call waitArbitrary 'change sprite priority based on who is agressor if gmode &&= SCARED_MODE gosub drawGhosts call drawPucMon else call drawPucMon gosub drawGhosts endif 'level complete if eatTicks.lo &&= 0 if livesDots.lo &&= 0 goto nextLevel endif endif 'puc died if flags.hi &&= 1 flags.hi = 0 livesDots.hi = livesDots.hi - 1 call drawDeath call drawDots, 0 'refresh if livesDots.hi &&= 0 goto reset endif goto init endif 'tunnels sprite NoFlip, Tunnel, (1+ORIGIN_X), (51+ORIGIN_Y) sprite NoFlip, Tunnel, (123+ORIGIN_X), (51+ORIGIN_Y) call drawPills gosub setGhostsMode call moveGhosts gosub handleInput gosub movePucMon inc timeTicks.lo forever 'fast divide by 5 for ci and cj, only works for the domain 0<->319 'this sub need to be fast, so place it early in the source file so that it will most likely 'be placed in a large RAM page and thus not require page jumps 'there is no contiguous memory left on a 32K system for 160byte and 120byte LUTs proc div5 local i, j ci = ci - (ci LSR 6) 'error term i = ci LSL 4 : ci = (i LSL 1) + i + (ci LSL 2) 'multiply by 52 ci = ci.hi 'divide by 256 cj = cj - (cj LSR 6) 'error term j = cj LSL 4 : cj = (j LSL 1) + j + (cj LSL 2) 'multiply by 52 cj = cj.hi 'divide by 256 endproc 'fast modulus by 5 for mx and my, only works for the domain 0<->20734 'this sub need to be fast, so place it early in the source file so that it will most likely 'be placed in a large RAM page and thus not require page jumps and allow us to use full BRA optimisations 'there is no contiguous memory left on a 32K system for 160byte and 120byte LUTs mod5: mx = mx.hi + mx.lo : mx = (mx LSR 4) + (mx AND &h000F) if mx &&> 14 mx = mx - 15 elseif mx &&> 9 mx = mx - 10 elseif mx &&> 4 mx = mx - 5 endif my = my.hi + my.lo : my = (my LSR 4) + (my AND &h000F) if my &&> 14 my = my - 15 elseif my &&> 9 my = my - 10 elseif my &&> 4 my = my - 5 endif return proc drawPucMon local xf, yf if mz &&= WALL sprite NoFlip, PucLt + 2, px, py return endif xf = (px LSR 1) AND 3 yf = (py LSR 1) AND 3 if pxd &&= 1 sprite FlipX, PucRt + xf, px, py elseif pxd &= -1 sprite NoFlip, PucLt + xf, px, py elseif pyd &&= 1 sprite FlipY, PucDn + yf, px, py else sprite NoFlip, PucUp + yf, px, py endif px = px + pxd py = py + pyd endproc drawGhosts: for gidx=0 &to NUM_GHOSTS-1 gosub getGhostVars 'if puc died or level is complete, then erase ghost if &((flags.hi) OR (livesDots.lo = 0)) sprite NoFlip, Erase12x9, gx, gy goto drawNextGhost endif 'ghost dead, scared or normal if &(flags.lo AND 1) gosub drawEyesGhost elseif gmode &&= SCARED_MODE if timeTicks.hi &&< (8-levelPain.hi) call drawScaredGhost else if &((timeTicks.lo LSR 3) AND 1) call drawNormalGhost else call drawScaredGhost endif endif else call drawNormalGhost endif drawNextGhost: next gidx return proc drawNormalGhost local i, xf, yf, yskirt i = gidx LSL 2 xf = (gx LSR 2) AND 1 yf = (gy LSR 2) AND 1 yskirt = gy + 7 if (gxd) if gxd &&= 1 sprite FlipX, peek(@gframes + 0 + i), gx, gy else sprite NoFlip, peek(@gframes + 1 + i), gx, gy endif if &(xf) then sprite NoFlip, peek(@gframes1 + gidx), gx, yskirt 'draw skirt else if gyd &&= 1 sprite NoFlip, peek(@gframes + 2 + i), gx, gy inc yskirt else sprite NoFlip, peek(@gframes + 3 + i), gx, gy endif if &(yf) then sprite NoFlip, peek(@gframes1 + gidx), gx, yskirt 'draw skirt endif endproc proc drawScaredGhost local xf, yf, yskirt xf = (gx LSR 2) AND 1 yf = (gy LSR 2) AND 1 yskirt = gy + 7 if (gxd) if gxd &&= 1 sprite FlipX, ScaredRt, gx, gy else sprite NoFlip, ScaredLt, gx, gy endif if (xf) then sprite NoFlip, Scared_1, gx, yskirt 'draw skirt else if gyd &&= 1 sprite NoFlip, ScaredDn, gx, gy inc yskirt else sprite NoFlip, ScaredUp, gx, gy endif if &(yf) then sprite NoFlip, Scared_1, gx, yskirt 'draw skirt endif endproc drawEyesGhost: if gxd &&= 1 sprite FlipX, EyesRt, gx+2, gy+3 elseif gxd &= -1 sprite NoFlip, EyesLt, gx+2, gy+3 elseif gyd &&= 1 sprite NoFlip, EyesDn, gx+2, gy+3 else sprite FlipY, EyesUp, gx+2, gy+3 endif return proc drawPills local i i = timeTicks.lo AND 7 if i &&= 0 i = (timeTicks.lo LSR 3) AND 1 if peek(PILL_LT) &= PILL then sprite NoFlip, Pill + i, (1*5) + (ORIGIN_X-2), (2*5) + (ORIGIN_Y - 3) if peek(PILL_RT) &&= PILL then sprite NoFlip, Pill + i, (26*5) + (ORIGIN_X-2), (2*5) + (ORIGIN_Y - 3) if peek(PILL_LB) &&= PILL then sprite NoFlip, Pill + i, (1*5) + (ORIGIN_X-2), (18*5) + (ORIGIN_Y - 3) if peek(PILL_RB) &&= PILL then sprite NoFlip, Pill + i, (26*5) + (ORIGIN_X-2), (18*5) + (ORIGIN_Y - 3) endif endproc movePucMon: gosub coordsPucMon puci = ci - pxd pucj = cj - pyd 'dots, pills, doors and tunnels eatTicks.hi = 0 mz = maze(cj, ci) if (mz AND &h0F) &&= DOT eatTicks.hi = 1 maze(cj, ci) = (mz AND &hF0) OR RDOT bcdint @pointsBCD, 10 call drawScore livesDots.lo = livesDots.lo - 1 elseif mz &&= PILL maze(cj, ci) = (mz AND &hF0) OR RPILL gmode = SCARED_MODE timeTicks = 0 bcdint @pointsBCD, 50 call drawScore livesDots.lo = livesDots.lo - 1 elseif mz &&>= DOOR pxd = oxd : pyd = oyd gosub coordsPucMon mz = maze(cj, ci) elseif mz &&= TUNNEL px = (129+ORIGIN_X) - px endif if &((eatTicks.hi) OR (eatTicks.lo)) call eatSound endif return proc moveGhosts for gidx=0 to NUM_GHOSTS-1 gosub getGhostVars 'ghost slowed slowed = 0 if (gmode = SCARED_MODE) OR (flags.lo AND 2) if &(timeTicks.lo XOR 255 AND 1) 'if timeTicks.lo AND 1 = 0 if &(flags.lo XOR 255 AND 1) 'if flags.lo AND 1 = 0 slowed = 1 gxd = 0 : gyd = gxd endif endif endif 'ghost position gx = gx + gxd gy = gy + gyd 'ghost colliding with puc if flags.lo XOR 255 AND 1 'if flags.lo AND 1 = 0 if abs(gx - px) &<= 3 if abs(gy - py) &<= 3 sprite NoFlip, Erase12x9, gx, gy if gmode &= SCARED_MODE sprite NoFlip, PucLt + 2, px, py call deadSound flags.lo = flags.lo OR 1 : ghostsFlags(gidx) = flags.lo bcdint @pointsBCD, bonus : bonus = bonus + bonus call drawScore else flags.hi = 1 endif endif endif endif 'ghost reverses direction if gmode &&= SCARED_MODE if timeTicks.hi &&= 0 if timeTicks.lo &&= 1 gxd = -gxd : gyd = -gyd endif endif endif 'skip ghost AI if not centered on a tile mx = gx : my = gy : gosub mod5 if mx &&<> 3 then goto moveNextGhost if my &&<> 3 then goto moveNextGhost 'ghost look ahead indices ci = gx + gxd + gxd + gxd + -(ORIGIN_X-6) cj = gy + gyd + gyd + gyd + (ORIGIN_Y+2) call div5 'ghost dot indices di = ci - gxd - gxd dj = cj - gyd - gyd 'ghost indices gi = ci - gxd gj = cj - gyd 'ghost replaces dot mz = maze(dj, di) if slowed &&= 0 then gosub replaceDot 'ghost leaves home if maze(gj, gi) &&= EXIT if &(flags.lo XOR 255 AND 1) if gmode &&<> SCARED_MODE gxd = 0 : gyd = -1 endif endif endif 'ghost's next target on gidx gosub getBlinkyTarget, getPinkyTarget, getInkyTarget, getClydeTarget 'ghost home if gi &= 13 if gj &= 10 flags.lo = flags.lo AND &hFE : ghostsFlags(gidx) = flags.lo endif endif 'ghost died, so head home if &(flags.lo AND 1) ti = 12 : tj = 11 endif 'ghost walls, tunnel and junctions mz = maze(cj, ci) if mz &= WALL gosub getWallDir call getGhostDir, gdir elseif mz &&= TUNNEL gx = (129+ORIGIN_X) - gx elseif mz &&= SLOW flags.lo = flags.lo OR 2 : ghostsFlags(gidx) = flags.lo else 'reset slow flag if &(flags.lo AND 2) flags.lo = flags.lo AND &hFD : ghostsFlags(gidx) = flags.lo endif 'junctions and entering cage whilst dead mz = maze(gj, gi) AND &hF0 if &((mz = JUNC) OR (((mz = DOOR) OR (mz = ENTER)) AND (flags.lo AND 1))) gosub getJuncDir call getGhostDir, gdir endif endif moveNextGhost: 'ghost vars update when not slowed if slowed &&= 0 gosub setGhostVars endif next gidx if gmode &&= SCARED_MODE call scaredSound else call chaseSound endif endproc getGhostVars: gx = ghostsX(gidx).lo : gy = ghostsY(gidx).lo gxd = ghostsXd(gidx) : gyd = ghostsYd(gidx) flags.lo = ghostsFlags(gidx) return setGhostVars: ghostsX(gidx) = gx : ghostsY(gidx) = gy ghostsXd(gidx) = gxd : ghostsYd(gidx) = gyd return proc getGhostDir, dir dir = dir LSL 2 gxd = deek(@GHOST_DIRS + dir + 0) gyd = deek(@GHOST_DIRS + dir + 2) endproc setGhostsMode: if &(timeTicks.lo AND &h1F) then return 'roughly every second when running at ~30fps, (mode 2) inc timeTicks.hi 'ghost scared mode lasts approx 10 seconds if gmode &&= SCARED_MODE if timeTicks.hi &&> (10 - levelPain.hi) timeTicks.hi = 0 bonus = 200 gmode = CHASE_MODE endif return endif 'ghosts return to chase mode if timeTicks.hi &&> 30 timeTicks.hi = 0 gmode = CHASE_MODE return endif 'ghosts scatter for 10 seconds out of every 30 seconds if timeTicks.hi &&> (20 + levelPain.hi) gmode = SCATTER_MODE endif return 'Blinkys target is Puc getBlinkyTarget: 'save Blinkys indices for inky's targeting bi = gi : bj = gj if gmode &&= CHASE_MODE ti = puci : tj = pucj return endif 'scatter target ti = 27 : tj = 0 return 'Pinkys target is 4 tiles ahead of Pucs current direction getPinkyTarget: if gmode &&= CHASE_MODE ti = puci + pxd + pxd + pxd + pxd tj = pucj + pyd + pyd + pyd + pyd return endif 'scatter target ti = 0 : tj = 0 return 'Inkys target is (vector from Blinky to (Puc + 2)) * 2 getInkyTarget: if gmode &&= CHASE_MODE ti = puci + pxd + pxd tj = pucj + pyd + pyd zi = ti - bi : zj = tj - bj 'vector from Blinky to (Puc + 2) ti = ti + zi tj = tj + zj return endif 'scatter target ti = 27 : tj = 23 return 'Clyde acts like Blinky until he gets within 8 tiles, then he scatters getClydeTarget: if gmode &= CHASE_MODE if abs(puci - gi) + abs(pucj - gj) &> 8 'taxi-cab distance ti = puci : tj = pucj return endif endif 'scatter target ti = 0 : tj = 23 return getJuncDir: if (gxd) if (abs(tj - gj)) if maze(gj - 1, gi) &&<> WALL gdir = 3 : if tj &&< gj then return endif if maze(gj + 1, gi) &&<> WALL gdir = 1 : if tj &&> gj then return endif endif else if (abs(ti - gi)) if maze(gj, gi - 1) &&<> WALL gdir = 2 : if ti &&< gi then return endif if maze(gj, gi + 1) &&<> WALL gdir = 0 : if ti &&> gi then return endif endif endif if gxd &&= 1 gdir = 0 elseif gxd &&= -1 gdir = 2 elseif gyd &&= 1 gdir = 1 else gdir = 3 endif return getWallDir: if (gxd) if maze(gj - 1, gi) &&<> WALL gdir = 3 : if tj &&< gj then return endif if maze(gj + 1, gi) &&<> WALL then gdir = 1 else if maze(gj, gi - 1) &<> WALL gdir = 2 : if ti &&< gi then return endif if maze(gj, gi + 1) &&<> WALL then gdir = 0 endif return replaceDot: if (mz AND &h0F) &&= DOT di = (di LSL 2) + di + ORIGIN_X 'di = di*5 + 12 dj = (dj LSL 2) + dj + ORIGIN_Y 'dj = dj*5 + 2 poke ((dj + 8) LSL 8) + di, &h2B 'convert dj, di to vram address endif return coordsPucMon: ci = px + pxd + pxd + pxd + -(ORIGIN_X-6) cj = py + pyd + pyd + pyd + (ORIGIN_Y+2) call div5 return handleInput: oxd = pxd : oyd = pyd mx = px : my = py : gosub mod5 gosub get("BUTTON_STATE") return 253: if my &&= 3 then pyd = 0 : pxd = pyd-1 return 254: if my &= 3 then pyd = 0 : pxd = pyd+1 return 247: if mx &&= 3 then pxd = 0 : pyd = pxd-1 return 251: if mx &&= 3 then pxd = 0 : pyd = pxd+1 return proc drawScore local i, char bcdadd @pointsBCD, @scoreBCD, SCORE_LEN char = SCORE_X+ORIGIN_X for i=0 to SCORE_LEN-1 sprite NoFlip, Digit + peek(@scoreBCD + SCORE_LEN-1 - i), char, SCORE_Y+ORIGIN_Y char = char + 6 next i 'bcdcmp requires bcd addrs to point to msd if bcdcmp(@scoreBCD+(SCORE_LEN-1), @highBCD+(SCORE_LEN-1), SCORE_LEN) &&= 1 bcdcpy @scoreBCD, @highBCD, SCORE_LEN call drawHigh endif endproc proc drawHigh local i, char char = HIGH_X+ORIGIN_X for i=0 to SCORE_LEN-1 sprite NoFlip, Digit + peek(@highBCD + SCORE_LEN-1 - i), char, HIGH_Y+ORIGIN_Y char = char + 6 next i endproc proc drawLevel local i, char sprite NoFlip, Level, LEVEL_X+ORIGIN_X, LEVEL_Y+ORIGIN_Y char = LEVEL_X+ORIGIN_X + 6 for i=0 &to LEVEL_LEN-4 sprite NoFlip, Digit + peek(@levelBCD + LEVEL_LEN-4 - i), char, LEVEL_Y+ORIGIN_Y char = char + 6 next i endproc proc drawLives local i, puc i = 1 puc = LIFE_X+ORIGIN_X while i &&<= livesDots.hi sprite NoFlip, Life, puc, LIFE_Y+ORIGIN_Y puc = puc + 6 inc i wend while i &&<= MAX_LIVES sprite NoFlip, Erase6x6, puc, LIFE_Y+ORIGIN_Y puc = puc + 6 inc i wend endproc dim enotes%(4) = 58, 61, 63, 60, 57 proc eatSound local n n = get("MIDI_NOTE", peek(@enotes + eatTicks.lo)) sound on, 2, n, 63, 1 inc eatTicks.lo if eatTicks.lo &&>= 4 eatTicks.lo = 0 sound off, 2 endif endproc proc scaredSound sound on, 1, scaredTicks, 63, 0 scaredTicks = scaredTicks - 650 if scaredTicks &&<= 5000 then scaredTicks = 10000 set SOUND_TIMER, 3 endproc dim gnotes%(15) = 69, 71, 72, 74, 76, 77, 79, 81, 83, 81, 79, 77, 76, 74, 72, 71 proc chaseSound local n, v, f 'volume of chase sound gets higher as blinky gets closer 'v = abs(puci - bi) + abs(pucj - bj) 'frequency escalates as dots decrease f = ((212 - livesDots.lo) LSR 4) LSL 8 n = get("MIDI_NOTE", peek(@gnotes + (timeTicks.lo AND 15))) sound on, 1, n + f, 63, 0 '48-v, 0 set SOUND_TIMER, 3 endproc proc deadSound local i, v, f f = 1000 sound off, 1 repeat for v=1 &to 62 step 2 sound on, 2, f, v, 2 set SOUND_TIMER, 1 next v for v=63 &downto 1 step 2 sound on, 2, f, v, 2 set SOUND_TIMER, 1 next v f = f + 400 until f &>= 10000 sound off, 2 endproc dim dnotes%(15) = 75, 74, 73, 72, 71, 70, 69, 68, 67, 68, 69, 70, 71, 72, 73, 74 proc drawDeath local i, j, n, f call drawLives f = 0 sound off, 1 for i=0 to 128 n = get("MIDI_NOTE", peek(@dnotes + (i AND 15))) sound on, 2, n - f, 63 - (i LSR 2), 3 set SOUND_TIMER, 1 for j=1 &to 250 : next j if ((i LSR 3) AND 1) &&= 0 sprite NoFlip, Erase12x9, px, py else sprite NoFlip, PucLt + 2, px, py endif if (i AND 7) &&= 0 f = f + 200 endif next i sound off, 2 endproc proc drawMaze local i set FG_COLOUR, &h30 for i=0 &to (NUM_MAZE_PIECES*2 - 2) step 4 set CURSOR_XY, deek(@MAZE_PIECES + i) : polyR deek(@MAZE_ADDRS + i) set CURSOR_XY, deek(@MAZE_PIECES + i + 2) : polyR deek(@MAZE_ADDRS + i + 2), FLIPX next i set FG_COLOUR, 0 '&h2B endproc 'redraw=0 is refresh, redraw=1 is redraw proc drawDots, redraw set FG_COLOUR, &h2B for cj=0 to 23 for ci=0 to 27 mz = maze(cj, ci) if redraw &&= 1 if (mz AND &h0F) &&= RDOT 'reset dots mz = (mz AND &hF0) OR DOT elseif (mz AND &h0F) &&= RPILL 'reset pills mz = (mz AND &hF0) OR PILL endif maze(cj, ci) = mz endif if (mz AND &h0F) &= DOT pset (ci LSL 2) + ci + ORIGIN_X, (cj LSL 2) + cj + ORIGIN_Y endif next ci next cj endproc nextLevel: if levelPain.lo &&= 99 then poke &h0101, &h40 'level100 easter egg, good luck once you get to level 100! inc levelPain.lo inc levelPain.hi goto start proc initVars local i, corner 'initialises all variables, (to zero), starting at @timeTicks init vars @timeTicks px = (63+ORIGIN_X) py = (86+ORIGIN_Y) pxd = -1 bonus = 200 scaredTicks = 10000 'reset ghost vars corner = (rnd(0) AND 3) for gidx=0 &to (NUM_GHOSTS - 1) if gidx &&= 0 i = gidx LSL 1 'blinky is always reset to the same position else i = (((corner + gidx) AND 3) + 1) LSL 1 'pinky, inky and clyde are reset to 1 of 4 random cage corners endif gx = deek(@ghostsXr + i) gy = deek(@ghostsYr + i) gxd = -1 'ghosts initially move left gosub setGhostVars next gidx endproc startLevel: sprite NoFlip, Erase12x9, px, py livesDots.lo = 212 bcdint @pointsBCD, 0 bcdint @levelBCD, levelPain.lo if levelPain.hi &&> 7 then levelPain.hi = 7 call drawDots, 1 'redraw call drawScore call drawHigh call drawLives call drawLevel if levelPain.lo &&= 0 play music, MUSIC_INTRO, 2 else wait 120 endif 'repeat 'until get("BUTTON_STATE") &<> 255 return resetLevel: levelPain = 0+0*256 livesDots = 3*256 bcdint @scoreBCD, 0 poke &h0101, 0 return initSystem: 'audio fix for ROMv5a poke &h21, peek(&h21) OR 3 px = (63+ORIGIN_X) : py = (86+ORIGIN_Y) 'scanline 0 and 1 are replaced with scanline 2, (hide scanlines 0 and 1) poke &h0100, &h0A poke &h0102, &h0A 'scanline 118 and 119 are replaced with scanline 117, (hide scanlines 118 and 119) poke &h01EC, &h7D poke &h01EE, &h7D 'use cls rect as we are using hidden parts of VRAM for code and data mode 2 set FGBG_COLOUR, 0 cls &h0A00, 160, 116 return proc waitArbitrary local i for i=0 &to 60 next i return 'proc drawGrid ' local i ' ' set FG_COLOUR, &h15 ' for i=ORIGIN_X to 140+ORIGIN_X step 5 ' line i, 0, i, 119 ' next i ' for i=ORIGIN_Y to 115+ORIGIN_Y step 5 ' line 0, i, 159, i ' next i 'endproc 'proc drawCells ' local i ' ' set FG_COLOUR, &h15 ' for i=10 to 150 step 5 ' line i, 0, i, 119 ' next i ' for i=0 to 115 step 5 ' line 0, i, 159, i ' next i 'endproc