*** How To Write a Pacman Game in JavaScript ***

 

A tutorial for advanced JavaScript techniques.
This text discribes the planing, set up, and development of a complex JavaScript-application.

This file is related to scripts to be found at
https://www.masswerk.at/JavaPac/legacy/JS-PacManPlus.htm.

 
Contents

 

Preface

The game described here differs from the original "Pac-Man"™ by Namco™ <http://www.namco.com/> in some ways:

The original game uses only one layout populated by four ghosts, each ghost guided by a distinguished strategy and moving at its own speed. The various levels differ by the ghosts' strategy and the speed of the game. Opposed to this, the game as described here will be populated by ghosts employing a shared set of movement strategies. (The speed will probably be the same as the speed of the pacman character or a fraction of it.) In order to entertain some difference to the game play each level will have its unique layout. This conforms with the typical appearance of a Pac-Man clone.

The original "Pac-Man" by Namco and its clones share the following features – as does the game described here: The player guides a little yellow ball (the pacman) through a maze-like labyrinth in order to clear all the food (embodied by tiny white dots) laid out in the passages of the labyrinth. His opponents are 4 ghosts of different color roaming the labyrinth in search for the pacman. If any of the ghosts comes in touch with the pacman, the pacman looses a life and is reset to its original position. Some of the food dots (usually four of them) are bit bigger and provide a reversed game play, when eaten by the pacman: For a limited amount of time the pacman now may haunt and eat the ghosts. (This phase is indicated by the ghosts' color – usually a deep blue.) If the pacman manages to get grip of a ghost in this special phase, the ghost is not killed, but set to the central cage, from which it emerged at the beginning of the level.

Pac-Man™and Namco™ are trademarks of the specific vendor(s).

1) Resources

Before we begin with our script, we should consider the elements of our game display (the actual user experience), what resources are therefore needed, and if any structure obliges to these.

For this example we'll base on the following assumptions:

  • The maze will be displayed on a grid of 20 x 14 tiles
  • there will be one pacman character
  • 4 ghost characters
  • food for the pacman
  • pills for pacman for the 'reverse' game play

for the characters we'll need some states for the animation phases:

  • for the pacman 3 phases (eating) for every direction but the back view (up) (we don't see the the mouth there)
  • for the ghosts only 2 phases (the wobbling bottom) in 4 colors (one color per ghost)
  • another set of ghost images for the 'panic' mode and the neutralizing phase (panic mode is just to be ended)
  • 3 animation phases for a shrinking pacman (life expired)
  • an image of just eyes for a killed ghost traveling home

To save some resources, we'll skip any scoring indicators (like "200", "400" and so on for any ghost). And we're not going to implement a direction dependent looking angle for the ghosts in order to safe some more resources as well.

All these informations will be displayed as gif-images, so we're left with the following images:

  • some tiles for the maze border
  • 3 x 3 + 1 animation phases for the pacman
  • 3 animation phases for a shrinking pacman
  • 6 x 2 + 1 images of ghosts
  • an extra image to indicate a ghost being caught by the pacman character

+ some extra images for level display (10 figures), a game over display, and so on.

1.1) Character Image Names

In order to have access to these images via JavaScript, we definitely should name them in some structured manner. For JavaScript robustness every name will begin with a character, since we're going to store these names in an associative array.

Here we decide to name them in the following way:

  • ghosts:
    We'll prefix them with a 'g' for "ghost" followed by a color code (c) and the animation phase (p).
    This gives 'g'+c+p+'.gif' as an image name, where the code c will be a number (we want to loop through them) – here 1..4 – or a character for the two special modes – here 'a' for the panic mode and 'n' for the neutralizing phases. Since we've only 2 animation phases per ghost, we'll use '1' or '2' for p. So we'll be able to map any states to theses images.
        g11.gif
        g12.gif
        g21.gif
        ...
        g42.gif
        ga1.gif
        ga2.gif
        gn1.gif
        gn2.gif
    
    So ghost #1 will loop over 'g11.gif' and 'g12.gif' during normal play and switch to 'ga1.gif'/'ga2.gif' in panic mode, returning over 'gn1.gif'/'gn2.gif' to normal state. To this we add an extra images for the eyes traveling home (gx.gif).
     
  • pacman:
    We could prefix the pacman's images by a 'p', but this will only waste some bytes and use some run time for string operations. So we're going to use a bit more simple scheme consisting of the direction (d) and the animation phase (p) only.
    We'll code our 4 directions with characters indicating the view ('r' – right, 'l' – left, 'f' – front, 'b' – back). There are 3 animation phases (but one for the back view) and we're using numbers again, since we are going to loop over these too.
    For the shrinking pacman animation, we're using 'x' for d and 3 animation phases as well.

    So our name scheme for the pacman is d+p+'.gif'.

1.2) Maze Tiles

How many tiles do we need?
Obviously one for the food, one for the pills (power spots), a blank one for any empty space, and a lot of border tiles.

borders:

The whole game is governed by the 4 directions of a 2D-surface. So most of the tiles can be ordered in quadruples. Here we'll list them by connected sides:

  • 1 connection
    • 4 tiles (ending in the middle to make nice endings)
     
  • 2 connections
    • 1 horizontal (left and right)
    • 1 vertical (top and bottom)
    • 4 corners (left/top, left/bottom, right/top, right/bottom)
     
  • 3 connections
    • 4 tiles with one side blank
     
  • 4 connections
    • 1 tile (a cross, not used here)
     
  • extra tiles
    • 1 to just fill a space (here a small cross with no connections)
    • 3 tiles for the ghosts' cage door
     

(The cage will consist of 4 empty tiles in the middle of the maze. The door (a dotted border tile) will be placed at the lower right. In order to have nice edges of this door we'll use 2 special corner tiles here.)

For the naming scheme we could any names and refer to them via a reference array. Since we're going to access them only while displaying a new maze at the beginning of a new level, this operation is not time critical. Here we're using a binary 4-bit vector, encoding the connected endings prefixed by 'tile', where 'tile0.gif' is just a blank space and 'tile15.gif' the tile with 4 connections (cross). We'll use 'tile16.gif' to 'tile19.gif' for our 4 special tiles.

1.3) Implementing the Maze (HTML)

Since we want to display the maze on a HTML-page, we're going to setup a HTML-table, with 20 images in 14 rows. In order to access these tiles via JavaScript, we must name them in some structured manner. Here we're using a scheme 'r'+row+'c'+col, where row will be an index from 1 to 14 and col an index from 1 to 20.

So an IMG-tag (displaying an empty tile) will look like <IMG SRC="images/tile0.gif" NAME="r1c1" ...>

1.4) Implementing the Characters (HTML/CSS)

Since our characters will be able to move smoothly, we'll need to implement them in layers or CSS-divisions. We need a naming scheme here too, since we'll have to access them via JavaScript.

We'll use 6 layers in total (1 for the pacman, 4 for the 4 ghost's, and 1 to display a "game over" information.) In order to not interfere with any possible browser's layout engine, we'll place them just before the BODY-end-tag (</BODY>). Since we're possibly going to loop through the ghosts, these layers will obviously be using an indexed name.

These layers hold each one single image to display the according character. Since these images will have to be replaced for every animation step, the will be named too.

Here we're using the following definition:

<DIV ID="gLr1" STYLE="height:27; width:27; position:absolute; top:0; left:0; visibility:hidden; z-index:2"><IMG NAME="g_i1" SRC="images/tile0.gif"></DIV>

<DIV ID="gLr2" STYLE="height:27; width:27; position:absolute; top:0; left:0; visibility:hidden; z-index:3"><IMG NAME="g_i2" SRC="images/tile0.gif"></DIV>

<DIV ID="gLr3" STYLE="height:27; width:27; position:absolute; top:0; left:0; visibility:hidden; z-index:4"><IMG NAME="g_i3" SRC="images/tile0.gif"></DIV>

<DIV ID="gLr4" STYLE="height:27; width:27; position:absolute; top:0; left:0; visibility:hidden; z-index:5"><IMG NAME="g_i4" SRC="images/tile0.gif"></DIV>

<DIV ID="pacLr" STYLE="height:27; width:27; position:absolute; top:0; left:0; visibility:hidden; z-index:6"><IMG NAME="pac_i" SRC="images/tile0.gif"></DIV>

<DIV ID="gameOver" STYLE="height:27; width:270; position:absolute; top:0; left:0; visibility:hidden; z-index:7"><IMG NAME="go_i" SRC="images/tile0.gif" WIDTH="270" HEIGHT="27"></DIV>

(We could have used some stand alone images as well, but in order to support Netscape 4, we have to place them in a layer in order to move them around.)

Remember for support of NS4: Don't use HTML4-tags like <TBODY> in the page to not confuse the CSS-engine of NS4. (Actually we do not offer a full support of NS4, since the very first versions of NS4.0 do not recognize CCS-divisions. But I think, there is probably no installation of generic NS4.0 not updated left out there.)

2) Setting up the Script

2.1) Resource Setup

For any animation images have to be preloaded and stored in an array. To access these in a structured way, we are using an associative array (using names rather than index numbers to identify an entry).

The implementation of this is quite trivial: we're using the base of our naming scheme (omitting the '.gif') as indices and store them in an array 'pacimgs'. So 'pacimgs.r1' will hold the image object associated with the first right movement phase of our pacman character. Since we're using a quite structured naming scheme, we can build some of these names on the fly, while few others have to be enumerated extensively.

We will trigger this at loading time

    // pre-loading
    
    function preLoad() {
        ...
    }
    
    // load only, if images are supported
    if (document.images) preLoad();

The array 'pacimgs' is a global variable, since these resources have to be accessible all over the script.

2.2) Browser Abstraction / Cross-Browser Animation

The other thing to do before we're going to script the game, is to form a connection between our HTML-elements and our script. Since this is highly browser depended, we're going to implement a cross-browser API abstracting these differences in a common front door.

We are supporting any browser newer than Netscape 3, using JavaScript 1.2 or higher.

These are basically 3 types of browsers:

  • Netscape 4 type
    NS 4 and compatible like some arcane browsers as Sun's HotJava
     
  • MS Internet Explorer 4 type
    MSIE 4, MSIE 5/windows, some versions of Opera and other compatible browsers
     
  • DOM type (the new generation)
    Netscape 6, Netscape 7, MSIE 6, MSIE 5/mac, Mozilla, never versions of Opera, Safari, and others
     

Definitely any new browser coming up in the next few years will be of the DOM-type.

 
** Besides: Functions and Variables **

In order to understand this approach (this is only one of others possible), we should understand that a JS-function is just another variable holding a bit of code. Declaring a function with a name just associates this bit of code with a variable name. Since in JavaScript passing a complex object from one variable to another just copies a reference to the object and not the object itself, we can pass a function's code from one variable to another without much overhead.

****

In fact with JavaScript1.0 the Function constructor was the only object constructor accessible. So even arrays had to be built using functions:

// using arrays with JS1.0

function makeArray() {
    this.length=0;
}

var a= new makeArray();
// first index is 1 since we used a[0] for a.length
// named indices where just numbered elements and length
// was the first element we used (when creating the instance)
a[1]=7; a.length=1;

var b= a; // b[1]==7 here!

// end of JS1.0

****

So we can define some GUI functions here and pass a reference to the according browser function to global variables abstracting any differences. The benefit of this approach is the absence of any conditions to be evaluated at run time.

Besides: never use eval() since this will start a whole compilation-interpretation cycle. You should always be able to construct a reference to the object you want. (Dreamveaver uses eval() all the time, but this is just poor style in terms of efficiency.) Just keep in mind that a point-notation of any JS-object is equivalent to a named array reference (document.layers.someLayer == document['layers']['someLayer']).

    // accessing JS-objects via associative array notation at run time:
    var a = 'someLayer';
    alert( document.layers.someLayer == document.layers[a] ); // "true"

We're defining the following global variables and pass references to them based on the browser in use:

setSprite – set a layer's/div's image to given source
showLayer – set a layer/div to visible
hideLayer – hide a layer/div
moveLayer – move a layer/div to x/y-position

To make things easier, we're going to abstract any layer references and hold them in a global array 'divisions'.

The array divisions holds references of these types:

    divisions[div]=document.getElementById(div)  // DOM
    divisions[div]=document.all[div]             // MSIE 4/5
    divisions[div]=document.layers[div]          // NS 4

(The array is used to not have to call document.getElementById(div) every time we access a layer/div. Also it is used to abstract some differences of MSIE and the DOM-API.)

In order to do this, we can call our cross-browser-abstraction-set-up function only at runtime, since the entries of our 'divisions' array refer to HTML/CSS elements, and these have to be present at the time the function is called. Else they will just hold a null reference. (So we'll check this on every call for a new game. Another reason for this is that 'document.layers' in NS4 is not present for any external script called in the HEADer-section and would fail.)

Since we're going to support as many browsers as possible, we're not going to identify them by some erratic navigator.userAgent sniffer, but on the presence of some top-level JS-Objects. In fact we did that earlier by checking 'document.images'. So all we need to know what features or API is supported is a set of well known top level objects:

    document.getElementById -> DOM
    document.all            -> MSIE 4/5
    document.layers         -> NS 4

Our abstracted API will take the following arguments:

    setSprite(lr,img,spr)
    showLayer(lr)
    hideLayer(lr)
    moveLayer(lr,x,y)

where

'lr' is a layer's/div's name/id,
'img' the name/id of the layer's image
'spr' the named index of an entry of 'pacimgs'
'x', 'y' page coordinates

And we'll use a fifth GUI-function used to change any image just embodied in the page-body:

setimg(img,spr) – set image ID img to spr (see above)

So we're done. But there's another issue left:
We've decided to place our maze just in the centered middle of our HTML-page.
So we have to know where exactly the origin of our maze is placed in order to place our sprites (layers/divs) above it.

To do this we use a function that evaluates the x and y coordinates of the maze's origin and stores it in the global vars 'mazeX', 'mazeY'. (For example we could use the position of an image with known ID for DOM and MSIE and an ILAYER for NS4. Since we placed these elements just next to our maze, we can easily get the coordinates of the maze's origin.) The calculations used to get an element's position are not trivial and not covered here.

We should connect this function to a window.onresize handler, because the origin of maze will change with any resize of the window. (As for window.Timeout and all other 'window'-methods, you can omit the 'window.' portion while in script context.)

Now we have all resources together:

  • images
  • pre-load to a reference array
  • GUI-API

and are ready for our main task...

3) Programming the Game

3.1) Defining the Rules

First we should consider the characters behaviors and how the game play could be defined.

We define the following rules

1)the maze has no dead ends (passages are connected at least at 2 sides)
2)the pacman moves in straight lines
3)if the pacman encounters a border, it stops
4)ghosts move in straight lines
5)ghosts never stop
6)if ghosts encounter a crossing, the next direction of movement is evaluated
7)ghost do not take opposite directions on crossings
(they will not reverse directly, but move in nested loops through the maze)
8)if a ghost encounters another ghost both will reverse
(the pacman can't be there and ghosts spread out more widely)
9)ghosts can evaluate their move
a)based on random calculation
b)based on strategic movement (get nearer to the pacman)
10)movements of ghosts should be combinations of these
11)ghosts should behave more strategic in higher levels to make them more difficult
12)teleports: if a character encounters the absolute border of the maze, it is teleported to the other side.
13)if a ghost encounters the pacman, the pacman's life ends
14)the pacman eats any food it encounters in a passage
15)if all food is eaten, the level ends
16)if the pacman encounters a pill, the game is reversed:
a)ghosts are in panic mode (half speed, strategic movement leads away from the pacman)
b)if the pacman encounters a ghost, the ghost's life ends
17)the reversed game ends after a short period of time and normal mode is acquired
18)a pill is food
19)a dead ghost travels back to the cage's entrance as eyes only.
20)a dead ghost is revived in the cage
21)there are 4 basic phases in ghost's life
a)in the cage
b)stepping out of the cage
c)regular maze movement
d)panic mode
22)the maze setup varies from level to level
23)if the last maze design is used, the next level reuses the first level's layout
24)the cage and its door is placed on the same spot on every level-layout
25)positions of teleports can vary from level to level (see rule 12)
26)if the third pacman life is expired, the game ends

As we can see, there is much more to do for the ghosts, and the most definitions are about movements.

Some of these rules are general features of Pac-Man, while others are specific to our implementation.
For example the original arcade game lacks the rules 7 and 8, but has rules for scoring. Here we skip these additional rules in favor of a smaller download footprint and faster run time cycles.

To be strict, we had to define some additional rules concerning the display ....

In short we define:

27)the pacman moves in alternating 3 animation phases in 4 directions
28)a stopped pacman is not animated
29)a ghost moves in 2 animation phases in 2 directions
30)there are 4 ghosts in different colors
31)a ghost changes color in panic mode
32)a ghost changes to another color just before the end of panic mode
33)panic and end-of-panic-mode color are the same for all ghosts
34)dead ghosts homebound are displayed as eyes only
34)the maze is defined by borders and passages
36)border tiles can have 0 to 4 connections to other tiles
37)a pacman life's end is animated as shrinking pacman in 3 phases
38)a caught ghost is indicated by a single image animation
39)levels are displayed in a level-display at the bottom of the maze
40)player lives are indicated as pacman characters at the bottom of the maze
41)the end of the game is indicated by a special "game over" display

In addition we stick with our naming convention for images:

    pacman: d + p
        d = {'r', 'l', 'f', 'b', 'x'} (direction code)
        p = {1, 2, 3}                 (animation phase)

    ghosts: 'g' + i + p | 'gx'
        i = {1, 2, 3, 4, 'a', 'n'}    (index/color code)
        p = {1, 2}                    (animation phase)

    tiles: 'tile'+n
        n = {1 .. 19}

and the maze tile ids 'r'+row+'c'+col, where row = {1 .. 14} and col = {1 .. 20}

3.2) Basic Layout

Basically the game is a big loop, entered at the start of a new game and only left when the game is over.
How could we implement that? Obviously not in a 'while'-loop for the following reasons:

  • most browsers will generate an endless-loop error
  • we must control the speed of this loop
  • we have to leave gaps in our run-time cycle to track user input

The later may need some explanation:
Client-side JavaScript (the version of JavaScript implemented in a browser) is a single threaded language, meaning there is only one task active at a time and that any calls are deferred until the current task is executed.

So if we would write:

    while (gameOn) {
        // do something
    }

and trigger a user input with a JavaScript handler, the handler will not be called until our 'while'-loop is ended. That's definitely not what we want.

What could we do?

We break up the loop in a single call that will fall back to idle after its completion and will be revoked later. This revocation can be done by a window.setTimeout call.

So we'll write:

    function doGame() {
        // do something
        if (gameOn) setTimeout('doGame()', 400);
    }

In fact our construct will be somewhat more complex for the following reasons:

  • variable game speed
    To provide different animation speeds, we store the timeout delay in a global variable.
     
  • no double control loops
    If a user double clicks our "new game" button, he would trigger two different calls to newGame() causing a doublet of games to be run (the second interleaved in the gaps left by the first – at least with MSIE as client). In order to do this, we'll store a reference to our timer in a variable, so we can clear any second load of this timer, if there should be one.
     
  • flatten any run time differences of our loop
    Since our control structure will execute quite different tasks per cycle (e.g.: all sprites are just running straight, a second time all ghosts have to perform crossing-tasks) and the timeout-delay is just added at the end of each cycle, we'll track our execution time and modify our timeout-delay accordingly.

So this is the way to do it:

    var pacTimer; // store the timer here
    
    function doMove() {
        // main function
        // delete any second timer first
        if (pacTimer) clearTimeout(pacTimer);
    
        // store our entry time
        var dateA = new Date();
    
        // do something
    
        // calculate execution time and final timeout delay
    
        if (gameOn) {
            var dateB=new Date;
            mTO= dateA.getTime()-dateB.getTime();
            mTO+=Math.round(aSpeed/aStep);
            if (mTO<5) mTO=5; // minimal delay 5 msecs
            pacTimer=setTimeout("doMove()",mTO);
        }
    }

In fact we'll go further and leave as many timeout gaps as possible to provide sensible user input at almost any time. Further we could pass the timeout-string to another function as parameter.

p.e. here the second function has 2 endings, a normal calling the passed timeout-string, and another changing the flow of our 'loose loop'.

    function someFunction() {
        // ....
        testCrash("doMoveF()");
    }

    function testCrash(tos) {
        // evaluate any encounters

        if (crash>0) {
            // pacman dies
            // call a specific function
        }
        else pacTimer= setTimeout(tos,1);
    }

So what are the basic tasks to be performed:

  • newGame
    • initiation tasks
    • set up the basic variables (initiate level counter, life counters)
     
  • newLevel
    • set up level specific information (food counter)
    • display the maze
    • set pacman to home position
    • set ghosts to home position
     
  • main loop
    • get entry time
    • control the pacman's movement
    • check, if all food is eaten (-> end of loop, start next level)
    • check for any crashes with ghosts (-> pacman or ghost expires)
    • move each ghost
    • check each ghosts for collisions with the pacman (-> pacman or ghost expires)
     
  • special functions
    • display level information
    • display "game over" and reset some values
     

As we can see there are possible alterations at the test for collisions of ghosts and the pacman, which occurs twice for every ghost, and another, if all food is eaten. Otherwise we just continue our main loop.

Moving the ghosts is not just as simple. In fact, as we look to our rules above, we'll have to decide which kind of movement is needed out of the following:

  • The ghost is in the cage
    This phase is entered when the pacman is set up or when a ghost is reset to the cage. We limit this phase by a minimum and maximum time controlled by a counter
     
  • the ghost is in the door
    just a single moment, but we should know about our next direction. As supposed by our rules, this could be either up or down.
     
  • The ghost is in the maze
    If the ghost is placed on a crossing, we have to decide whether to move
    a)based on random or
    b)based on strategic calculations (further to the pacman or away, if in panic mode)
     
    This decission should be weighted by the level the player currently is in, so in a low level there will be more random movement than in higher ones. We should check, if the direction chosen is free, or occupied by another ghost, reversing the direction of movements to avoid ghosts being stuck in a passage.
     
  • The ghost is dead and traveling home
    Here we can re-use our strategic movement routine, but use the cage's entrance as a target in stead of the pacman's position.

3.3) Data

So what (global) data structures do we need?

  • we do need to know our maze's origin for display purposes (mazeX, mazeY).
  • we'll have some data associated with the timeout
    (cycle delay, a variable to store the timer)
  • we need to store a reference to our preloaded images
  • a boolean control variable (flag) to know, if our game is already running
  • counters for lives, levels, food left, panic mode
  • individual data for each character

The character based data is obviously best encapsulated in objects. One for the pacman and a ghost type object held in an array (in order to loop over the ghosts).

We'll need:

  • x, y position
  • animation phase
  • the movement direction
  • some space to store intermediate results to calculate them only once per cycle

for the ghosts:

  • a counter for the individual states
  • a counter for the in-the-cage phase
  • information on the individual home position (could be stored elsewhere)

Now we're ready for the "big thing":

3.4) Maze-Data: Layout and Directions

Since this script was designed in the early days and JavaScript as well as computers were rather slow, it was decissivly important to save as much runtime as possible. Since this is not a bad habit, we are going a bit into details:

First, it would be really bad, if any character moving would have to calculate a 'wish'-direction, then look up the maze map, recalculate again, if there was a wall and not a passage, and so on. So it would be nice to have some map encoding any possible direction for the exact position in a handy way.

So we're coming up with two types of maze data to store:

  • one for the layout of the display
  • and another encoding the direction information

In fact we could calculate the second from the first one, but this would use some extra time for the game to come up. So we have two sets of arrays for every level:

  • the border layout (+ the placing of pills, food, and empty spaces)
  • the directions map

The layout map is quite trivial: We use any encoding (here alphabetic characters) referring to our border-tiles. Further a simple blank will refer to a normal passages, a 'p' to pills, and a 'x' to empty spaces (mainly the teleport passages and the pacman's start position). Any normal passage will be inhabited by a food (we'll have to count this at level set up).

For the directions it might be wise to invest some more consideration.
We'll find that, if a character is passing through a straight passage, it just has to move further (or possibly reverse its direction). So if a character is on its way, we don't have to consider any directions to be encoded in the map. We just have to know that there is no crossing.
So all information to be handled are the crossings' possible connections.

On a 2D surface (as our maze obviously is) there are 4 possible directions. So we could encode them using binary numbers as a 4-bit vector (one bit for every possible direction).

Here we define (as powers of 2):

    2 ** 0 => 1 ... right
    2 ** 1 => 2 ... left
    2 ** 3 => 4 ... up
    2 ** 4 => 8 ... down

leaving 0 (zero) for a straight passage (no crossing).

This enables us to access this information via bitwise operations (| = bitwise "or", & = bitwise "and"):

    left, right, up => 1 | 2 | 4 = 7
    left, right, up, down => 1 | 2 | 4 | 8 = 15

    7 & 2 != 0  => yes, we can go right
    7 & 8 == 0  => no way down here

Now we define an array 't1', a table holding the references between an arbitrary map code and its according bit-vector-value. (We're going to store a row's code data in a string of digits; see below.)

    var t1= new Array();
    t1[0]= 0;
    t1[1]= 9; // rd (right and down)
    t1[2]=10; // ld
    t1[3]= 5; // ru
    t1[4]= 6; // lu
    t1[5]=13; // rdu
    t1[6]=14; // ldu
    t1[7]=11; // rld
    t1[8]= 7; // rlu
    t1[9]=15; // rlud

Now we can store the maze directions in a handy manner, so we can edit them manually. Any single digit represents a point on our grid. We'll have to decode this map to the 't1'-values at the beginning of each level.

    mazeGrid[0]= new Array(
        "00000000000000000000",   // 1st row
        "01000205000060100020",   // 2nd row
        "00000506000050600000",   // ...
        "05070605000060507060",
        "00000000000000000000",
        "00000000000000000000",
        "06030600000000504050",
        "00000000000000000000",
        "03700908700780900740",
        "00000000000000000000",
        "01820001800820001820",
        "00000000000000000000",
        "03080809000090808040",
        "00000000000000000000"    // last row
    );

So a '1' will indicate a corner with connections on the right and down way (t1[1] == 9 == 1 | 8).

Exploring our bit-vector approach further, we find that we could store results of common calculation in arrays just to safe runtime calculations – especially for the ghosts' movements.

So we define some arrays storing information of this type using our bit-vector grand design:

    var tx= new Array();
    var ty= new Array();
    tx[0]= 0; ty[0]= 0;  // no movement
    tx[1]= 1; ty[1]= 0;  // right
    tx[2]=-1; ty[2]= 0;  // left
    tx[4]= 0; ty[4]=-1;  // up
    tx[8]= 0; ty[8]= 1;  // down

    var t2 = new Array();
    t2[1]= [1];
    t2[2]= [2];
    t2[4]= [4];
    t2[8]= [8];
    t2[3]= [1, 2];
    t2[9]= [1, 8];
    t2[10]=[2, 8];
    t2[12]=[4, 8];
    t2[5]= [1, 4];
    t2[6]= [2, 4];
    t2[7]= [1, 2, 4];
    t2[11]=[1, 2, 8];
    t2[13]=[1, 4, 8];
    t2[14]=[2, 4, 8];
    t2[15]=[1, 2, 4, 8];

    var t3 = new Array();
    t3[0]=0; t3[1]=2; t3[2]=1; t3[4]=8; t3[8]=4;
'tx' and 'ty' store the delta x and delta y information associated with our bit-vector definition. We'll need these for movement. Now we know that a character with direction value d==2 will have a dx value of -1 and a dy value of 0. (In effect it's for these definitions that our bit-vector-values get some semantical meaning.)

We can simply access this by

    dx=tx[d];
    dy=ty[d];

No calculations here, just variable-look-ups!

The complex array 't2' encodes possible movement directions for ghosts: The first index indicates the crossing-code, the second index encodes each possible direction. In mathematical terms 't2' just enumerates the binary atoms of any 4 bit digit.

Now we can simply calculate a random movement for a ghost:

Given a ghost is positioned at a crossing holding the direction-vector 'k', we know that there are t2[k].length possible directions. So with just one random number 'n' we know our new direction:

    d = t2[k][n]

or in detail:

    n = Math.floor(Math.random() * t2[k].length);
    d = t2[k][n]

or short:

    d = t2[k][Math.floor(Math.random() * t2[k].length)]

Just one line of code for this!

For strategic movement we'll have to calculate the offset to the pacman and encode our best move's direction. (If the pacman is above and right, we would want to move either up or right, giving 1 | 4 == 5)
Now we can find the best possible movement by a bitwise <and> operation with the crossing's bit-vector:

    // bd: best direction, cd: crossing's directions
    d = bd & cd;
    if (d==0) {
        // no way in our direction here
        // move randomly
    }

So if bd == 5 and cd == 13, d == 4 meaning we're going up.
But 'd' could hold a complex bit vector encoding more than one possible direction, so we'll have to chose a random position here:

    d = bd & cd;
    if (d>0) {
        // get random direction out of possible directions
        d = t2[d][Math.floor(Math.random() * t2[d].length)];
    }
    else {
        // no way in our direction bd here
        // get random direction for this crossing
        d = t2[cd][Math.floor(Math.random() * t2[cd].length)];
    }

In an actual script we would put our random-formula in a function so we're left with:

    function getRandomDir(k) {
        return t2[k][Math.floor(Math.random() * t2[k].length)];
    }

    d = bd & cd;
    if (d>0) {
        d = getRandomDir(d);
    }
    else {
        d = getRandomDir(cd);
    }

or shorter:

    d = bd & cd;
    d = (d>0)? getRandomDir(d) : getRandomDir(cd);

Nice, isn't it?

The array 't3' stores opposite directions. So for d==4 => t3[d]==8. We'll need this to reverse movements.
 

Just a word on rule 7:

How could it be done simply to not reverse a ghost's movement in a random choice?

    md = cd & (15^t3[d]);

with 'md' being the resulting vector of movements, 'cd' being the current crossing's value, and 't3[d]' being the opposite of the current direction. (Since our assignment of bits to encode a direction was just arbitrary, we can't access the opposites via bitwise operation.)

The operation "c = a & (15^b)" is about masking a 4 bit binary digit. We ex-or 15 (binary 1111) on the direction 'b' and perform a binary <and> on 'a'. So we just cleared those bits that are set in 'b'.

Now we can use 'md' to get any direction encoded in 'cd' but that in 'd':

    d = getRandomDir(md);

That's all.

3.5) Moving Around

Now we're just implementing the stuff necessary for a pacman character to roam the maze. (So we do not implement ghosts or any food.) We take for granted, that the layout is being displayed some way and that our crossing-encodings are stored in the 2D-array 'f2' representing a 20 x 14 grid in rows and columns (f2[1..20][1..14]).

For this purpose we're mainly interested in the tile's positions (or grid points). We'll just hop from tile to tile and fix the intermediate steps via offsets.

// moving a pacman through the maze
// (c) N. Landsteiner 1997

// object constructors

function Pacman() {
    this.r= 0;   // row
    this.c= 0;   // col
    this.p= 0;   // animation phase
    this.pn= 0;  // next delta for p (+1 or -1)
    this.dir= 0; // the directions we could go
    this.md= 0;  // the current moving direction
    this.dx= 0;  // delta value for x-movement
    this.dy= 0;  // delta value for y-movement
    this.osx=0;  // x-offset for smooth animation
    this.osy=0;  // y-offset for smooth animation
}

// global vars

var pac= new PacMan();    // our instance of Pacman

var aSpeed=400;           // the cycle delay (from tile to tile) in msecs
var aStep=4;              // 4 intermediate steps in both directions for
                             smooth animation
var aSpan= new Array();   // we'll store animation offsets in pixels here

var movedir;              // stores the current user input
var mazeX= 0;             // maze's origin on the page in px
var mazeY= 0;
var pacTimer;             // timer stored here
var gameOn=flase;         // boolean flag: game on?
var runThru=false;        // boolean flag: pacman is moving?
var f2=new Array();       // array holding our maze grid information

var pacStr= new Array();  // used for display tasks
                          // associates possible direction codes
                          // with chars to compose image-names
pacStr[1]='r'; pacStr[2]='l';
pacStr[4]='b'; pacStr[8]='f';
pacStr[0]='f';

// vector-code : dx/dy reference

var tx= new Array();
var ty= new Array();
tx[0]=0;  ty[0]=0;
tx[1]=1;  ty[1]=0;  //r
tx[2]=-1; ty[2]=0;  //l
tx[4]=0;  ty[4]=-1; //u
tx[8]=0;  ty[8]=1;  //d

// *** things not defined here

function moveLayer(lr, x, y) {
    // move a layer to a given position
}

function setSprite(lr, img, s) {
    // exchange the image img of layer lr to src of s
}

function buildMaze() {
    // parse maze date
    // display the maze
    // and put direction data into 2D-array f2
}

// *** sorry (we want to keep this short)

// set up (must be called on start up)

function setSpan() {
    // a function to calculate our offsets
    // our tile width is 27px
    // offsets will be stored in array aSpan for any value of osx or osy
    // using negative indices is JavaScript 1.2 or higher

    for (n=1-aStep; n<aStep; n++) {
        if (n==0) aSpan[0]=0
        else aSpan[n]=Math.round(n*27/aStep);
    }
}

// (re)set pacman to home position

function pHome() {
    movedir=0;  // reset any user input
    // reset the pacman
    with (pac) {
        r=9; c=10;        // start position: row 9, col 10
        osx=0; osy=0;     // offsets for x and y values to zero
        md=0;             // not moving anywhere
        dir= 3;           // the directions we can move (left or right)
        dx=0; dy=0;       // dx and dy are set to zero
        pn=1; p=1;        // reset animation values
        setPac(r,c,'r1'); // display the pacman
    }
}

// display function

function setPac(r,c,s) {
    // displays the pacman with offsets and given image-source
    // unfortunatly we started our grid at origin [1,1],
    // so we have to substract 1 from r and c
    // rows and cols are multiplied by tile width 27px

    moveLayer(
    	'pacLr',
    	mazeX+27*(c-1)+aSpan[pac.osx],
    	mazeY+27*(r-1)+aSpan[pac.osy]
    );
    setSprite('pacLr','pac_i',s);
}

// start here

function newGame() {
    // leave, if we are running already
    if (gameOn) return;
    // set up the maze
    buildMaze();
    // initiate the pacman
    pHome();
    runThru=false;
    gameOn=true;
    // enter main loop
    doMove();
}

function move(x) {
    // store current user input in a global var
    // triggered by some javascript-handler

    movedir=x;
}

function doMove() {
    if (pacTimer) clearTimeout(pacTimer); // clear any duplicate timer
    var dateA=new Date(); // store entry time
    with (pac) {
        // "with": in this block look up properties of pac first

        // only if the offset is zero we're exactly over a tile.
        // any new directions of the pacman have to be evaluated
        if (osx+osy==0) pMove();

        // in case we're moving, we have to adjust our offset values
        // and display our new position
        if (runThru) {
            // add delta values to our x and y offsets
            osx+=dx;
            osy+=dy;
            // in case we're going out of bounds, reset any offsets
            // to be prepared for teleporting (moving from side to side)
            if ((c+dx)%21==0) osx=0;
            if ((r+dy)%15==0) osy=0;
            // limit our offsets to aStep
            osx%=aStep;
            osy%=aStep;
            // if offsets are zero, increment r, c
            // and handle any teleporting as well -> trunkBy(value, limit)
            if (osx==0) c=trunkBy(c+dx,20);
            if (osy==0) r=trunkBy(r+dy,14);
            // set phase pac.p to next step
            p+=pn;
            // if phase pac.p is either 3 or 1 reverse the direction
            // so we are going 1, 2, 3, 2, 1 ...
            if (p==3) pn=-1;
            if (p==1) pn=1;
            // if we are going up, just display our only backside image,
            // else compose the string for the correct movement phase.
            // pacStr associates pac.md with the chars 'r', 'l', 'f'
            if (md==4) setPac(r,c,'b1')
            else setPac(r,c,pacStr[md]+p);
            // in case we're placed on the center of a tile
            // (both offsets are zero) and we are at a crossing
            // (f2[r][c] != 0), read the crossing's information
            // and store it in pac.dir for later use
            // so we take with us the information of the last
            // crossing passed
            if ((osx+osy==0) && (f2[r][c])) dir=f2[r][c];
        }
    }
    // if the game is still on, calculate execution time
    // and substract it from our standard delay aSpeed/aStep
    // (game speed should not depend on animation step settings)
    if (gameOn) {
        var dateB=new Date();
        var mTO= dateA.getTime()-dateB.getTime();
        mTO+=Math.round(aSpeed/aStep);
        if (mTO<5) mTO=5; // minimum delay 5 msecs
        // set up the timout and store a reference in pacTimer
        pacTimer= setTimeout("doMove()",mTO);
    }
}

// getting a new direction

function pMove() {
    with (pac) {
        // evaluate the direction to go

        // pac.dir holds the current crossing's information
        // (current crossing: last grid code stored in pac.dir)
        // let's see, if there is a passage in the direction
        // of the last user input. if so, store it in pac.md

        // for we do not clear movedir, the last input will be
        // re-evaluated at the next grid point. This enables the
        // user to stear ahead without further input.

        if (dir&movedir) md= dir&movedir
        else {
            // no valid user input, so reuse our current
            // direction and mask it with the current crossing
            // (just binary and)
            md&=dir;
            // no direction left, so we're stuck agains a wall
            // stop the animation
            if (md==0) runThru=false;
        }

        // in case we are now moving
        if (md) {
            // yes we're moving (just in case we were stuck earlier)
            runThru=true;
            // intermediatly store any x and y values of our new direction
            // (don't waste another var here)
            dx=(md&3);
            dy=(md&12);
            // if there is a dx value
            if (dx) {
                // if we came this way, it must be legal to reverse later.
                // just store that horizontal movement is possible
                dir=3;
                // look up our current dx in tx
                dx=tx[dx];
            };
            if (dy) {
                // just as above with vertical and dy
                dir=12;
                dy=ty[dy];
            }
        }

        // whow, everything done here.
        // if not for intermediate offset calculations and
        // teleporting, this would be all we need.
    }
}

function trunkBy(v,m) {
    // evaluate any teleportation
    // resulting value v: 0 > v < m
    // so with m==20 => (v>=1) && (v<=19)
    // that's why we startet our grid at [1,1]

    v%=m;
    if (v==0) v=m;
    return v;
}

// end of roaming script

To complete this basic script to a full featured game we have to add the following features

  • food
    On level set up store the position of any food (or pill) in a 2D array (p.e. 'f1'), calculate the total number. If the pacman is on a grid node, check for any food and adjust a global counter. In case the food is a power pill, we'll set a counter controlling this special phase of the game.
     
  • implement the ghosts
    By now the movement of the ghosts should be quite easy to implement using the rules and techniques described earlier. Probably we'll use an array to store instances of ghost-objects and loop over them. To keep track of the individual phases (states) of a ghost's life, we'll use a individual counter property. While roaming the maze, the ghosts' movements should be evaluated using either strategic assumptions (based on the pacman's position) or random calculations.
    (=> See examples below.)
     
  • implement some chrome to display lives left, levels, and so on
     

 
*** start edit (2009) ***
 

Examples: Moving Ghosts by Table Lookups

(Compare the descriptions given in 3.4) Maze-Data: Layout and Directions.)

The following code snipplets were inserted in 05 2009 to exemplify algorithms presented earlier. (It just seemed more suitable to go into detail here, as we refer to this logic in the excursions on the original game's A.I. below.)
 

Moving a ghost by random (where k is is a bit-vector of directions):

    // a simple constructor for our ghost
    function Ghost() {
        this.r=0;
        this.c=0;
        this.d=0;
        this.dx=0;
        this.dy=0;
    }
    
    var ghost = new Ghost();

    ...
    // get directions for a ghost's position
    var k = f2[ghost.r][ghost.c];
    if (k) {
        // we are at a junction or crossing, select a new direction to go:
        // exclude the direction opposite to the movement (don't turn)
        k &= (15^t3[ghost.d]);
        // and set the ghost's direction to a random choice from this bit-vector
        ghost.d = getRandomDir(k);
        // done.
        // just store values of delta x, delta y for this direction
        ghost.dx = tx[ghost.d];
        ghost.dy = ty[ghost.d];
    }
    ...

    // a function to select a random choice from a bit-vector (c.f.: 3.4):
    function getRandomDir(k) {
        return t2[k][Math.floor(Math.random() * t2[k].length)];
    }

Moving a ghost towards a target position:

    ...
    // get directions, if at a crossing
    // exclude opposite to movement and get closer to pacman
    var k = f2[ghost.r][ghost.c];
    if (k) {
        k &= (15^t3[ghost.d]);
        ghostMove2Target(ghost, k, pac.r, pac.c);
        ...
    }
    ...

    function ghostMove2Target(ghost, k, tr, tc) {
        // get distances to target
        var dx = ghost.c-tc;
        var dy = ghost.r-tr;
        // compile a bit-vector of directions
        // that bring us closer to the target
        var v = 0;
        if (dx!=0) v = (dx>0)? 1:2;
        if (dy!=0) v |= (dy>0)? 8:4;
        // multiply it with possible directions (AND k)
        v &= k;
        // if there is an intersecting range of directions from v and k,
        // get one of them, else select a random choice from all possible
        // directions
        ghost.d = (v)? getRandomDir(v) : getRandomDir(k);
    }

Note:
If JavaScript had a native method Math.sign(), we could do all our strategic logic by a single lookup like:
  var v = tdelta[Math.sign(ghost.r-tr)][Math.sign(ghost.c-tc])] & k;
which would save the calculations in function ghostMove2Target().
Since JavaScript lacks such a native method, it's faster to do it as shown above.
 

Putting ghost movements together:

    // basic ghost movements
    // based on script (c) N. Landsteiner 1997
    
    /*
      does not implement in-cage and at-doorstep modes,
      sorry - we want to keep it short!
    */

    // ghost constructor
    function Ghost() {
        this.r=0;
        this.c=0;
        this.d=0;
        this.dx=0;
        this.dy=0;
        this.osx=0;
        this.osy=0;
        this.p=1;
        // state: 0 = in cage, 1 = doorstep, 2 = cruise, 3 = homebound
        this.s=2;
    }

    var g = new Array();
    for (var i=1; i<=4; i++) g[i] = new Ghost();

    // a flag for panic mode (pacman eats a power pill)
    var panicmode = false;
    // a phase flag for panic mode
    var skipCycle = false;
    // a counter for panic mode
    var panicCnt = 0;
    // duration of panic mode in tiles to move
    var panicDuration = 12;

    // ghost home target
    var gHome_r = 9;
    var gHome_c = 10;

    // the higher this value (0..1), the smarter the ghosts
    // (adjust this per level and/or for individual ghosts)
    var ghostAITreshold = 0.5;

    function ghostLoop() {
        // main loop for all ghost related tasks
        if (panicmode) {
            if (--panicCnt==0) {
                // panic mode is over, reset it
                setPanicMode(false);
            }
            else {
                // prepare to skip every 2nd cycle in panic mode => half speed
                skipCycle = !skipCycle;
            }
        }
        for (var i=1; i<=4; i++) {
            var ghost = g[i];
            // move the ghost
            ghostMove(ghost);
            // animate it (we ignore the "neutralizing" phase here)
            if (ghost.s == 3) {
                // display eyes with direction (e.g.: 'ge4.gif')
                setGhost(i, 'ge'+ghost.d);
            }
            else {
                var m = (panicmode)? 'a' : i;
                var s = 'g'+m+ghost.p;
                setGhost(i, s);
            }
        }
    }

    function ghostMove(ghost) {
        with (ghost) {
            // advance the animation phase
            p = (p==2)? 1:2;
            // skip every 2nd cycle in cruise mode and panic mode
            if (skipCycle && (s==2)) return;
            if (osx+osy==0) {
                // just moved a whole tile
                if ((s==3) && (r==gHome_r) && (c==gHome_c)) {
                    // homebound and home => change state to in-cage-mode
                    // (not implemented here)
                    s = 0;
                    return;
                }
                var k = f2[r][c];
                if (k) {
                    // at a crossing, get new direction ...
                    k &= (15^t3[d]);
                    if (s==3) {
                        // homebound, move to home target
                        ghostMove2Target(ghost, k, gHome_r, gHome_c, false);
                    }
                    else if (Math.random() < ghostAITreshold) {
                        ghostMove2Target(ghost, k, pac.r, pac.c, panicmode);
                    }
                    else {
                        d = getRandomDir(k);
                    }
                    dx = tx[d];
                    dy = ty[d];
                }
            }
            // advance, check for ranges and teleports
            if (dx) {
                osx += dx;
                osx %= aStep;
                if (osx==0) c = trunkBy(c+dx,20);
            }
            else if (dy) {
                osy += dy;
                osy %= aStep;
                if (osy==0) r = trunkBy(r+dy,14);
            }
        }
    }

    function ghostMove2Target(ghost, k, tr, tc, reverse) {
        var dx = ghost.c-tc;
        var dy = ghost.r-tr;
        if (reverse) {
            // revert (move away in panic mode)
            dx = -dx;
            dy = -dy;
        }
        var v = 0;
        if (dx!=0) v = (dx>0)? 1:2;
        if (dy!=0) v |= (dy>0)? 8:4;
        v &= k;
        ghost.d = (v)? getRandomDir(v) : getRandomDir(k);
    }

    function getRandomDir(k) {
        return t2[k][Math.floor(Math.random() * t2[k].length)];
    }

    function setGhost(i, s) {
        // move the ghost sprite and set the image accordingly
        // cf.: "setPac()" above
        var ghost = g[i];
        var lr = 'gLr'+i;
        moveLayer(
    	    lr,
    	    mazeX+27*(ghost.c-1)+aSpan[ghost.osx],
    	    mazeY+27*(ghost.r-1)+aSpan[ghost.osy]
        );
        setSprite(lr, 'g_i'+i, s);
    }
    
    function setPanicMode(v) {
        if (v) {
             // enter panic mode
             panicmode = true;
             skipCycle = false;
             panicCnt = panicDuration*aStep;
        }
        else {
             // exit panic mode
             panicmode = false;
             skipCycle = false;
             panicCnt = 0;
        }
    }

 
*** end of edit ***
 

Now we're left with one last question: How could we track any collisions?
Since we're dealing with offsets, comparing just row and col values doesn't do the thing. So we're left with the task of calculating some kind of bounding boxes.

For this purpose we decide to store the exact position in px of each character in the object properties 'posx' and 'posy'. So we do not need to recalculate them while comparing them.

Now we can implement a function to detect any crashs between objects:

    function isCollision(x1,y1,x2,y2, radius) {
        return (Math.abs(x1-x2)+Math.abs(y1-y2)<radius);
    }

    if (isCollision(pac.posx,pac.posy, g[i].posx,g[i].posy, 25)) {
        // pacman and ghost #i collide
    }

Maybe you wonder, why we're not using some vector projection like:

    c = Math.sqrt(a*a+b*b)

There are two reasons for this:

  1. we're saving some run time, and
  2. this would detect a collision when characters might overlap while going around corners. Since they were ok before and are ok when in straight lines again, the user would experience this as a bug and not as feature. Luckily just comparing the sum of the distances will do the thing.

So for basic techniques, we're done here.
 

With this you should be able to

  1. understand the script and
  2. write your own.

3.6) Ghost Movements Revisited

Please skip this section as it is definitely out of date and refer to the descriptions given in sect. 5 and sect. 6!

The implementation of ghost movements as described above is mainly based on the need for quick runtime evaluation. The feature that impressed me most of the ghosts' behaviour in the original game, was that they seem to let you one single chance to escape, but come down on you when you miss it. The remodelling of this behaviour with as little resources as possible was the main design goal of "JavaScript-PacMan".

Meanwhile I found some texts on the original Pacman AI (Artificial Intelligence), describing the ghosts behaviour as follows:

  • each ghost has its unique behaviour
  • the first ghost will move following to a preprogrammed pattern of movements through the maze (e.g. right, up, left, down, left, ...).
    This pattern is always the same for a level.
  • the second ghost (the red one) moves using a pathfinder routine, trying to minimize x/y-distance to the pacman (this we described earlier as "strategic movement")
  • the third ghost moves on pure random
  • the forth ghost uses another pathfinder routine, trying to predict the next movements of the pacman (trying to intercept him)

While this garantees an interesting and quite predictable gameplay, this approach has some drawbacks: First, for the first ghost, you have to design carefully an extensive path (movement pattern) per level. Second, the movement of the forth ghost can only be implemented by defining some key crossings (as entrances to loops) and by tracking the pacman's movement. So you could predict the key position the pacman will be likely to be next. But in order to do this, each level-layout has to tribute to these key positions, meaning that it has be modelled around these crossings, which will obviously limit the range of possible designs.

On the other hand our approach - as given above - enables us to set our ghosts free in almost any maze of any design. This minimizes not only efforts of level design, we could even implement random levels using a maze generator.

4) Things to Complex to be Covered Here:

  • a suitable cross-browser key-handler
    (just try, there is quite a number of special browsers out there)
     
  • calculating the positions of CSS-elemets

 

*** update ***

5) Ghost Movements Revisited (once more)

Some years after writing this I found another, more accurate description of the original Pac-Man AI and the various "psychologies" or "personalities" of the ghosts:

Shadow / Blinky   [the red ghost; N.L.]

Blinky begins each level moving at the same speed as all of the other ghosts, but after you've eaten a certain number of dots, he begins to speed up. It is customary to refer to this change as the point when he takes on the identity of 'Cruise Elroy'. Blinky becomes Cruise Elroy earlier and earlier as you progress to higher and higher levels [...].
Unlike the other ghosts, Blinky will also tend to follow close on your tail even when you turn and will often still chase you even in scatter mode.

Speedy / Pinky   [the pink ghost; N.L.]

Pinky, Inky and Clyde always move at the same speed relative to one another so the name Speedy is plainly misleading. However, the name may have been earned by the fact that Pinky is almost always the first of the ghosts to reverse direction when going in and out of scatter mode.

Pinky seems to have a tendency to go around blocks in an anticlockwise direction unlike Blinky and Clyde who seem to prefer going clockwise. This means that if Blinky and Pinky reach the opposite side of a block to where you are, they'll come at you from opposite sides of it. They can often trap you like this so be careful of this deadly duo.

Bashful / Inky   [the blue ghost; N.L.]

Inky is dangerous because he's unpredictable. Given the same choices, he will often take different turns at different times. There might be rhyme and reason to his behaviour, but we haven't recognised it yet. One theory is that Inky's behaviour depends on his proximity to Blinky almost as if he is too afraid to act on his own (like some people who never go to a cinema by themselves). Another unconfirmed theory about Inky is that he will often turn off if Pac-Man charges him. These theories might have emerged as a result Inky's nickname (Bashful), the same kind of reasoning that has lead many authorities to believe erroneously that Pinky (aka Speedy) is actually faster than the other ghosts. For this reason, we have decided to reserve our judgement until further evidence can be obtained.

Pokey / Clyde   [the yellow ghost; N.L.]

Clyde is either short-sighted or stupid. He will often turn off rather than approach you. His heart doesn't seem to be in it at all. A consequence of Clyde's unwillingness to take part is that it's often hard to round all of the ghosts up into a single cluster which is nice to do just before eating a power pill.
 

Ghost modes

I felt it would be too stressful for a human being like Pac Man to be continually surrounded and hunted down. So I created the monsters' invasions to come in waves. They'd attack and then they'd retreat. As time went by they would regroup, attack, and disperse again. It seemed more natural than having constant attack.
-- Toru Iwatani, creator of Pac-Man

Every now and then all of the ghosts will simultaneously cease their pursuit of Pac-Man and head for their home corners. We call this scatter mode and contrast it with the ghosts' usual attack mode. Scatter mode means the ghosts will leave you alone for a while so it's a good opportunity to clean up those awkward areas of the screen. But be careful, the ghosts don't stay in scatter mode for long and will only enter it a maximum of four times on the one life and level (i.e. the count resets after losing a life or when a level is completed). It is particularly important to take advantage of scatters on levels where the ghosts don't turn blue at all after eating power pills.

The moments when the ghosts change in and out of scatter mode are determined by a timer, not by the number of dots eaten and are marked by the ghosts all simultaneously reversing direction. At the beginning of a level or after losing a life, the ghosts emerge from the ghost-hut already in scatter mode. This and the second scatter are both 7 seconds long, but the third and fourth scatters are only 5 seconds each. The attack waves between scatter periods last for 20 seconds each. [...]

When Pac-Man eats a power pill, the ghosts enter blue mode. During this period, the scatter timer is paused. When ghosts leave blue mode, they return to whatever mode they were in when the power pill was eaten and the timer count continues. The ghosts move more slowly while in blue mode and decisions are not made in the same way they are in either scatter or attack modes. Note that all mode changes are marked by the ghosts reversing direction with the exception of when the ghosts leave blue mode. Note also that the ghosts don't actually all reverse at exactly the same time, but within a few time steps of one another.

Source: http://www.webpacman.com/

 
The quote above is from an interview with Toru Iwatani, the designer of Pac-Man.
The interview was published in the book "Programmers at Work" by Susan Lammers (1986) and can now be found at various websites.

These are the passages concerning the game's AI in full extend:

IWATANI: [...] To give the game some tension, I wanted the monsters to surround Pac Man at some stage of the game. But I felt it would be too stressful for a human being like Pac Man to be continually surrounded and hunted down. So I created the monsters' invasions to come in waves. They'd attack and then they'd retreat. As time went by they would regroup, attack, and disperse again. It seemed more natural than having constant attack. Then there was the design of the spirit (kokoro), or the energy forces of Pac Man. If you've played the game, you know that Pac Man had some ammunition of his own. If he eats an energiser at one of the four corners of the screen, he can retaliate by eating the enemy. This gives Pac Man the opportunity to be the hunter as well as the hunted. [...]

INTERVIEWER: What was the most difficult part of designing the game?

IWATANI: The algorithm for the four ghosts who are dire enemies of the Pac Man – getting all the movements lined up correctly. It was tricky because the monster movements are quite complex. This is the heart of the game. I wanted each ghostly enemy to have a specific character and its own particular movements, so they weren't all just chasing after Pac Man in single file, which would have been tiresome and flat. One of them, the red one called Blinky, did chase directly after Pac Man. The second ghost is positioned at a point a few dots in front of Pac Man's mouth. That is his position. If Pac Man is in the centre then Monster A and Monster B are equidistant from him, but each moves independently almost "sandwiching" him. The other ghosts move more at random. That way they get closer to Pac Man in a natural way. When a human being is constantly under attack like this, he becomes discouraged. So we developed the wave-patterned attack-attack then disperse; as time goes by the ghosts regroup and attack again. Gradually the peaks and valleys in the curve of the wave become less pronounced so that the ghosts attack more frequently.

Source: Retrogamer fanzine site: http://retrogamer.merseyworld.com/

 

Playing the original game also reveals that the game does very much rely on a complex timing pattern:

  • Pac-man slows down when munching food, moving slower than the ghosts.
  • Pac-man skips a movement step while going around corners in case that there is user input for this direction. (This results in Pac-Man keeping pace with the ghosts while taking some corners when in "munching mode".)
  • Ghosts slow down significantly while moving through the passages leading from and to the "tele-ports".
  • While ghost usually do not reverse directions when in "normal" mode, they do so now and then (possibly depending on a counter and/or the position and movement direction of Pac-Man). While the quote above suggests this Pokey/Clyde only, this is a regular pattern for all ghosts.

5.1 More Thoughts on Ghost Movements

As concerned with movement patterns and programming them, the text passages quoted above conclude to the following:

  • Blinky's tracking behavior can be easily modelled by the "strategic movement" described above.
     
  • While the original game doesn't allow for any random, the movements of Inky and Clyde can be modelled by a combination of random and strategic movement as described above.
    (As to my knowledge the specific movements of Inky and Clyde haven't been analyzed to well by now.)
     
  • The "scatter mode" can be modelled by our strategic movement approach with the "home corners" as movement targets rather than Pac-Man's position. (C.f.: The eyes of caught ghost travelling home to the cage.)
     
  • Leaving the question of Pinky's place a few steps in front of Pac-Man "sandwiching" him with Blinky.
     

Getting in front of Pac-Man

How could we get Pinky in front of Pac-Man?

Remember our array 't1' holding direction informations coded as bitvectors and our directions grid 'mazeGrid'? We could utilize both of them to build a (complex) table, holding the next crossing's position where Pac-Man should occur when taking a corner one director or the other.

To illustrate this, here's an algorithmic layout:

  • Pac-man is at a crossing/junction at position row 4 col 6
  • The grid value of this position is 5 (meaning up and right)
  • Pac-Man takes the way up
  • The next grid position up with a value greater than zero is at row 2 col 6
  • So we store 2 / 6 as Pac-man's target position
  • As Pac-Man moves toward's the target position his distance from this point decreases below a treshold value
  • So we extrapolate the next target position with Pac-man's current movement direction (as above). In case that there is no way in this direction, we just take a random guess, ex-oring out the opposite to the current movement direction. (C.f.: The ghost's movements as described above.)
  • As Pac-man actually passes the next crossing/junction, the target position is re-evaluated as in the first step.
  • Facit: The target position with a minimum distance from Pac-Man greater or equal the treshold value features as Pinky's strategic movement's target.

It can be easily seen that most of the calculations can be done before the start of a level resulting in a 3D-table holding the target position for any direction of each row/col position of the 2D array 'mazeGrid'. (We would only calculate them for those grid positions with a value greater zero.)

We could even refine this by storing only those positions as targets that end up at a crossing or junction with more than 2 open endings. (So we would treat a winding path as a single passage leading round a corner.) For this we could utilize the 'length' property of the individual sub-arrays of array 't2'. If 't2[d].length > 2', it's a valid target position, else we would go for the next one.

We could employ this new movement pattern as another possibility to be selected by random for Clyde or Inky to separate their movements.

And even more – to our great amazement this approach does also work with random mazes or mazes resulting from a maze-editor!

(This approach is used as a part of the enhanced ghost's AI of "JavaScript-PacMan2" and "FlashPac II" both © N. Landsteiner.)

 

*** update ***

6) Original Ghost A.I. – The Final Word

Meanwhile (2009) there is a definite documentaion of the original Pac-Man A.I.: The Pac-Man Dossier by Jamey Pittman. Have a look at this page for detailed information on the A.I. and timing of the original Pac-Man game!

Here is a short description of the ghosts' movement algorithm as given in "The Pac-Man Dossiers":

6.1 The Target Tile

As in our tutorial the original Pac-Man A.I. evaluates positions per tile, i.e.: Pac.Man and any of the ghost's inhabit always a specific tile, which attributes to their position as for the A.I., while intermediary positions are handled by offsets (4 steps to the next tile).

As a ghost reaches the tile before the next junction or corner, the movement or turn to be executed when reaching the junction is evaluted by the determination of a target tile. (This may also be described as a one-tile-look-ahead.) The ghost will then try to reach this target tile by appling the following rules:

  • A ghost will never turn to the opposite direction (this occurs only at the begin and end of scatter mode or while entering a pill phase).
  • A ghost will take the direction of the tile adjacent to the junction that has the shortest vertical or horizontal distance to the target tile.
  • In case that there would be two or more directions left with eqal distance to the target tile, a ghost prefers directions in the following order: up, left, down, right.
  • When in frightened mode (Pac-Man has just eaten a pill) a ghost will evaluate the target tile by a fixed pseudo-random algorithm that garentees identical movements for identical timings. (Thus you may play patterns with the original Pac-Man.)

6.2 Ghost Personalities: Target Tile Evaluation

Each ghost evalutes the target tile in a specific manner resulting in its individual "personality":

  • Blinky (Red Ghost)
    Blinky uses the most straight forward chase strategy by using Pac-Man's current position as the target tile. Thus it will follow in the tracks of the player.
  • Pinky (Pink Ghost)
    Pinky employs also Pac-Man's position for the evaluation of the target tile, but will add 4 tiles in the direction of Pac-Man's current direction. Thus it will try to get in front of Pac-Man to waylay the player.
    Due to a bug in the algorithm (or maybe a late correction to make the game more easy to play) Pinky will also add 4 tiles to the left if Pac-Man is going up. (The same bug applies to Inky, see below.)
  • Inky (Blue Ghost)
    Inky uses the most complex evaluation scheme by using the positions of both Pac-Man and Blinky. To get Inky's target, aim at the tile two tiles away from Pac-Man's position in the direction Pac-Man is moving, then add Blinky's offset from this tile in the opposite direction. (So if Blinky is 3 tiles right and 1 below of the intermediary target, add an offset of 3 tiles to the left and 1 up.).
    As for the same reasons as with Blinky the intermediary target will be 2 tiles up and 2 tiles to the left in case Pac-Man is going up.
  • Clyde (Yellow Ghost)
    Clyde's target evaluation depends on a distance-treshold: If Clyde's distance to Pac-Man is 8 tiles or more, it will target Pac-Man's position. If inside of this 8-step area, it will aim at it's scatter target in the lower left corner. Thus it will close in on Pac-Man and then turn away to the lower left corner in an endless loop.

6.3 Timing

Please mind that the effect of this movement logic does heavily depend on the complex timing scheme of the original Pac-Man game, where Pac-Man and the ghosts move at different speeds with the additional effect of Pac-Man slowing down while munching food dots and speeding up while going around corners.

(Blinky can catch up with the player while Pac-Man is munching dots, otherwise Pac-Man is in danger to pump into Pinky while moving on clear terrain. The only way to evade a tracking ghost is by taking corners frequently while munching along straight passages is prone to a sad ending. On the other hand a smart player might trick a ghost into a wrong direction by the clever use of look-aheads and target-tile mechanisms.)

The following table provides an overview of the essential timing factors based on Pac-Man's top speed (= 100%):
 

  PAC-MAN SPEED GHOST SPEED
LEVEL NORM NORMDOTS FRIGHT FRIGHT DOTS NORM FRIGHT TUNNEL
1 80% 71% 90% 79% 75% 50% 40%
2 – 4 90% 79% 95% 83% 85% 55% 45%
5 – 20 100% 87% 100% 87% 95% 60% 50%
21+ 90% 79% 95% 50%

Source: Pittman, Jamey: The Pac-Man Dossiers
http://home.comcast.net/~jpittman2/pacman/pacmandossier.html

 
Note:
As can be easily deduced from this table (in respect to the varying base speeds and their numerous subdividers), the game play of the original Pac-Man game can only be achieved by a quite high frame rate. (The original game runs at 60 cycles/frames per second). So – at the current state of technology – it does not seem very likely to sucessfully implement the same algorithms for a browser-based game.

 
Facit:
The benefit of the original A.I. is a repeatable evaluation scheme that allows for identical movements in identical situations and timings. – And it is for this that you may be able to evolve specific patterns that will take you through any of Pac-Man's levels for sure. – At the same time the steadyness of the evaluation is the drawback of this movement logic, as the only way to alter the difficulty of a level is to alter the timing diagram. Also it takes some (while not much) processor load for calculations and possible sorting of alternate targets. This also describes the strong propositions of the targeting scheme as described in this tutorial, as it only depends on the selecting of random numbers and table-lookups, while providing an easy way of adjusting a level-based difficulty by implementing parameters for the probabilties of any of the three target modes (random, strategic movement, getting in front), where ghost personalities could be easily created by the settings of these parameters.

 
 

"How To Write a Pacman Game in JavaScript"

Norbert Landsteiner
https://www.masswerk.at/
Vienna, 2004-07-18
Updated 2008-03-19
Updated 2009-05-23

This tutorial (c) N. Landsteiner 2004
Scripts enclosed (c) N.Landsteiner 1997

Document location: <URL: https://www.masswerk.at/JavaPac/pacman-howto.html>