A while back I made a video about what direction ghosts move in Pac-Man, and how they decide what direction to turn at an intersection. In that video I noted that ghosts can never turn around 180 degrees (except in special circumstances). So that begs the question: what would a ghost do if it hit a dead end?
Oh my gosh, what are they going to do? This is new territory!
Of course this can never happen in the original game, but suppose we modified the maze to block off certain paths, so the ghosts have no choice but to run into a dead end. What would happen? Here are some possibilities:
- The ghost turns around 180 degrees
- The ghost just stops in its tracks
- The ghost phases through the wall
- The game freezes, as if it became stuck in an infinite loop
- The game crashes, as if it ran into an error
The answer is number 3! The ghost just goes right through the wall as if it weren't there. Actually, there are different behaviors regarding which wall the ghost will pass through which depends on if the ghost is frightened or not. Today we'll just look at the case where the ghost is not frightened. We'll save the other case for another day. (It's arguably more interesting!)
Let's go through the algorithm that determines which direction ghosts turn. Remember that this is run every time a ghost enters any tile, not just intersection tiles. (That is, other than the special tiles outside of the ghost house and near Pac-Man's spawning area, where ghosts always just move forward and never change directions.)
; $2966 ld (curPosition),hl ; init position of ld (curTarget),de ; ghost, its target, ld (curDirection),a ; its direction, xor 2 ; and its opposite ld (revDirection),a ; direction ld hl,$FFFF ; init min distance ld (minDistance),hl ; to be large number ld ix,offsetPointers ; ix = pointer to direction offset ld iy,curPosition ; iy = pointer to ghost position ld hl,myDirection ld (hl),0 ; init loop counter
This is all the prep work at the start of the function before it loops through each of the four directions. Since this routine is run for each of the four ghosts which have their own properties, this function is called with all the necessary parameters in the HL, DE, and A registers. Those are copied to another set of memory locations for manipulation in this function. The opposite direction is found with a simple xor 2, and the minimum distance is set to the maximum value of 65535. The first valid direction will bring this down considerably. The IX and IY registers hold pointers to the current direction offset vector and the ghost's current position respectively. Then a local variable is set up as the loop counter. This is also treated as the IDs of the directions.
The directions (right, down, left, up) have two different forms. Their ID form (0, 1, 2, 3), and their vector form ([0,-1], [1,0], [0,1], [-1,0]). The loop counter myDirection holds the ID form, while the pointer in IX points to the current vector form. Here's that table, found elsewhere in the ROM:
; $32FF offsetPointers: db 0,-1 ; right db 1,0 ; down db 0,1 ; left db -1,0 ; up
It's useful to have both forms of representing a direction, so that's why both are used here. One is a loop counter, and the other can be added directly to a maze coordinate to get another maze coordinate.
; $2988 check: ld a,(revDirection) ; if the current direction cp (hl) ; to check is the ghost's jp z,next ; opposite direction, skip
At the start of the loop, the very first thing that's checked is if this direction matches the ghost's opposite facing direction. If so, this direction is skipped entirely.
; $298F call calcOffsetPosition ; get the ID of the ld (myPosition),hl ; tile in the maze call getMazePosition ; corresponding to ld a,(hl) ; this direction and $C0 ; if it is sub $C0 ; a solid tile, jr z,next ; skip
Next, it's determined if the maze tile corresponding to moving in this direction is solid or not. All tile IDs that are $C0 or greater are treated as solid, all others are not. This is essentially the game's entire collision detection for the non-frightened ghosts. If the tile is solid, we skip forward to the next direction in the loop.
; $299F push ix push iy ld ix,curTarget ; get the distance (squared) ld iy,myPosition ; between this tile and call calcDistance ; the ghost's target pop iy pop ix ex de,hl ld hl,(minDistance) ; if the distance is and a ; greater than the sbc hl,de ; current minimum, jp c,next ; skip
Now that we know the ghost can actually move into this tile, we can take the effort to calculate the distance between it and the ghost's target tile. I say "distance", but really I mean "distance squared". The actual distance doesn't matter, just the relative sizes of the distances we're comparing. Therefore we don't need to bother with the square root operation required to get that true distance. Once it's found, we check that against the current minimum distance. If it's bigger, we can skip to the next direction in the loop.
; $29BC ld (minDistance),de ; else, update the ld a,(myDirection) ; current minimum, and ld (curDirection),a ; which direction it is
If it's smaller, we update the current minimum distance, and make the ghost face this direction. By the end of this function, the ghost will be facing in the direction that results in the minimum distance to the target.
; $29C6 next: inc ix ; point to the next offset inc ix ; in the list ld hl,myDirection ; advance the loop inc (hl) ; counter, and if ld a,4 ; we did all 4 cp (hl) ; directions, jp nz,check ; we're done
Here is the work that is needed to advance to the next direction in the loop. IX is incremented by 2 (since those vectors are 2 bytes wide), and the counter is incremented by just one. If the counter hits 4, we know we hit all four directions, so we can exit the loop.
; $29D4 ld a,(curDirection) ; load the offset add a,a ; from the list that ld e,a ; corresponds to the ld d,0 ; direction we ld ix,offsetPointers ; told the ghost add ix,de ; to go ld l,(ix+0) ; put it in ld h,(ix+1) ; hl register srl a ; and return ret
Finally, the caller of this function expects the HL register to contain the direction vector that the ghost chose to move. This chunk of code just converts the ghost's current direction ID form into its vector form by indexing into the table of vectors we saw above. (In fact, that table is referenced all over the place in the game, not just here.)
Now we can tell what happens when a ghost hits a dead end. Hitting a dead end would mean that turning all four directions would be invalid. In that case, none of the distance calculations get done since the loop is broken out of early each time.
Notice that the only time the ghost's direction is modified is when curDirection is written to. But that is after all the checks for invalid turning directions! This means that if all directions are invalid, the ghost will just keep trucking forward without turning any direction.
If we modify the maze to put some dead ends in there, that's exactly what happens!
If ghosts can run through walls, why didn't they just think of that in the first place to catch Pac-Man?
Stay tuned for what happens with frightened ghosts!
You can support Retro Game Mechanics Explained on Patreon here! Any and all support is greatly appreciated!
