From 532c3af641773a171d1619326611ba17f8981188 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Fri, 29 Sep 2023 20:14:07 -0500 Subject: [PATCH] Shmup (#86) shmupdate --- .github/workflows/Shmup Build.yml | 20 + .vscode/launch.json | 10 + .vscode/tasks.json | 13 + Projects/Shmup/Enemies/Helicopter.cs | 97 +++ Projects/Shmup/Enemies/IEnemy.cs | 14 + Projects/Shmup/Enemies/Tank.cs | 111 +++ Projects/Shmup/Enemies/UFO1.cs | 95 +++ Projects/Shmup/Enemies/UFO2.cs | 93 +++ Projects/Shmup/Player.cs | 142 ++++ Projects/Shmup/PlayerBullet.cs | 13 + Projects/Shmup/Program.cs | 395 +++++++++++ Projects/Shmup/README.md | 84 +++ Projects/Shmup/Shmup.csproj | 8 + Projects/Website/Games/Shmup/Shmup.cs | 940 ++++++++++++++++++++++++++ Projects/Website/Pages/Shmup.razor | 93 +++ Projects/Website/Shared/NavMenu.razor | 5 + README.md | 1 + dotnet-console-games.sln | 6 + dotnet-console-games.slnf | 1 + 19 files changed, 2141 insertions(+) create mode 100644 .github/workflows/Shmup Build.yml create mode 100644 Projects/Shmup/Enemies/Helicopter.cs create mode 100644 Projects/Shmup/Enemies/IEnemy.cs create mode 100644 Projects/Shmup/Enemies/Tank.cs create mode 100644 Projects/Shmup/Enemies/UFO1.cs create mode 100644 Projects/Shmup/Enemies/UFO2.cs create mode 100644 Projects/Shmup/Player.cs create mode 100644 Projects/Shmup/PlayerBullet.cs create mode 100644 Projects/Shmup/Program.cs create mode 100644 Projects/Shmup/README.md create mode 100644 Projects/Shmup/Shmup.csproj create mode 100644 Projects/Website/Games/Shmup/Shmup.cs create mode 100644 Projects/Website/Pages/Shmup.razor 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 new file mode 100644 index 00000000..8da54076 --- /dev/null +++ b/Projects/Shmup/Enemies/Helicopter.cs @@ -0,0 +1,97 @@ +using System; +using System.Linq; + +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; + 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 < Program.frameBuffer.GetLength(1)) + { + for (int x = 0; x < Sprite[y].Length; x++) + { + int xo = (int)X + x; + if (xo >= 0 && xo < Program.frameBuffer.GetLength(0)) + { + if (Sprite[yi][x] is not ' ') + { + Program.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 > 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 new file mode 100644 index 00000000..4153b102 --- /dev/null +++ b/Projects/Shmup/Enemies/IEnemy.cs @@ -0,0 +1,14 @@ +namespace Shmup.Enemies; + +internal interface IEnemy +{ + public void Shot(); + + public void Render(); + + public void Update(); + + public bool CollidingWith(int x, int y); + + public bool IsOutOfBounds(); +} diff --git a/Projects/Shmup/Enemies/Tank.cs b/Projects/Shmup/Enemies/Tank.cs new file mode 100644 index 00000000..1075f860 --- /dev/null +++ b/Projects/Shmup/Enemies/Tank.cs @@ -0,0 +1,111 @@ +using System; +using System.Linq; + +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 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 < Program.frameBuffer.GetLength(1)) + { + for (int x = 0; x < Sprite[y].Length; x++) + { + int xo = (int)X + x; + if (xo >= 0 && xo < Program.frameBuffer.GetLength(0)) + { + if (Sprite[yi][x] is not ' ') + { + Program.frameBuffer[xo, yo] = Sprite[yi][x]; + } + } + } + } + } + } + + public void Update() + { + int xDifToPlayer = (int)Program.player.X - (int)X; + int yDifToPlayer = (int)Program.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 > 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 new file mode 100644 index 00000000..2e7b04d1 --- /dev/null +++ b/Projects/Shmup/Enemies/UFO1.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; + +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 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 < Program.frameBuffer.GetLength(1)) + { + for (int x = 0; x < Sprite[y].Length; x++) + { + int xo = (int)X + x; + if (xo >= 0 && xo < Program.frameBuffer.GetLength(0)) + { + if (Sprite[yi][x] is not ' ') + { + Program.frameBuffer[xo, yo] = Sprite[yi][x]; + } + } + } + } + } + } + + public void Update() + { + if (Program.player.X < X) + { + X = Math.Max(Program.player.X, X - XVelocity); + } + else + { + X = Math.Min(Program.player.X, X + XVelocity); + } + if (Program.player.Y < Y) + { + Y = Math.Max(Program.player.Y, Y - YVelocity); + } + else + { + Y = Math.Min(Program.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 > 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 new file mode 100644 index 00000000..e5350b9e --- /dev/null +++ b/Projects/Shmup/Enemies/UFO2.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; + +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 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(Program.gameWidth - XMax) + XMax / 2; + Y = Random.Shared.Next(Program.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 < Program.frameBuffer.GetLength(1)) + { + for (int x = 0; x < Sprite[y].Length; x++) + { + int xo = (int)X + x; + if (xo >= 0 && xo < Program.frameBuffer.GetLength(0)) + { + if (Sprite[yi][x] is not ' ') + { + Program.frameBuffer[xo, yo] = Sprite[yi][x]; + } + } + } + } + } + } + + public void Update() + { + UpdatesSinceTeleport++; + if (UpdatesSinceTeleport > TeleportFrequency) + { + X = Random.Shared.Next(Program.gameWidth - XMax) + XMax / 2; + Y = Random.Shared.Next(Program.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 < Program.gameWidth && + 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 new file mode 100644 index 00000000..a172db92 --- /dev/null +++ b/Projects/Shmup/Player.cs @@ -0,0 +1,142 @@ +using System; + +namespace Shmup; + +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 < Program.frameBuffer.GetLength(1)) + { + for (int x = 0; x < sprite[y].Length; x++) + { + int xo = (int)X + x + offset.X; + if (xo >= 0 && xo < Program.frameBuffer.GetLength(0)) + { + 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/PlayerBullet.cs b/Projects/Shmup/PlayerBullet.cs new file mode 100644 index 00000000..a6925962 --- /dev/null +++ b/Projects/Shmup/PlayerBullet.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shmup; + +internal class PlayerBullet +{ + public int X; + public int Y; +} diff --git a/Projects/Shmup/Program.cs b/Projects/Shmup/Program.cs new file mode 100644 index 00000000..cb01be5b --- /dev/null +++ b/Projects/Shmup/Program.cs @@ -0,0 +1,395 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Shmup.Enemies; + +namespace Shmup; + +internal static class Program +{ + 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 = 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() + { + if (OperatingSystem.IsWindows() && (consoleWidth < intendedMinConsoleWidth || consoleHeight < intendedMinConsoleHeight)) + { + try + { + Console.WindowWidth = intendedMinConsoleWidth; + Console.WindowHeight = intendedMinConsoleHeight; + } + catch + { + // nothing + } + consoleWidth = Console.WindowWidth; + consoleHeight = Console.WindowHeight; + } + Console.Clear(); + if (Console.OutputEncoding != Encoding.UTF8) + { + Console.OutputEncoding = Encoding.UTF8; + } + while (!closeRequested) + { + Initialize(); + while (!closeRequested && playing) + { + Update(); + if (closeRequested) + { + return; + } + 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; + bool d = false; + bool l = false; + bool r = false; + bool shoot = false; + while (Console.KeyAvailable) + { + 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; + case ConsoleKey.D or ConsoleKey.RightArrow: r = true; break; + case ConsoleKey.Spacebar: shoot = true; break; + } + } + if (OperatingSystem.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 || 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; + r = r || User32_dll.GetAsyncKeyState((int)ConsoleKey.D) is not 0; + + u = u || User32_dll.GetAsyncKeyState((int)ConsoleKey.UpArrow) is not 0; + l = l || User32_dll.GetAsyncKeyState((int)ConsoleKey.LeftArrow) is not 0; + d = d || User32_dll.GetAsyncKeyState((int)ConsoleKey.DownArrow) is not 0; + 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) + { + 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--; + } + } + } + + internal static 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()); + } + } + + internal static void 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; + Console.Clear(); + } + if (consoleWidth < intendedMinConsoleWidth || consoleHeight < intendedMinConsoleHeight) + { + Console.Clear(); + 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; + Console.SetCursorPosition(0, 0); + Console.Write(render); + } + catch + { + retry++; + goto Retry; + } + } + + internal static void ClearFrameBuffer() + { + for (int x = 0; x < gameWidth; x++) + { + for (int y = 0; y < gameHeight; y++) + { + frameBuffer[x, y] = ' '; + } + } + } + + internal static void SleepAfterRender() + { + TimeSpan sleep = TimeSpan.FromSeconds(1d / 120d) - stopwatch.Elapsed; + if (sleep > TimeSpan.Zero) + { + Thread.Sleep(sleep); + } + stopwatch.Restart(); + } + + internal static class User32_dll + { + [DllImport("user32.dll")] + internal static extern short GetAsyncKeyState(int vKey); + } +} diff --git a/Projects/Shmup/README.md b/Projects/Shmup/README.md new file mode 100644 index 00000000..8c5236e3 --- /dev/null +++ b/Projects/Shmup/README.md @@ -0,0 +1,84 @@ +

+ Shmup +

+ +

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

+ +

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

+ +Shmup (aka "Shoot Em Up") is a vertically scrolling shooter. + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ ┃ +┃ ___ ┃ +┃ |_O_| ┃ +┃ [ooo] ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ -----+----- ┃ +┃ *\===<[_]L) ┃ +┃ -'-`- -----+----- ┃ +┃ -----+----- *\===<[_]L) ┃ +┃ *\===<[_]L) -'-`- ┃ +┃ |__-'-`- ┃ +┃ [ooo] ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ __ ┃ +┃ =|__| ┃ +┃ [ooo] ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ╱‾╲ ┃ +┃ __ ╱╱‾╲╲ ┃ +┃ |__|= ╱'╲O╱'╲ ┃ +┃ [ooo] ╱ / ‾ \ ╲ ┃ +┃ ╲_╱───╲_╱ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` + +## Input + +- `W`, `A`, `S`, `D`, `↑`, `↓`, `←`, `→`: movement +- `spacebar`: shoot +- `enter`: start new game +- `escape`: exit game + +## Downloads + +[win-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/win-x64/Shmup.exe) + +[linux-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/linux-x64/Shmup) + +[osx-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/osx-x64/Shmup) diff --git a/Projects/Shmup/Shmup.csproj b/Projects/Shmup/Shmup.csproj new file mode 100644 index 00000000..0e17b8ef --- /dev/null +++ b/Projects/Shmup/Shmup.csproj @@ -0,0 +1,8 @@ + + + Exe + net7.0 + disable + enable + + 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._
diff --git a/dotnet-console-games.sln b/dotnet-console-games.sln index 709f158e..ac9bbc35 100644 --- a/dotnet-console-games.sln +++ b/dotnet-console-games.sln @@ -99,6 +99,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Darts", "Projects\Darts\Dar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flash Cards", "Projects\Flash Cards\Flash Cards.csproj", "{62BD025E-693C-4524-AE89-07D74EB2931B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shmup", "Projects\Shmup\Shmup.csproj", "{E5B5E93F-CEE2-431C-B371-D7AC9C9A4973}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -297,6 +299,10 @@ Global {62BD025E-693C-4524-AE89-07D74EB2931B}.Debug|Any CPU.Build.0 = Debug|Any CPU {62BD025E-693C-4524-AE89-07D74EB2931B}.Release|Any CPU.ActiveCfg = Release|Any CPU {62BD025E-693C-4524-AE89-07D74EB2931B}.Release|Any CPU.Build.0 = Release|Any CPU + {E5B5E93F-CEE2-431C-B371-D7AC9C9A4973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5B5E93F-CEE2-431C-B371-D7AC9C9A4973}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5B5E93F-CEE2-431C-B371-D7AC9C9A4973}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5B5E93F-CEE2-431C-B371-D7AC9C9A4973}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/dotnet-console-games.slnf b/dotnet-console-games.slnf index 40b73909..fb3c5b44 100644 --- a/dotnet-console-games.slnf +++ b/dotnet-console-games.slnf @@ -35,6 +35,7 @@ "Projects\\Role Playing Game\\Role Playing Game.csproj", "Projects\\Roll And Move\\Roll And Move.csproj", "Projects\\Rythm\\Rythm.csproj", + "Projects\\Shmup\\Shmup.csproj", "Projects\\Simon\\Simon.csproj", "Projects\\Sliding Puzzle\\Sliding Puzzle.csproj", "Projects\\Snake\\Snake.csproj",