An emoji Pac-Man game in your terminal emulator :)
Use arrow keys to move, eat all food to win.
| Item | Emoji | Points | Effect |
|---|---|---|---|
| Food | ๐ฉ | 1 | Basic food |
| Cake | ๐ฐ | 5 | โ |
| Meat | ๐ฅฉ | 10 | โ |
| Candy | ๐ญ | 50 | Power-up! Ghosts turn ๐ฐ and flee โ eat them for 200 pts each |
You have 3 lives (โค๏ธโค๏ธโค๏ธ). Touching a normal ghost costs one life; touching a ๐ฐ frightened ghost eats it instead.
Press P to pause/resume.
Compile and run pm.c, that's it.
cc pm.c && ./a.out- ๐บ๏ธ Custom map
- ๐ป Personality-based ghost AI (BFS pathfinding)
- โก Frame-diff incremental rendering
The original implementation used Manhattan-distance greedy pathfinding: at each step, the ghost picks the neighbor that minimizes |dx| + |dy| to the player. This had three major issues:
-
Blind to walls โ The greedy approach only looks one step ahead. When a wall sits between the ghost and player in an L-shape, the ghost oscillates against the wall because the Manhattan-optimal direction is blocked, but the second-best direction keeps pulling it back.
-
Homogeneous behavior โ All ghosts use the identical strategy and target, so they cluster on the same path and block each other (the
isghost()check makes trailing ghosts freeze). -
Fake difficulty โ
rand() % 2randomly skips ghost movement. This doesn't make ghosts smarter, just slower and inconsistent.
To fix these issues, we replaced the greedy algorithm with BFS pathfinding and gave each ghost a distinct personality, inspired by the classic arcade game.
| Ghost | Personality | Strategy | Speed |
|---|---|---|---|
| ๐ A | Chase (Blinky) | BFS directly to player position | Every frame |
| ๐ป B | Ambush (Pinky) | BFS to 4 cells ahead of player | Every 2 frames |
| ๐น C | Random (Inky) | 30% BFS to player, 70% random valid direction | Every 2 frames |
How BFS solves the wall-blindness problem: BFS explores the grid level by level from the ghost's position to its target. Since it considers all reachable cells, it always finds the true shortest path around walls โ no more oscillation. The map is at most 21ร20 = 420 cells, so BFS completes in O(420) โ negligible cost even at high frame rates. The function returns the direction of the first step on the shortest path, and the ghost takes that step.
How personality solves the homogeneity problem: Instead of all ghosts targeting the player's current position, each ghost aims at a different point โ the player's current position, a position ahead of the player, or a random direction. This spreads ghosts across different paths and creates a more organic encirclement pattern.
How tick-based speed solves the fake-difficulty problem: Ghost speed is now controlled deterministically via a tick % speed counter rather than random skipping. This gives consistent, predictable behavior that's still tunable per ghost.
For the Ambush ghost, the target is offset 4 cells in the player's last movement direction. If that position is off the map or a wall, it falls back to the player's current position.
For the Random ghost, the 70% random walk avoids walls and adds unpredictability without being aimless.
The game uses double-buffered frame-diff incremental rendering:
- Two buffers:
buf(current frame) andprev(previous frame) - Each frame, only cells that changed between
bufandprevare redrawn - Changed cells use ANSI CSI escape sequences (
\x1b[row;colH) to reposition the cursor - A typical frame redraws only 8โ10 cells instead of all 420
This reduces output from ~4 KB/frame (full redraw) to ~200 bytes/frame (diff only).
This program is written in pure ANSI C without any game engine or GUI framework, but the way it calculates and renders frames borrows ideas from real rendering pipelines: double-buffered frame diffing for output, and BFS for pathfinding.
The source code is MIT licensed and also available at git.sr.ht/~zz/pm.