Back to Hardware

Pac-man

22 Dec 2018
Progress: Complete

This is page 10 of the Games Console project.

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.

Sprites

The background map is a vector polygon list, but the bulk of the gameplay is with sprites. If being an oscilloscope "vector games console" was ever anything other than a novelty, this wasn't it.

Comparison of pacman ghosts on the oscilloscope and in the pixel-perfect prototype

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.

Memory instructions

It's really not too hard, but it is quite different to programming in higher-level languages. There's a bunch of ways we can read or write data to memory.

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.

proto-Javascript

The demo above, highlighted for your viewing pleasure.


<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>

Assembly

The real deal, the proper job, the big kahuna.

Oscilloscope screenshot of Pac-Man


### 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.