- Year
- 2024
- Team
- Solo
- My role
- Game logic + graphics, solo
- Built with
- C#, Windows Forms
/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.
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
Start solid
Every cell begins as wall. Pick a random starting cell and open it.
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.
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.
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.
96private void RecursiveBacktracking(Point currentCell)97{98maze[currentCell.X, currentCell.Y] = 0; // Mark current cell as part of the maze99100// Get a random direction to move101Direction[] directions = { Direction.Up, Direction.Down, Direction.Left, Direction.Right };102Shuffle(directions); // Randomize the order of directions103104foreach (Direction direction in directions)105{106Point nextCell = GetNextCell(currentCell, direction, 2);107Point betweenCell = GetNextCell(currentCell, direction);108109// Check if the cells are within bounds110if (IsWithinBounds(nextCell) && IsWithinBounds(betweenCell) && maze[nextCell.X, nextCell.Y] == 1)111{112// Carve a passage113maze[betweenCell.X, betweenCell.Y] = 0; // Mark the cell between as part of the maze114RecursiveBacktracking(nextCell); // Recursively call the function115}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+.
337private void Timer_Tick(object sender, EventArgs e)338{339if (!mazeGenerated)340{341return; // If maze is not generated, do not proceed with game logic342}343344MovePacMan(); // Move Pac-Man345346// Check for collision with ghosts347if (CheckGhostCollision())348{349score -= 50;350collisionCount++; // Increment collision count351352if (collisionCount == 3) // Check if three collisions occurred353{354timer.Stop(); // Stop the game loop355356// Stop background music357gameplayMusicPlayer.Stop();358359// Show message box360DialogResult result = MessageBox.Show("You lose! Do you want to start again?", "Game Over", MessageBoxButtons.YesNo);361if (result == DialogResult.Yes)362{363RestartGame(); // Restart the game364}365return;366}367}368369// Check if all pellets are eaten370if (pellets.Count == 0)371{372timer.Stop(); // Stop the game loop373374// Stop background music375gameplayMusicPlayer.Stop();376377DialogResult result = MessageBox.Show("Congratulations! You've eaten all the pellets.", "Victory!");378if (result == DialogResult.OK)379{380RestartGame(); // Restart the game381}382return;383}384385MoveGhosts();386Refresh(); // Redraw the form387}
Read the full source · WindowsFormsApp1/Form1.cs · 488 lines
1using System;2using System.Collections.Generic;3using System.Drawing;4using System.Threading.Tasks;5using System.Windows.Forms;6using System.Media;7namespace WindowsFormsApp18{9public partial class Form1 : Form10{11private const int cellSize = 20; // Size of each cell in pixels12private const int mazeWidth = 35; // Number of cells horizontally13private const int mazeHeight = 25; // Number of cells vertically14private int[,] maze; // Represents the maze, 1 means a wall, 0 means an empty space15private Point pacmanPosition; // Pac-Man's current position16private Point initialPacmanPosition; // Pac-Man's initial position17private Direction pacmanDirection; // Pac-Man's current direction18private List<Point> pellets;19private List<Point> ghostPositions;20private List<Direction> ghostDirections;21private System.Windows.Forms.Timer timer;// List of pellet positions22private Random random = new Random();23private int score = 0;24private PictureBox startPictureBox;25private Button startButton;26private bool mazeGenerated = false;27private int collisionCount = 0;28private SoundPlayer gameplayMusicPlayer;293031public Form1()32{33InitializeComponent();34InitializeStartControls();35KeyPreview = true; // Set KeyPreview property to true36KeyDown += MainForm_KeyDown; // Handle KeyDown event37InitializeBackgroundMusic();38gameplayMusicPlayer.PlayLooping();39}40private void InitializeBackgroundMusic()41{42// Initialize SoundPlayer object with background music file43gameplayMusicPlayer = new SoundPlayer(Properties.Resources.pacman_intro);44}4546protected override void OnFormClosing(FormClosingEventArgs e)47{48// Stop playing background music when the form is closing49gameplayMusicPlayer.Stop();50base.OnFormClosing(e);51}52private void InitializeGame()53{54score = 0;55timer = new System.Windows.Forms.Timer();56mazePictureBox.Size = new Size(mazeWidth * cellSize, mazeHeight * cellSize);5758// Initialize enemy after maze generation59InitializeGhosts();6061pacmanDirection = Direction.Right; // Initial direction of Pac-Man62InitializePellets();63timer.Interval = 50; // Game loop interval (milliseconds)64timer.Tick += Timer_Tick;65timer.Start();66}676869private void GenerateMaze()70{71// Fill the maze with walls72maze = new int[mazeWidth, mazeHeight];73for (int x = 0; x < mazeWidth; x++)74{75for (int y = 0; y < mazeHeight; y++)76{77maze[x, y] = 1; // Initialize every cell as a wall78}79}8081// Create a random starting point82Point startingPoint = new Point(random.Next(1, mazeWidth - 1), random.Next(1, mazeHeight - 1));8384// Generate the maze starting from the random point85RecursiveBacktracking(startingPoint);8687// Ensure that Pac-Man's initial position is not inside a wall88do89{90pacmanPosition = initialPacmanPosition = new Point(random.Next(mazeWidth), random.Next(mazeHeight));91} while (maze[pacmanPosition.X, pacmanPosition.Y] == 1); // Repeat until a free cell is found9293mazeGenerated = true;94}9596private void RecursiveBacktracking(Point currentCell)97{98maze[currentCell.X, currentCell.Y] = 0; // Mark current cell as part of the maze99100// Get a random direction to move101Direction[] directions = { Direction.Up, Direction.Down, Direction.Left, Direction.Right };102Shuffle(directions); // Randomize the order of directions103104foreach (Direction direction in directions)105{106Point nextCell = GetNextCell(currentCell, direction, 2);107Point betweenCell = GetNextCell(currentCell, direction);108109// Check if the cells are within bounds110if (IsWithinBounds(nextCell) && IsWithinBounds(betweenCell) && maze[nextCell.X, nextCell.Y] == 1)111{112// Carve a passage113maze[betweenCell.X, betweenCell.Y] = 0; // Mark the cell between as part of the maze114RecursiveBacktracking(nextCell); // Recursively call the function115}116}117}118private void InitializeGhosts()119{120ghostPositions = new List<Point>();121ghostDirections = new List<Direction>();122123// Initialize initial ghost positions and directions124for (int i = 0; i < 2; i++) // Initial two ghosts125{126Point ghostPosition;127Direction ghostDirection;128129do130{131ghostPosition = new Point(random.Next(mazeWidth), random.Next(mazeHeight));132} while (!IsWithinBounds(ghostPosition) || maze[ghostPosition.X, ghostPosition.Y] == 1);133134ghostDirection = (Direction)random.Next(4); // Random initial direction135136ghostPositions.Add(ghostPosition);137ghostDirections.Add(ghostDirection);138}139}140141private void MoveGhosts()142{143for (int i = 0; i < ghostPositions.Count; i++)144{145// Get possible directions for the ghost146List<Direction> possibleDirections = new List<Direction>();147148// Check Up149Point nextPosition = GetNextPosition(ghostPositions[i], Direction.Up);150if (IsWithinBounds(nextPosition) && (maze[nextPosition.X, nextPosition.Y] == 0 || maze[nextPosition.X, nextPosition.Y] == -1))151{152possibleDirections.Add(Direction.Up);153}154155// Check Down156nextPosition = GetNextPosition(ghostPositions[i], Direction.Down);157if (IsWithinBounds(nextPosition) && (maze[nextPosition.X, nextPosition.Y] == 0 || maze[nextPosition.X, nextPosition.Y] == -1))158{159possibleDirections.Add(Direction.Down);160}161162// Check Left163nextPosition = GetNextPosition(ghostPositions[i], Direction.Left);164if (IsWithinBounds(nextPosition) && (maze[nextPosition.X, nextPosition.Y] == 0 || maze[nextPosition.X, nextPosition.Y] == -1))165{166possibleDirections.Add(Direction.Left);167}168169// Check Right170nextPosition = GetNextPosition(ghostPositions[i], Direction.Right);171if (IsWithinBounds(nextPosition) && (maze[nextPosition.X, nextPosition.Y] == 0 || maze[nextPosition.X, nextPosition.Y] == -1))172{173possibleDirections.Add(Direction.Right);174}175176// If there are possible directions, randomly select one177if (possibleDirections.Count > 0)178{179Direction nextDirection = possibleDirections[random.Next(possibleDirections.Count)];180ghostPositions[i] = GetNextPosition(ghostPositions[i], nextDirection); // Move the ghost181}182}183}184185186private void InitializePellets()187{188pellets = new List<Point>();189// Add pellets to the maze190for (int y = 0; y < mazeHeight; y++)191{192for (int x = 0; x < mazeWidth; x++)193{194if (maze[x, y] == 0)195{196pellets.Add(new Point(x, y));197}198}199}200}201private void DrawMaze(Graphics g)202{203// Draw the maze on the PictureBox control204for (int y = 0; y < mazeHeight; y++)205{206for (int x = 0; x < mazeWidth; x++)207{208if (maze[x, y] == 1)209g.FillRectangle(Brushes.Blue, x * cellSize, y * cellSize, cellSize, cellSize); // Draw walls210}211}212213// Draw pellets214foreach (Point pellet in pellets)215{216int pelletX = pellet.X * cellSize + cellSize / 3;217int pelletY = pellet.Y * cellSize + cellSize / 3;218if (maze[pellet.X, pellet.Y] != -1) // Check if the pellet is not eaten219{220g.FillEllipse(Brushes.White, pelletX, pelletY, cellSize / 3, cellSize / 3);221}222}223224// Draw Pac-Man225g.FillEllipse(Brushes.Yellow, pacmanPosition.X * cellSize, pacmanPosition.Y * cellSize, cellSize, cellSize);226227// Draw Ghosts228foreach (Point ghostPos in ghostPositions)229{230g.FillEllipse(Brushes.Red, ghostPos.X * cellSize, ghostPos.Y * cellSize, cellSize, cellSize);231}232233// Update label1 with the score234label1.Text = "Score: " + score;235}236237238private void MainForm_Paint(object sender, PaintEventArgs e)239{240if (mazeGenerated)241{242DrawMaze(e.Graphics);243}244}245246private void MovePacMan()247{248// Move Pac-Man in the current direction249Point newPosition = GetNextPosition(pacmanPosition, pacmanDirection);250if (CanMoveTo(newPosition))251{252RemovePelletFromMaze(newPosition);253pacmanPosition = newPosition;254}255}256257private void RemovePelletFromMaze(Point position)258{259// Check if the specified position overlaps with any pellets260for (int i = 0; i < pellets.Count; i++)261{262if (pellets[i] == position)263{264pellets.RemoveAt(i); // Remove the pellet from the list of pellets265maze[position.X, position.Y] = -1; // Mark the pellet as eaten in the maze266score += 10;267break; // Exit the loop after removing the pellet268}269}270}271272private Point GetNextPosition(Point position, Direction direction)273{274// Calculate the next position of Pac-Man based on the current direction275switch (direction)276{277case Direction.Up:278return new Point(position.X, position.Y - 1);279case Direction.Down:280return new Point(position.X, position.Y + 1);281case Direction.Left:282return new Point(position.X - 1, position.Y);283case Direction.Right:284return new Point(position.X + 1, position.Y);285default:286return position;287}288}289290private bool CanMoveTo(Point position)291{292// Check if Pac-Man can move to the specified position (not hitting a wall)293if (position.X < 0 || position.X >= mazeWidth || position.Y < 0 || position.Y >= mazeHeight)294return false; // Out of bounds295return maze[position.X, position.Y] != 1; // Not a wall296}297298private void MainForm_KeyDown(object sender, KeyEventArgs e)299{300// Handle arrow key presses to change Pac-Man's direction301switch (e.KeyCode)302{303case Keys.Up:304pacmanDirection = Direction.Up;305break;306case Keys.Down:307pacmanDirection = Direction.Down;308break;309case Keys.Left:310pacmanDirection = Direction.Left;311break;312case Keys.Right:313pacmanDirection = Direction.Right;314break;315}316}317private void RestartGame()318{319score = 0;320collisionCount = 0;321GenerateMaze(); // Reset maze322InitializeGhosts(); // Reset ghosts323pacmanPosition = initialPacmanPosition; // Reset Pac-Man's position324pacmanDirection = Direction.Right; // Reset Pac-Man's direction325InitializePellets(); // Reset pellets326timer.Start(); // Start the game loop timer327328// Reinitialize the background music player329gameplayMusicPlayer = new SoundPlayer(Properties.Resources.pacman_intro);330331// Restart background music332gameplayMusicPlayer.PlayLooping();333}334335336337private void Timer_Tick(object sender, EventArgs e)338{339if (!mazeGenerated)340{341return; // If maze is not generated, do not proceed with game logic342}343344MovePacMan(); // Move Pac-Man345346// Check for collision with ghosts347if (CheckGhostCollision())348{349score -= 50;350collisionCount++; // Increment collision count351352if (collisionCount == 3) // Check if three collisions occurred353{354timer.Stop(); // Stop the game loop355356// Stop background music357gameplayMusicPlayer.Stop();358359// Show message box360DialogResult result = MessageBox.Show("You lose! Do you want to start again?", "Game Over", MessageBoxButtons.YesNo);361if (result == DialogResult.Yes)362{363RestartGame(); // Restart the game364}365return;366}367}368369// Check if all pellets are eaten370if (pellets.Count == 0)371{372timer.Stop(); // Stop the game loop373374// Stop background music375gameplayMusicPlayer.Stop();376377DialogResult result = MessageBox.Show("Congratulations! You've eaten all the pellets.", "Victory!");378if (result == DialogResult.OK)379{380RestartGame(); // Restart the game381}382return;383}384385MoveGhosts();386Refresh(); // Redraw the form387}388389390391392393private bool CheckGhostCollision()394{395// Check if Pac-Man's position overlaps with any ghost396foreach (Point ghostPos in ghostPositions)397{398if (pacmanPosition == ghostPos)399{400// Increment collision count when Pac-Man collides with a ghost401return true; // Collision detected402}403}404return false; // No collision detected405}406407private void Shuffle<T>(T[] array)408{409for (int i = array.Length - 1; i > 0; i--)410{411int j = random.Next(0, i + 1);412T temp = array[i];413array[i] = array[j];414array[j] = temp;415}416}417418private Point GetNextCell(Point currentCell, Direction direction, int steps = 1)419{420switch (direction)421{422case Direction.Up:423return new Point(currentCell.X, currentCell.Y - steps);424case Direction.Down:425return new Point(currentCell.X, currentCell.Y + steps);426case Direction.Left:427return new Point(currentCell.X - steps, currentCell.Y);428case Direction.Right:429return new Point(currentCell.X + steps, currentCell.Y);430default:431return currentCell;432}433}434435private bool IsWithinBounds(Point cell)436{437return cell.X >= 0 && cell.X < mazeWidth && cell.Y >= 0 && cell.Y < mazeHeight;438}439440private void label1_Click(object sender, EventArgs e)441{ }442public enum Direction443{444Up,445Down,446Left,447Right448}449private void InitializeStartControls()450{451// Initialize start PictureBox452startPictureBox = new PictureBox();453startPictureBox.Image = Properties.Resources.pacMan; // Load your start image here454startPictureBox.SizeMode = PictureBoxSizeMode.StretchImage;455startPictureBox.Size = new Size(700, 700); // Set the size of the start image456startPictureBox.Location = new Point((mazePictureBox.Width - startPictureBox.Width) / 2, (mazePictureBox.Height - startPictureBox.Height) / 2);457startPictureBox.Click += StartPictureBox_Click;458mazePictureBox.Controls.Add(startPictureBox); // Add the start PictureBox to the maze PictureBox459460// Initialize start Button461startButton = new Button();462startButton.Text = "Start Game";463startButton.Size = new Size(100, 30);464startButton.Location = new Point((mazePictureBox.Width - startButton.Width) / 2, startPictureBox.Bottom + 20);465startButton.Click += StartButton_Click;466mazePictureBox.Controls.Add(startButton); // Add the start Button to the maze PictureBox467label1.Visible = false;468}469470private void StartButton_Click(object sender, EventArgs e)471{472// Hide start PictureBox and start Button473startPictureBox.Visible = false;474startButton.Visible = false;475label1.Visible = true;476// Generate the maze477GenerateMaze();478479// Initialize game after maze generation480InitializeGame();481}482483private void StartPictureBox_Click(object sender, EventArgs e)484{485// Handle click event of start PictureBox (if needed)486}487}488}