diff --git a/.github/workflows/Shmup Build.yml b/.github/workflows/Shmup Build.yml new file mode 100644 index 00000000..1740e45f --- /dev/null +++ b/.github/workflows/Shmup Build.yml @@ -0,0 +1,20 @@ +name: Shmup Build +on: + push: + paths: + - 'Projects/Shmup/**' + - '!**.md' + pull_request: + paths: + - 'Projects/Shmup/**' + - '!**.md' + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - run: dotnet build "Projects\Shmup\Shmup.csproj" --configuration Release diff --git a/.vscode/launch.json b/.vscode/launch.json index 7b41c281..a6a89bac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -472,5 +472,15 @@ "console": "externalTerminal", "stopAtEntry": false, }, + { + "name": "Shmup", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build Shmup", + "program": "${workspaceFolder}/Projects/Shmup/bin/Debug/Shmup.dll", + "cwd": "${workspaceFolder}/Projects/Shmup/bin/Debug", + "console": "externalTerminal", + "stopAtEntry": false, + }, ], } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e03affaf..6d9ec4a1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -613,6 +613,19 @@ ], "problemMatcher": "$msCompile", }, + { + "label": "Build Shmup", + "command": "dotnet", + "type": "process", + "args": + [ + "build", + "${workspaceFolder}/Projects/Shmup/Shmup.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + ], + "problemMatcher": "$msCompile", + }, { "label": "Build Solution", "command": "dotnet", diff --git a/Projects/Shmup/Enemies/Helicopter.cs b/Projects/Shmup/Enemies/Helicopter.cs index 2f9477db..8da54076 100644 --- a/Projects/Shmup/Enemies/Helicopter.cs +++ b/Projects/Shmup/Enemies/Helicopter.cs @@ -5,6 +5,8 @@ namespace Shmup.Enemies; internal class Helicopter : IEnemy { + public static int scorePerKill = 100; + public int Health = 70; public float X; public float Y; public float XVelocity; @@ -12,14 +14,14 @@ internal class Helicopter : IEnemy private int Frame; private string[] Sprite = Random.Shared.Next(2) is 0 ? spriteA : spriteB; - static string[] spriteA = + static readonly string[] spriteA = { @" ~~~~~+~~~~~", @"'\===<[_]L) ", @" -'-`- ", }; - static string[] spriteB = + static readonly string[] spriteB = { @" -----+-----", @"*\===<[_]L) ", @@ -82,4 +84,14 @@ public bool IsOutOfBounds() XVelocity >= 0 && X > Program.gameWidth + XMax || YVelocity >= 0 && Y > Program.gameHeight + YMax; } + + public void Shot() + { + Health--; + if (Health <= 0) + { + Program.enemies.Remove(this); + Program.score += scorePerKill; + } + } } diff --git a/Projects/Shmup/Enemies/IEnemy.cs b/Projects/Shmup/Enemies/IEnemy.cs index b4bb8493..4153b102 100644 --- a/Projects/Shmup/Enemies/IEnemy.cs +++ b/Projects/Shmup/Enemies/IEnemy.cs @@ -2,6 +2,8 @@ internal interface IEnemy { + public void Shot(); + public void Render(); public void Update(); diff --git a/Projects/Shmup/Enemies/Tank.cs b/Projects/Shmup/Enemies/Tank.cs index f733ed63..1075f860 100644 --- a/Projects/Shmup/Enemies/Tank.cs +++ b/Projects/Shmup/Enemies/Tank.cs @@ -5,34 +5,36 @@ namespace Shmup.Enemies; internal class Tank : IEnemy { + public static int scorePerKill = 20; + public int Health = 20; public float X; public float Y; public float XVelocity; public float YVelocity; private string[] Sprite; - static string[] spriteDown = + static readonly string[] spriteDown = { @" ___ ", @"|_O_|", @"[ooo]", }; - static string[] spriteUp = + static readonly string[] spriteUp = { @" _^_ ", @"|___|", @"[ooo]", }; - static string[] spriteLeft = + static readonly string[] spriteLeft = { @" __ ", @"=|__|", @"[ooo]", }; - static string[] spriteRight = + static readonly string[] spriteRight = { @" __ ", @"|__|=", @@ -96,4 +98,14 @@ public bool IsOutOfBounds() XVelocity >= 0 && X > Program.gameWidth + XMax || YVelocity >= 0 && Y > Program.gameHeight + YMax; } + + public void Shot() + { + Health--; + if (Health <= 0) + { + Program.enemies.Remove(this); + Program.score += scorePerKill; + } + } } diff --git a/Projects/Shmup/Enemies/UFO1.cs b/Projects/Shmup/Enemies/UFO1.cs index 4f039e56..2e7b04d1 100644 --- a/Projects/Shmup/Enemies/UFO1.cs +++ b/Projects/Shmup/Enemies/UFO1.cs @@ -5,11 +5,13 @@ namespace Shmup.Enemies; internal class UFO1 : IEnemy { + public static int scorePerKill = 10; + public int Health = 10; public float X; public float Y; public float XVelocity = 1f / 8f; public float YVelocity = 1f / 8f; - private static string[] Sprite = + private static readonly string[] Sprite = { @" _!_ ", @"(_o_)", @@ -80,4 +82,14 @@ public bool IsOutOfBounds() XVelocity >= 0 && X > Program.gameWidth + XMax || YVelocity >= 0 && Y > Program.gameHeight + YMax; } + + public void Shot() + { + Health--; + if (Health <= 0) + { + Program.enemies.Remove(this); + Program.score += scorePerKill; + } + } } diff --git a/Projects/Shmup/Enemies/UFO2.cs b/Projects/Shmup/Enemies/UFO2.cs index 7e87e248..e5350b9e 100644 --- a/Projects/Shmup/Enemies/UFO2.cs +++ b/Projects/Shmup/Enemies/UFO2.cs @@ -5,12 +5,14 @@ namespace Shmup.Enemies; internal class UFO2 : IEnemy { + public static int scorePerKill = 80; + public int Health = 50; public float X; public float Y; public int UpdatesSinceTeleport; public int TeleportFrequency = 360; - private static string[] Sprite = + private static readonly string[] Sprite = { @" _!_ ", @" /_O_\ ", @@ -78,4 +80,14 @@ public bool IsOutOfBounds() Y > 0 && Y < Program.gameHeight); } + + public void Shot() + { + Health--; + if (Health <= 0) + { + Program.enemies.Remove(this); + Program.score += scorePerKill; + } + } } diff --git a/Projects/Shmup/Player.cs b/Projects/Shmup/Player.cs index 1eec5b95..a172db92 100644 --- a/Projects/Shmup/Player.cs +++ b/Projects/Shmup/Player.cs @@ -18,7 +18,7 @@ internal enum States public float Y; public States State; - static string[] neutral = + static readonly string[] Sprite = { @" ╱‾╲ ", @" ╱╱‾╲╲ ", @@ -27,26 +27,116 @@ internal enum States @"╲_╱───╲_╱", }; - const int neutralOffsetX = -4; - const int neutralOffsetY = -2; + static readonly string[] SpriteUp = + { + @" ╱‾╲ ", + @" ╱╱‾╲╲ ", + @" ╱'╲O╱'╲ ", + @"╱ / ‾ \ ╲", + @"╲_╱───╲_╱", + @"/V\ /V\", + }; + + static readonly string[] SpriteDown = + { + @" ╱‾╲ ", + @" ╱╱‾╲╲ ", + @"-╱'╲O╱'╲-", + @"╱-/ ‾ \-╲", + @"╲_╱───╲_╱", + }; + + static readonly string[] SpriteLeft = + { + @" ╱╲ ", + @" ╱‾╲╲ ", + @" ╱╲O╱'╲ ", + @"╱/ ‾ \ ╲", + @"╲╱───╲_╱", + }; + + static readonly string[] SpriteRight = + { + @" ╱╲ ", + @" ╱╱‾╲ ", + @" ╱'╲O╱╲ ", + @"╱ / ‾ \╲", + @"╲_╱───╲╱", + }; + + static readonly string[] SpriteUpLeft = + { + @" ╱╲ ", + @" ╱‾╲╲ ", + @" ╱╲O╱'╲ ", + @"╱/ ‾ \ ╲", + @"╲╱───╲_╱", + @"/\ /V\", + }; + + static readonly string[] SpriteUpRight = + { + @" ╱╲ ", + @" ╱╱‾╲ ", + @" ╱'╲O╱╲ ", + @"╱ / ‾ \╲", + @"╲_╱───╲╱", + @"/V\ /\", + }; + + static readonly string[] SpriteDownLeft = + { + @" ╱╲ ", + @" ╱‾╲╲ ", + @"-╱╲O╱'╲-", + @"-/ ‾ \-╲", + @"╲╱───╲_╱", + }; + + static readonly string[] SpriteDownRight = + { + @" ╱╲ ", + @" ╱╱‾╲ ", + @"-╱'╲O╱╲-", + @"╱-/ ‾ \-", + @"╲_╱───╲╱", + }; public void Render() { - for (int y = 0; y < neutral.Length; y++) + var (sprite, offset) = GetSpriteAndOffset(); + for (int y = 0; y < sprite.Length; y++) { - int yo = (int)Y + y + neutralOffsetY; - int yi = neutral.Length - y - 1; + int yo = (int)Y + y + offset.Y; + int yi = sprite.Length - y - 1; if (yo >= 0 && yo < Program.frameBuffer.GetLength(1)) { - for (int x = 0; x < neutral[y].Length; x++) + for (int x = 0; x < sprite[y].Length; x++) { - int xo = (int)X + x + neutralOffsetX; + int xo = (int)X + x + offset.X; if (xo >= 0 && xo < Program.frameBuffer.GetLength(0)) { - Program.frameBuffer[xo, yo] = neutral[yi][x]; + Program.frameBuffer[xo, yo] = sprite[yi][x]; } } } } } + + internal (string[] Sprite, (int X, int Y) offset) GetSpriteAndOffset() + { + return State switch + { + States.None => (Sprite, (-4, -2)), + States.Up => (SpriteUp, (-4, -3)), + States.Down => (SpriteDown, (-4, -2)), + States.Left => (SpriteLeft, (-3, -2)), + States.Right => (SpriteRight, (-4, -2)), + States.Up | States.Left => (SpriteUpLeft, (-3, -3)), + States.Up | States.Right => (SpriteUpRight, (-4, -3)), + States.Down | States.Left => (SpriteDownLeft, (-3, -2)), + States.Down | States.Right => (SpriteDownRight, (-4, -2)), + _ => throw new NotImplementedException(), + }; + } } diff --git a/Projects/Shmup/Program.cs b/Projects/Shmup/Program.cs index 2566b08e..cb01be5b 100644 --- a/Projects/Shmup/Program.cs +++ b/Projects/Shmup/Program.cs @@ -14,7 +14,7 @@ internal static class Program internal static Stopwatch stopwatch = new(); internal static bool pauseUpdates = false; - internal static int gameWidth = 60; + internal static int gameWidth = 80; internal static int gameHeight = 40; internal static int intendedMinConsoleWidth = gameWidth + 3; internal static int intendedMinConsoleHeight = gameHeight + 3; @@ -22,24 +22,23 @@ internal static class Program internal static string topBorder = '┏' + new string('━', gameWidth) + '┓'; internal static string bottomBorder = '┗' + new string('━', gameWidth) + '┛'; - internal static int update = 0; - internal static int consoleWidth = Console.WindowWidth; internal static int consoleHeight = Console.WindowHeight; + internal static StringBuilder render = new(gameWidth * gameHeight); + internal static long score = 0; + internal static int update = 0; internal static bool isDead = false; - internal static Player player = new() { X = gameWidth / 2, Y = gameHeight / 4, }; - internal static List playerBullets = new(); - internal static List explodingBullets = new(); - internal static List enemies = new(); + internal static bool playing = false; + internal static bool waitingForInput = true; internal static void Main() { @@ -64,16 +63,37 @@ internal static void Main() } while (!closeRequested) { - Update(); - if (closeRequested) + Initialize(); + while (!closeRequested && playing) { - return; + Update(); + if (closeRequested) + { + return; + } + Render(); + SleepAfterRender(); } - Render(); - SleepAfterRender(); } } + internal static void Initialize() + { + score = 0; + update = 0; + isDead = false; + player = new() + { + X = gameWidth / 2, + Y = gameHeight / 4, + }; + playerBullets = new(); + explodingBullets = new(); + enemies = new(); + playing = true; + waitingForInput = true; + } + internal static void Update() { bool u = false; @@ -86,6 +106,7 @@ internal static void Update() switch (Console.ReadKey(true).Key) { case ConsoleKey.Escape: closeRequested = true; return; + case ConsoleKey.Enter: playing = !isDead; return; case ConsoleKey.W or ConsoleKey.UpArrow: u = true; break; case ConsoleKey.A or ConsoleKey.LeftArrow: l = true; break; case ConsoleKey.S or ConsoleKey.DownArrow: d = true; break; @@ -101,6 +122,11 @@ internal static void Update() return; } + if (isDead) + { + playing = !(User32_dll.GetAsyncKeyState((int)ConsoleKey.Enter) is not 0); + } + u = u || User32_dll.GetAsyncKeyState((int)ConsoleKey.W) is not 0; l = l || User32_dll.GetAsyncKeyState((int)ConsoleKey.A) is not 0; d = d || User32_dll.GetAsyncKeyState((int)ConsoleKey.S) is not 0; @@ -112,6 +138,11 @@ internal static void Update() r = r || User32_dll.GetAsyncKeyState((int)ConsoleKey.RightArrow) is not 0; shoot = shoot || User32_dll.GetAsyncKeyState((int)ConsoleKey.Spacebar) is not 0; + + if (waitingForInput) + { + waitingForInput = !(u || d || l || r || shoot); + } } if (pauseUpdates) @@ -124,6 +155,11 @@ internal static void Update() return; } + if (waitingForInput) + { + return; + } + update++; if (update % 50 is 0) @@ -141,21 +177,26 @@ internal static void Update() enemy.Update(); } + player.State = Player.States.None; if (l && !r) { player.X = Math.Max(0, player.X - 1); + player.State |= Player.States.Left; } if (r && !l) { player.X = Math.Min(gameWidth - 1, player.X + 1); + player.State |= Player.States.Right; } if (u && !d) { player.Y = Math.Min(gameHeight - 1, player.Y + 1); + player.State |= Player.States.Up; } if (d && !u) { player.Y = Math.Max(0, player.Y - 1); + player.State |= Player.States.Down; } if (shoot) { @@ -169,9 +210,10 @@ internal static void Update() { PlayerBullet bullet = playerBullets[i]; bool exploded = false; - for (int j = 0; j < enemies.Count; j++) + IEnemy[] enemiesClone = enemies.ToArray(); + for (int j = 0; j < enemiesClone.Length; j++) { - if (enemies[j].CollidingWith(bullet.X, bullet.Y)) + if (enemiesClone[j].CollidingWith(bullet.X, bullet.Y)) { if (!exploded) { @@ -180,8 +222,7 @@ internal static void Update() i--; exploded = true; } - enemies.RemoveAt(j); - j--; + enemiesClone[j].Shot(); } } if (!exploded && (bullet.X < 0 || bullet.Y < 0 || bullet.X >= gameWidth || bullet.Y >= gameHeight)) @@ -287,7 +328,7 @@ internal static void Render() frameBuffer[explode.X, explode.Y] = '#'; } } - StringBuilder render = new(); + render.Clear(); render.AppendLine(topBorder); for (int y = gameHeight - 1; y >= 0; y--) { @@ -299,9 +340,18 @@ internal static void Render() render.AppendLine("┃"); } render.AppendLine(bottomBorder); + render.AppendLine($"Score: {score} "); + if (waitingForInput) + { + render.AppendLine("Press [WASD] or [SPACEBAR] to start... "); + } if (isDead) { - render.AppendLine("YOU DIED!"); + render.AppendLine("YOU DIED! Press [ENTER] to play again..."); + } + else + { + render.AppendLine(" "); } try { diff --git a/Projects/Shmup/README.md b/Projects/Shmup/README.md index e4783bc9..8c5236e3 100644 --- a/Projects/Shmup/README.md +++ b/Projects/Shmup/README.md @@ -11,7 +11,6 @@ License

- Shmup (aka "Shoot Em Up") is a vertically scrolling shooter. @@ -72,24 +70,15 @@ Shmup (aka "Shoot Em Up") is a vertically scrolling shooter. ## Input -_todo_ - - ## Downloads -_todo_ - - +[osx-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/osx-x64/Shmup) diff --git a/Projects/Website/Games/Shmup/Shmup.cs b/Projects/Website/Games/Shmup/Shmup.cs new file mode 100644 index 00000000..c6b5aed5 --- /dev/null +++ b/Projects/Website/Games/Shmup/Shmup.cs @@ -0,0 +1,940 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Website.Games.Shmup; + +public class Shmup +{ + public readonly BlazorConsole Console = new(); + + internal static bool closeRequested = false; + internal static Stopwatch stopwatch = new(); + internal static bool pauseUpdates = false; + + internal static int gameWidth = 80; + internal static int gameHeight = 40; + internal static int intendedMinConsoleWidth = gameWidth + 3; + internal static int intendedMinConsoleHeight = gameHeight + 3; + internal static char[,] frameBuffer = new char[gameWidth, gameHeight]; + internal static string topBorder = '┏' + new string('━', gameWidth) + '┓'; + internal static string bottomBorder = '┗' + new string('━', gameWidth) + '┛'; + + internal static int consoleWidth = intendedMinConsoleWidth; + internal static int consoleHeight = intendedMinConsoleHeight; + internal static StringBuilder render = new(gameWidth * gameHeight); + + internal static long score = 0; + internal static int update = 0; + internal static bool isDead = false; + internal static Player player = new() + { + X = gameWidth / 2, + Y = gameHeight / 4, + }; + internal static List playerBullets = new(); + internal static List explodingBullets = new(); + internal static List enemies = new(); + internal static bool playing = false; + internal static bool waitingForInput = true; + + internal static bool w_down = false; + internal static bool a_down = false; + internal static bool s_down = false; + internal static bool d_down = false; + + internal static bool uparrow_down = false; + internal static bool leftarrow_down = false; + internal static bool downarrow_down = false; + internal static bool rightarrow_down = false; + + internal static bool spacebar_down = false; + + public async Task Run() + { + if (OperatingSystem.IsWindows() && (consoleWidth < intendedMinConsoleWidth || consoleHeight < intendedMinConsoleHeight)) + { + try + { + Console.WindowWidth = intendedMinConsoleWidth; + Console.WindowHeight = intendedMinConsoleHeight; + } + catch + { + // nothing + } + consoleWidth = Console.WindowWidth; + consoleHeight = Console.WindowHeight; + } + await Console.Clear(); + if (Console.OutputEncoding != Encoding.UTF8) + { + Console.OutputEncoding = Encoding.UTF8; + } + while (!closeRequested) + { + Initialize(); + while (!closeRequested && playing) + { + await Update(); + if (closeRequested) + { + return; + } + await Render(); + await SleepAfterRender(); + } + } + + void Initialize() + { + score = 0; + update = 0; + isDead = false; + player = new() + { + X = gameWidth / 2, + Y = gameHeight / 4, + }; + playerBullets = new(); + explodingBullets = new(); + enemies = new(); + playing = true; + waitingForInput = true; + } + + async Task Update() + { + bool u = false; + bool d = false; + bool l = false; + bool r = false; + bool shoot = false; + while (await Console.KeyAvailable()) + { + switch ((await Console.ReadKey(true)).Key) + { + case ConsoleKey.Escape: closeRequested = true; return; + case ConsoleKey.Enter: playing = !isDead; return; + case ConsoleKey.W or ConsoleKey.UpArrow: u = true; break; + case ConsoleKey.A or ConsoleKey.LeftArrow: l = true; break; + case ConsoleKey.S or ConsoleKey.DownArrow: d = true; break; + case ConsoleKey.D or ConsoleKey.RightArrow: r = true; break; + case ConsoleKey.Spacebar: shoot = true; break; + } + } + if (Console.IsWindows()) + { + //if (User32_dll.GetAsyncKeyState((int)ConsoleKey.Escape) is not 0) + //{ + // closeRequested = true; + // return; + //} + + //if (isDead) + //{ + // playing = !(User32_dll.GetAsyncKeyState((int)ConsoleKey.Enter) is not 0); + //} + + u = u || w_down; + l = l || a_down; + d = d || s_down; + r = r || d_down; + + u = u || uparrow_down; + l = l || leftarrow_down; + d = d || downarrow_down; + r = r || rightarrow_down; + + shoot = shoot || spacebar_down; + + if (waitingForInput) + { + waitingForInput = !(u || d || l || r || shoot); + } + } + + if (pauseUpdates) + { + return; + } + + if (isDead) + { + return; + } + + if (waitingForInput) + { + return; + } + + update++; + + if (update % 50 is 0) + { + SpawnARandomEnemy(); + } + + for (int i = 0; i < playerBullets.Count; i++) + { + playerBullets[i].Y++; + } + + foreach (IEnemy enemy in enemies) + { + enemy.Update(); + } + + player.State = Player.States.None; + if (l && !r) + { + player.X = Math.Max(0, player.X - 1); + player.State |= Player.States.Left; + } + if (r && !l) + { + player.X = Math.Min(gameWidth - 1, player.X + 1); + player.State |= Player.States.Right; + } + if (u && !d) + { + player.Y = Math.Min(gameHeight - 1, player.Y + 1); + player.State |= Player.States.Up; + } + if (d && !u) + { + player.Y = Math.Max(0, player.Y - 1); + player.State |= Player.States.Down; + } + if (shoot) + { + playerBullets.Add(new() { X = (int)player.X - 2, Y = (int)player.Y }); + playerBullets.Add(new() { X = (int)player.X + 2, Y = (int)player.Y }); + } + + explodingBullets.Clear(); + + for (int i = 0; i < playerBullets.Count; i++) + { + PlayerBullet bullet = playerBullets[i]; + bool exploded = false; + IEnemy[] enemiesClone = enemies.ToArray(); + for (int j = 0; j < enemiesClone.Length; j++) + { + if (enemiesClone[j].CollidingWith(bullet.X, bullet.Y)) + { + if (!exploded) + { + playerBullets.RemoveAt(i); + explodingBullets.Add(bullet); + i--; + exploded = true; + } + enemiesClone[j].Shot(); + } + } + if (!exploded && (bullet.X < 0 || bullet.Y < 0 || bullet.X >= gameWidth || bullet.Y >= gameHeight)) + { + playerBullets.RemoveAt(i); + i--; + } + } + + foreach (IEnemy enemy in enemies) + { + if (enemy.CollidingWith((int)player.X, (int)player.Y)) + { + isDead = true; + return; + } + } + + for (int i = 0; i < enemies.Count; i++) + { + if (enemies[i].IsOutOfBounds()) + { + enemies.RemoveAt(i); + i--; + } + } + } + + void SpawnARandomEnemy() + { + if (Random.Shared.Next(2) is 0) + { + enemies.Add(new Tank() + { + X = Random.Shared.Next(gameWidth - 10) + 5, + Y = gameHeight + Tank.YMax, + YVelocity = -1f / 10f, + }); + } + else if (Random.Shared.Next(2) is 0) + { + enemies.Add(new Helicopter() + { + X = -Helicopter.XMax, + XVelocity = 1f / 3f, + Y = Random.Shared.Next(gameHeight - 10) + 5, + }); + } + else if (Random.Shared.Next(3) is 0 or 1) + { + enemies.Add(new UFO1() + { + X = Random.Shared.Next(gameWidth - 10) + 5, + Y = gameHeight + UFO1.YMax, + }); + } + else + { + enemies.Add(new UFO2()); + } + } + + async Task Render() + { + const int maxRetryCount = 10; + int retry = 0; + Retry: + if (retry > maxRetryCount) + { + return; + } + if (consoleWidth != Console.WindowWidth || consoleHeight != Console.WindowHeight) + { + consoleWidth = Console.WindowWidth; + consoleHeight = Console.WindowHeight; + await Console.Clear(); + } + if (consoleWidth < intendedMinConsoleWidth || consoleHeight < intendedMinConsoleHeight) + { + await Console.Clear(); + await Console.Write($"Console too small at {consoleWidth}w x {consoleHeight}h. Please increase to at least {intendedMinConsoleWidth}w x {intendedMinConsoleHeight}h."); + pauseUpdates = true; + return; + } + pauseUpdates = false; + ClearFrameBuffer(); + player.Render(); + foreach (IEnemy enemy in enemies) + { + enemy.Render(); + } + foreach (PlayerBullet bullet in playerBullets) + { + if (bullet.X >= 0 && bullet.X < gameWidth && bullet.Y >= 0 && bullet.Y < gameHeight) + { + frameBuffer[bullet.X, bullet.Y] = '^'; + } + } + foreach (PlayerBullet explode in explodingBullets) + { + if (explode.X >= 0 && explode.X < gameWidth && explode.Y >= 0 && explode.Y < gameHeight) + { + frameBuffer[explode.X, explode.Y] = '#'; + } + } + render.Clear(); + render.AppendLine(topBorder); + for (int y = gameHeight - 1; y >= 0; y--) + { + render.Append('┃'); + for (int x = 0; x < gameWidth; x++) + { + render.Append(frameBuffer[x, y]); + } + render.AppendLine("┃"); + } + render.AppendLine(bottomBorder); + render.AppendLine($"Score: {score} "); + if (waitingForInput) + { + render.AppendLine("Press [WASD] or [SPACEBAR] to start... "); + } + if (isDead) + { + render.AppendLine("YOU DIED! Press [ENTER] to play again..."); + } + else + { + render.AppendLine(" "); + } + try + { + Console.CursorVisible = false; + await Console.SetCursorPosition(0, 0); + await Console.Write(render); + } + catch + { + retry++; + goto Retry; + } + } + + void ClearFrameBuffer() + { + for (int x = 0; x < gameWidth; x++) + { + for (int y = 0; y < gameHeight; y++) + { + frameBuffer[x, y] = ' '; + } + } + } + + async Task SleepAfterRender() + { + TimeSpan sleep = TimeSpan.FromSeconds(1d / 120d) - stopwatch.Elapsed; + if (sleep > TimeSpan.Zero) + { + await Console.RefreshAndDelay(sleep); + } + stopwatch.Restart(); + } + } + + internal class Player + { + [Flags] + internal enum States + { + None = 0, + Up = 1 << 0, + Down = 1 << 1, + Left = 1 << 2, + Right = 1 << 3, + } + + public float X; + public float Y; + public States State; + + static readonly string[] Sprite = + { + @" ╱‾╲ ", + @" ╱╱‾╲╲ ", + @" ╱'╲O╱'╲ ", + @"╱ / ‾ \ ╲", + @"╲_╱───╲_╱", + }; + + static readonly string[] SpriteUp = + { + @" ╱‾╲ ", + @" ╱╱‾╲╲ ", + @" ╱'╲O╱'╲ ", + @"╱ / ‾ \ ╲", + @"╲_╱───╲_╱", + @"/V\ /V\", + }; + + static readonly string[] SpriteDown = + { + @" ╱‾╲ ", + @" ╱╱‾╲╲ ", + @"-╱'╲O╱'╲-", + @"╱-/ ‾ \-╲", + @"╲_╱───╲_╱", + }; + + static readonly string[] SpriteLeft = + { + @" ╱╲ ", + @" ╱‾╲╲ ", + @" ╱╲O╱'╲ ", + @"╱/ ‾ \ ╲", + @"╲╱───╲_╱", + }; + + static readonly string[] SpriteRight = + { + @" ╱╲ ", + @" ╱╱‾╲ ", + @" ╱'╲O╱╲ ", + @"╱ / ‾ \╲", + @"╲_╱───╲╱", + }; + + static readonly string[] SpriteUpLeft = + { + @" ╱╲ ", + @" ╱‾╲╲ ", + @" ╱╲O╱'╲ ", + @"╱/ ‾ \ ╲", + @"╲╱───╲_╱", + @"/\ /V\", + }; + + static readonly string[] SpriteUpRight = + { + @" ╱╲ ", + @" ╱╱‾╲ ", + @" ╱'╲O╱╲ ", + @"╱ / ‾ \╲", + @"╲_╱───╲╱", + @"/V\ /\", + }; + + static readonly string[] SpriteDownLeft = + { + @" ╱╲ ", + @" ╱‾╲╲ ", + @"-╱╲O╱'╲-", + @"-/ ‾ \-╲", + @"╲╱───╲_╱", + }; + + static readonly string[] SpriteDownRight = + { + @" ╱╲ ", + @" ╱╱‾╲ ", + @"-╱'╲O╱╲-", + @"╱-/ ‾ \-", + @"╲_╱───╲╱", + }; + + public void Render() + { + var (sprite, offset) = GetSpriteAndOffset(); + for (int y = 0; y < sprite.Length; y++) + { + int yo = (int)Y + y + offset.Y; + int yi = sprite.Length - y - 1; + if (yo >= 0 && yo < Shmup.frameBuffer.GetLength(1)) + { + for (int x = 0; x < sprite[y].Length; x++) + { + int xo = (int)X + x + offset.X; + if (xo >= 0 && xo < Shmup.frameBuffer.GetLength(0)) + { + Shmup.frameBuffer[xo, yo] = sprite[yi][x]; + } + } + } + } + } + + internal (string[] Sprite, (int X, int Y) offset) GetSpriteAndOffset() + { + return State switch + { + States.None => (Sprite, (-4, -2)), + States.Up => (SpriteUp, (-4, -3)), + States.Down => (SpriteDown, (-4, -2)), + States.Left => (SpriteLeft, (-3, -2)), + States.Right => (SpriteRight, (-4, -2)), + States.Up | States.Left => (SpriteUpLeft, (-3, -3)), + States.Up | States.Right => (SpriteUpRight, (-4, -3)), + States.Down | States.Left => (SpriteDownLeft, (-3, -2)), + States.Down | States.Right => (SpriteDownRight, (-4, -2)), + _ => throw new NotImplementedException(), + }; + } + } + + internal class PlayerBullet + { + public int X; + public int Y; + } + + internal interface IEnemy + { + public void Shot(); + + public void Render(); + + public void Update(); + + public bool CollidingWith(int x, int y); + + public bool IsOutOfBounds(); + } + + internal class Helicopter : IEnemy + { + public static int scorePerKill = 100; + public int Health = 70; + public float X; + public float Y; + public float XVelocity; + public float YVelocity; + private int Frame; + private string[] Sprite = Random.Shared.Next(2) is 0 ? spriteA : spriteB; + + static readonly string[] spriteA = + { + @" ~~~~~+~~~~~", + @"'\===<[_]L) ", + @" -'-`- ", + }; + + static readonly string[] spriteB = + { + @" -----+-----", + @"*\===<[_]L) ", + @" -'-`- ", + }; + + internal static int XMax = Math.Max(spriteA.Max(s => s.Length), spriteB.Max(s => s.Length)); + internal static int YMax = Math.Max(spriteA.Length, spriteB.Length); + + public void Render() + { + for (int y = 0; y < Sprite.Length; y++) + { + int yo = (int)Y + y; + int yi = Sprite.Length - y - 1; + if (yo >= 0 && yo < Shmup.frameBuffer.GetLength(1)) + { + for (int x = 0; x < Sprite[y].Length; x++) + { + int xo = (int)X + x; + if (xo >= 0 && xo < Shmup.frameBuffer.GetLength(0)) + { + if (Sprite[yi][x] is not ' ') + { + Shmup.frameBuffer[xo, yo] = Sprite[yi][x]; + } + } + } + } + } + } + + public void Update() + { + Frame++; + if (Frame > 10) + { + Sprite = Sprite == spriteB ? spriteA : spriteB; + Frame = 0; + } + X += XVelocity; + Y += YVelocity; + } + + public bool CollidingWith(int x, int y) + { + int xo = x - (int)X; + int yo = y - (int)Y; + return + yo >= 0 && yo < Sprite.Length && + xo >= 0 && xo < Sprite[yo].Length && + Sprite[yo][xo] is not ' '; + } + + public bool IsOutOfBounds() + { + return + XVelocity <= 0 && X < -XMax || + YVelocity <= 0 && Y < -YMax || + XVelocity >= 0 && X > Shmup.gameWidth + XMax || + YVelocity >= 0 && Y > Shmup.gameHeight + YMax; + } + + public void Shot() + { + Health--; + if (Health <= 0) + { + Shmup.enemies.Remove(this); + Shmup.score += scorePerKill; + } + } + } + + internal class Tank : IEnemy + { + public static int scorePerKill = 20; + public int Health = 20; + public float X; + public float Y; + public float XVelocity; + public float YVelocity; + private string[] Sprite; + + static readonly string[] spriteDown = + { + @" ___ ", + @"|_O_|", + @"[ooo]", + }; + + static readonly string[] spriteUp = + { + @" _^_ ", + @"|___|", + @"[ooo]", + }; + + static readonly string[] spriteLeft = + { + @" __ ", + @"=|__|", + @"[ooo]", + }; + + static readonly string[] spriteRight = + { + @" __ ", + @"|__|=", + @"[ooo]", + }; + + internal static int XMax = new[] { spriteDown.Max(s => s.Length), spriteUp.Max(s => s.Length), spriteLeft.Max(s => s.Length), spriteRight.Max(s => s.Length), }.Max(); + internal static int YMax = new[] { spriteDown.Length, spriteUp.Length, spriteLeft.Length, spriteRight.Length, }.Max(); + + public void Render() + { + for (int y = 0; y < Sprite.Length; y++) + { + int yo = (int)Y + y; + int yi = Sprite.Length - y - 1; + if (yo >= 0 && yo < Shmup.frameBuffer.GetLength(1)) + { + for (int x = 0; x < Sprite[y].Length; x++) + { + int xo = (int)X + x; + if (xo >= 0 && xo < Shmup.frameBuffer.GetLength(0)) + { + if (Sprite[yi][x] is not ' ') + { + Shmup.frameBuffer[xo, yo] = Sprite[yi][x]; + } + } + } + } + } + } + + public void Update() + { + int xDifToPlayer = (int)Shmup.player.X - (int)X; + int yDifToPlayer = (int)Shmup.player.Y - (int)Y; + + Sprite = Math.Abs(xDifToPlayer) > Math.Abs(yDifToPlayer) + ? xDifToPlayer > 0 ? spriteRight : spriteLeft + : yDifToPlayer > 0 ? spriteUp : spriteDown; + + X += XVelocity; + Y += YVelocity; + } + + public bool CollidingWith(int x, int y) + { + int xo = x - (int)X; + int yo = y - (int)Y; + return + yo >= 0 && yo < Sprite.Length && + xo >= 0 && xo < Sprite[yo].Length && + Sprite[yo][xo] is not ' '; + } + + public bool IsOutOfBounds() + { + return + XVelocity <= 0 && X < -XMax || + YVelocity <= 0 && Y < -YMax || + XVelocity >= 0 && X > Shmup.gameWidth + XMax || + YVelocity >= 0 && Y > Shmup.gameHeight + YMax; + } + + public void Shot() + { + Health--; + if (Health <= 0) + { + Shmup.enemies.Remove(this); + Shmup.score += scorePerKill; + } + } + } + + internal class UFO1 : IEnemy + { + public static int scorePerKill = 10; + public int Health = 10; + public float X; + public float Y; + public float XVelocity = 1f / 8f; + public float YVelocity = 1f / 8f; + private static readonly string[] Sprite = + { + @" _!_ ", + @"(_o_)", + @" ''' ", + }; + + internal static int XMax = Sprite.Max(s => s.Length); + internal static int YMax = Sprite.Length; + + public void Render() + { + for (int y = 0; y < Sprite.Length; y++) + { + int yo = (int)Y + y; + int yi = Sprite.Length - y - 1; + if (yo >= 0 && yo < Shmup.frameBuffer.GetLength(1)) + { + for (int x = 0; x < Sprite[y].Length; x++) + { + int xo = (int)X + x; + if (xo >= 0 && xo < Shmup.frameBuffer.GetLength(0)) + { + if (Sprite[yi][x] is not ' ') + { + Shmup.frameBuffer[xo, yo] = Sprite[yi][x]; + } + } + } + } + } + } + + public void Update() + { + if (Shmup.player.X < X) + { + X = Math.Max(Shmup.player.X, X - XVelocity); + } + else + { + X = Math.Min(Shmup.player.X, X + XVelocity); + } + if (Shmup.player.Y < Y) + { + Y = Math.Max(Shmup.player.Y, Y - YVelocity); + } + else + { + Y = Math.Min(Shmup.player.Y, Y + YVelocity); + } + } + + public bool CollidingWith(int x, int y) + { + int xo = x - (int)X; + int yo = y - (int)Y; + return + yo >= 0 && yo < Sprite.Length && + xo >= 0 && xo < Sprite[yo].Length && + Sprite[yo][xo] is not ' '; + } + + public bool IsOutOfBounds() + { + return + XVelocity <= 0 && X < -XMax || + YVelocity <= 0 && Y < -YMax || + XVelocity >= 0 && X > Shmup.gameWidth + XMax || + YVelocity >= 0 && Y > Shmup.gameHeight + YMax; + } + + public void Shot() + { + Health--; + if (Health <= 0) + { + Shmup.enemies.Remove(this); + Shmup.score += scorePerKill; + } + } + } + + internal class UFO2 : IEnemy + { + public static int scorePerKill = 80; + public int Health = 50; + public float X; + public float Y; + public int UpdatesSinceTeleport; + public int TeleportFrequency = 360; + + private static readonly string[] Sprite = + { + @" _!_ ", + @" /_O_\ ", + @"-==<_‗_‗_>==-", + }; + + internal static int XMax = Sprite.Max(s => s.Length); + internal static int YMax = Sprite.Length; + + public UFO2() + { + X = Random.Shared.Next(Shmup.gameWidth - XMax) + XMax / 2; + Y = Random.Shared.Next(Shmup.gameHeight - YMax) + YMax / 2; + } + + public void Render() + { + for (int y = 0; y < Sprite.Length; y++) + { + int yo = (int)Y + y; + int yi = Sprite.Length - y - 1; + if (yo >= 0 && yo < Shmup.frameBuffer.GetLength(1)) + { + for (int x = 0; x < Sprite[y].Length; x++) + { + int xo = (int)X + x; + if (xo >= 0 && xo < Shmup.frameBuffer.GetLength(0)) + { + if (Sprite[yi][x] is not ' ') + { + Shmup.frameBuffer[xo, yo] = Sprite[yi][x]; + } + } + } + } + } + } + + public void Update() + { + UpdatesSinceTeleport++; + if (UpdatesSinceTeleport > TeleportFrequency) + { + X = Random.Shared.Next(Shmup.gameWidth - XMax) + XMax / 2; + Y = Random.Shared.Next(Shmup.gameHeight - YMax) + YMax / 2; + UpdatesSinceTeleport = 0; + } + } + + public bool CollidingWith(int x, int y) + { + int xo = x - (int)X; + int yo = y - (int)Y; + return + yo >= 0 && yo < Sprite.Length && + xo >= 0 && xo < Sprite[yo].Length && + Sprite[yo][xo] is not ' '; + } + + public bool IsOutOfBounds() + { + return ! + (X > 0 && + X < Shmup.gameWidth && + Y > 0 && + Y < Shmup.gameHeight); + } + + public void Shot() + { + Health--; + if (Health <= 0) + { + Shmup.enemies.Remove(this); + Shmup.score += scorePerKill; + } + } + } +} diff --git a/Projects/Website/Pages/Shmup.razor b/Projects/Website/Pages/Shmup.razor new file mode 100644 index 00000000..829b3523 --- /dev/null +++ b/Projects/Website/Pages/Shmup.razor @@ -0,0 +1,93 @@ +@using System + +@page "/Shmup" + +Shmup + +

Shmup

+ + + Go To Readme + + + + +
+
+
+			@Console.State
+		
+
+
+ + + + + + + + + + +
+
+ + + + + +@code +{ + Games.Shmup.Shmup Game; + BlazorConsole Console; + + public Shmup() + { + Game = new(); + Console = Game.Console; + Console.WindowWidth = Games.Shmup.Shmup.intendedMinConsoleWidth + 3; + Console.WindowHeight = Games.Shmup.Shmup.intendedMinConsoleHeight + 3; + Console.TriggerRefresh = StateHasChanged; + } + + public void OnKeyDown(KeyboardEventArgs e) + { + Console.OnKeyDown(e); + switch (e.Key) + { + case " ": Games.Shmup.Shmup.spacebar_down = true; break; + case "ArrowLeft": Games.Shmup.Shmup.leftarrow_down = true; break; + case "ArrowRight": Games.Shmup.Shmup.rightarrow_down = true; break; + case "ArrowUp": Games.Shmup.Shmup.uparrow_down = true; break; + case "ArrowDown": Games.Shmup.Shmup.downarrow_down = true; break; + case "w": Games.Shmup.Shmup.w_down = true; break; + case "a": Games.Shmup.Shmup.a_down = true; break; + case "s": Games.Shmup.Shmup.s_down = true; break; + case "d": Games.Shmup.Shmup.d_down = true; break; + } + } + + public void OnKeyUp(KeyboardEventArgs e) + { + switch (e.Key) + { + case " ": Games.Shmup.Shmup.spacebar_down = false; break; + case "ArrowLeft": Games.Shmup.Shmup.leftarrow_down = false; break; + case "ArrowRight": Games.Shmup.Shmup.rightarrow_down = false; break; + case "ArrowUp": Games.Shmup.Shmup.uparrow_down = false; break; + case "ArrowDown": Games.Shmup.Shmup.downarrow_down = false; break; + case "w": Games.Shmup.Shmup.w_down = false; break; + case "a": Games.Shmup.Shmup.a_down = false; break; + case "s": Games.Shmup.Shmup.s_down = false; break; + case "d": Games.Shmup.Shmup.d_down = false; break; + } + } + + protected override void OnInitialized() => InvokeAsync(Game.Run); +} diff --git a/Projects/Website/Shared/NavMenu.razor b/Projects/Website/Shared/NavMenu.razor index abcf149c..13e74349 100644 --- a/Projects/Website/Shared/NavMenu.razor +++ b/Projects/Website/Shared/NavMenu.razor @@ -238,6 +238,11 @@ Console Monsters + diff --git a/README.md b/README.md index a9d9da3b..19663231 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ |[Gravity](Projects/Gravity)|5|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Gravity) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Gravity%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[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_| +|[Shmup](Projects/Shmup)|?|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Shmup) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Shmup%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)](#) _Work In Progress_ & _Only Supported On Windows OS (+WEB)_| \*_**Weight**: A relative rating for how advanced the source code is._