From b5469b31e26287741d1bf67a5feb71f97e472edf Mon Sep 17 00:00:00 2001 From: Geoff Thompson Date: Tue, 4 Feb 2025 14:58:02 +1300 Subject: [PATCH 1/2] Adding Monster Maze --- .github/workflows/Monster Maze Build.yml | 20 ++ .vscode/launch.json | 10 + .vscode/tasks.json | 13 + Projects/MonsterMaze/DirectionEnum.cs | 37 ++ Projects/MonsterMaze/GameUtils.cs | 26 ++ Projects/MonsterMaze/MazePoint.cs | 28 ++ .../MonsterMaze/MazeRecursiveGenerator.cs | 140 ++++++++ Projects/MonsterMaze/MazeStep.cs | 11 + Projects/MonsterMaze/MonsterMaze.csproj | 18 + Projects/MonsterMaze/MonsterMazeGame.cs | 321 +++++++++++++++++ Projects/MonsterMaze/Program.cs | 72 ++++ Projects/MonsterMaze/README.md | 65 ++++ Projects/MonsterMaze/monstermaze.ico | Bin 0 -> 95278 bytes .../Games/MonsterMaze/DirectionEnum.cs | 41 +++ .../Website/Games/MonsterMaze/GameUtils.cs | 31 ++ .../Website/Games/MonsterMaze/MazePoint.cs | 32 ++ .../MonsterMaze/MazeRecursiveGenerator.cs | 145 ++++++++ .../Website/Games/MonsterMaze/MazeStep.cs | 13 + .../Games/MonsterMaze/MonsterMazeGame.cs | 331 ++++++++++++++++++ Projects/Website/Games/MonsterMaze/Program.cs | 76 ++++ Projects/Website/Pages/Monster Maze.razor | 51 +++ README.md | 1 + dotnet-console-games.sln | 11 + dotnet-console-games.slnf | 1 + 24 files changed, 1494 insertions(+) create mode 100644 .github/workflows/Monster Maze Build.yml create mode 100644 Projects/MonsterMaze/DirectionEnum.cs create mode 100644 Projects/MonsterMaze/GameUtils.cs create mode 100644 Projects/MonsterMaze/MazePoint.cs create mode 100644 Projects/MonsterMaze/MazeRecursiveGenerator.cs create mode 100644 Projects/MonsterMaze/MazeStep.cs create mode 100644 Projects/MonsterMaze/MonsterMaze.csproj create mode 100644 Projects/MonsterMaze/MonsterMazeGame.cs create mode 100644 Projects/MonsterMaze/Program.cs create mode 100644 Projects/MonsterMaze/README.md create mode 100644 Projects/MonsterMaze/monstermaze.ico create mode 100644 Projects/Website/Games/MonsterMaze/DirectionEnum.cs create mode 100644 Projects/Website/Games/MonsterMaze/GameUtils.cs create mode 100644 Projects/Website/Games/MonsterMaze/MazePoint.cs create mode 100644 Projects/Website/Games/MonsterMaze/MazeRecursiveGenerator.cs create mode 100644 Projects/Website/Games/MonsterMaze/MazeStep.cs create mode 100644 Projects/Website/Games/MonsterMaze/MonsterMazeGame.cs create mode 100644 Projects/Website/Games/MonsterMaze/Program.cs create mode 100644 Projects/Website/Pages/Monster Maze.razor diff --git a/.github/workflows/Monster Maze Build.yml b/.github/workflows/Monster Maze Build.yml new file mode 100644 index 00000000..e396a4b5 --- /dev/null +++ b/.github/workflows/Monster Maze Build.yml @@ -0,0 +1,20 @@ +name: Monster Maze Build +on: + push: + paths: + - 'Projects/MonsterMaze/**' + - '!**.md' + pull_request: + paths: + - 'Projects/MonsterMaze/**' + - '!**.md' + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.0.x + - run: dotnet build "Projects\MonsterMaze\MonsterMaze.csproj" --configuration Release diff --git a/.vscode/launch.json b/.vscode/launch.json index 77b7a819..cf4f234a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -542,5 +542,15 @@ "console": "externalTerminal", "stopAtEntry": false, }, + { + "name": "Monster Maze", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build Monster Maze", + "program": "${workspaceFolder}/Projects/MonsterMaze/bin/Debug/MonsterMaze.dll", + "cwd": "${workspaceFolder}/Projects/MonsterMaze/bin/Debug", + "console": "externalTerminal", + "stopAtEntry": false, + }, ], } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0250913c..909f65e1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -704,6 +704,19 @@ ], "problemMatcher": "$msCompile", }, + { + "label": "Build Monster Maze", + "command": "dotnet", + "type": "process", + "args": + [ + "build", + "${workspaceFolder}/Projects/MonsterMaze/MonsterMaze.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + ], + "problemMatcher": "$msCompile", + }, { "label": "Build Solution", "command": "dotnet", diff --git a/Projects/MonsterMaze/DirectionEnum.cs b/Projects/MonsterMaze/DirectionEnum.cs new file mode 100644 index 00000000..6885b98e --- /dev/null +++ b/Projects/MonsterMaze/DirectionEnum.cs @@ -0,0 +1,37 @@ +public enum EntityAction +{ + None, + Left, + Up, + Right, + Down, + Quit +} + +public static class EntityActionExtensions +{ + public static EntityAction FromConsoleKey(ConsoleKeyInfo key) + { + return key.Key switch + { + ConsoleKey.LeftArrow => EntityAction.Left, + ConsoleKey.RightArrow => EntityAction.Right, + ConsoleKey.UpArrow => EntityAction.Up, + ConsoleKey.DownArrow => EntityAction.Down, + ConsoleKey.Escape => EntityAction.Quit, + _ => key.KeyChar switch { + 'a' => EntityAction.Left, + 'A' => EntityAction.Left, + 'w' => EntityAction.Up, + 'W' => EntityAction.Up, + 'd' => EntityAction.Right, + 'D' => EntityAction.Right, + 's' => EntityAction.Down, + 'S' => EntityAction.Down, + 'q' => EntityAction.Quit, + 'Q' => EntityAction.Quit, + _ => EntityAction.None + } + }; + } +} \ No newline at end of file diff --git a/Projects/MonsterMaze/GameUtils.cs b/Projects/MonsterMaze/GameUtils.cs new file mode 100644 index 00000000..5d6c3d7b --- /dev/null +++ b/Projects/MonsterMaze/GameUtils.cs @@ -0,0 +1,26 @@ +public static class GameUtils +{ + public static char[,] ConvertToCharMaze(bool[,] maze, char wallCharacter = '#') + { + var result = new char[maze.GetLength(0), maze.GetLength(1)]; + for (int i = 0; i < maze.GetLength(0); i++) + { + for (int j = 0; j < maze.GetLength(1); j++) + { + result[i, j] = maze[i, j] ? ' ' : wallCharacter; + } + } + return result; + } + + public static bool WaitForEscapeOrSpace() + { + var key = Console.ReadKey(true).Key; + while(key != ConsoleKey.Spacebar && key != ConsoleKey.Escape) + { + key = Console.ReadKey(true).Key; + } + return key == ConsoleKey.Escape; + } +} + diff --git a/Projects/MonsterMaze/MazePoint.cs b/Projects/MonsterMaze/MazePoint.cs new file mode 100644 index 00000000..e69200c0 --- /dev/null +++ b/Projects/MonsterMaze/MazePoint.cs @@ -0,0 +1,28 @@ +public struct MazePoint(int x, int y) +{ + public int X { get; set; } = x; + public int Y { get; set; } = y; + + public override bool Equals(object? obj) + { + if(obj is not MazePoint) + return false; + + var other = (MazePoint)obj; + return other.X == X && other.Y == Y; + } + public static bool operator ==(MazePoint left, MazePoint right) + { + return left.Equals(right); + } + + public static bool operator !=(MazePoint left, MazePoint right) + { + return !(left == right); + } + + public override readonly int GetHashCode() + { + return HashCode.Combine(X, Y); + } +} diff --git a/Projects/MonsterMaze/MazeRecursiveGenerator.cs b/Projects/MonsterMaze/MazeRecursiveGenerator.cs new file mode 100644 index 00000000..6170a019 --- /dev/null +++ b/Projects/MonsterMaze/MazeRecursiveGenerator.cs @@ -0,0 +1,140 @@ +public class MazeRecursiveGenerator +{ + public enum MazeMode { + OnePath, + FilledDeadEnds, + Loops + }; + + private static readonly (int, int)[] Directions = { (0, -1), (1, 0), (0, 1), (-1, 0) }; // Up, Right, Down, Left + private static Random random = new Random(); + + public static bool[,] GenerateMaze(int width, int height, MazeMode mazeMode = MazeMode.OnePath) + { + if (width % 2 == 0 || height % 2 == 0) + throw new ArgumentException("Width and height must be odd numbers for a proper maze."); + + bool[,] maze = new bool[width, height]; // by default, everything is a wall (cell value == false) + + // Start the maze generation + GenerateMazeRecursive(maze, 1, 1); + + // Make sure the entrance and exit are open + maze[0, 1] = true; // Entrance + maze[width - 1, height - 2] = true; // Exit + + if(mazeMode == MazeMode.FilledDeadEnds) + FillDeadEnds(maze); + + else if(mazeMode == MazeMode.Loops) + RemoveDeadEnds(maze); + + return maze; + } + + private static void GenerateMazeRecursive(bool[,] maze, int x, int y) + { + maze[x, y] = true; + + // Shuffle directions + var shuffledDirections = ShuffleDirections(); + + foreach (var (dx, dy) in shuffledDirections) + { + int nx = x + dx * 2; + int ny = y + dy * 2; + + // Check if the new position is within bounds and not visited + if (IsInBounds(maze, nx, ny) && !maze[nx, ny]) + { + // Carve a path + maze[x + dx, y + dy] = true; + GenerateMazeRecursive(maze, nx, ny); + } + } + } + + private static List<(int, int)> ShuffleDirections() + { + var directions = new List<(int, int)>(Directions); + for (int i = directions.Count - 1; i > 0; i--) + { + int j = random.Next(i + 1); + (directions[i], directions[j]) = (directions[j], directions[i]); + } + return directions; + } + + private static bool IsInBounds(bool[,] maze, int x, int y) + { + return x > 0 && y > 0 && x < maze.GetLength(0) - 1 && y < maze.GetLength(1) - 1; + } + + private static void FillDeadEnds(bool[,] maze) + { + bool removed; + do + { + removed = false; + for (int x = 1; x < maze.GetLength(0) - 1; x++) + { + for (int y = 1; y < maze.GetLength(1) - 1; y++) + { + if (maze[x, y]) // If it's a path + { + int neighbors = 0; + foreach (var (dx, dy) in Directions) + { + if (maze[x + dx, y + dy]) + neighbors++; + } + if (neighbors <= 1) // If it's a dead end + { + maze[x, y] = false; + removed = true; + } + } + } + } + } while (removed); + } + + private static void RemoveDeadEnds(bool[,] maze) + { + bool removed; + do + { + removed = false; + for (int x = 1; x < maze.GetLength(0) - 1; x++) + { + for (int y = 1; y < maze.GetLength(1) - 1; y++) + { + if (maze[x, y]) // If it's a path + { + int neighbors = 0; + foreach (var (dx, dy) in Directions) + { + if (maze[x + dx, y + dy]) + neighbors++; + } + if (neighbors <= 1) // If it's a dead end + { + // Pick a random neighbor to keep open + var shuffledDirections = ShuffleDirections(); + foreach(var (dx, dy) in shuffledDirections) + { + if(IsInBounds(maze, x + dx, y + dy) && !maze[x + dx, y + dy]) + { + maze[x + dx, y + dy] = true; + break; + } + } + removed = true; + } + } + } + } + } while (removed); + } + +} diff --git a/Projects/MonsterMaze/MazeStep.cs b/Projects/MonsterMaze/MazeStep.cs new file mode 100644 index 00000000..87f91894 --- /dev/null +++ b/Projects/MonsterMaze/MazeStep.cs @@ -0,0 +1,11 @@ +public class MazeStep +{ + public MazePoint Position {get; set;} + public EntityAction Direction {get; set;} + + public MazeStep(MazePoint position, EntityAction direction) + { + Position = position; + Direction = direction; + } +} \ No newline at end of file diff --git a/Projects/MonsterMaze/MonsterMaze.csproj b/Projects/MonsterMaze/MonsterMaze.csproj new file mode 100644 index 00000000..52370344 --- /dev/null +++ b/Projects/MonsterMaze/MonsterMaze.csproj @@ -0,0 +1,18 @@ + + + Exe + net8.0 + enable + enable + monstermaze.ico + + Geoff Thompson (geoff.t.nz2 @ gmail.com) + A console window game, where the player has to escape the maze while being chased by monsters. + + + True + true + true + win-x64 + + diff --git a/Projects/MonsterMaze/MonsterMazeGame.cs b/Projects/MonsterMaze/MonsterMazeGame.cs new file mode 100644 index 00000000..23fd0639 --- /dev/null +++ b/Projects/MonsterMaze/MonsterMazeGame.cs @@ -0,0 +1,321 @@ +public class MonsterMazeGame +{ + public const int MaxLevel = 3; + + // Found by looking at the available options in the "Character Map" windows system app + // viewing the Lucida Console font. + const char WallCharacter = '\u2588'; + + // Windows 11 Cascadia Code font doesn't have smiley face characters. Sigh. + // So reverting back to using standard text for the player and monsters, rather + // than using smiley faces, etc. + const char PlayerCharacterA = 'O'; + const char PlayerCharacterB = 'o'; + const char MonsterCharacterA = 'M'; + const char MonsterCharacterB = 'm'; + const char CaughtCharacter = 'X'; + + // Game state. + private MazePoint playerPos; + private int numMonsters; // also the level number + + private MazePoint?[] monsterPos = new MazePoint?[MaxLevel]; // a point per monster (depending on the level) + private List[] monsterPath = new List[MaxLevel]; // a list of steps per monster + private CancellationTokenSource[] monsterPathCalcCancelSources = new CancellationTokenSource[MaxLevel]; + + private char[,] theMaze = new char[1,1]; + + private readonly int MaxWidth; + private readonly int MaxHeight; + + public MonsterMazeGame(int maxWidth, int maxHeight) + { + MaxWidth = maxWidth; + MaxHeight = maxHeight; + } + + public bool PlayLevel(int levelNumber) + { + MakeMaze(MaxWidth, MaxHeight); + + // Initial positions + numMonsters = levelNumber; + playerPos = new MazePoint(0, 1); + monsterPos[0] = new MazePoint(theMaze.GetLength(0)-1, theMaze.GetLength(1)-2); + monsterPos[1] = levelNumber > 1 ? new MazePoint(1, theMaze.GetLength(1)-2) : null; + monsterPos[2] = levelNumber > 2 ? new MazePoint(theMaze.GetLength(0)-2, 1) : null; + + for(int i = 0; i < levelNumber; i++) + { + StartMonsterPathCalculation(playerPos, i); + } + + DisplayMaze(levelNumber: numMonsters); + + // returns true if the game is over, or the user wants to quit. + return RunGameLoop(); + } + + protected bool RunGameLoop() + { + int loopCount = 0; + while(true) + { + // Show the player and the monsters. Using the loopCount as the basis for animation. + ShowEntity(playerPos, loopCount % 20 < 10 ? PlayerCharacterA : PlayerCharacterB, ConsoleColor.Green); + for(int i = 0; i < numMonsters; i++) + { + ShowEntity(monsterPos[i]!.Value, loopCount % 50 < 25 ? MonsterCharacterA : MonsterCharacterB, ConsoleColor.Red); + } + + // Check to see if any of the monsters have reached the player. + for(int i = 0; i < numMonsters; i++) + { + if(playerPos.X == monsterPos[i]?.X && playerPos.Y == monsterPos[i]?.Y) + { + return DisplayCaught(); + } + } + + if(Console.KeyAvailable) + { + var userAction = EntityActionExtensions.FromConsoleKey(Console.ReadKey(true)); + + if(userAction == EntityAction.Quit) + { + return true; + } + + // Soak up any other keypresses (avoid key buffering) + while(Console.KeyAvailable) + { + Console.ReadKey(true); + } + + // Try to move the player, and start recalculating monster paths if the player does move + MazePoint playerOldPos = playerPos; + (playerPos, var validPlayerMove) = MoveInDirection(userAction, playerPos); + if(validPlayerMove) + { + Console.SetCursorPosition(playerOldPos.X, playerOldPos.Y); + Console.ForegroundColor = ConsoleColor.Blue; + Console.Write("."); + + // If the player is "outside of the border" on the right hand side, they've reached the one gap that is the exit. + if(playerPos.X == theMaze.GetLength(0)-1) + { + return ShowLevelComplete(); + } + + // Start a new calculation of the monster's path + for(int i = 0; i < numMonsters; i++) + { + StartMonsterPathCalculation(playerPos, i); + } + } + } + + // Move the monsters slower than the player can move. + if(loopCount % 10 == 1) + { + // Move the monster towards the player along the path previously calculated from the calculation tasks. + bool validMonsterMove; + for(int i = 0; i < numMonsters; i++) + { + // If there is a path + if(monsterPath[i] != null && monsterPath[i].Count > 0) + { + MazePoint newPos; + ShowEntity(monsterPos[i]!.Value, ' ', ConsoleColor.Black); // Clear where the monster was. + + (newPos, validMonsterMove) = MoveInDirection(monsterPath[i].First().Direction, monsterPos[i]!.Value); + + monsterPos[i] = newPos; + monsterPath[i].RemoveAt(0); + + if(!validMonsterMove) + { + // Um, something went wrong with following the steps (bug in code). + // issue a recalculate + monsterPath[i] = []; + StartMonsterPathCalculation(playerPos, i); + } + } + } + } + + loopCount++; + if(loopCount > 100) + loopCount = 0; + Thread.Sleep(50); + } + } + + protected void MakeMaze(int maxX, int maxY) + { + bool [,] mazeData; + + // Make sure dimensions are odd, as per the requirements of this algorithm + if(maxX % 2 == 0) + maxX--; + + if(maxY % 2 == 0) + maxY--; + + mazeData = MazeRecursiveGenerator.GenerateMaze(maxX, maxY, MazeRecursiveGenerator.MazeMode.Loops); + theMaze = GameUtils.ConvertToCharMaze(mazeData, WallCharacter); + } + + protected static void ShowEntity(MazePoint entityPosition, char displayCharacter, ConsoleColor colour) + { + // A small helper to show either the player, or the monsters (depending on the parameters provided). + Console.ForegroundColor = colour; + Console.SetCursorPosition(entityPosition.X, entityPosition.Y); + Console.Write(displayCharacter); + } + + protected void DisplayMaze(int levelNumber) + { + Console.Clear(); + Console.ForegroundColor = ConsoleColor.White; + + for(int y = 0; y < theMaze.GetLength(1); y++) + { + Console.SetCursorPosition(0,y); + for(int x = 0; x < theMaze.GetLength(0); x++) + { + Console.Write(theMaze[x,y]); + } + } + + Console.SetCursorPosition(0, theMaze.GetLength(1)); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($" Lvl: {levelNumber}. WASD or arrow keys to move. Esc to exit."); + } + + protected Tuple MoveInDirection(EntityAction userAction, MazePoint pos) + { + var newPos = userAction switch + { + EntityAction.Up => new MazePoint(pos.X, pos.Y - 1), + EntityAction.Left => new MazePoint(pos.X - 1, pos.Y), + EntityAction.Down => new MazePoint(pos.X, pos.Y + 1), + EntityAction.Right => new MazePoint(pos.X + 1, pos.Y), + _ => new MazePoint(pos.X, pos.Y), + }; + + if(newPos.X < 0 || newPos.Y < 0 || newPos.X >= theMaze.GetLength(0) || newPos.Y >= theMaze.GetLength(1) || theMaze[newPos.X,newPos.Y] != ' ' ) + { + return new (pos, false); // can't move to the new location. + } + + return new (newPos, true); + } + + + protected bool DisplayCaught() + { + ShowEntity(playerPos, CaughtCharacter, ConsoleColor.Red); + + Console.SetCursorPosition((Console.WindowWidth-14)/2, Console.WindowHeight/2); + Console.WriteLine(" You were caught! "); + + Console.SetCursorPosition((Console.WindowWidth-14)/2, (Console.WindowHeight/2) +2); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Press space to continue"); + + GameUtils.WaitForEscapeOrSpace(); + return true; + } + + protected bool ShowLevelComplete() + { + ShowEntity(playerPos, PlayerCharacterA, ConsoleColor.Green); // Show the player at the exit. + + if(numMonsters < MaxLevel) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.SetCursorPosition((Console.WindowWidth-40)/2, Console.WindowHeight/2); + Console.WriteLine(" You escaped, ready for the next level? "); + } + else + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.SetCursorPosition((Console.WindowWidth-14)/2, Console.WindowHeight/2); + Console.WriteLine(" You won! "); + } + + Console.SetCursorPosition((Console.WindowWidth-38)/2, (Console.WindowHeight/2)+2); + Console.WriteLine("Press space to continue or Esc to exit"); + + return GameUtils.WaitForEscapeOrSpace(); + } + + protected void StartMonsterPathCalculation(MazePoint playerPos, int monsterIndex) + { + if(monsterPathCalcCancelSources[monsterIndex] != null) + { + monsterPathCalcCancelSources[monsterIndex].Cancel(); + monsterPathCalcCancelSources[monsterIndex].Dispose(); + }; + monsterPathCalcCancelSources[monsterIndex] = new CancellationTokenSource(); + Task.Run(async () => monsterPath[monsterIndex] = await FindPathToTargetAsync(playerPos, monsterPos[monsterIndex]!.Value, monsterPathCalcCancelSources[monsterIndex].Token)); + } + + // This method should is a background task, ran on a threadpool thread, to calculate where the monsters should move. +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + protected async Task> FindPathToTargetAsync(MazePoint targetPos, MazePoint currentPos, + CancellationToken cancellationToken) +#pragma warning restore CS1998 + { + var directions = new List { EntityAction.Left, EntityAction.Right, EntityAction.Up, EntityAction.Down }; + var queue = new Queue(); + var cameFrom = new Dictionary(); // To reconstruct the path + var visited = new HashSet(); + + queue.Enqueue(new MazeStep(currentPos, EntityAction.None)); + visited.Add(currentPos); + + while (queue.Count > 0 && !cancellationToken.IsCancellationRequested) + { + var currentStep = queue.Dequeue(); + var current = currentStep.Position; + + // If we've reached the target, reconstruct the path + if (current.X == targetPos.X && current.Y == targetPos.Y) + return ReconstructPath(cameFrom, currentPos, targetPos); + + foreach (var direction in directions) + { + var (nextPos, isValid) = MoveInDirection(direction, current); + if (isValid && !visited.Contains(nextPos)) + { + visited.Add(nextPos); + queue.Enqueue(new MazeStep(nextPos, direction)); + cameFrom[nextPos] = new MazeStep(current, direction); + } + } + } + return []; // No path found + } + + private static List ReconstructPath(Dictionary cameFrom, MazePoint start, MazePoint end) + { + var path = new List(); + var current = end; + + while (current != start) + { + var prevStep = cameFrom[current]; + if (prevStep == null) + break; + + var direction = prevStep.Direction; + path.Add(new MazeStep(current, direction)); + current = prevStep.Position; + } + + path.Reverse(); + return path; + } +} \ No newline at end of file diff --git a/Projects/MonsterMaze/Program.cs b/Projects/MonsterMaze/Program.cs new file mode 100644 index 00000000..479a282e --- /dev/null +++ b/Projects/MonsterMaze/Program.cs @@ -0,0 +1,72 @@ +public class Program +{ + + // Save console colours, to restore state after the game ends. + private static ConsoleColor originalBackgroundColor; + private static ConsoleColor originalForegroundColor; + + public static void Main() + { + Console.CursorVisible = false; + Console.CancelKeyPress += new ConsoleCancelEventHandler(CleanupHandler); + + originalBackgroundColor = Console.BackgroundColor; + originalForegroundColor = Console.ForegroundColor; + + var maxWidth = Console.WindowWidth > 50 ? 50 : Console.WindowWidth-1; + var maxHeight = Console.WindowHeight > 24 ? 24: Console.WindowHeight-2; + + var game = new MonsterMazeGame(maxWidth, maxHeight); + + bool quitGame = false; + while(!quitGame) + { + ShowTitleScreen(); + + if(GameUtils.WaitForEscapeOrSpace() != true) + { + bool gameOver = false; + for(int levelNumber = 1; levelNumber <= MonsterMazeGame.MaxLevel && !gameOver; levelNumber++) + { + gameOver = game.PlayLevel(levelNumber); + } + } + else + { + // Player wants to quit the game + quitGame = true; + } + } + CleanupHandler(null, null); + } + + protected static void ShowTitleScreen() + { + Console.Clear(); + + + Console.SetCursorPosition(Console.WindowWidth/2-20, 5); + Console.ForegroundColor = ConsoleColor.Red; + Console.Write("### "); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Monster Maze"); + Console.ForegroundColor = ConsoleColor.Red; + Console.Write(" ###"); + + Console.SetCursorPosition(0, 10); + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine("You are trapped in a maze with monsters. Your goal is to escape."); + Console.WriteLine("Use the arrow keys to move, avoid the monsters."); + Console.WriteLine(); + Console.WriteLine("Press space to start, or escape to quit."); + } + + // If "escape" or "control-c" is pressed, try to get the console window back into a clean state. + protected static void CleanupHandler(object? sender, ConsoleCancelEventArgs? args) + { + Console.ForegroundColor = originalForegroundColor; + Console.BackgroundColor = originalBackgroundColor; + Console.Clear(); + } + +} diff --git a/Projects/MonsterMaze/README.md b/Projects/MonsterMaze/README.md new file mode 100644 index 00000000..178db8e8 --- /dev/null +++ b/Projects/MonsterMaze/README.md @@ -0,0 +1,65 @@ +

+ Monster Maze +

+ +

+ GitHub repo + Language C# + Discord + License +

+ +

+ You can play this game in your browser: +
+ + Play Now + +
+ Hosted On GitHub Pages +

+ +Monster Maze - Escape the maze without the monsters catching you. + +Your position is shown by the "O". Monsters are shown as "M". + +``` +█████████████████████████████████████████████████ +O █ █ █ █ █ █ █ +█████ █ █ █ ███ ███ █ █████ █ █ █ █ ███ █ ███ █ █ +█ █ █ █ █ █ █ █ █ █ █ █ █ █ +█ █ █ █ █████ █ █ ███████ █ ███ █ ███ ███ █ ███ █ +█ █ █ █ █ █ █ █ █ █ █ █ █ +█ ███████ █ █████ █ █ █ ███ █ ███ █ ███ █ █ █ ███ +█ █ █ █ █ █ █ █ █ █ █ +███ ███████ █ █ ███████████ █ █ █ █ █ ███████ █ █ +█ █ █ █ █ █ █ █ █ █ █ █ █ +█ ███ ███████ █ █ ███████ ███ █ █ █ █ █ ███████ █ +█ █ █ █ █ █ █ █ █ █ █ █ █ +█ █ ███ ███ █ █ ███ █ █ ███ █ █ █████ █ █ █ █ █ █ +█ █ █ █ █ █ █ █ █ █ █ █ █ █ +█ ███ █ █ ███████████ ███ █ █ ███ █ █████ █ ███ █ +█ █ █ █ █ █ █ █ █ █ █ +███ ███ █████████████ █ ███ ███████ █ █████ █ █ █ +█ █ █ █ █ █ █ █ █ █ +█ ███ █████████ █ █ █ █ █ ███ █ █ █████ █ ███████ +█ █ █ █ █ █ █ █ █ █ █ +█ █ █████ █ ███████████ █ █ ███████ █ ███ █████ █ +█ █ M +█████████████████████████████████████████████████ + Lvl: 1. WASD or arrow keys to move. Esc to exit. +``` + +## Input + +- `↑`, `↓`, `←`, `→` or `W`,`A`,`S`,`D`: movement +- `space`: start game, continue to next level. +- `escape`: exit to menu, quit game + +## Downloads + +[win-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/win-x64/MonsterMaze.exe) + +[linux-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/linux-x64/MonsterMaze) + +[osx-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/osx-x64/MonsterMaze) diff --git a/Projects/MonsterMaze/monstermaze.ico b/Projects/MonsterMaze/monstermaze.ico new file mode 100644 index 0000000000000000000000000000000000000000..dd90f795d0b9eef1c0676b64a8113ce28000acd2 GIT binary patch literal 95278 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 EntityAction.Left, + ConsoleKey.RightArrow => EntityAction.Right, + ConsoleKey.UpArrow => EntityAction.Up, + ConsoleKey.DownArrow => EntityAction.Down, + ConsoleKey.Escape => EntityAction.Quit, + _ => key.KeyChar switch { + 'a' => EntityAction.Left, + 'A' => EntityAction.Left, + 'w' => EntityAction.Up, + 'W' => EntityAction.Up, + 'd' => EntityAction.Right, + 'D' => EntityAction.Right, + 's' => EntityAction.Down, + 'S' => EntityAction.Down, + 'q' => EntityAction.Quit, + 'Q' => EntityAction.Quit, + _ => EntityAction.None + } + }; + } +} \ No newline at end of file diff --git a/Projects/Website/Games/MonsterMaze/GameUtils.cs b/Projects/Website/Games/MonsterMaze/GameUtils.cs new file mode 100644 index 00000000..f4594f79 --- /dev/null +++ b/Projects/Website/Games/MonsterMaze/GameUtils.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace Website.Games.MonsterMaze; + +public static class GameUtils +{ + public static char[,] ConvertToCharMaze(bool[,] maze, char wallCharacter = '#') + { + var result = new char[maze.GetLength(0), maze.GetLength(1)]; + for (int i = 0; i < maze.GetLength(0); i++) + { + for (int j = 0; j < maze.GetLength(1); j++) + { + result[i, j] = maze[i, j] ? ' ' : wallCharacter; + } + } + return result; + } + + public static async Task WaitForEscapeOrSpace(BlazorConsole console) + { + var key = await console.ReadKey(true); + while(key.Key != ConsoleKey.Spacebar && key.Key != ConsoleKey.Escape) + { + key = await console.ReadKey(true); + } + return key.Key == ConsoleKey.Escape; + } +} + diff --git a/Projects/Website/Games/MonsterMaze/MazePoint.cs b/Projects/Website/Games/MonsterMaze/MazePoint.cs new file mode 100644 index 00000000..ed110501 --- /dev/null +++ b/Projects/Website/Games/MonsterMaze/MazePoint.cs @@ -0,0 +1,32 @@ +using System; + +namespace Website.Games.MonsterMaze; + +public struct MazePoint(int x, int y) +{ + public int X { get; set; } = x; + public int Y { get; set; } = y; + + public override bool Equals(object? obj) + { + if(obj is not MazePoint) + return false; + + var other = (MazePoint)obj; + return other.X == X && other.Y == Y; + } + public static bool operator ==(MazePoint left, MazePoint right) + { + return left.Equals(right); + } + + public static bool operator !=(MazePoint left, MazePoint right) + { + return !(left == right); + } + + public override readonly int GetHashCode() + { + return HashCode.Combine(X, Y); + } +} diff --git a/Projects/Website/Games/MonsterMaze/MazeRecursiveGenerator.cs b/Projects/Website/Games/MonsterMaze/MazeRecursiveGenerator.cs new file mode 100644 index 00000000..1c3adb38 --- /dev/null +++ b/Projects/Website/Games/MonsterMaze/MazeRecursiveGenerator.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; + +namespace Website.Games.MonsterMaze; + +public class MazeRecursiveGenerator +{ + public enum MazeMode { + OnePath, + FilledDeadEnds, + Loops + }; + + private static readonly (int, int)[] Directions = { (0, -1), (1, 0), (0, 1), (-1, 0) }; // Up, Right, Down, Left + private static Random random = new Random(); + + public static bool[,] GenerateMaze(int width, int height, MazeMode mazeMode = MazeMode.OnePath) + { + if (width % 2 == 0 || height % 2 == 0) + throw new ArgumentException("Width and height must be odd numbers for a proper maze."); + + bool[,] maze = new bool[width, height]; // by default, everything is a wall (cell value == false) + + // Start the maze generation + GenerateMazeRecursive(maze, 1, 1); + + // Make sure the entrance and exit are open + maze[0, 1] = true; // Entrance + maze[width - 1, height - 2] = true; // Exit + + if(mazeMode == MazeMode.FilledDeadEnds) + FillDeadEnds(maze); + + else if(mazeMode == MazeMode.Loops) + RemoveDeadEnds(maze); + + return maze; + } + + private static void GenerateMazeRecursive(bool[,] maze, int x, int y) + { + maze[x, y] = true; + + // Shuffle directions + var shuffledDirections = ShuffleDirections(); + + foreach (var (dx, dy) in shuffledDirections) + { + int nx = x + dx * 2; + int ny = y + dy * 2; + + // Check if the new position is within bounds and not visited + if (IsInBounds(maze, nx, ny) && !maze[nx, ny]) + { + // Carve a path + maze[x + dx, y + dy] = true; + GenerateMazeRecursive(maze, nx, ny); + } + } + } + + private static List<(int, int)> ShuffleDirections() + { + var directions = new List<(int, int)>(Directions); + for (int i = directions.Count - 1; i > 0; i--) + { + int j = random.Next(i + 1); + (directions[i], directions[j]) = (directions[j], directions[i]); + } + return directions; + } + + private static bool IsInBounds(bool[,] maze, int x, int y) + { + return x > 0 && y > 0 && x < maze.GetLength(0) - 1 && y < maze.GetLength(1) - 1; + } + + private static void FillDeadEnds(bool[,] maze) + { + bool removed; + do + { + removed = false; + for (int x = 1; x < maze.GetLength(0) - 1; x++) + { + for (int y = 1; y < maze.GetLength(1) - 1; y++) + { + if (maze[x, y]) // If it's a path + { + int neighbors = 0; + foreach (var (dx, dy) in Directions) + { + if (maze[x + dx, y + dy]) + neighbors++; + } + if (neighbors <= 1) // If it's a dead end + { + maze[x, y] = false; + removed = true; + } + } + } + } + } while (removed); + } + + private static void RemoveDeadEnds(bool[,] maze) + { + bool removed; + do + { + removed = false; + for (int x = 1; x < maze.GetLength(0) - 1; x++) + { + for (int y = 1; y < maze.GetLength(1) - 1; y++) + { + if (maze[x, y]) // If it's a path + { + int neighbors = 0; + foreach (var (dx, dy) in Directions) + { + if (maze[x + dx, y + dy]) + neighbors++; + } + if (neighbors <= 1) // If it's a dead end + { + // Pick a random neighbor to keep open + var shuffledDirections = ShuffleDirections(); + foreach(var (dx, dy) in shuffledDirections) + { + if(IsInBounds(maze, x + dx, y + dy) && !maze[x + dx, y + dy]) + { + maze[x + dx, y + dy] = true; + break; + } + } + removed = true; + } + } + } + } + } while (removed); + } + +} diff --git a/Projects/Website/Games/MonsterMaze/MazeStep.cs b/Projects/Website/Games/MonsterMaze/MazeStep.cs new file mode 100644 index 00000000..d89928af --- /dev/null +++ b/Projects/Website/Games/MonsterMaze/MazeStep.cs @@ -0,0 +1,13 @@ +namespace Website.Games.MonsterMaze; + +public class MazeStep +{ + public MazePoint Position {get; set;} + public EntityAction Direction {get; set;} + + public MazeStep(MazePoint position, EntityAction direction) + { + Position = position; + Direction = direction; + } +} \ No newline at end of file diff --git a/Projects/Website/Games/MonsterMaze/MonsterMazeGame.cs b/Projects/Website/Games/MonsterMaze/MonsterMazeGame.cs new file mode 100644 index 00000000..9ed4c4cc --- /dev/null +++ b/Projects/Website/Games/MonsterMaze/MonsterMazeGame.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Website.Games.MonsterMaze; + +public class MonsterMazeGame +{ + public const int MaxLevel = 3; + + // Found by looking at the available options in the "Character Map" windows system app + // viewing the Lucida Console font. + const char WallCharacter = '\u2588'; + + // Windows 11 Cascadia Code font doesn't have smiley face characters. Sigh. + // So reverting back to using standard text for the player and monsters, rather + // than using smiley faces, etc. + const char PlayerCharacterA = 'O'; + const char PlayerCharacterB = 'o'; + const char MonsterCharacterA = 'M'; + const char MonsterCharacterB = 'm'; + const char CaughtCharacter = 'X'; + + // Game state. + private MazePoint playerPos; + private int numMonsters; // also the level number + + private MazePoint?[] monsterPos = new MazePoint?[MaxLevel]; // a point per monster (depending on the level) + private List[] monsterPath = new List[MaxLevel]; // a list of steps per monster + private CancellationTokenSource[] monsterPathCalcCancelSources = new CancellationTokenSource[MaxLevel]; + + private char[,] theMaze = new char[1,1]; + + private readonly int MaxWidth; + private readonly int MaxHeight; + + private BlazorConsole Console; + + public MonsterMazeGame(int maxWidth, int maxHeight, BlazorConsole console) + { + MaxWidth = maxWidth; + MaxHeight = maxHeight; + Console = console; + } + + public async Task PlayLevel(int levelNumber) + { + MakeMaze(MaxWidth, MaxHeight); + + // Initial positions + numMonsters = levelNumber; + playerPos = new MazePoint(0, 1); + monsterPos[0] = new MazePoint(theMaze.GetLength(0)-1, theMaze.GetLength(1)-2); + monsterPos[1] = levelNumber > 1 ? new MazePoint(1, theMaze.GetLength(1)-2) : null; + monsterPos[2] = levelNumber > 2 ? new MazePoint(theMaze.GetLength(0)-2, 1) : null; + + for(int i = 0; i < levelNumber; i++) + { + StartMonsterPathCalculation(playerPos, i); + } + + await DisplayMaze(levelNumber: numMonsters); + + // returns true if the game is over, or the user wants to quit. + return await RunGameLoop(); + } + + protected async Task RunGameLoop() + { + int loopCount = 0; + while(true) + { + // Show the player and the monsters. Using the loopCount as the basis for animation. + await ShowEntity(playerPos, loopCount % 20 < 10 ? PlayerCharacterA : PlayerCharacterB, ConsoleColor.Green); + for(int i = 0; i < numMonsters; i++) + { + await ShowEntity(monsterPos[i]!.Value, loopCount % 50 < 25 ? MonsterCharacterA : MonsterCharacterB, ConsoleColor.Red); + } + + // Check to see if any of the monsters have reached the player. + for(int i = 0; i < numMonsters; i++) + { + if(playerPos.X == monsterPos[i]?.X && playerPos.Y == monsterPos[i]?.Y) + { + return await DisplayCaught(); + } + } + + if(Console.KeyAvailable().Result) + { + var userAction = EntityActionExtensions.FromConsoleKey(await Console.ReadKey(true)); + + if(userAction == EntityAction.Quit) + { + return true; + } + + // Soak up any other keypresses (avoid key buffering) + while(Console.KeyAvailable().Result) + { + await Console.ReadKey(true); + } + + // Try to move the player, and start recalculating monster paths if the player does move + MazePoint playerOldPos = playerPos; + (playerPos, var validPlayerMove) = MoveInDirection(userAction, playerPos); + if(validPlayerMove) + { + await Console.SetCursorPosition(playerOldPos.X, playerOldPos.Y); + Console.ForegroundColor = ConsoleColor.Blue; + await Console.Write("."); + + // If the player is "outside of the border" on the right hand side, they've reached the one gap that is the exit. + if(playerPos.X == theMaze.GetLength(0)-1) + { + return await ShowLevelComplete(); + } + + // Start a new calculation of the monster's path + for(int i = 0; i < numMonsters; i++) + { + StartMonsterPathCalculation(playerPos, i); + } + } + } + + // Move the monsters slower than the player can move. + if(loopCount % 10 == 1) + { + // Move the monster towards the player along the path previously calculated from the calculation tasks. + bool validMonsterMove; + for(int i = 0; i < numMonsters; i++) + { + // If there is a path + if(monsterPath[i] != null && monsterPath[i].Count > 0) + { + MazePoint newPos; + await ShowEntity(monsterPos[i]!.Value, ' ', ConsoleColor.Black); // Clear where the monster was. + + (newPos, validMonsterMove) = MoveInDirection(monsterPath[i].First().Direction, monsterPos[i]!.Value); + + monsterPos[i] = newPos; + monsterPath[i].RemoveAt(0); + + if(!validMonsterMove) + { + // Um, something went wrong with following the steps (bug in code). + // issue a recalculate + monsterPath[i] = []; + StartMonsterPathCalculation(playerPos, i); + } + } + } + } + + loopCount++; + if(loopCount > 100) + loopCount = 0; + await Task.Delay(50); + } + } + + protected void MakeMaze(int maxX, int maxY) + { + bool [,] mazeData; + + // Make sure dimensions are odd, as per the requirements of this algorithm + if(maxX % 2 == 0) + maxX--; + + if(maxY % 2 == 0) + maxY--; + + mazeData = MazeRecursiveGenerator.GenerateMaze(maxX, maxY, MazeRecursiveGenerator.MazeMode.Loops); + theMaze = GameUtils.ConvertToCharMaze(mazeData, WallCharacter); + } + + protected async Task ShowEntity(MazePoint entityPosition, char displayCharacter, ConsoleColor colour) + { + // A small helper to show either the player, or the monsters (depending on the parameters provided). + Console.ForegroundColor = colour; + await Console.SetCursorPosition(entityPosition.X, entityPosition.Y); + await Console.Write(displayCharacter); + } + + protected async Task DisplayMaze(int levelNumber) + { + await Console.Clear(); + Console.ForegroundColor = ConsoleColor.White; + + for(int y = 0; y < theMaze.GetLength(1); y++) + { + await Console.SetCursorPosition(0,y); + for(int x = 0; x < theMaze.GetLength(0); x++) + { + await Console.Write(theMaze[x,y]); + } + } + + await Console.SetCursorPosition(0, theMaze.GetLength(1)); + Console.ForegroundColor = ConsoleColor.Green; + await Console.WriteLine($" Lvl: {levelNumber}. WASD or arrow keys to move. Esc to exit."); + } + + protected Tuple MoveInDirection(EntityAction userAction, MazePoint pos) + { + var newPos = userAction switch + { + EntityAction.Up => new MazePoint(pos.X, pos.Y - 1), + EntityAction.Left => new MazePoint(pos.X - 1, pos.Y), + EntityAction.Down => new MazePoint(pos.X, pos.Y + 1), + EntityAction.Right => new MazePoint(pos.X + 1, pos.Y), + _ => new MazePoint(pos.X, pos.Y), + }; + + if(newPos.X < 0 || newPos.Y < 0 || newPos.X >= theMaze.GetLength(0) || newPos.Y >= theMaze.GetLength(1) || theMaze[newPos.X,newPos.Y] != ' ' ) + { + return new (pos, false); // can't move to the new location. + } + + return new (newPos, true); + } + + protected async Task DisplayCaught() + { + await ShowEntity(playerPos, CaughtCharacter, ConsoleColor.Red); + + await Console.SetCursorPosition((Console.WindowWidth-14)/2, Console.WindowHeight/2); + await Console.WriteLine(" You were caught! "); + + await Console.SetCursorPosition((Console.WindowWidth-14)/2, (Console.WindowHeight/2) +2); + Console.ForegroundColor = ConsoleColor.Yellow; + await Console.WriteLine("Press space to continue"); + + await GameUtils.WaitForEscapeOrSpace(Console); + return true; + } + + protected async Task ShowLevelComplete() + { + await ShowEntity(playerPos, PlayerCharacterA, ConsoleColor.Green); // Show the player at the exit. + + if(numMonsters < MaxLevel) + { + Console.ForegroundColor = ConsoleColor.Yellow; + await Console.SetCursorPosition((Console.WindowWidth-40)/2, Console.WindowHeight/2); + await Console.WriteLine(" You escaped, ready for the next level? "); + } + else + { + Console.ForegroundColor = ConsoleColor.Yellow; + await Console.SetCursorPosition((Console.WindowWidth-14)/2, Console.WindowHeight/2); + await Console.WriteLine(" You won! "); + } + + await Console.SetCursorPosition((Console.WindowWidth-38)/2, (Console.WindowHeight/2)+2); + await Console.WriteLine("Press space to continue or Esc to exit"); + + return await GameUtils.WaitForEscapeOrSpace(Console); + } + + protected void StartMonsterPathCalculation(MazePoint playerPos, int monsterIndex) + { + if(monsterPathCalcCancelSources[monsterIndex] != null) + { + monsterPathCalcCancelSources[monsterIndex].Cancel(); + monsterPathCalcCancelSources[monsterIndex].Dispose(); + }; + monsterPathCalcCancelSources[monsterIndex] = new CancellationTokenSource(); + Task.Run(async () => monsterPath[monsterIndex] = await FindPathToTargetAsync(playerPos, monsterPos[monsterIndex]!.Value, monsterPathCalcCancelSources[monsterIndex].Token)); + } + + // This method should is a background task, ran on a threadpool thread, to calculate where the monsters should move. +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + protected async Task> FindPathToTargetAsync(MazePoint targetPos, MazePoint currentPos, + CancellationToken cancellationToken) +#pragma warning restore CS1998 + { + var directions = new List { EntityAction.Left, EntityAction.Right, EntityAction.Up, EntityAction.Down }; + var queue = new Queue(); + var cameFrom = new Dictionary(); // To reconstruct the path + var visited = new HashSet(); + + queue.Enqueue(new MazeStep(currentPos, EntityAction.None)); + visited.Add(currentPos); + + while (queue.Count > 0 && !cancellationToken.IsCancellationRequested) + { + var currentStep = queue.Dequeue(); + var current = currentStep.Position; + + // If we've reached the target, reconstruct the path + if (current.X == targetPos.X && current.Y == targetPos.Y) + return ReconstructPath(cameFrom, currentPos, targetPos); + + foreach (var direction in directions) + { + var (nextPos, isValid) = MoveInDirection(direction, current); + if (isValid && !visited.Contains(nextPos)) + { + visited.Add(nextPos); + queue.Enqueue(new MazeStep(nextPos, direction)); + cameFrom[nextPos] = new MazeStep(current, direction); + } + } + } + return []; // No path found + } + + private static List ReconstructPath(Dictionary cameFrom, MazePoint start, MazePoint end) + { + var path = new List(); + var current = end; + + while (current != start) + { + var prevStep = cameFrom[current]; + if (prevStep == null) + break; + + var direction = prevStep.Direction; + path.Add(new MazeStep(current, direction)); + current = prevStep.Position; + } + + path.Reverse(); + return path; + } +} \ No newline at end of file diff --git a/Projects/Website/Games/MonsterMaze/Program.cs b/Projects/Website/Games/MonsterMaze/Program.cs new file mode 100644 index 00000000..163d7f77 --- /dev/null +++ b/Projects/Website/Games/MonsterMaze/Program.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; + +namespace Website.Games.MonsterMaze; + +public class Program +{ + public readonly BlazorConsole Console = new(); + + // Save console colours, to restore state after the game ends. + private ConsoleColor originalBackgroundColor; + private ConsoleColor originalForegroundColor; + + public async Task Run() + { + Console.CursorVisible = false; + //Console.CancelKeyPress += new ConsoleCancelEventHandler(CleanupHandler); + + originalBackgroundColor = Console.BackgroundColor; + originalForegroundColor = Console.ForegroundColor; + + var maxWidth = Console.WindowWidth > 50 ? 50 : Console.WindowWidth-1; + var maxHeight = Console.WindowHeight > 24 ? 24: Console.WindowHeight-2; + + var game = new MonsterMazeGame(maxWidth, maxHeight, Console); + + bool quitGame = false; + while(!quitGame) + { + await ShowTitleScreen(); + + if(await GameUtils.WaitForEscapeOrSpace(Console) != true) + { + bool gameOver = false; + for(int levelNumber = 1; levelNumber <= MonsterMazeGame.MaxLevel && !gameOver; levelNumber++) + { + gameOver = await game.PlayLevel(levelNumber); + } + } + else + { + // Player wants to quit the game + quitGame = true; + } + } + await CleanupHandler(null, null); + } + + protected async Task ShowTitleScreen() + { + await Console.Clear(); + + await Console.SetCursorPosition(Console.WindowWidth/2-20, 5); + Console.ForegroundColor = ConsoleColor.Red; + await Console.Write("### "); + Console.ForegroundColor = ConsoleColor.Yellow; + await Console.Write("Monster Maze"); + Console.ForegroundColor = ConsoleColor.Red; + await Console.Write(" ###"); + + await Console.SetCursorPosition(0, 10); + Console.ForegroundColor = ConsoleColor.White; + await Console.WriteLine("You are trapped in a maze with monsters. Your goal is to escape."); + await Console.WriteLine("Use the arrow keys to move, avoid the monsters."); + await Console.WriteLine(); + await Console.WriteLine("Press space to start, or escape to quit."); + } + + // If "escape" or "control-c" is pressed, try to get the console window back into a clean state. + protected async Task CleanupHandler(object? sender, ConsoleCancelEventArgs? args) + { + Console.ForegroundColor = originalForegroundColor; + Console.BackgroundColor = originalBackgroundColor; + await Console.Clear(); + } +} diff --git a/Projects/Website/Pages/Monster Maze.razor b/Projects/Website/Pages/Monster Maze.razor new file mode 100644 index 00000000..b91122ac --- /dev/null +++ b/Projects/Website/Pages/Monster Maze.razor @@ -0,0 +1,51 @@ +@using System + +@page "/MonsterMaze" + +Monster Maze + +

Monster Maze

+ + + Go To Readme + + +
+
+
+			@Console.State
+		
+
+
+ + + + + + +
+
+ + + + + +@code +{ + Games.MonsterMaze.Program Game; + BlazorConsole Console; + + public Monster_Maze() + { + Game = new(); + Console = Game.Console; + Console.WindowWidth = 121; + Console.WindowHeight = 41; + Console.TriggerRefresh = StateHasChanged; + } + protected override void OnInitialized() => InvokeAsync(Game.Run); +} diff --git a/README.md b/README.md index c6d2637e..937c654a 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ |[Role Playing Game](Projects/Role%20Playing%20Game)|6|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Role%20Playing%20Game) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Role%20Playing%20Game%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[Console Monsters](Projects/Console%20Monsters)|7|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Console%20Monsters) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Console%20Monsters%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_Community Collaboration_
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Work In Progress_| |[First Person Shooter](Projects/First%20Person%20Shooter)|8|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/First%20Person%20Shooter) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/First%20Person%20Shooter%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Only Supported On Windows OS_| +|[Monster Maze](Projects/MonsterMaze)|5|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/MonsterMaze) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Monster%20Maze%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Only Supported On Windows OS_| \*_**Weight**: A relative rating for how advanced the source code is._
diff --git a/dotnet-console-games.sln b/dotnet-console-games.sln index 19180f80..61b8648f 100644 --- a/dotnet-console-games.sln +++ b/dotnet-console-games.sln @@ -113,6 +113,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reversi", "Projects\Reversi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "First Person Shooter", "Projects\First Person Shooter\First Person Shooter.csproj", "{5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projects", "Projects", "{39609F51-A68A-4FF1-9418-D8AD2E3B2829}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonsterMaze", "Projects\MonsterMaze\MonsterMaze.csproj", "{73ED2A21-8479-46E6-A060-37D4FBFCABC8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -339,6 +343,10 @@ Global {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Release|Any CPU.Build.0 = Release|Any CPU + {73ED2A21-8479-46E6-A060-37D4FBFCABC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73ED2A21-8479-46E6-A060-37D4FBFCABC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73ED2A21-8479-46E6-A060-37D4FBFCABC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73ED2A21-8479-46E6-A060-37D4FBFCABC8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -346,4 +354,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EC4CAF97-A0CE-4999-8062-EC511A41764F} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {73ED2A21-8479-46E6-A060-37D4FBFCABC8} = {39609F51-A68A-4FF1-9418-D8AD2E3B2829} + EndGlobalSection EndGlobal diff --git a/dotnet-console-games.slnf b/dotnet-console-games.slnf index 6c8623c5..5d198e40 100644 --- a/dotnet-console-games.slnf +++ b/dotnet-console-games.slnf @@ -30,6 +30,7 @@ "Projects\\Maze\\Maze.csproj", "Projects\\Memory\\Memory.csproj", "Projects\\Minesweeper\\Minesweeper.csproj", + "Projects\\MonsterMaze\\MonsterMaze.csproj", "Projects\\Oligopoly\\Oligopoly.csproj", "Projects\\PacMan\\PacMan.csproj", "Projects\\Pong\\Pong.csproj", From 17e1d02b1f2c6b614ce9b7c0d7024c5dfe3bf967 Mon Sep 17 00:00:00 2001 From: Geoff Thompson Date: Tue, 4 Feb 2025 15:05:31 +1300 Subject: [PATCH 2/2] added monster maze to website navmenu --- Projects/Website/Shared/NavMenu.razor | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Projects/Website/Shared/NavMenu.razor b/Projects/Website/Shared/NavMenu.razor index fd3f3cb0..624c084d 100644 --- a/Projects/Website/Shared/NavMenu.razor +++ b/Projects/Website/Shared/NavMenu.razor @@ -273,6 +273,12 @@ First Person Shooter + +