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 00000000..dd90f795 Binary files /dev/null and b/Projects/MonsterMaze/monstermaze.ico differ diff --git a/Projects/Website/Games/MonsterMaze/DirectionEnum.cs b/Projects/Website/Games/MonsterMaze/DirectionEnum.cs new file mode 100644 index 00000000..cfefbb50 --- /dev/null +++ b/Projects/Website/Games/MonsterMaze/DirectionEnum.cs @@ -0,0 +1,41 @@ +using System; + +namespace Website.Games.MonsterMaze; + +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/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/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 + + 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",