Pacman in Perl/Tk
Revisiting a Pac-Man Implementation in Perl/Tk
More or less by accident, I recently came across an old project of mine from around 2013/2014. In it, I had tried to approximate Pac-Man using Perl and Tk.
It was never intended to be an exact reimplementation of the original game. There are plenty of those in almost every programming language imaginable. What interested me more was a different question: how could the structure of a game like Pac-Man be represented as a relatively simple data structure, with the Perl/Tk user interface merely visualizing that structure on a Canvas?
Back then, of course, there was no generative AI sitting next to me explaining what I had built.
More than ten years later, there is.
So I fed the old implementation to an AI and asked it to describe the underlying concept. Maybe, the approach is interesting for you.
So here it is.
Have fun reading.
The current implementation
Here is a short video showing the current state of the implementation:
The basic idea: the game world is a grid
At the heart of the implementation is a two-dimensional array.
The Pac-Man maze is not primarily a collection of graphical objects. Instead, every position in the maze is represented by one cell in a table-like data structure.
Conceptually, it looks something like this:
1 1 1 1 1 1 1 1 o o o o o 1 1 o 1 1 1 o 1 1 o 0 0 0 o 1 1 1 1 1 1 1 1
Each value describes the logical state of one tile.
For example:
1 = wall 0 = free space o = pill O = power pill W = warp point
The visual maze that appears on screen is generated from this data structure.
That distinction is important: the Canvas is not the game world. It is a visual representation of the game world.
Pac-Man does not really move in pixels
Pac-Man's position is represented by two integer values:
row column
The same principle applies to the ghosts.
So from the game's point of view, Pac-Man may be located at:
row = 17 column = 17
There is no requirement for the actual game logic to know that his center might currently be at pixel position 350/350 on the screen.
The pixel position can simply be calculated from the tile position.
In this implementation, a tile is 20 by 20 pixels. Rendering therefore essentially follows the rule:
x = column * 20 y = row * 20
with a small offset added to place the image in the center of the tile.
This creates a useful separation between two different things:
Logical game state
|
v
row / column
|
v
Rendering
|
v
pixel x / pixel y
I planned to make the movement look smoother later.
Movement is a transition between tiles
This also means that movement is discrete.
If Pac-Man is standing on tile:
[17,17]
and moves to the right, the game calculates:
[17,17] -> [17,18]
Before that transition is accepted, the target tile is inspected.
If the target contains a wall, Pac-Man cannot move there.
If it is accessible, his logical row and column are updated.
The basic process is therefore roughly:
Player input
|
v
Determine intended direction
|
v
Calculate neighbouring tile
|
v
Check whether the tile is accessible
|
v
Update logical position
|
v
Handle events
|
v
Update visualization
There is no physics engine involved, no collision rectangle moving continuously through the maze and no need to determine whether two polygons intersect.
For this kind of game, the grid already contains most of the information required by the game logic.
Smooth movement can still be just an animation
The early version simply made Pac-Man jump visually from one tile to the next.
That obviously works, but it does not look particularly elegant.
I also found versions with smoother movement animations, but I still didn't get them to work that properly.
Conceptually, it was designed like this:
Logical state: [17,17] -> [17,18] Visual transition: 350 -> 354 -> 358 -> 362 -> 366 -> 370
This does not necessarily require changing the logical model.
The game can still reason about tiles, while the user interface interpolates between their pixel positions.
In other words, smooth movement can be a presentation concern rather than a game-logic concern.
The tiles contain game state as well
...which might not be the best thing. But hey. We all started once.
A pill is not necessarily an independent object with its own coordinates.
Instead, the board itself can say:
board[row][column] = "o"
When Pac-Man enters that tile, the game detects the pill, increases the score and changes the tile to free space:
o -> 0
The maze therefore acts as both a description of the level geometry and a container for parts of the current game state.
This makes interactions rather straightforward.
Entering a tile can trigger events depending on what that tile contains:
wall -> movement rejected pill -> increase score, clear tile power pill -> special behaviour warp point -> change target position free space -> nothing special
Collisions become very simple
The same model simplifies collisions with ghosts.
Pac-Man and a ghost collide if they occupy the same logical tile:
pacman.row == ghost.row and pacman.column == ghost.column
Again, no pixel-perfect collision detection used.
Warp tunnels are just special tile transitions
The warp tunnel fits naturally into the same concept.
A warp point is simply another special tile type:
W
When Pac-Man enters such a tile, the normal target position is replaced by a configured position on the other side of the board.
One detail turned out to be useful here: the exit position does not have to be the other warp tile itself.
Instead, Pac-Man can be placed on the first normal tile immediately inside the maze.
For example:
[W] [ ] [ ] ... [ ] [W]
^ ^
| |
exit entrance
Entering the warp on the right while moving to the right places Pac-Man just inside the left side of the maze.
His movement direction remains unchanged.
So logically:
move right
|
v
enter right warp
|
v
appear on left side
|
v
continue moving right
The same principle works for vertical warp tunnels.
A warp changes the position. It does not have to change the movement direction.
The maze can also be viewed as a graph
There is another way of looking at this data structure which I find interesting today.
Although the board is stored as a two-dimensional grid, the accessible part of the maze can effectively be interpreted as a graph.
Every accessible tile is a node.
Possible movements to neighbouring tiles are edges.
A corridor therefore looks roughly like this:
[o] -- [o] -- [o] -- [o]
while an intersection might look like this:
[ ]
|
[ ] -- [ ] -- [ ]
|
[ ]
The old implementation already models ghosts using row and column coordinates just like Pac-Man, although the ghost movement itself was still largely unfinished in the version I rediscovered.
Model first, visualization second
Looking at the code today, AI would describe its central idea as:
GAME MODEL
Board Entities
board[row][col] Pac-Man: row, col
Ghost: row, col
| |
+------------+------------+
|
Game logic
|
tile-to-tile movement
|
collisions / pills / warp
|
v
VIEW
Tk Canvas
|
row/col -> x/y
|
optional interpolation
|
v
Screen
It is obviously not a formal Model-View architecture, and I certainly did not sit down in 2013 thinking, "I am going to create an elegant architectural separation here."
But the basic separation is there.
The authoritative state of the game is not the Canvas.
It is the data behind it.
I like Perl.
Leave a comment