All work
Arcade game2024

Maze Chase

A Pac-Man-style game where every run gets a brand-new maze.

Year
2024
Team
Solo
My role
Game logic + graphics, solo
Built with
C#, Windows Forms
35×25
cells in every maze
∞
different mazes
+10 / −50
per pellet / per ghost hit
488
lines of C#, no engine

/play

Play it right here

The original is a Windows desktop app. So you can play it here, I rebuilt it in TypeScript with the same rules: the same maze algorithm and grid, pellets, ghosts and scoring.

Score 00 pellets left

Maze Chase

Eat every pellet. Dodge the ghosts: three hits and you're out. Every game carves a brand-new maze.

Arrow keys or WASD · swipe on a phone

/how

A new maze every time, carved by recursive backtracking

01

Start solid

Every cell begins as wall. Pick a random starting cell and open it.

02

Dig two steps

Shuffle the four directions. For each one, if the cell two steps away is still wall, open it and the cell in between, then dig on from there.

03

Back up when stuck

When no direction is left, the recursion unwinds to the last cell that still has options. The result is a maze where every cell can be reached.

04

Fill it with life

Every open cell gets a pellet. Pac-Man and two ghosts drop onto random open cells, and the timer starts the game loop.

Form1.cs · RecursiveBacktracking
96 private void RecursiveBacktracking(Point currentCell)
97 {
98 maze[currentCell.X, currentCell.Y] = 0; // Mark current cell as part of the maze
99
100 // Get a random direction to move
101 Direction[] directions = { Direction.Up, Direction.Down, Direction.Left, Direction.Right };
102 Shuffle(directions); // Randomize the order of directions
103
104 foreach (Direction direction in directions)
105 {
106 Point nextCell = GetNextCell(currentCell, direction, 2);
107 Point betweenCell = GetNextCell(currentCell, direction);
108
109 // Check if the cells are within bounds
110 if (IsWithinBounds(nextCell) && IsWithinBounds(betweenCell) && maze[nextCell.X, nextCell.Y] == 1)
111 {
112 // Carve a passage
113 maze[betweenCell.X, betweenCell.Y] = 0; // Mark the cell between as part of the maze
114 RecursiveBacktracking(nextCell); // Recursively call the function
115 }
116 }
117 }

/game-loop

One timer drives the whole game

Each tick moves Pac-Man, checks for ghosts (three hits and it’s game over), checks whether the maze is clear, moves the ghosts and redraws everything with GDI+.

Form1.cs · Timer_Tick
337 private void Timer_Tick(object sender, EventArgs e)
338 {
339 if (!mazeGenerated)
340 {
341 return; // If maze is not generated, do not proceed with game logic
342 }
343
344 MovePacMan(); // Move Pac-Man
345
346 // Check for collision with ghosts
347 if (CheckGhostCollision())
348 {
349 score -= 50;
350 collisionCount++; // Increment collision count
351
352 if (collisionCount == 3) // Check if three collisions occurred
353 {
354 timer.Stop(); // Stop the game loop
355
356 // Stop background music
357 gameplayMusicPlayer.Stop();
358
359 // Show message box
360 DialogResult result = MessageBox.Show("You lose! Do you want to start again?", "Game Over", MessageBoxButtons.YesNo);
361 if (result == DialogResult.Yes)
362 {
363 RestartGame(); // Restart the game
364 }
365 return;
366 }
367 }
368
369 // Check if all pellets are eaten
370 if (pellets.Count == 0)
371 {
372 timer.Stop(); // Stop the game loop
373
374 // Stop background music
375 gameplayMusicPlayer.Stop();
376
377 DialogResult result = MessageBox.Show("Congratulations! You've eaten all the pellets.", "Victory!");
378 if (result == DialogResult.OK)
379 {
380 RestartGame(); // Restart the game
381 }
382 return;
383 }
384
385 MoveGhosts();
386 Refresh(); // Redraw the form
387 }
Read the full source · WindowsFormsApp1/Form1.cs · 488 lines
WindowsFormsApp1/Form1.cs
1using System;
2using System.Collections.Generic;
3using System.Drawing;
4using System.Threading.Tasks;
5using System.Windows.Forms;
6using System.Media;
7namespace WindowsFormsApp1
8{
9 public partial class Form1 : Form
10 {
11 private const int cellSize = 20; // Size of each cell in pixels
12 private const int mazeWidth = 35; // Number of cells horizontally
13 private const int mazeHeight = 25; // Number of cells vertically
14 private int[,] maze; // Represents the maze, 1 means a wall, 0 means an empty space
15 private Point pacmanPosition; // Pac-Man's current position
16 private Point initialPacmanPosition; // Pac-Man's initial position
17 private Direction pacmanDirection; // Pac-Man's current direction
18 private List<Point> pellets;
19 private List<Point> ghostPositions;
20 private List<Direction> ghostDirections;
21 private System.Windows.Forms.Timer timer;// List of pellet positions
22 private Random random = new Random();
23 private int score = 0;
24 private PictureBox startPictureBox;
25 private Button startButton;
26 private bool mazeGenerated = false;
27 private int collisionCount = 0;
28 private SoundPlayer gameplayMusicPlayer;
29
30
31 public Form1()
32 {
33 InitializeComponent();
34 InitializeStartControls();
35 KeyPreview = true; // Set KeyPreview property to true
36 KeyDown += MainForm_KeyDown; // Handle KeyDown event
37 InitializeBackgroundMusic();
38 gameplayMusicPlayer.PlayLooping();
39 }
40 private void InitializeBackgroundMusic()
41 {
42 // Initialize SoundPlayer object with background music file
43 gameplayMusicPlayer = new SoundPlayer(Properties.Resources.pacman_intro);
44 }
45
46 protected override void OnFormClosing(FormClosingEventArgs e)
47 {
48 // Stop playing background music when the form is closing
49 gameplayMusicPlayer.Stop();
50 base.OnFormClosing(e);
51 }
52 private void InitializeGame()
53 {
54 score = 0;
55 timer = new System.Windows.Forms.Timer();
56 mazePictureBox.Size = new Size(mazeWidth * cellSize, mazeHeight * cellSize);
57
58 // Initialize enemy after maze generation
59 InitializeGhosts();
60
61 pacmanDirection = Direction.Right; // Initial direction of Pac-Man
62 InitializePellets();
63 timer.Interval = 50; // Game loop interval (milliseconds)
64 timer.Tick += Timer_Tick;
65 timer.Start();
66 }
67
68
69 private void GenerateMaze()
70 {
71 // Fill the maze with walls
72 maze = new int[mazeWidth, mazeHeight];
73 for (int x = 0; x < mazeWidth; x++)
74 {
75 for (int y = 0; y < mazeHeight; y++)
76 {
77 maze[x, y] = 1; // Initialize every cell as a wall
78 }
79 }
80
81 // Create a random starting point
82 Point startingPoint = new Point(random.Next(1, mazeWidth - 1), random.Next(1, mazeHeight - 1));
83
84 // Generate the maze starting from the random point
85 RecursiveBacktracking(startingPoint);
86
87 // Ensure that Pac-Man's initial position is not inside a wall
88 do
89 {
90 pacmanPosition = initialPacmanPosition = new Point(random.Next(mazeWidth), random.Next(mazeHeight));
91 } while (maze[pacmanPosition.X, pacmanPosition.Y] == 1); // Repeat until a free cell is found
92
93 mazeGenerated = true;
94 }
95
96 private void RecursiveBacktracking(Point currentCell)
97 {
98 maze[currentCell.X, currentCell.Y] = 0; // Mark current cell as part of the maze
99
100 // Get a random direction to move
101 Direction[] directions = { Direction.Up, Direction.Down, Direction.Left, Direction.Right };
102 Shuffle(directions); // Randomize the order of directions
103
104 foreach (Direction direction in directions)
105 {
106 Point nextCell = GetNextCell(currentCell, direction, 2);
107 Point betweenCell = GetNextCell(currentCell, direction);
108
109 // Check if the cells are within bounds
110 if (IsWithinBounds(nextCell) && IsWithinBounds(betweenCell) && maze[nextCell.X, nextCell.Y] == 1)
111 {
112 // Carve a passage
113 maze[betweenCell.X, betweenCell.Y] = 0; // Mark the cell between as part of the maze
114 RecursiveBacktracking(nextCell); // Recursively call the function
115 }
116 }
117 }
118 private void InitializeGhosts()
119 {
120 ghostPositions = new List<Point>();
121 ghostDirections = new List<Direction>();
122
123 // Initialize initial ghost positions and directions
124 for (int i = 0; i < 2; i++) // Initial two ghosts
125 {
126 Point ghostPosition;
127 Direction ghostDirection;
128
129 do
130 {
131 ghostPosition = new Point(random.Next(mazeWidth), random.Next(mazeHeight));
132 } while (!IsWithinBounds(ghostPosition) || maze[ghostPosition.X, ghostPosition.Y] == 1);
133
134 ghostDirection = (Direction)random.Next(4); // Random initial direction
135
136 ghostPositions.Add(ghostPosition);
137 ghostDirections.Add(ghostDirection);
138 }
139 }
140
141 private void MoveGhosts()
142 {
143 for (int i = 0; i < ghostPositions.Count; i++)
144 {
145 // Get possible directions for the ghost
146 List<Direction> possibleDirections = new List<Direction>();
147
148 // Check Up
149 Point nextPosition = GetNextPosition(ghostPositions[i], Direction.Up);
150 if (IsWithinBounds(nextPosition) && (maze[nextPosition.X, nextPosition.Y] == 0 || maze[nextPosition.X, nextPosition.Y] == -1))
151 {
152 possibleDirections.Add(Direction.Up);
153 }
154
155 // Check Down
156 nextPosition = GetNextPosition(ghostPositions[i], Direction.Down);
157 if (IsWithinBounds(nextPosition) && (maze[nextPosition.X, nextPosition.Y] == 0 || maze[nextPosition.X, nextPosition.Y] == -1))
158 {
159 possibleDirections.Add(Direction.Down);
160 }
161
162 // Check Left
163 nextPosition = GetNextPosition(ghostPositions[i], Direction.Left);
164 if (IsWithinBounds(nextPosition) && (maze[nextPosition.X, nextPosition.Y] == 0 || maze[nextPosition.X, nextPosition.Y] == -1))
165 {
166 possibleDirections.Add(Direction.Left);
167 }
168
169 // Check Right
170 nextPosition = GetNextPosition(ghostPositions[i], Direction.Right);
171 if (IsWithinBounds(nextPosition) && (maze[nextPosition.X, nextPosition.Y] == 0 || maze[nextPosition.X, nextPosition.Y] == -1))
172 {
173 possibleDirections.Add(Direction.Right);
174 }
175
176 // If there are possible directions, randomly select one
177 if (possibleDirections.Count > 0)
178 {
179 Direction nextDirection = possibleDirections[random.Next(possibleDirections.Count)];
180 ghostPositions[i] = GetNextPosition(ghostPositions[i], nextDirection); // Move the ghost
181 }
182 }
183 }
184
185
186 private void InitializePellets()
187 {
188 pellets = new List<Point>();
189 // Add pellets to the maze
190 for (int y = 0; y < mazeHeight; y++)
191 {
192 for (int x = 0; x < mazeWidth; x++)
193 {
194 if (maze[x, y] == 0)
195 {
196 pellets.Add(new Point(x, y));
197 }
198 }
199 }
200 }
201 private void DrawMaze(Graphics g)
202 {
203 // Draw the maze on the PictureBox control
204 for (int y = 0; y < mazeHeight; y++)
205 {
206 for (int x = 0; x < mazeWidth; x++)
207 {
208 if (maze[x, y] == 1)
209 g.FillRectangle(Brushes.Blue, x * cellSize, y * cellSize, cellSize, cellSize); // Draw walls
210 }
211 }
212
213 // Draw pellets
214 foreach (Point pellet in pellets)
215 {
216 int pelletX = pellet.X * cellSize + cellSize / 3;
217 int pelletY = pellet.Y * cellSize + cellSize / 3;
218 if (maze[pellet.X, pellet.Y] != -1) // Check if the pellet is not eaten
219 {
220 g.FillEllipse(Brushes.White, pelletX, pelletY, cellSize / 3, cellSize / 3);
221 }
222 }
223
224 // Draw Pac-Man
225 g.FillEllipse(Brushes.Yellow, pacmanPosition.X * cellSize, pacmanPosition.Y * cellSize, cellSize, cellSize);
226
227 // Draw Ghosts
228 foreach (Point ghostPos in ghostPositions)
229 {
230 g.FillEllipse(Brushes.Red, ghostPos.X * cellSize, ghostPos.Y * cellSize, cellSize, cellSize);
231 }
232
233 // Update label1 with the score
234 label1.Text = "Score: " + score;
235 }
236
237
238 private void MainForm_Paint(object sender, PaintEventArgs e)
239 {
240 if (mazeGenerated)
241 {
242 DrawMaze(e.Graphics);
243 }
244 }
245
246 private void MovePacMan()
247 {
248 // Move Pac-Man in the current direction
249 Point newPosition = GetNextPosition(pacmanPosition, pacmanDirection);
250 if (CanMoveTo(newPosition))
251 {
252 RemovePelletFromMaze(newPosition);
253 pacmanPosition = newPosition;
254 }
255 }
256
257 private void RemovePelletFromMaze(Point position)
258 {
259 // Check if the specified position overlaps with any pellets
260 for (int i = 0; i < pellets.Count; i++)
261 {
262 if (pellets[i] == position)
263 {
264 pellets.RemoveAt(i); // Remove the pellet from the list of pellets
265 maze[position.X, position.Y] = -1; // Mark the pellet as eaten in the maze
266 score += 10;
267 break; // Exit the loop after removing the pellet
268 }
269 }
270 }
271
272 private Point GetNextPosition(Point position, Direction direction)
273 {
274 // Calculate the next position of Pac-Man based on the current direction
275 switch (direction)
276 {
277 case Direction.Up:
278 return new Point(position.X, position.Y - 1);
279 case Direction.Down:
280 return new Point(position.X, position.Y + 1);
281 case Direction.Left:
282 return new Point(position.X - 1, position.Y);
283 case Direction.Right:
284 return new Point(position.X + 1, position.Y);
285 default:
286 return position;
287 }
288 }
289
290 private bool CanMoveTo(Point position)
291 {
292 // Check if Pac-Man can move to the specified position (not hitting a wall)
293 if (position.X < 0 || position.X >= mazeWidth || position.Y < 0 || position.Y >= mazeHeight)
294 return false; // Out of bounds
295 return maze[position.X, position.Y] != 1; // Not a wall
296 }
297
298 private void MainForm_KeyDown(object sender, KeyEventArgs e)
299 {
300 // Handle arrow key presses to change Pac-Man's direction
301 switch (e.KeyCode)
302 {
303 case Keys.Up:
304 pacmanDirection = Direction.Up;
305 break;
306 case Keys.Down:
307 pacmanDirection = Direction.Down;
308 break;
309 case Keys.Left:
310 pacmanDirection = Direction.Left;
311 break;
312 case Keys.Right:
313 pacmanDirection = Direction.Right;
314 break;
315 }
316 }
317 private void RestartGame()
318 {
319 score = 0;
320 collisionCount = 0;
321 GenerateMaze(); // Reset maze
322 InitializeGhosts(); // Reset ghosts
323 pacmanPosition = initialPacmanPosition; // Reset Pac-Man's position
324 pacmanDirection = Direction.Right; // Reset Pac-Man's direction
325 InitializePellets(); // Reset pellets
326 timer.Start(); // Start the game loop timer
327
328 // Reinitialize the background music player
329 gameplayMusicPlayer = new SoundPlayer(Properties.Resources.pacman_intro);
330
331 // Restart background music
332 gameplayMusicPlayer.PlayLooping();
333 }
334
335
336
337 private void Timer_Tick(object sender, EventArgs e)
338 {
339 if (!mazeGenerated)
340 {
341 return; // If maze is not generated, do not proceed with game logic
342 }
343
344 MovePacMan(); // Move Pac-Man
345
346 // Check for collision with ghosts
347 if (CheckGhostCollision())
348 {
349 score -= 50;
350 collisionCount++; // Increment collision count
351
352 if (collisionCount == 3) // Check if three collisions occurred
353 {
354 timer.Stop(); // Stop the game loop
355
356 // Stop background music
357 gameplayMusicPlayer.Stop();
358
359 // Show message box
360 DialogResult result = MessageBox.Show("You lose! Do you want to start again?", "Game Over", MessageBoxButtons.YesNo);
361 if (result == DialogResult.Yes)
362 {
363 RestartGame(); // Restart the game
364 }
365 return;
366 }
367 }
368
369 // Check if all pellets are eaten
370 if (pellets.Count == 0)
371 {
372 timer.Stop(); // Stop the game loop
373
374 // Stop background music
375 gameplayMusicPlayer.Stop();
376
377 DialogResult result = MessageBox.Show("Congratulations! You've eaten all the pellets.", "Victory!");
378 if (result == DialogResult.OK)
379 {
380 RestartGame(); // Restart the game
381 }
382 return;
383 }
384
385 MoveGhosts();
386 Refresh(); // Redraw the form
387 }
388
389
390
391
392
393 private bool CheckGhostCollision()
394 {
395 // Check if Pac-Man's position overlaps with any ghost
396 foreach (Point ghostPos in ghostPositions)
397 {
398 if (pacmanPosition == ghostPos)
399 {
400 // Increment collision count when Pac-Man collides with a ghost
401 return true; // Collision detected
402 }
403 }
404 return false; // No collision detected
405 }
406
407 private void Shuffle<T>(T[] array)
408 {
409 for (int i = array.Length - 1; i > 0; i--)
410 {
411 int j = random.Next(0, i + 1);
412 T temp = array[i];
413 array[i] = array[j];
414 array[j] = temp;
415 }
416 }
417
418 private Point GetNextCell(Point currentCell, Direction direction, int steps = 1)
419 {
420 switch (direction)
421 {
422 case Direction.Up:
423 return new Point(currentCell.X, currentCell.Y - steps);
424 case Direction.Down:
425 return new Point(currentCell.X, currentCell.Y + steps);
426 case Direction.Left:
427 return new Point(currentCell.X - steps, currentCell.Y);
428 case Direction.Right:
429 return new Point(currentCell.X + steps, currentCell.Y);
430 default:
431 return currentCell;
432 }
433 }
434
435 private bool IsWithinBounds(Point cell)
436 {
437 return cell.X >= 0 && cell.X < mazeWidth && cell.Y >= 0 && cell.Y < mazeHeight;
438 }
439
440 private void label1_Click(object sender, EventArgs e)
441 { }
442 public enum Direction
443 {
444 Up,
445 Down,
446 Left,
447 Right
448 }
449 private void InitializeStartControls()
450 {
451 // Initialize start PictureBox
452 startPictureBox = new PictureBox();
453 startPictureBox.Image = Properties.Resources.pacMan; // Load your start image here
454 startPictureBox.SizeMode = PictureBoxSizeMode.StretchImage;
455 startPictureBox.Size = new Size(700, 700); // Set the size of the start image
456 startPictureBox.Location = new Point((mazePictureBox.Width - startPictureBox.Width) / 2, (mazePictureBox.Height - startPictureBox.Height) / 2);
457 startPictureBox.Click += StartPictureBox_Click;
458 mazePictureBox.Controls.Add(startPictureBox); // Add the start PictureBox to the maze PictureBox
459
460 // Initialize start Button
461 startButton = new Button();
462 startButton.Text = "Start Game";
463 startButton.Size = new Size(100, 30);
464 startButton.Location = new Point((mazePictureBox.Width - startButton.Width) / 2, startPictureBox.Bottom + 20);
465 startButton.Click += StartButton_Click;
466 mazePictureBox.Controls.Add(startButton); // Add the start Button to the maze PictureBox
467 label1.Visible = false;
468 }
469
470 private void StartButton_Click(object sender, EventArgs e)
471 {
472 // Hide start PictureBox and start Button
473 startPictureBox.Visible = false;
474 startButton.Visible = false;
475 label1.Visible = true;
476 // Generate the maze
477 GenerateMaze();
478
479 // Initialize game after maze generation
480 InitializeGame();
481 }
482
483 private void StartPictureBox_Click(object sender, EventArgs e)
484 {
485 // Handle click event of start PictureBox (if needed)
486 }
487 }
488}

/contact

Let’s design and build something people love using.

I’m looking for a UI/UX or frontend role on a team that ships. Email is the fastest way to reach me.

© 2026 Mayar Mohamed Issa. Designed and built by me, with Next.js, Tailwind CSS and Motion.