Phew, page 10. I've been typing non-stop, so while I fetch another cup of tea, you can have a go with this Pac-man demo. Hit Play and then use the arrow keys to move.
(If you want to use the arrow keys to scroll this page, pause the demo to un-swallow them)
What can I say? It's not perfect. The main (only) difference between level difficulty is the timing between attack and retreat phases. In the harder levels, the ghosts attack continuously, and looking at the source code, I seem to have forgotten to add any easy levels at all. Pac-man has the longest source code of all the games I made for the console at 2,180 lines, not including the audio functions, music, or bootloader.
I could describe the mechanics of Pac-man in detail, but I probably couldn't do as good a job as other, more dedicated people have done already. The "Pac-man Dossier" was my reference for this, but the site seems to have gone down and isn't in the Wayback Machine. Here's a similarly in-depth explanation.
Looking through my assembly source code, you can see that I hadn't yet realized that avrasm2.exe puts the entire C preprocessor at your service. The memory offsets are labelled with .equ
directives. Also, there isn't a single .dseg
segment in the entirety of the games console, but to be honest I still don't find that functionality all that useful.
Compared to the dense bit-packing you can find on some games consoles, our sprite handling here is very simple. Without the luxury of dedicated video hardware, we want our sprites to be as easy to decode as possible.
Our sprites are either 8x8 or 16x16. The 8x8 ones are stored, astonishingly, as 8 bytes in memory, where each bit is one pixel. The drawing routine loads the byte, then steps through each bit, skipping the delay routine if the bit is clear.
Believe it or not, the 16x16 sprites are stored as 32 bytes. The first two bytes correspond to the first scan-line, and so on. In a way the sprite routines are even simpler than the text routines.
Preparing the binary data from the sprites was done by (gasp!) a javascript routine.
One interesting thing to note is that because of the way the display works, if two sprites overlap, their intensities will add. This can be seen a couple of times in the video, where the ghosts end up on top of each other and appear to be double the brightness.
First, we can do direct memory addressing. This is storing something at a fixed point in memory.
ldi r16, 123 sts $0105, r16
This loads the immediate value 123 into register r16, then puts the contents or r16 at address $0105. That code will always end up putting 123 into address $0105, no matter what.
If we want to use lots of memory we need to use pointers. This is called indirect addressing. For instance, the paint program (which I didn't demonstrate in the video) has a list of points which are drawn. We loop through this list to draw them to the screen. There are three hardware pointers in the AVR architecture, called X, Y, and Z. (They correspond to register pairs r27:r26, r29:r28 and r31:r30.)
Not only does using pointers let us loop over data, the opcodes are shorter and the instructions run faster, since we don't have to re-load a 16-bit address each time.
ldi r16, 99 st Y, r16
That stores 99 to wherever Y is pointing to. In the paint program, when we press the button, the position under the cursor is added to the list. We want to store the data, and then increment the pointer to the next position. This is such a common operation that there are special instructions for it, and they have this syntax.
ldi r16, 77 ldi r17, 88 st Y+, r16 st Y+, r17
This instruction is called post-increment, there is also a pre-decrement version, which has the syntax st -Y, r16
.
Now we get to the fun bit. Supposing we want to implement a structure, or an object. We might have several instances of it, and each instance has a number of properties. In the case of pac-man, each ghost is one of these objects. The specific logic for a ghost's behaviour only deals with its target tile. Everything else about it, how it routes itself through the maze to that tile, is shared with all the ghosts.
The load-with-displacement (ldd) and store-with-displacement (std) instructions are meant for this scenario. In it's simplest form, the syntax looks like this:
ldi r16, 22 std Y+5, r16
That puts 22 in the memory location 5 bytes ahead of where Y is pointing. Now we can make a bunch of definitions (either with #define
or .equ
directives) to label the start of each ghost's allocated memory, and similar definitions to serve as an enum for their properties.
To draw a ghost, we do this.
ldi YH, HIGH(blinky) ldi YL, LOW(blinky) rcall drawGhost
YH and YL are the 8-bit components of the Y register. In the drawGhost routine, we use the displacement instructions to access the properties, starting out checking which sprite to draw.
drawGhost: ;if dead... ldd r17, Y+GHOST_MODE cpi r17, DEAD breq drawDeadGhost ldd r17, Y+GHOST_SCARED cpi r17,1 brne drawGhostNotScared ; and so on...
Note that because we have the preprocessor, we can do basic (or even complex) arithmetic with the definitions. If we want to directly access the property of a ghost, such as seeing which mode blinky is in, we can use this very similar syntax:
lds r16, blinky+GHOST_MODE
It's a subtle difference. In this case, the preprocessor resolves the addition of blinky and GHOST_MODE (something like $0114), then sends the result to the assembler. In contrast, with the ldd
instruction, the Y+ is not resolved, it's used as a separator between arguments to the instruction.
Of course, without these displacement instructions, we could still implement the tabular data, by adding and subtracting to the pointer each time, but the result would be a lot slower and there'd be a risk of it going out of sync.
One other thing I should say, after taking a brief look at the code. I did all my memory addresses with .equ
directives, but the "proper" way to do this is with a data segment (.dseg
). This is pretty well documented, I just hadn't run in to it at that point in time. With it, you can reserve areas of memory with .byte
and so on, and place labels which can be used as constants by the preprocessor.
Now that I think about it, it's not that different to C at all, just that the syntax is different. The square brackets in C denote an addition. No, really – you can prove it by doing something like this:
char asdf[] = {5, 6, 7, 8, 9}; printf("%d", asdf[2] ); // Prints 7 printf("%d", 2[asdf] ); // Also prints 7
Crazy huh?
Hopefully that makes the code below a little easier to follow. I'll include the highlighted Javascript source code too, but remember that it was written for my own reference while writing the real thing in assembly, so I make no promises about it.
If skipping through largely uncommented source code isn't your thing, the escapades escalate on page 11.
<canvas width=224 height=248 id=cnv style='background-color:#000'></canvas> <script> paused=true c=document.getElementById('cnv') ctx=c.getContext('2d'); c.width*=2;c.height*=2;ctx.scale(2,2) ctx.mozImageSmoothingEnabled=false; ctx.webkitImageSmoothingEnabled=false; ctx.imageSmoothingEnabled=false; //0 = empty //1 = wall //2 = food //3 = energizer map = [ [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1], [1,3,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,3,1], [1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], [0,0,0,0,1,1,2,1,1,0,0,0,0,0,0,0,0,0,0,1,1,2,1,1,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], [1,1,1,1,1,1,2,1,1,0,1,1,0,0,0,0,1,1,0,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,0,2,0,0,0,1,1,0,0,0,0,1,1,0,0,0,2,0,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], [1,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,1], [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1], [1,3,2,2,1,1,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,1,1,2,2,3,1], [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] ]; ///////////// boundary=5 cornering=2 GHOST_X=0 GHOST_Y=1 GHOST_DIR=2 GHOST_MODE=3 GHOST_SCARED=4 GHOST_SPEED=5 NORMAL=0 DEAD =1 RELEASED=2 TRAPPED =3 // or greater RIGHT =0 LEFT =1 UP =2 DOWN =3 MODELENGTH=1000 ENERGYLENGTH=500 ///////////// PacX = 112 PacY = 190 PacDir=UP PacFrame=0 FoodEaten=0 PacDead=0; PacEatGhost=0; PacGhostCounter=0; GlobalMode = 1 // 1=chase, 0=scatter cherry=false EnergizerTimer=0 ModeTimer=1 score=0 GhostFrame=0 blinky=[112,88,0,0,0,0] pinky =[112,96,0,4,0,0] inky =[112,96,0,255,0,0] clyde =[112,96,0,255,0,0] function draw(){ if (PacDead>0) { ctx.drawImage(img,0,0); if (PacDead>15){ ctx.drawImage(img,229+32+Math.floor(15-PacDead/5)*16,1,14,14,PacX-7,PacY-7,14,14) ctx.drawImage(img,229+32+Math.floor(15-PacFrame/5)*16,1,14,14,PacX-7-232,PacY-7,14,14) }else { PacX = 112 PacY = 190 PacDir=UP cherry=false } PacDead--; requestAnimationFrame(draw); return; } if (PacEatGhost>0) { ctx.drawImage(img,0,0); ctx.drawImage(img,229-16+PacGhostCounter*16,133,16,7,PacX-8,PacY-4,16,7); PacEatGhost--; requestAnimationFrame(draw); return; } var temp=map[PacY>>3][PacX>>3] if (temp==2) { // Eat Food map[PacY>>3][PacX>>3]=0 score+=10 eatFood() } else if (temp==3) { // Eat Energizer map[PacY>>3][PacX>>3]=0 score+=50 frighten(blinky) frighten( inky) frighten( pinky) frighten( clyde) reverseDirection(blinky); reverseDirection( inky); reverseDirection( pinky); reverseDirection( clyde); EnergizerTimer=ENERGYLENGTH eatFood() } else if (cherry&&PacX==112&&PacY==140){ score+=100 cherry=false }else{ //Do not move while eating var tunnel=inTunnel(PacX,PacY); temp=(PacY+4)%8 if ((keys[39] && (temp<cornering||temp>8-cornering)) || PacDir==RIGHT) { if (map[PacY>>3][PacX+boundary>>3]!=1) { PacX++ PacDir=0 PacFrame++ snapY(); } } if ((keys[37]&& (temp<cornering||temp>8-cornering)) || PacDir==LEFT) { if (map[PacY>>3][PacX-boundary>>3]!=1) { PacX-- PacDir=1 PacFrame++ snapY(); } } temp=(PacX+4)%8 if ((keys[40]&& (temp<cornering||temp>8-cornering) &&!tunnel) || PacDir==DOWN) { if (map[PacY+boundary>>3][PacX>>3]!=1) { PacY++ PacDir=3 PacFrame++ snapX(); } } if ((keys[38]&& (temp<cornering||temp>8-cornering) &&!tunnel) || PacDir==UP) { if (map[PacY-boundary>>3][PacX>>3]!=1) { PacY-- PacDir=2 PacFrame++ snapX(); } } PacX=wrap(PacX) } if (!EnergizerTimer) { PacGhostCounter=0; /* ModeTimer--; if (ModeTimer==0) { if (GlobalMode) GlobalMode=0,ModeTimer=MODELENGTH>>2; else GlobalMode=1,ModeTimer=MODELENGTH reverseDirection(blinky); reverseDirection( inky); reverseDirection( pinky); reverseDirection( clyde); } */ } else { EnergizerTimer--; } processGhost(blinky) processGhost(inky) processGhost(pinky) processGhost(clyde) ctx.drawImage(img,0,0); for (var j=0;j<map.length;j++) for (var i=0;i<map[j].length;i++) { if (map[j][i]==2) ctx.drawImage(img,225,240,8,8,i*8,j*8,8,8) // Dots if (map[j][i]==3 && GhostFrame>5) ctx.drawImage(img,233,240,8,8,i*8,j*8,8,8) // Energizers } if (cherry) ctx.drawImage(img,279,50,12,12,108,134,12,12) drawGhost(blinky,0) drawGhost(pinky,1) drawGhost(inky,2) drawGhost(clyde,3) GhostFrame=(GhostFrame+1)%(8*2) PacFrame=PacFrame%(3*3) ctx.drawImage(img,229+Math.floor(PacFrame/3)*16,1+PacDir*16,14,14,PacX-7,PacY-7,14,14) ctx.drawImage(img,229+Math.floor(PacFrame/3)*16,1+PacDir*16,14,14,PacX-7-232,PacY-7,14,14) if (!paused) requestAnimationFrame(draw); } function drawGhost(ghost,offset){ var x=230+ghost[GHOST_DIR]*32+Math.floor(GhostFrame/8)*16, y=65+offset*16; if (ghost[GHOST_SCARED]==true) x=358 +Math.floor(GhostFrame/8)*16 + (EnergizerTimer<128 &&(EnergizerTimer%32)<16?32:0),y=65 if (ghost[GHOST_MODE]==DEAD) x=358 + ghost[GHOST_DIR]*16,y=81; ctx.drawImage(img,x,y,14,14,ghost[GHOST_X]-2,ghost[GHOST_Y]-2,14,14); ctx.drawImage(img,x,y,14,14,ghost[GHOST_X]-2-232,ghost[GHOST_Y]-2,14,14); } function snapY(){ if (map[PacY+3>>3][PacX>>3]==1) { PacY-- } if (map[PacY-4>>3][PacX>>3]==1) { PacY++ } } function snapX(){ if (map[PacY>>3][PacX+3>>3]==1) { PacX-- } if (map[PacY>>3][PacX-4>>3]==1) { PacX++ } } function eatFood(){ FoodEaten++ // at 70 and at 170, make cherry appear if (FoodEaten==70||FoodEaten==170) cherry=true if (FoodEaten==30) inky[GHOST_MODE]=RELEASED; if (FoodEaten==82) clyde[GHOST_MODE]=RELEASED; if (FoodEaten==244) alert('over') } function wrap(x){ if (x==0) x=232 if (x==233) x=1 return x; } function xDir(a){ if (a==RIGHT) return 1; if (a==LEFT) return -1; return 0; } function yDir(a){ if (a==DOWN) return 1; if (a==UP) return -1; return 0; } function inTunnel(x,y){ return (y<126&&y>105&& (x<44 || x>180)); } function reverseDirection(ghost){ if (ghost[GHOST_DIR]==RIGHT) ghost[GHOST_DIR]=LEFT; else if (ghost[GHOST_DIR]==LEFT) ghost[GHOST_DIR]=RIGHT; else if (ghost[GHOST_DIR]==UP) ghost[GHOST_DIR]=DOWN; else ghost[GHOST_DIR]=UP; } function frighten(ghost){ ghost[GHOST_SCARED]=true } function resetGhost(ghost,time){ if(ghost[GHOST_MODE]<TRAPPED) { ghost[GHOST_MODE]=4+time ghost[GHOST_X]=112 ghost[GHOST_Y]=96 } } function processGhost(ghost){ var targetX,targetY; var tunnel=inTunnel(ghost[GHOST_X],ghost[GHOST_Y]) if (EnergizerTimer==0 && ghost[GHOST_SCARED]==true) ghost[GHOST_SCARED]=false; if ((PacX>>3==(ghost[GHOST_X]+4)>>3) && (PacY>>3==(ghost[GHOST_Y]+4)>>3)) { if (ghost[GHOST_SCARED]==true) { ghost[GHOST_MODE]=DEAD ghost[GHOST_SCARED]=false ghost[GHOST_X]=ghost[GHOST_X]>>1 //Clear LSB ghost[GHOST_X]=ghost[GHOST_X]<<1 ghost[GHOST_Y]=ghost[GHOST_Y]>>1 ghost[GHOST_Y]=ghost[GHOST_Y]<<1 //score... //animation... PacGhostCounter++; score+= 100<<PacGhostCounter PacEatGhost=40; }else if (ghost[GHOST_MODE]!=DEAD) { PacDead=15*5; resetGhost(blinky,0) resetGhost(pinky,10) resetGhost(inky,20) resetGhost(clyde,30) } } newDir: if (ghost[GHOST_X]%8==0 && ghost[GHOST_Y]%8==0 &&!tunnel) { if (ghost[GHOST_SCARED]==true){ targetX = Math.floor(Math.random()*2)*28 targetY = Math.floor(Math.random()*2)*31 } else if (ghost[GHOST_MODE]==NORMAL){ if (GlobalMode==0) { //Scatter if (ghost==pinky) targetX=3, targetY=0; if (ghost==blinky) targetX=25,targetY=0; if (ghost==inky) targetX=28,targetY=31; if (ghost==clyde) targetX=0, targetY=31; } else { // Chase targetX=PacX>>3; targetY=PacY>>3; if (ghost==pinky) { targetX+=xDir(PacDir)<<2 targetY+=yDir(PacDir)<<2 } if (ghost==clyde) { var distance = (ghost[GHOST_Y]/8-(PacY>>3))*(ghost[GHOST_Y]/8-(PacY>>3)) + (ghost[GHOST_X]/8-1-(PacX>>3))*(ghost[GHOST_X]/8-1-(PacX>>3)); if (distance<64) targetX=0, targetY=31; } if (ghost==inky) { var xd,yd; targetX+=xDir(PacDir)<<1 targetY+=yDir(PacDir)<<1 xd = blinky[GHOST_X]>>3 yd = blinky[GHOST_Y]>>3 xd -= targetX yd -= targetY xd=xd<<1 yd=yd<<1 targetX-=xd targetY-=yd } } } if (ghost[GHOST_MODE]==DEAD){ targetX = 14 targetY = 11 if (ghost[GHOST_X]>>3 ==13 && ghost[GHOST_Y]>>3 ==11) { ghost[GHOST_MODE]=TRAPPED+30 ghost[GHOST_Y]++ ghost[GHOST_DIR]=DOWN break newDir; } } if (ghost[GHOST_MODE]==RELEASED){ targetX = 13 targetY = 11 if (ghost[GHOST_Y]>>3 ==13 && ghost[GHOST_X]>>3 ==13) { ghost[GHOST_MODE]=NORMAL ghost[GHOST_DIR]=UP ghost[GHOST_Y]--; } } if (ghost[GHOST_MODE]>=TRAPPED){ if (ghost[GHOST_MODE]<255) ghost[GHOST_MODE]--; targetY = 14 targetX = 13 if (ghost==inky) targetX = 11 if (ghost==clyde) targetX = 15 } //Find paths var distanceRight=10000, distanceLeft=10000, distanceUp =10000, distanceDown = 10000, minimum=10000; //If tile is not the one we came from, and is not a wall... if ((ghost[GHOST_DIR]!=LEFT) && map[(ghost[GHOST_Y]>>3)][(ghost[GHOST_X]>>3) +1 ]!=1) { distanceRight= (ghost[GHOST_Y]/8-targetY)*(ghost[GHOST_Y]/8-targetY) + (ghost[GHOST_X]/8+1-targetX)*(ghost[GHOST_X]/8+1-targetX) if (distanceRight<minimum) minimum=distanceRight } if ((ghost[GHOST_DIR]!=RIGHT) && map[(ghost[GHOST_Y]>>3)][(ghost[GHOST_X]>>3) -1 ]!=1) { distanceLeft= (ghost[GHOST_Y]/8-targetY)*(ghost[GHOST_Y]/8-targetY) + (ghost[GHOST_X]/8-1-targetX)*(ghost[GHOST_X]/8-1-targetX) if (distanceLeft<minimum) minimum=distanceLeft } if ((ghost[GHOST_DIR]!=DOWN) && map[(ghost[GHOST_Y]>>3) -1 ][(ghost[GHOST_X]>>3) ]!=1) { distanceUp= (ghost[GHOST_Y]/8-1-targetY)*(ghost[GHOST_Y]/8-1-targetY) + (ghost[GHOST_X]/8-targetX)*(ghost[GHOST_X]/8-targetX) if (distanceUp<minimum) minimum=distanceUp } if ((ghost[GHOST_DIR]!=UP) && map[(ghost[GHOST_Y]>>3) +1 ][(ghost[GHOST_X]>>3) ]!=1) { distanceDown= (ghost[GHOST_Y]/8+1-targetY)*(ghost[GHOST_Y]/8+1-targetY) + (ghost[GHOST_X]/8-targetX)*(ghost[GHOST_X]/8-targetX) if (distanceDown<minimum) minimum=distanceDown } if (distanceUp==minimum) ghost[GHOST_DIR]=UP else if (distanceRight==minimum) ghost[GHOST_DIR]=RIGHT else if (distanceDown==minimum) ghost[GHOST_DIR]=DOWN else if (distanceLeft==minimum) ghost[GHOST_DIR]=LEFT } var doubleSpeed=1, speed=9 if (tunnel) speed=5 if (ghost[GHOST_SCARED]) speed=6 if (ghost[GHOST_X]%8==0 && ghost[GHOST_Y]%8==0) speed=10 if (ghost[GHOST_MODE]==DEAD) doubleSpeed=2,speed=10; while (doubleSpeed--){ ghost[GHOST_SPEED]+=speed if (ghost[GHOST_SPEED]>9) { if (ghost[GHOST_DIR]==RIGHT) ghost[GHOST_X]++; if (ghost[GHOST_DIR]==LEFT) ghost[GHOST_X]--; if (ghost[GHOST_DIR]==UP) ghost[GHOST_Y]--; if (ghost[GHOST_DIR]==DOWN) ghost[GHOST_Y]++; ghost[GHOST_SPEED]-=10; } ghost[GHOST_X]=wrap(ghost[GHOST_X]); } } keys=[]; keys[37]=keys[38]=keys[39]=keys[40]=0 document.onkeydown=function(e){keys[e.keyCode]=true; if (!paused) {e.preventDefault(); e.stopPropagation(); return false;}} document.onkeyup=function(e){keys[e.keyCode]=false; if (!paused) {e.preventDefault(); e.stopPropagation(); return false;}}; (img=new Image()).onload=function(){ draw(); } img.src="PacManSprites.png"; </script>
### PacMan.asm ### .def PacDir = r24 .def PacX = r15 .def PacY = r14 .def PacEatGhost = r13 .def FoodEaten = r12 .def PacGhostCounter = r11 .def EnergizerTimer = r10 .def GhostFrame = r9 .def PacDead = r8 .def Cherry = r7 .def Lives = r6 ; r2 is controller read .equ RIGHT =0 .equ LEFT =1 .equ UP =2 .equ DOWN =3 ; ghost modes .equ NORMAL =0 .equ DEAD =1 .equ RELEASED=2 .equ TRAPPED =3 .equ GHOST_PARAMS = 6 ; number of columns in ghost data table .equ GHOST_X=0 .equ GHOST_Y=1 .equ GHOST_DIR=2 .equ GHOST_MODE=3 .equ GHOST_SCARED=4 .equ GHOST_SPEED=5 .equ blinky = D_START .equ inky = D_START +GHOST_PARAMS .equ pinky = D_START +(2*GHOST_PARAMS) .equ clyde = D_START +(3*GHOST_PARAMS) .equ MAPDATA = D_START+(4*GHOST_PARAMS) ;init frame counter ldi r16,0 out TCNT0, r16 ldi r16, (1<<CS02)|(1<<CS01)|(1<<CS00) out TCCR0, r16 ldi r16, 128 out OCR0, r16 ; Reset score ldi r16,0 sts SCORE_L,r16 sts SCORE_M,r16 sts SCORE_H,r16 ldi r16,4 mov Lives,r16 resetLevel: ; Load whole map into ram ldi ZH, HIGH(Map*2) ldi ZL, LOW(Map*2) ldi YH, HIGH(MAPDATA) ldi YL, LOW(MAPDATA) loadWholeMapLoop: lpm r16,Z+ ; Cycle through each byte st Y+,r16 cpi ZH, HIGH(Map*2 + 28*31) ;Map has 28 * 31 tiles brne loadWholeMapLoop cpi ZL, LOW(Map*2 + 28*31) brne loadWholeMapLoop ; Fill registers with initial values ldi r16, 112 mov PacX,r16 ldi r16, 190 mov PacY,r16 ldi r16, LEFT mov PacDir,r16 ldi r16, 0 mov FoodEaten,r16 mov PacGhostCounter,r16 mov EnergizerTimer, r16 mov PacEatGhost, r16 mov GhostFrame, r16 mov PacDead, r16 mov Cherry, r16 ; Load ghosts ldi r16,112 sts blinky+GHOST_X , r16 ldi r16,88 sts blinky+GHOST_Y , r16 ldi r16,0 sts blinky+GHOST_DIR , r16 sts blinky+GHOST_MODE , r16 sts blinky+GHOST_SCARED , r16 sts blinky+GHOST_SPEED , r16 ;pinky ldi r16,112 sts pinky+GHOST_X , r16 ldi r16,96 sts pinky+GHOST_Y , r16 ldi r16,4 sts pinky+GHOST_MODE , r16 ldi r16,0 sts pinky+GHOST_DIR , r16 sts pinky+GHOST_SCARED , r16 sts pinky+GHOST_SPEED , r16 ;inky and clyde (same init values) ldi r16,112 sts inky+GHOST_X , r16 sts clyde+GHOST_X , r16 ldi r16,96 sts inky+GHOST_Y , r16 sts clyde+GHOST_Y , r16 ldi r16,255 sts inky+GHOST_MODE , r16 sts clyde+GHOST_MODE , r16 ldi r16,0 sts inky+GHOST_DIR , r16 sts inky+GHOST_SCARED , r16 sts inky+GHOST_SPEED , r16 sts clyde+GHOST_DIR , r16 sts clyde+GHOST_SCARED , r16 sts clyde+GHOST_SPEED , r16 ; Init music ldi r16, HIGH(Music*2) sts MUSIC_START_H, r16 ldi r16, LOW(Music*2) sts MUSIC_START_L, r16 call resetMusic levelStartLoop: rcall drawMap ldi ZL, LOW(PacSprites*2+32*3) ldi ZH,HIGH(PacSprites*2+32*3) ldi r16,118 ldi r17,181 rcall drawSprite16 ; draw static ghosts ; draw "ready" ldi ZL, LOW(GhostSprites*2) ldi ZH,HIGH(GhostSprites*2) ldi r16,135 ldi r17,110 rcall drawSprite16 ldi ZL, LOW(GhostSprites*2+192) ldi ZH,HIGH(GhostSprites*2+192) ldi r16,119 ldi r17,110 rcall drawSprite16 ldi ZL, LOW(GhostSprites*2+64) ldi ZH,HIGH(GhostSprites*2+64) ldi r16,103 ldi r17,110 rcall drawSprite16 ldi ZL, LOW(GhostSprites*2+64) ldi ZH,HIGH(GhostSprites*2+64) ldi r16,119 ldi r17,85 rcall drawSprite16 ldi r16, 128 ldi r17, 142 ldi r18,'R' rcall drawChar ldi r18,'E' rcall drawChar ldi r18,'A' rcall drawChar ldi r18,'D' rcall drawChar ldi r18,'Y' rcall drawChar ldi r18,'!' rcall drawChar lds r16, MUSIC_H lds r17, MUSIC_L ldi r18, HIGH(Music*2+792) ldi r19, LOW(Music*2+792) cp r17,r19 cpc r16,r18 breq beginLevel rjmp levelStartLoop beginLevel: ldi r16, HIGH(Siren1*2) sts MUSIC_START_H, r16 ldi r16, LOW(Siren1*2) sts MUSIC_START_L, r16 call resetMusic Main: ;Main pacman loop rcall readController ; if PacDead... ldi r16,0 cp PacDead, r16 breq pacTestEatGhost ; Death animation rcall drawMap ldi ZH, HIGH(PacDeathAnimation*2) ldi ZL, LOW(PacDeathAnimation*2) ldi r17,0 mov r16,PacDead andi r16, 0b11111000 lsl r16 rol r17 lsl r16 rol r17 add ZL, r16 adc ZH, r17 mov r16, PacX mov r17, PacY subi r16,-6 subi r17,7 rcall drawSprite16 ldi r16, HIGH(musicOff*2) sts MUSIC_START_H, r16 ldi r16, LOW(musicOff*2) sts MUSIC_START_L, r16 ; reset if ended dec PacDead breq PacDeathAnimOver rjmp waitForFrame GameOver: ;jmp 0 jmp BootRunHighscores PacDeathAnimOver: ldi r16,112 mov PacX,r16 ldi r16,190 mov PacY,r16 ldi r16,UP mov PacDir,r16 ldi r16,0 mov Cherry,r16 dec Lives breq GameOver pacTestEatGhost: ; if PacEatGhost... ldi r16,0 cp PacEatGhost,r16 breq pacTestEatFood rcall drawMap ;[ draw score number at pacx, pacy] ldi ZH, HIGH(ScoreNumberSprites*2-32) ldi ZL, LOW(ScoreNumberSprites*2-32) ldi r16, 32 mul PacGhostCounter,r16 add ZL,r0 add ZH,r1 mov r16, PacX mov r17, PacY subi r16,-6 subi r17,7 rcall drawSprite16 ;call allnotesoff ;ldi r16,100 ;sub r16,PacEatGhost ;call setNote2 dec PacEatGhost rjmp waitForFrame pacTestEatFood: mov r16,PacX mov r17,PacY rcall getMapTile ; check for eating stuff cpi r16,2 brne pacTestNotDot rjmp EatDot pacTestNotDot: cpi r16,3 brne pacTestNotEnergizer rjmp EatEnergizer pacTestNotEnergizer: ldi r16,20 cp Cherry,r16 brne pacMovementMain ldi r16,112 ; if we are exactly where the cherry is ldi r17,140 cp PacX,r16 cpc PacY,r17 brne pacMovementMain ldi r16,100 ; increase score ldi r17,0 rcall incScore dec Cherry ; and start cherry score anim ; Pacman Movement pacMovementMain: mov r25,PacY ; For cornering: subi r25,-4 ; How well lined up with the grid are we? andi r25, 0b00000111 ; Get the tile 'overflow' amount PacCheckRight: cpi PacDir, RIGHT ; Already facing right? move breq PacMoveRight sbrs r2, ctrlLeft ; Are we trying to go right? rjmp PacCheckLeft ; No? move along cpi r25, 1 brcs PacMoveRight ; Are we within the cornering radius? cpi r25, 7 brcc PacMoveRight rjmp PacCheckLeft PacMoveRight: mov r16,PacX subi r16,-4 mov r17,PacY rcall getMapTile cpi r16,1 breq PacCheckLeft inc PacX ldi PacDir,RIGHT rcall snapY PacCheckLeft: cpi PacDir, LEFT breq PacMoveLeft sbrs r2,ctrlRight rjmp PacCheckLeftEnd cpi r25, 1 brcs PacMoveLeft cpi r25, 7 brcc PacMoveLeft rjmp PacCheckLeftEnd PacMoveLeft: mov r16,PacX subi r16,5 mov r17,PacY rcall getMapTile cpi r16,1 breq PacCheckLeftEnd dec PacX ldi PacDir,LEFT rcall snapY PacCheckLeftEnd: mov r25,PacX ; Now check cornering for X subi r25,-4 ; How well lined up with the grid are we? andi r25, 0b00000111 ; Get the tile 'overflow' amount mov r16,PacX mov r17,PacY ;rcall inTunnel ; Don't move up or down if we're in the tunnel ;breq PacMovementEnd ; ;;;redundant PacCheckDown: cpi PacDir, DOWN breq PacMoveDown sbrs r2,ctrlDown rjmp PacCheckUp cpi r25, 1 brcs PacMoveDown cpi r25, 7 brcc PacMoveDown rjmp PacCheckUp PacMoveDown: mov r16,PacX mov r17,PacY subi r17,-4 rcall getMapTile cpi r16,1 breq PacCheckUp inc PacY ldi PacDir,DOWN rcall snapX PacCheckUp: cpi PacDir, UP breq PacMoveUp sbrs r2,ctrlUp rjmp PacMovementEnd cpi r25, 1 brcs PacMoveUp cpi r25, 7 brcc PacMoveUp rjmp PacMovementEnd PacMoveUp: mov r16,PacX mov r17,PacY subi r17,5 rcall getMapTile cpi r16,1 breq PacMovementEnd dec PacY ldi PacDir,UP rcall snapX PacMovementEnd: mov r16, PacX rcall wrapX mov PacX, r16 dec EnergizerTimer brne EnergizerNotZero inc EnergizerTimer ldi r16,0 mov PacGhostCounter, r16 ldi r16, HIGH(Siren1*2) sts MUSIC_START_H, r16 ldi r16, LOW(Siren1*2) sts MUSIC_START_L, r16 EnergizerNotZero: inc GhostFrame ; counts up to 16, then wraps ldi r16, $0F and GhostFrame, r16 ; Process Ghosts... ldi ZH, HIGH(blinky) ldi ZL, LOW(blinky) rcall processGhost adiw Z, GHOST_PARAMS rcall processGhost adiw Z, GHOST_PARAMS rcall processGhost adiw Z, GHOST_PARAMS rcall processGhost rcall drawMap ; Draw ghosts... ldi YH, HIGH(blinky) ldi YL, LOW(blinky) rcall drawGhost adiw Y, GHOST_PARAMS rcall drawGhost adiw Y, GHOST_PARAMS rcall drawGhost adiw Y, GHOST_PARAMS rcall drawGhost ; Work out which pacman sprite to draw ; Factor in direction and animation frame ldi ZL, LOW(PacSprites*2) ldi ZH,HIGH(PacSprites*2) mov r17,PacX ; Animation frame - this is just a add r17,PacY ; simple way to get a frame number lsr r17 andi r17, 0b00000011 ; (animation should stop when we stop moving) cpi r17, 3 brne setFrame ldi r17,0 setFrame: ; r17 is now either 0, 1 or 2 ldi r16, 32 mul r17, r16 ; Now r0 is either 0, 32 or 64 add ZL,r0 adc ZH,r1 ldi r16, 96 mul PacDir,r16 ; Advance to direction sprite add ZL,r0 adc ZH,r1 mov r16,PacX mov r17,PacY subi r16,-6 subi r17,6 rcall drawSprite16 waitforframe: in r16, TIFR sbrs r16, OCF0 rjmp waitforframe ldi r16, (1<<OCF0) out TIFR, r16 ldi r16, 0 out TCNT0, r16 rjmp Main incScore: lds r0,SCORE_L lds r1,SCORE_M add r0,r16 adc r1,r17 sts SCORE_L,r0 sts SCORE_M,r1 lds r0,SCORE_H ; carry into third byte ldi r16,0 adc r0,r16 ret EatDot: ; score+=10 ldi r16,10 ldi r17,0 rcall incScore ldi r16, HIGH(soundEatDot*2) sts EFFECT_H, r16 ldi r16, LOW(soundEatDot*2) sts EFFECT_L, r16 rcall eatFood rjmp PacMovementEnd EatEnergizer: ; score +=50 ldi r16,50 ldi r17,0 rcall incScore ldi r16, HIGH(EnegSiren*2) sts MUSIC_START_H, r16 ldi r16, LOW(EnegSiren*2) sts MUSIC_START_L, r16 call resetMusic ; frighten ghosts ldi r16,1 sts blinky+GHOST_SCARED,r16 sts inky + GHOST_SCARED,r16 sts pinky+ GHOST_SCARED,r16 sts clyde+ GHOST_SCARED,r16 ; reverse their directions lds r16, blinky+GHOST_DIR rcall reverseDirection sts blinky+GHOST_DIR, r16 lds r16, inky+GHOST_DIR rcall reverseDirection sts inky+GHOST_DIR, r16 lds r16, pinky+GHOST_DIR rcall reverseDirection sts pinky+GHOST_DIR, r16 lds r16, clyde+GHOST_DIR rcall reverseDirection sts clyde+GHOST_DIR, r16 ; set timer ldi r16, 255 mov EnergizerTimer,r16 rcall eatFood rjmp PacMovementEnd ; Snap to grid functions. Although the grid is 8pixels a side, ; there is one pixel we want to be in. So arbitrarily check 3 pixels ; to one side and 4 pixels to the other, and call it centred. snapY: mov r16,PacX mov r17,PacY subi r17,-3 ; check 3 pixels above rcall getMapTile ; getMapTile returns in r16 cpi r16,1 brne snapYCheckbelow ; if r16=1, we're in a wall dec PacY ; move away snapYCheckbelow: mov r16,PacX mov r17,PacY subi r17,4 ; check 4 pixels below rcall getMapTile cpi r16,1 brne snapYend inc PacY snapYend: ret snapX: mov r16,PacX mov r17,PacY subi r16,-3 ; check 3 pixels right rcall getMapTile ; getMapTile returns in r16 cpi r16,1 brne snapXcheckLeft dec PacX ; move away snapXcheckLeft: mov r16,PacX mov r17,PacY subi r16,4 ; check 4 pixels left rcall getMapTile cpi r16,1 brne snapXend inc PacX snapXend: ret ; Function getMapTile(x pixels,y pixels) ; x=16,y=17, getMapTile: ldi YH, HIGH(MAPDATA) ; Map data is 28 bytes wide, 31 tall ldi YL, LOW(MAPDATA) ; We need to divide X by 8 and round down lsr r16 lsr r16 lsr r16 ; shifts round down for us add YL, r16 ; increase pointer by this amount ldi r16,0 adc YH, r16 ; We need to round down and multiply Y by 28/8 lsr r17 ; divide by 2 andi r17,0b11111100 ; Round down ldi r16,7 ; multiply by 7 mul r17,r16 add YL, r0 ; result is in r1:r0 adc YH, r1 ld r16,Y ret ; Function inTunnel(x=r16,y=r17) ; just check if we're in a tunnel area /* inTunnel: cpi r17,126 brcc notInTunnel ; y>126 cpi r17,105 brcs notInTunnel ; y<105 cpi r16,44 brcs yesInTunnel ; x<44 cpi r16,180 brcc yesInTunnel ; x>180 notInTunnel: clz ret yesInTunnel: sez ret */ ; Function wrapX - loop around through tunnel wrapX: cpi r16,4 brne wrapXright ldi r16, (28*8-6) rjmp wrapXend wrapXright: cpi r16, (28*8-5) brne wrapXend ldi r16, 5 wrapXend: ret ; Function eatFood - clear the current cell. eatFood: ldi r16,0 st Y, r16 ; Y is still set from the comparison inc FoodEaten ; if food=70 or 170, cherry appears ldi r16,70 cp r16, FoodEaten breq MakeCherryAppear ldi r16,170 cp r16, FoodEaten breq MakeCherryAppear ldi r16,30 ; if food=30, release inky cp r16,FoodEaten brne eatFoodCheck82 ldi r16, RELEASED sts inky+GHOST_MODE, r16 ret eatFoodCheck82: ldi r16, 82 ; if food=82 release clyde cp r16,FoodEaten brne eatFoodCheck244 ldi r16, RELEASED sts clyde+GHOST_MODE, r16 eatFoodCheck244: ; if food=244, over, restart ldi r16, 244 cp r16,FoodEaten brne eatFoodChecksOver rjmp resetLevel eatFoodChecksOver: ret MakeCherryAppear: ldi r16,20 mov Cherry, r16 ret ; Function reverse direction ; returns the opposite dir number reverseDirection: cpi r16,RIGHT brne revDirCheckLeft ldi r16,LEFT ret revDirCheckLeft: cpi r16,LEFT brne revDirCheckDown ldi r16,RIGHT ret revDirCheckDown: cpi r16,DOWN brne revDirMustBeUp ldi r16,UP ret revDirMustBeUp: ldi r16,DOWN ret ; Function xDir - translates the PacDir register into ; an x coordinate. Returns 1, 0 or -1 xDir: mov r16, PacDir cpi r16,RIGHT breq xRight cpi r16,LEFT breq xLeft ldi r16,0 ret xRight: ldi r16,1 ret xLeft: ldi r16,-1 ret ; Function yDir - translates the PacDir register into ; a y coordinate. Returns 1, 0 or -1 yDir: mov r16, PacDir cpi r16,DOWN breq yDown cpi r16,UP breq yUp ldi r16,0 ret yDown: ldi r16,1 ret yUp: ldi r16,-1 ret ; Function resetGhost - moves the ghost back to the ; ghost house and sets its release timer to r16 resetGhost: ldd r17, Z+GHOST_MODE cpi r17, 255 ; Do not affect timing if already trapped. breq dontResetGhost ; (that could lead to early release, since after std Z+GHOST_MODE, r16 ; dying, ghosts have a shorter release time than ldi r17, 112 ; when the level started) std Z+GHOST_X, r17 ldi r17, 96 std Z+GHOST_Y, r17 dontResetGhost: ret ; Function processGhost (Z pointer) processGhost: .def ghostTileX = r19 .def ghostTileY = r20 ldd ghostTileX, Z+GHOST_X ldd ghostTileY, Z+GHOST_Y lsr ghostTileX ; go from pixel coords... lsr ghostTileX lsr ghostTileX lsr ghostTileY lsr ghostTileY lsr ghostTileY ; ...into tile coords .def targetX = r26 .def targetY = r27 mov targetX,PacX ; The default target is PacMan's position mov targetY,PacY ; (only strictly true for Blinky) subi targetX,4 subi targetY,4 lsr targetX lsr targetX lsr targetX lsr targetY lsr targetY lsr targetY ;energizerTimer ldi r16,1 cp EnergizerTimer, r16 ; if ernergizer time is up, brne pGhostEnegNotZero ldi r16,0 std Z+GHOST_SCARED, r16 ; set scared = 0 pGhostEnegNotZero: ; collision with pacman cp targetX,ghostTileX cpc targetY,ghostTileY brne pGhostNoCollision ldd r16, Z+GHOST_MODE cpi r16, DEAD breq pGhostNoCollision ; Don't collide if dead ldd r16, Z+GHOST_SCARED cpi r16,1 brne pGhostPacManHit ; Ghost is scared and hence edible. ldi r16,0 std Z+GHOST_SCARED,r16 ldi r16,DEAD std Z+GHOST_MODE,r16 ; We'll be moving doublespeed, so round coordinates to grid ldd r16, Z+GHOST_Y andi r16,0b11111110 std Z+GHOST_Y,r16 ldd r16, Z+GHOST_X andi r16,0b11111110 std Z+GHOST_X,r16 ; Score inc PacGhostCounter mov r18,PacGhostCounter ldi r16, 100 ldi r17, 0 ; if n = number of ghosts eaten, eatGhostScoreLoop: ; then score is 100 * 2^n lsl r16 rol r17 dec r18 brne eatGhostScoreLoop rcall incScore ldi r16, HIGH(SirenDead*2) sts MUSIC_START_H, r16 ldi r16, LOW(SirenDead*2) sts MUSIC_START_L, r16 ldi r16, 40 mov PacEatGhost, r16 rjmp pGhostNoCollision ; Continue moving (in dead mode) pGhostPacManHit: ldi r16, (15*8) mov PacDead,r16 ldi r16, HIGH(PacDeadSound*2) sts EFFECT_H, r16 ldi r16, LOW(PacDeadSound*2) sts EFFECT_L, r16 ; Reset all ghosts ldi ZH, HIGH(blinky) ldi ZL, LOW(blinky) ldi r16, 4 call resetGhost adiw Z, GHOST_PARAMS ldi r16, 14 call resetGhost adiw Z, GHOST_PARAMS ldi r16, 24 call resetGhost adiw Z, GHOST_PARAMS ldi r16, 34 call resetGhost rcall drawMap rjmp waitForFrame pGhostNoCollision: ; in tunnel ...? ldd r16, Z+GHOST_DIR ; This is a functional, but very unsatisfying cpi r16, LEFT ; solution to a particular glitch when ghosts brne tempGhostNotLeft ; are leaving the ghost house - sometimes they ldd r16, Z+GHOST_Y ; misalign with the grid when going left. subi r16,-1 andi r16, 0b11111110 std Z+GHOST_Y, r16 tempGhostNotLeft: ; Skip calculating a direction unless we're ; on a tile boundary. ldi r17,0 ldd r16, Z+GHOST_X andi r16, 0b00000111 cpse r16,r17 rjmp pGhostMove ldd r16, Z+GHOST_Y andi r16, 0b00000111 cpse r16,r17 rjmp pGhostMove ; skip to pGhostMove if in tunnel...? ; Work out the target tile we want to head to. ldd r16,Z+GHOST_SCARED cpi r16,1 brne ghostNotScared ;load random target - use bootrand call BootRandomNumber ; or in main text? ldi r16, 0b00010000 lds targetX,RND_H and targetX,r16 ; use different bits of the random byte lds targetY,RND_H ; set targets to 0 or 32 swap targetY ; - so any of the corners and targetY,r16 rjmp pGhostFindPath ghostNotScared: ldd r16,Z+GHOST_MODE cpi r16, NORMAL brne pGhostCheckDead1 ;if scatter enabled.... ; The ghost data table is much less than 256 bytes, so ; each ghost is uniquely defined by its low byte address. cpi ZL, LOW(pinky) brne checkTargetIfClyde rcall xDir ; Pinky always aims for 4 spaces lsl r16 ; in front of PacMan. lsl r16 ; So grab his direction vector, multiply add targetX, r16 ; by 4 and add to target position. rcall yDir lsl r16 lsl r16 add targetY, r16 rjmp pGhostFindPath checkTargetIfClyde: cpi ZL, LOW(clyde) brne checkTargetIfInky ; Clyde heads towards pacman if far away, but runs away if nearby. ldd r16, Z+GHOST_X ldd r17, Z+GHOST_Y rcall calcDistance ldi r16,64 ; The cutoff radius is 8 tiles, so check if ldi r17,0 ; squared distance is less than 64 cp r0,r16 cpc r1,r17 brcc clydeIsFarAway ldi targetX,0 ; run away ldi targetY,31 clydeIsFarAway: rjmp pGhostFindPath checkTargetIfInky: ; Inky has the most complicated behaviour. We draw a vector between ; Blinky's position and the tile 2 spaces in front of pacman, then ; double it to find our target. rcall xDir lsl r16 add targetX, r16 ; First work out the tile 2 ahead of pacman rcall yDir lsl r16 add targetY, r16 lds r0, (blinky+GHOST_X) ; Now load blinky's coords into a temp reg lds r1, (blinky+GHOST_Y) lsr r0 lsr r0 lsr r0 ; convert to tile coordinates lsr r1 lsr r1 lsr r1 sub r0, targetX ; Take the difference, which gives us a sub r1, targetY ; signed vector lsl r0 ; Double it lsl r1 ; and shift the target by that amount. sub targetX, r0 ; (subtract because the vector went sub targetY, r1 ; towards blinky) rjmp pGhostFindPath pGhostCheckDead1: cpi r16,DEAD brne pGhostCheckReleased1 ldi r16,14 mov targetX,r16 ldi r16,11 mov targetY,r16 cp targetX,ghostTileX ; Have we reached the ghost house? cpc targetY,ghostTileY brne pGhostFindPath ldi r16, (TRAPPED+30) ; use mode byte as a trap timer std Z+GHOST_MODE, r16 ldi r16,DOWN std Z+GHOST_DIR, r16 ldd r16,Z+GHOST_Y inc r16 std Z+GHOST_Y,r16 mov r16,EnergizerTimer cpi r16,1 breq setNormalSiren ldi r16, HIGH(EnegSiren*2) sts MUSIC_START_H, r16 ldi r16, LOW(EnegSiren*2) sts MUSIC_START_L, r16 call resetMusic rjmp pGhostMove setNormalSiren: ldi r16, HIGH(Siren1*2) sts MUSIC_START_H, r16 ldi r16, LOW(Siren1*2) sts MUSIC_START_L, r16 rjmp pGhostMove pGhostCheckReleased1: cpi r16,RELEASED brne pGhostIsTrapped ldi r16,13 mov targetX,r16 ldi r16,13 mov targetY,r16 ;cp targetX,ghostTileX cp targetY,ghostTileY brcs pGhostFindPath pGhostOutsideHouse: ldi r16,NORMAL std Z+GHOST_MODE, r16 ldi r16,UP std Z+GHOST_DIR, r16 ldd r16,Z+GHOST_Y dec r16 std Z+GHOST_Y,r16 rjmp pGhostFindPath pGhostIsTrapped: ; by elimination we must be trapped ldd r16, Z+GHOST_MODE cpi r16, 255 breq pGhostTrappedDontTimer dec r16 std Z+GHOST_MODE,r16 pGhostTrappedDontTimer: ldi r16,13 mov targetX,r16 ldi r16,14 mov targetY,r16 cpi ZL, LOW(inky) ; Just to keep the trapped ghosts brne pNotInkyTrapped ; from staying on top of each other ldi r16,11 mov targetX,r16 rjmp pGhostFindPath pNotInkyTrapped: cpi ZL, LOW(clyde) brne pGhostFindPath ldi r16,15 mov targetX,r16 pGhostFindPath: push r24 ;needed? push r25 ; Find Paths .def newDirection = r18 .def minDistanceL = r24 .def minDistanceH = r25 .def ghostDir = r21 ldi minDistanceH,255 ldi minDistanceL,255 ldd r16, Z+GHOST_X ldd r17, Z+GHOST_Y rcall getMapTile ; Sets Y pointer to map cell ; Load with displacement can only point forwards, so ; wind the pointer back to be able to check all around sbiw Y, 28 ; Y now points to cell above ; First try going up ldd ghostDir, Z+GHOST_DIR ; Never change direction by 180 degrees. cpi ghostDir, DOWN ; so if we're going down breq pGhostTryRight ; skip trying to go up ld r16, Y ; cell directly above cpi r16, 1 ; is it a wall? breq pGhostTryRight mov r16, ghostTileX mov r17, ghostTileY dec r17 ; One tile above. rcall calcDistance cp r0, minDistanceL cpc r1, minDistanceH brcc pGhostTryRight movw minDistanceL, r0 ldi newDirection, UP pGhostTryRight: cpi ghostDir,LEFT breq pGhostTryLeft ; or jump straight to down? ldd r16, Y+29 ; One cell to the right cpi r16,1 breq pGhostTryLeft mov r16, ghostTileX mov r17, ghostTileY inc r16 ; one tile to the right rcall calcDistance cp r0, minDistanceL cpc r1, minDistanceH brcc pGhostTryLeft movw minDistanceL, r0 ldi newDirection, RIGHT pGhostTryLeft: cpi ghostDir,RIGHT breq pGhostTryDown ldd r16, Y+27 ; One tile to the left cpi r16,1 breq pGhostTryDown mov r16, ghostTileX mov r17, ghostTileY dec r16 rcall calcDistance cp r0, minDistanceL cpc r1, minDistanceH brcc pGhostTryDown movw minDistanceL, r0 ldi newDirection, LEFT pGhostTryDown: cpi ghostDir,UP breq pGhostSetDirection ldd r16, Y+56 ; The tile below cpi r16,1 breq pGhostSetDirection mov r16, ghostTileX mov r17, ghostTileY inc r17 rcall calcDistance cp r0, minDistanceL cpc r1, minDistanceH brcc pGhostSetDirection movw minDistanceL, r0 ldi newDirection, DOWN pGhostSetDirection: std Z+GHOST_DIR, newDirection pop r25 pop r24 pGhostMove: ldi r20, 1 ; DoubleSpeed ldi r21, 10 ; Speed ldd r16, Z+GHOST_X ; If on a tile boundary, andi r16, 0b00000111 ; don't slow down - there's a risk brne pGhostSlowDown ; of backtracking. ldd r16, Z+GHOST_Y andi r16, 0b00000111 brne pGhostSlowDown rjmp pGhostCheckDead pGhostSlowDown: ldi r21,9 ; default speed ; if scared, speed = 6 ldd r16, Z+GHOST_SCARED cpi r16, 1 brne pGhostInTunnel ldi r21, 6 pGhostInTunnel: ; if in tunnel, speed = 5 ldd r17, Z+GHOST_Y cpi r17,126 brcc pGhostCheckDead ; y>126, AND cpi r17,105 brcs pGhostCheckDead ; y<105 ldd r16, Z+GHOST_X cpi r16,44 brcs pGisInTunnel ; x<44, OR cpi r16,180 brcc pGisInTunnel ; x>180 rjmp pGhostCheckDead pGisInTunnel: ldi r21,5 pGhostCheckDead: ; if dead, go doublespeed ldd r16, Z+GHOST_MODE cpi r16,DEAD brne pGhostStartMoving ldi r20,2 ldi r21,10 ;ldi r18,0 ;std Z+GHOST_SPEED, r18 pGhostStartMoving: ldd r16, Z+GHOST_X ldd r17, Z+GHOST_Y ldd r18, Z+GHOST_SPEED ldd r19, Z+GHOST_DIR pGhostMoveLoop: add r18, r21 ; Add to speed timer cpi r18, 9 brcs pGhostMoveLoopEnd ; if timer is > 9, continue cpi r19, RIGHT brne pGhostMoveLeft inc r16 rjmp pGhostMoveEnd pGhostMoveLeft: cpi r19, LEFT brne pGhostMoveUp dec r16 rjmp pGhostMoveEnd pGhostMoveUp: cpi r19,UP brne pGhostMoveDown dec r17 rjmp pGhostMoveEnd pGhostMoveDown: inc r17 pGhostMoveEnd: subi r18,10 rcall wrapX pGhostMoveLoopEnd: dec r20 brne pGhostMoveLoop std Z+GHOST_X, r16 std Z+GHOST_Y, r17 std Z+GHOST_SPEED, r18 ;std Z+GHOST_DIR, r19 ret ; Function calcDistance ; Calculates the squared distance between two tiles calcDistance: sub r16, targetX ; Differences in positions sub r17, targetY muls r16, r16 movw r4,r0 ; check r5:r4 isn't used? muls r17,r17 add r0, r4 adc r1, r5 ret ; Function drawGhost (Y pointer) drawGhost: ;if dead... ldd r17, Y+GHOST_MODE cpi r17, DEAD breq drawDeadGhost ldd r17, Y+GHOST_SCARED cpi r17,1 brne drawGhostNotScared ; 32 bytes per sprite. ldi ZL, LOW(GhostSprites*2 + 32*8) ; 9th sprite is scared sprite ldi ZH,HIGH(GhostSprites*2 + 32*8) mov r16,EnergizerTimer cpi r16,96 ; if timer is nearing the end, start flashing brcc drawGhostAnimFrame andi r16,0b00001000 brne drawGhostAnimFrame ; alternate on bit 3 for a strobe effect subi ZL, -64 ; advance to inverted colour sprite sbci ZH, -1 rjmp drawGhostAnimFrame drawGhostNotScared: ldi ZL, LOW(GhostSprites*2) ldi ZH,HIGH(GhostSprites*2) ldi r16, 64 ldd r17, Y+GHOST_DIR mul r17,r16 add ZL,r0 adc ZH,r1 drawGhostAnimFrame: ldi r16,8 cp GhostFrame,r16 brcc dGhostAltFrame ; every certain amount of time, adiw Z, 32 ; load alternate frame (32 bytes ahead) dGhostAltFrame: ldd r16, Y+GHOST_X ldd r17, Y+GHOST_Y subi r16,-11 subi r17,3 rcall drawSprite16 ret drawDeadGhost: ldi ZL, LOW(GhostEyeSprites*2) ldi ZH,HIGH(GhostEyeSprites*2) ldi r16, 32 ; Eye sprites have direction ldd r17, Y+GHOST_DIR ; but no animation frame. mul r17,r16 add ZL,r0 adc ZH,r1 rjmp dGhostAltFrame ; Function drawMap drawMap: call processAudio ldi r16, 224 ldi r17, 255 ldi r18,'S' rcall drawChar ldi r18,'C' rcall drawChar ldi r18,'O' rcall drawChar ldi r18,'R' rcall drawChar ldi r18,'E' rcall drawChar subi r16,6 lds r20, SCORE_L lds r21, SCORE_M lds r22, SCORE_H rcall drawDec24bit mov r0,Lives dec r0 breq drawLivesEnd ldi r17,10 drawLivesLoop: ldi ZL, LOW(PacSprites*2+32*3) ldi ZH,HIGH(PacSprites*2+32*3) ldi r16,248 rcall drawSprite16 dec r0 brne drawLivesLoop drawLivesEnd: ldi ZL, LOW(PacmanBackground*2) ldi ZH,HIGH(PacmanBackground*2) rcall drawPolygonListPM ; draw dots ldi YH, HIGH(MAPDATA) ldi YL, LOW(MAPDATA) ldi r16,4 ldi r17,4 drawDotLoop: ld r18, Y+ cpi r18,2 breq drawDot brcc drawEnergizer drawDotLoop2: subi r16,-8 cpi r16,(28*8+4) breq drawDotNextLine rjmp drawDotLoop drawDotNextLine: ldi r16,4 subi r17,-8 cpi r17,(31*8+4) brne drawDotLoop mov r16,Cherry cpi r16, 0 breq drawMapEnd cpi r16,20 breq drawCherry ldi ZL, LOW(ScoreNumberSprites*2+32*4) ldi ZH,HIGH(ScoreNumberSprites*2+32*4) dec Cherry rjmp drawCherrySprite drawCherry: ldi ZL, LOW(BonusCherrySprites*2) ldi ZH,HIGH(BonusCherrySprites*2) ; [ add levelNumber%4 *32] drawCherrySprite: ldi r16,116 ldi r17,134 rcall drawSprite16 drawMapEnd: ret drawDot: out PORTA, r16 out PORTC, r17 rcall delayDot inc r16 out PORTA, r16 rcall delayDot inc r17 out PORTC, r17 rcall delayDot dec r16 out PORTA, r16 rcall delayDot dec r17 out PORTC, r17 rcall delayDot rjmp drawDotLoop2 drawEnergizer: ldi r18, 5 cp GhostFrame, r18 brcs drawDotLoop2 ; draw sprite? subi r16,-4 subi r17,4 ldi ZH, HIGH(EnergizerSprite*2) ldi ZL, LOW(EnergizerSprite*2) rcall drawSprite8 subi r16,4 subi r17,4 rjmp drawDotLoop2 delayDot: nop nop ret ; Function drawSprite16 drawSprite16: ldi r20,16 drawSpriteWordRow: push r16 lpm r19,Z+ lpm r18,Z+ drawSpriteWordLoop: sbrs r18,0 rjmp drawSpriteBit out PORTA, r16 out PORTC, r17 rcall delaySprite16 drawSpriteBit: dec r16 lsr r19 ror r18 brne drawSpriteWordLoop cpi r19,0 brne drawSpriteWordLoop inc r17 pop r16 dec r20 brne drawSpriteWordRow ret delaySprite16: push r16 ldi r16,7 delSprite16Loop: dec r16 brne delSprite16Loop pop r16 ret drawSprite8: ldi r20,8 drawSprite8Row: push r16 lpm r18,Z+ drawSprite8Loop: sbrs r18,0 rjmp drawSprite8Bit out PORTA, r16 out PORTC, r17 rcall delaySprite8 drawSprite8Bit: dec r16 lsr r18 brne drawSprite8Loop inc r17 pop r16 dec r20 brne drawSprite8Row ret delaySprite8: push r16 ldi r16,3 delSprite8Loop: dec r16 brne delSprite8Loop pop r16 ret ; Function readController, loads into r2 readController: sbis SPSR,SPIF rjmp readController ; Wait for reception complete in r2,SPDR ; Read received data com r2 push r16 ldi r16,0b00000000 ; init controller read again out SPDR,r16 pop r16 ret /* Function drawPolygonPM(Z pointer) * Draw from a list of coords in program memory * First byte is number of vertices, followed by pairs of x,y bytes */ drawPolygonPM: push r20 .def vertexCount=r20 lpm vertexCount, Z+ lpm r16,Z+ ; load starting point lpm r17,Z+ ; all polygons must have at least 2 vertices dec vertexCount polyPMnextVert: lpm r18,Z+ ; load next vertex lpm r19,Z+ rcall drawLine ; drawLine updates r16, r17 for us dec vertexCount brne polyPMnextVert ; repeat until end of polygon pop r20 ret /* Function drawPolygonListPM(Z pointer) * Draws lots of polygons from program memory. * First byte is number of polygons */ drawPolygonListPM: push r20 lpm r20, Z+ ; load number of polygons polyListPMnext: rcall drawPolygonPM ; just call drawPolygonPM that many times dec r20 brne polyListPMnext pop r20 ret /* Function drawLine (x0,y0,x1,y1) * My implementation of Bresenham line algorithm * based on pseudocode/description from wikipedia */ drawLine: ;Function arguments .def x0=r16 .def y0=r17 .def dx=r18 .def dy=r19 ;Local variables push r20 push r21 push r22 push r23 push r24 push r25 .def slowx=r20 .def slowy=r21 .def fastx=r22 .def fasty=r23 .def err = r24 .def t = r25 ; First we split the magnitude and direction ; Need to be careful/explicit because mag could be >127 cp dx,x0 breq lineXeq brcs lineXneg ; Carry flag set if it overflowed ldi slowx,1 ; slowx is the direction sub dx,x0 ; dx is now the magnitude rjmp lineY lineXneg: ldi slowx,-1 mov t, x0 ; t as temp var sub t, dx mov dx, t rjmp lineY lineXeq: clr slowx clr dx lineY: ; Now do the same for y cp dy,y0 breq lineYeq brcs lineYneg ldi slowy,1 sub dy,y0 rjmp lineCheckDistance lineYneg: ldi slowy,-1 mov t, y0 ; t as temp var sub t, dy mov dy, t rjmp lineCheckDistance lineYeq: clr slowy clr dy lineCheckDistance: ; Now to find which direction is "faster" cp dx, dy brcs lineDxLessthanDy mov fastx, slowx ; x is the fast direction clr fasty eor dx, dy ; Tripple XOR, the classy way to eor dy, dx ; swap two registers eor dx, dy rjmp lineInit lineDxLessthanDy: mov fasty, slowy ; y is the fast direction, no need to swap clr fastx lineInit: mov err, dy ; set err to half the furthest distance lsr err ;out PORTA, x0 ; We are now ready to draw the line ;out PORTC, y0 rcall delayPixel mov t, dy lineNextPixel: sub err, dx ; Update error term brcc lineStepParallel add err, dy ; Make error term positive again add x0,slowx ; Step diagonally add y0,slowy rjmp lineLoopBack lineStepParallel: add x0,fastx ; Step parallel add y0,fasty lineLoopBack: out PORTA, x0 ; Plot point out PORTC, y0 rcall delayPixel dec t brne lineNextPixel pop r25 pop r24 pop r23 pop r22 pop r21 pop r20 ret ; End of drawLine delayPixel: nop nop nop ret /* Function drawChar(x0,y0,char) */ drawChar: .def x0=r16 .def y0=r17 .def char=r18 push r19 push ZH push ZL ; Each character is a 5 byte bitmap ldi ZL, LOW(bitmapFontTable*2) ldi ZH, HIGH(bitmapFontTable*2) subi char,32 ; Font starts at ascii 32 ldi r19,5 mul char,r19 ; Multiply char by 5 movw char,r0 ; Result is in R1:R0 add ZL, char ; Add this to Z pointer adc ZH, r19 ldi r19,5 drawCharLoopByte: lpm char, Z+ push y0 drawCharLoopBit: sbrs char,0 rjmp drawCharNextBit out PORTC,y0 out PORTA,x0 rcall delayChar drawCharNextBit: dec y0 lsr char brne drawCharLoopBit dec x0 pop y0 dec r19 brne drawCharLoopByte dec x0 ; Move cursor ready to draw next character pop ZL pop ZH pop r19 ret delayChar: push r16 ldi r16,$07 count2: dec r16 brne count2 pop r16 ret /* Function drawDec24bit(x,y,low,med,high) * draws a three byte number in decimal up to 9,999,999 */ drawDec24bit: ; arguments ; x = r16 ; x and y pass through to drawChar unchanged ; y = r17 .def byteL = r20 .def byteM = r21 .def byteH = r22 ; local push r18 push r23 ldi r18, '0'-1 cpi r20, BYTE1(1000000) ; Remove leading zeros... ldi r23, BYTE2(1000000) cpc r21, r23 ldi r23, BYTE3(1000000) cpc r22, r23 brcc d24bitChkMil cpi r20, BYTE1(100000) ldi r23, BYTE2(100000) cpc r21, r23 ldi r23, BYTE3(100000) cpc r22, r23 brcc d24bitChkHundThou cpi r20, LOW(10000) ldi r23, HIGH(10000) cpc r21, r23 brcc d24bitChkTenThou cpi r20, LOW(1000) ldi r23, HIGH(1000) cpc r21, r23 brcc d24bitChkThou cpi r20, LOW(100) ldi r23, HIGH(100) cpc r21, r23 brcc d24bitChkHund cpi r20, 10 brcc d24bitChkTens rjmp d24bitRemainder d24bitChkMil: inc r18 subi byteL,BYTE1(1000000) sbci byteM,BYTE2(1000000) sbci byteH,BYTE3(1000000) brcc d24bitChkMil subi byteL,BYTE1(-1000000) sbci byteM,BYTE2(-1000000) sbci byteH,BYTE3(-1000000) rcall drawChar ldi r18, '0'-1 d24bitChkHundThou: inc r18 subi byteL,BYTE1(100000) sbci byteM,BYTE2(100000) sbci byteH,BYTE3(100000) brcc d24bitChkHundThou subi byteL,BYTE1(-100000) sbci byteM,BYTE2(-100000) sbci byteH,BYTE3(-100000) rcall drawChar ldi r18, '0'-1 ; From here on we know byteH = 0 d24bitChkTenThou: inc r18 subi byteL, LOW(10000) sbci byteM,HIGH(10000) brcc d24bitChkTenThou subi byteL, LOW(-10000) sbci byteM,HIGH(-10000) rcall drawChar ldi r18, '0'-1 d24bitChkThou: inc r18 subi byteL, LOW(1000) sbci byteM,HIGH(1000) brcc d24bitChkThou subi byteL, LOW(-1000) sbci byteM,HIGH(-1000) rcall drawChar ldi r18, '0'-1 d24bitChkHund: inc r18 subi byteL, LOW(100) sbci byteM,HIGH(100) brcc d24bitChkHund subi byteL, LOW(-100) sbci byteM,HIGH(-100) rcall drawChar ldi r18, '0'-1 d24bitChkTens: inc r18 subi byteL, LOW(10) sbci byteM,HIGH(10) brcc d24bitChkTens subi byteL, LOW(-10) sbci byteM,HIGH(-10) rcall drawChar d24bitRemainder: ldi r18, '0' add r18, byteL rcall drawChar pop r23 pop r18 ret ;16x16 sprite lists PacSprites: .db 0,248, 3,248, 7,240, 7,224, 15,192, 15,128, 15,0, 15,128, 15,192, 7,224, 7,240, 3,248, 0,248, 0,0, 0,0, 0,0, 0,248, 3,254, 7,255, 7,255, 15,252, 15,224, 15,0, 15,224, 15,252, 7,255, 7,255, 3,254, 0,248, 0,0, 0,0, 0,0, 1,240, 7,252, 15,254, 15,254, 31,255, 31,255, 31,255, 31,255, 31,255, 15,254, 15,254, 7,252, 1,240, 0,0, 0,0, 0,0, 1,240, 1,252, 0,254, 0,126, 0,63, 0,31, 0,15, 0,31, 0,63, 0,126, 0,254, 1,252, 1,240, 0,0, 0,0, 0,0, 1,240, 7,252, 15,254, 15,254, 3,255, 0,127, 0,15, 0,127, 3,255, 15,254, 15,254, 7,252, 1,240, 0,0, 0,0, 0,0, 1,240, 7,252, 15,254, 15,254, 31,255, 31,255, 31,255, 31,255, 31,255, 15,254, 15,254, 7,252, 1,240, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 24,3, 28,7, 30,15, 31,31, 31,191, 15,254, 15,254, 7,252, 1,240, 0,0, 0,0, 0,0, 0,0, 6,12, 14,14, 15,30, 31,31, 31,31, 31,191, 31,191, 31,191, 15,254, 15,254, 7,252, 1,240, 0,0, 0,0, 0,0, 1,240, 7,252, 15,254, 15,254, 31,255, 31,255, 31,255, 31,255, 31,255, 15,254, 15,254, 7,252, 1,240, 0,0, 0,0, 0,0, 1,240, 7,252, 15,254, 15,254, 31,191, 31,31, 30,15, 28,7, 24,3, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 1,240, 7,252, 15,254, 15,254, 31,191, 31,191, 31,191, 31,31, 31,31, 15,30, 14,14, 6,12, 0,0, 0,0, 0,0, 0,0, 1,240, 7,252, 15,254, 15,254, 31,255, 31,255, 31,255, 31,255, 31,255, 15,254, 15,254, 7,252, 1,240, 0,0, 0,0, 0,0 PacDeathAnimation: .db 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 2,32, 1,72, 8,16, 4,0, 0,12, 24,0, 0,16, 4,8, 9,64, 2,32, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,128, 0,128, 0,128, 0,128, 0,128, 0,128, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,128, 0,128, 1,192, 1,192, 1,192, 3,224, 1,64, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,128, 1,192, 3,224, 3,224, 7,240, 15,248, 7,112, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,128, 1,192, 7,240, 15,248, 63,254, 63,126, 30,60, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,\ 1,192, 7,240, 63,254, 127,255, 63,126, 14,56, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 7,240, 127,255, 127,255, 63,254, 15,120, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 120,15, 127,255, 127,255, 63,254, 15,120, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 112,7, 126,63, 127,255, 63,254, 31,252, 7,112, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 32,2, 120,15, 124,31, 127,127, 63,254, 63,254, 31,252, 7,112, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 48,6, 56,14, 60,30, 62,62, 63,126, 31,252, 31,252, 15,248, 3,224, 0,0, 0,0, 0,0, 0,0, 0,0, 3,224, 15,248, 31,252, 31,252, 63,254, 63,254, 63,254, 63,254, 63,254, 31,252, 31,252, 15,248, 3,224, 0,0, 0,0, 0,0 GhostSprites: .db 1,224, 7,248, 15,252, 28,242, 24,96, 25,230, 57,231, 60,243, 63,255, 63,255, 63,255, 63,255, 55,59, 35,49, 0,0, 0,0, 1,224, 7,248, 15,252, 28,242, 24,96, 25,230, 57,231, 60,243, 63,255, 63,255, 63,255, 63,255, 61,239, 24,198, 0,0, 0,0, 1,224, 7,248, 15,252, 19,206, 1,134, 25,230, 57,231, 51,207, 63,255, 63,255, 63,255, 63,255, 55,59, 35,49, 0,0, 0,0, 1,224, 7,248, 15,252, 19,206, 1,134, 25,230, 57,231, 51,207, 63,255, 63,255, 63,255, 63,255, 61,239, 24,198, 0,0, 0,0, 1,224, 7,248, 6,216, 16,194, 16,194, 25,230, 63,255, 63,255, 63,255, 63,255, 63,255, 63,255, 55,59, 35,49, 0,0, 0,0, 1,224, 7,248, 6,216, 16,194, 16,194, 25,230, 63,255, 63,255, 63,255, 63,255, 63,255, 63,255, 61,239, 24,198, 0,0, 0,0, 1,224, 7,248, 15,252, 31,254, 25,230, 16,194, 48,195, 50,203, 57,231, 63,255, 63,255, 63,255, 55,59, 35,49, 0,0, 0,0, 1,224, 7,248, 15,252, 31,254, 25,230, 16,194, 48,195, 50,203, 57,231, 63,255, 63,255, 63,255, 61,239, 24,198, 0,0, 0,0, 1,224, 6,24, 8,4, 16,2, 19,50, 35,49, 32,1, 32,1, 35,49, 36,201, 32,1, 34,17, 37,41, 24,198, 0,0, 0,0, 1,224, 6,24, 8,4, 16,2, 19,50, 35,49, 32,1, 32,1, 35,49, 36,201, 32,1, 40,197, 53,43, 35,49, 0,0, 0,0, 0,0, 1,224, 7,248, 15,252, 12,204, 28,206, 31,254, 31,254, 28,206, 27,54, 31,254, 29,238, 24,198, 0,0, 0,0, 0,0, 0,0, 1,224, 7,248, 15,252, 12,204, 28,206, 31,254, 31,254, 28,206, 27,54, 31,254, 23,58, 2,16, 0,0, 0,0, 0,0 GhostEyeSprites: .db 0,0, 0,0, 0,0, 3,12, 7,158, 6,154, 7,158, 3,12, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 12,48, 30,120, 22,88, 30,120, 12,48, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 6,24, 13,52, 15,60, 15,60, 6,24, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 6,24, 15,60, 15,60, 11,44, 6,24, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0 BonusCherrySprites: .db 12,0, 15,0, 2,192, 2,32, 1,30, 0,183, 3,95, 7,109, 7,235, 7,174, 7,96, 3,192, 0,0, 0,0, 0,0, 0,0, 0,32, 1,220, 2,250, 5,39, 7,93, 7,247, 6,255, 3,218, 3,254, 1,108, 0,248, 0,32, 0,0, 0,0, 0,0, 0,0, 0,96, 1,152, 3,252, 3,236, 3,244, 7,246, 7,246, 7,254, 15,251, 15,251, 15,255, 12,127, 7,254, 0,0, 0,0, 0,0, 0,112, 1,140, 1,252, 1,252, 1,252, 0,80, 0,208, 0,80, 0,16, 0,80, 0,208, 0,80, 0,32, 0,0, 0,0, 0,0 ScoreNumberSprites: .db 0,0, 0,0, 0,0, 0,0, 49,142, 74,81, 74,80, 74,72, 74,68, 74,66, 49,159, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 49,136, 74,76, 74,74, 74,73, 74,95, 74,72, 49,136, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 49,142, 74,81, 74,81, 74,78, 74,81, 74,81, 49,142, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 99,57, 148,133, 148,133, 148,189, 148,165, 148,165, 99,25, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 24,196, 37,38, 37,36, 37,36, 37,36, 37,36, 24,206, 0,0, 0,0, 0,0, 0,0, 0,0 ;8x8 sprites EnergizerSprite: .db 60, 126, 255, 255, 255, 255, 126, 60 ; Map data ; 0 = empty ; 1 = wall ; 2 = food ; 3 = energizer Map: .db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 .db 1,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,1 .db 1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1 .db 1,3,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,3,1 .db 1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1 .db 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1 .db 1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1 .db 1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1 .db 1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1 .db 1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1 .db 1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1 .db 0,0,0,0,1,1,2,1,1,0,0,0,0,0,0,0,0,0,0,1,1,2,1,1,0,0,0,0 .db 1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1 .db 1,1,1,1,1,1,2,1,1,0,1,1,0,0,0,0,1,1,0,1,1,2,1,1,1,1,1,1 .db 0,0,0,0,0,0,2,0,0,0,1,1,0,0,0,0,1,1,0,0,0,2,0,0,0,0,0,0 .db 1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1 .db 1,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,1 .db 0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0 .db 0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0 .db 1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1 .db 1,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,1 .db 1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1 .db 1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1 .db 1,3,2,2,1,1,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,1,1,2,2,3,1 .db 1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1 .db 1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1 .db 1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1 .db 1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1 .db 1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1 .db 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1 .db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 ; Polygon List PacmanBackground: .db 24, \ 34, \ 224,104, 185,104, 184,103, 184,80, 185,79, 220,79, 221,78, 222,78, 223,77, 223,76, 224,75, 224,4, 223,3, 223,2, 222,1, 221,1, 220,0, 5,0, 4,1, 3,1, 2,2, 2,3, 1,4, 1,75, 2,76, 2,77, 3,78, 4,78, 5,79, 40,79, 41,80, 41,103, 40,104, 1,104, \ 34, \ 1,127, 40,127, 41,128, 41,151, 40,152, 5,152, 4,153, 3,153, 2,154, 2,155, 1,156, 1,243, 2,244, 2,245, 3,246, 4,246, 5,247, 220,247, 221,246, 222,246, 223,245, 223,244, 224,243, 224,156, 223,155, 223,154, 222,153, 221,153, 220,152, 185,152, 184,151, 184,128, 185,127, 224,127, \ 26, \ 224,107, 183,107, 181,105, 181,78, 183,76, 219,76, 221,74, 221,5, 219,3, 118,3, 116,5, 116,33, 114,35, 111,35, 109,33, 109,5, 107,3, 6,3, 4,5, 4,74, 6,76, 42,76, 44,78, 44,105, 42,107, 1,107, \ 34, \ 1,124, 42,124, 44,126, 44,153, 42,155, 6,155, 4,157, 4,194, 6,196, 18,196, 20,198, 20,201, 18,203, 6,203, 4,205, 4,242, 6,244, 219,244, 221,242, 221,205, 219,203, 207,203, 205,201, 205,198, 207,196, 219,196, 221,194, 221,157, 219,155, 183,155, 181,153, 181,126, 183,124, 224,124, \ 9, \ 164,126, 164,153, 162,155, 159,155, 157,153, 157,126, 159,124, 162,124, 164,126, \ 17, \ 164,54, 164,105, 162,107, 159,107, 157,105, 157,85, 155,83, 135,83, 133,81, 133,78, 135,76, 155,76, 157,74, 157,54, 159,52, 162,52, 164,54, \ 9, \ 181,57, 183,59, 202,59, 204,57, 204,54, 202,52, 183,52, 181,54, 181,57, \ 9, \ 181,33, 183,35, 202,35, 204,33, 204,22, 202,20, 183,20, 181,22, 181,33, \ 9, \ 135,35, 162,35, 164,33, 164,22, 162,20, 135,20, 133,22, 133,33, 135,35, \ 17, \ 87,52, 138,52, 140,54, 140,57, 138,59, 118,59, 116,61, 116,81, 114,83, 111,83, 109,81, 109,61, 107,59, 87,59, 85,57, 85,54, 87,52, \ 9, \ 63,35, 90,35, 92,33, 92,22, 90,20, 63,20, 61,22, 61,33, 63,35, \ 9, \ 42,35, 23,35, 21,33, 21,22, 23,20, 42,20, 44,22, 44,33, 42,35, \ 9, \ 42,52, 23,52, 21,54, 21,57, 23,59, 42,59, 44,57, 44,54, 42,52, \ 17, \ 70,76, 68,74, 68,54, 66,52, 63,52, 61,54, 61,105, 63,107, 66,107, 68,105, 68,85, 70,83, 90,83, 92,81, 92,78, 90,76, 70,76, \ 17, \ 85,100, 104,100, 105,101, 120,101, 121,103, 137,103, 137,128, 88,128, 88,103, 104,103, 104,102, 120,102, 121,100, 140,100, 140,131, 85,131, 85,100, \ 9, \ 68,126, 66,124, 63,124, 61,126, 61,153, 63,155, 66,155, 68,153, 68,126, \ 9, \ 63,172, 90,172, 92,174, 92,177, 90,179, 63,179, 61,177, 61,174, 63,172, \ 13, \ 44,174, 42,172, 23,172, 21,174, 21,177, 23,179, 35,179, 37,181, 37,201, 39,203, 42,203, 44,201, 44,174, \ 17, \ 61,198, 61,218, 59,220, 23,220, 21,222, 21,225, 23,227, 90,227, 92,225, 92,222, 90,220, 70,220, 68,218, 68,198, 66,196, 63,196, 61,198, \ 17, \ 87,196, 85,198, 85,201, 87,203, 107,203, 109,205, 109,225, 111,227, 114,227, 116,225, 116,205, 118,203, 138,203, 140,201, 140,198, 138,196, 87,196, \ 17, \ 116,177, 116,157, 118,155, 138,155, 140,153, 140,150, 138,148, 87,148, 85,150, 85,153, 87,155, 107,155, 109,157, 109,177, 111,179, 114,179, 116,177, \ 9, \ 135,179, 133,177, 133,174, 135,172, 162,172, 164,174, 164,177, 162,179, 135,179, \ 17, \ 157,198, 157,218, 155,220, 135,220, 133,222, 133,225, 135,227, 202,227, 204,225, 204,222, 202,220, 166,220, 164,218, 164,198, 162,196, 159,196, 157,198, \ 13, \ 181,174, 181,201, 183,203, 186,203, 188,201, 188,181, 190,179, 202,179, 204,177, 204,174, 202,172, 183,172, 181,174 Siren1: .db 40,127,127,41,127,127,41,127,127,42,127,127,42,127,127,43,127,127,43,127,127,44,127,127,44,127,127,45,127,127,45,127,127,46,127,127,46,127,127,47,127,127,47,127,127,48,127,127,47,127,127,47,127,127,46,127,127,46,127,127,45,127,127,45,127,127,44,127,127,44,127,127,43,127,127,43,127,127,42,127,127,42,127,127,41,127,127,41,127,127,0 EnegSiren: .db 10,127,127,20,127,127,30,127,127,40,127,127,50,127,127,60,127,127,0 SirenDead: .db 50,127,127,49,127,127,48,127,127,47,127,127,46,127,127,45,127,127,44,127,127,43,127,127,42,127,127,0 PacDeadSound: .db 50,49,48,47,46,45,46,47,48,47,46,45,44,43,42,41,42,43,44,43,42,41,40,39,38,37,36,37,38,39,38,37,36,35,34,33,32,31,30,0 soundEatDot: .db 50,35,10,45,60,0 soundEatGhost: .db 20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,0 musicOff: .db 127,127,127,0 bitmapFontTable: .db $00,$00,$00,$00,$00,$00,$00,$FA,$00,$00,$00,$E0,$00,$E0,$00,$28,$FE,$28,$FE,$28,$24,$54,$FE,$54,$48,$C4,$C8,$10,$26,$46,$6C,$92,$6A,$04,$0A,$00,$00,$E0,$00,$00,$38,$44,$82,$00,$00,$00,$00,$82,$44,$38,$44,$28,$FE,$28,$44,$10,$10,$7C,$10,$10,$00,$02,$0C,$00,$00,$10,$10,$10,$10,$10,$00,$00,$02,$00,$00,$04,$08,$10,$20,$40,$7C,$8A,$92,$A2,$7C,$00,$42,$FE,$02,$00,$46,$8A,$92,$92,$62,$84,$82,$92,$B2,$CC,$18,$28,$48,$FE,$08,$E4,$A2,$A2,$A2,$9C,$3C,$52,$92,$92,$8C,$80,$8E,$90,$A0,$C0,$6C,$92,$92,$92,$6C,$62,$92,$92,$94,$78,$00,$00,$28,$00,$00,$00,$02,$2C,$00,$00,$10,$28,$44,$82,$00,$28,$28,$28,$28,$28,$00,$82,$44,$28,$10,$40,$80,$9A,$A0,$40,$7C,$82,$BA,$9A,$72,$3E,$48,$88,$48,$3E,$FE,$92,$92,$92,$6C,$7C,$82,$82,$82,$44,$FE,$82,$82,$82,$7C,$FE,$92,$92,$92,$82,$FE,$90,$90,$90,$80,$7C,$82,$82,$8A,$8E,$FE,$10,$10,$10,$FE,$00,$82,$FE,$82,$00,$04,$02,$02,$02,$FC,$FE,$10,$28,$44,$82,$FE,$02,$02,$02,$02,$FE,$40,$30,$40,$FE,$FE,$20,$10,$08,$FE,$7C,$82,$82,$82,$7C,$FE,$90,$90,$90,$60,$7C,$82,$8A,$84,$7A,$FE,$90,$98,$94,$62,$64,$92,$92,$92,$4C,$80,$80,$FE,$80,$80,$FC,$02,$02,$02,$FC,$F8,$04,$02,$04,$F8,$FE,$04,$18,$04,$FE,$C6,$28,$10,$28,$C6,$C0,$20,$1E,$20,$C0,$86,$8A,$92,$A2,$C2,$00,$FE,$82,$82,$00,$40,$20,$10,$08,$04,$00,$82,$82,$FE,$00,$20,$40,$80,$40,$20,$01,$01,$01,$01,$01,$00,$80,$40,$20,$00,$04,$2A,$2A,$2A,$1E,$FE,$22,$22,$22,$1C,$1C,$22,$22,$22,$22,$1C,$22,$22,$22,$FE,$1C,$2A,$2A,$2A,$1A,$10,$7E,$90,$90,$40,$18,$25,$25,$25,$1E,$FE,$20,$20,$20,$1E,$00,$22,$BE,$02,$00,$02,$01,$21,$BE,$00,$FE,$08,$08,$14,$22,$00,$82,$FE,$02,$00,$3E,$20,$1C,$20,$3E,$3E,$20,$20,$20,$1E,$1C,$22,$22,$22,$1C,$3F,$24,$24,$24,$18,$18,$24,$24,$24,$3F,$3E,$10,$20,$20,$20,$12,$2A,$2A,$2A,$24,$20,$FC,$22,$22,$04,$3C,$02,$02,$04,$3E,$38,$04,$02,$04,$38,$3E,$02,$0C,$02,$3E,$22,$14,$08,$14,$22,$38,$05,$05,$05,$3E,$22,$26,$2A,$32,$22,$10,$6C,$82,$82,$00,$00,$00,$FF,$00,$00,$00,$82,$82,$6C,$10,$40,$80,$C0,$40,$80,$54,$28,$54,$28,$54,$00,$00,$00,$00,$00 .include "audio.asm" Music: .include "Music/PacManMusic.asm" .include "Bootloader.asm"
Gosh. There's no easy path to enlightenment, is there?
At least the end is in sight – Mario manifests in the next section.