Professional Documents
Culture Documents
PACMAN
PACMAN
PACMAN
Contents
Preface
1) Resources
3.1)
3.2)
3.3)
3.4)
3.5)
3.6)
4) Things to
6.1)
6.2)
6.3)
6.4)
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.
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:
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 extra images for level display (10 figures), a game over display, and so on.
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' forp. 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'.
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
o 4 tiles (ending in the middle to make nice endings)
2 connections
o 1 horizontal (left and right)
o 1 vertical (top and bottom)
o 4 corners (left/top, left/bottom, right/top, right/bottom)
3 connections
o 4 tiles with one side blank
4 connections
o 1 tile (a cross, not used here)
extra tiles
o 1 to just fill a space (here a small cross with no connections)
o 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.
Netscape 4 type
NS 4 and compatible like some arcane browsers as Sun's HotJava
Definitely any new browser coming up in the next few years will be of the DOM-type.
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
showLayer
hideLayer
moveLayer
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)
divisions[div]=document.all[div]
divisions[div]=document.layers[div]
// DOM
// MSIE 4/5
// 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 HEADersection and would fail.)
Since we're going to support as many browsers as possible, we're not going to identify them by some
erraticnavigator.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
the maze has no dead ends (passages are connected at least at 2 sides)
2)
3)
4)
5)
6)
7)
8)
9)
10)
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)
14)
15)
16)
17)
the reversed game ends after a short period of time and normal mode is acquired
18)
a pill is food
19)
20)
21)
c)
d)
panic mode
22)
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)
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)
28)
29)
30)
31)
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)
34)
36)
37)
38)
39)
40)
player lives are indicated as pacman characters at the bottom of the maze
41)
(index/color code)
(animation phase)
n = {1 .. 19}
and the maze tile ids 'r'+row+'c'+col, where row = {1 .. 14} and col = {1 .. 20}
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
o initiation tasks
o set up the basic variables (initiate level counter, life counters)
newLevel
o set up level specific information (food counter)
o display the maze
o set pacman to home position
o set ghosts to home position
main loop
o get entry time
o
o
o
o
o
special functions
o display level information
o 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:
based on random or
b)
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.
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
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
7 & 8 == 0
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",
"01000205000060100020",
"00000506000050600000",
"05070605000060507060",
"00000000000000000000",
"00000000000000000000",
"06030600000000504050",
"00000000000000000000",
"03700908700780900740",
"00000000000000000000",
"01820001800820001820",
"00000000000000000000",
"03080809000090808040",
"00000000000000000000"
);
// 1st row
// 2nd row
// ...
// 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;
tx[1]= 1; ty[1]= 0;
tx[2]=-1; ty[2]= 0;
tx[4]= 0; ty[4]=-1;
//
//
//
//
no movement
right
left
up
tx[8]= 0; ty[8]= 1;
// down
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.
That's all.
row
col
animation phase
next delta for p (+1 or -1)
the directions we could go
the current moving direction
delta value for x-movement
delta value for y-movement
x-offset for smooth animation
y-offset for smooth animation
// global vars
var pac= new PacMan();
var aSpeed=400;
var aStep=4;
movedir;
mazeX= 0;
mazeY= 0;
pacTimer;
gameOn=flase;
runThru=false;
f2=new Array();
tx[8]=0;
ty[8]=1;
//d
user input
start position: row 9, col 10
offsets for x and y values to zero
not moving anywhere
the directions we can move (left or right)
dx and dy are set to zero
reset animation values
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
}
}
// getting a new direction
function pMove() {
with (pac) {
// evaluate the direction to go
//
//
//
//
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.
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.
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;
}
}
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:
a. we're saving some run time, and
b. 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.
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.
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
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
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
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
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.
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.
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.
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.)
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.)
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.
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
FRIGHT
LEVEL NORM NORMDOTS FRIGHT DOTS NORM FRIGHT TUNNEL
80%
71%
90%
79%
75%
50%
40%
24
90%
79%
95%
83%
85%
55%
45%
5
20
100%
87%
100%
87%
95%
60%
50%
21+
90%
79%
95%
50%
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