• he/him

Avatar by @DrDubz.
Banner by one of Colin Jackson, Rick Lodge, Steve Noake, or David Severn from Bubsy in: Fractured Furry Tales for the Atari Jaguar.


rgmechex
@rgmechex

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?

Clyde running into a dead end in a made up maze 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:

  1. The ghost turns around 180 degrees
  2. The ghost just stops in its tracks
  3. The ghost phases through the wall
  4. The game freezes, as if it became stuck in an infinite loop
  5. 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!

Blinky and Pinky running through a couple walls 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!


You must log in to comment.

in reply to @rgmechex's post:

hmm, I'd think it's either of two things:

  • the ghost wraps around, as if it entered a warp tunnel
  • the ghost disappears off into memory, and begins Causing Havoc outside the bounds of the map, like an OOB Lost Soul in Doom

this might be tangentially related.

there are well known glitches on the arcade machine, where, when you
eat an energizer, and then stay near the ghost home, and eat a ghost right when it exits as its flashing.

  1. either the eyes will wander around the maze until you exit
  2. the ghost will go straight up through the maze, and wraparound from top to bottom, passing through all walls.
    https://www.youtube.com/shorts/lY5yxYDUSyI

so even if there are no dead ends, the code adjusts for a ghost going through the walls.

off course, this can all be tested on the killscreen too,
where the ghost routinely go through walls.

later
| ||| ||| | |
ne gative 1