using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Threading; // Idea /* Game name 2D Flappy Blyord Syntax rules global variables CAPTIALIZED_AND_UNDERSCORED functions, classes, structs, fields, properties, paramaters ArePerWordCapitalized normal variables are lower_case_underscored Controls Space - move up Shift - move down 1, 2, 3 - ability Platform types player - custom on input functions 'n stuff ground - colidable on top and bottom hostile - removes a life and give invincibility ability - gives player ability to replace or add and ability Platform logic internal physics update overridable logic update ( Polyolyjollymorphism ) User interface ability : keybind (num) : description distance traveled in characters : num best run distance traveled in characters : num lives left : symbol ability : freezez game, clears screen, and displays isolated selection, a choice between replacing, adding, or ignoring the given ability Logical prerequisites Levels are random When colliding with a ground object and losing a life you can clip through walls After picking up an ability the player becomes invincible for 3 seconds You're better than all of us, miles. Ability cooldowns are displayed as a bar Game runs on DELTATIME All tiles get an UPDATE function called Gravity - move down Confined canvas top - bottom ~ 50 chacters Enemies are classes saved in a List<> Enemies are dispalyed using their position and draw symbol every frame usign logic from old code */ namespace PLAYGROUND { internal class Game { // General game related variables private static int CANVAS_HEIGHT = 25; private static int CANVAS_WIDTH = 64; private static int GRAVITY = 45; private static int MAX_PLAYER_Y_VELOCITY = 55; private static int TILE_SPEED = 12; private static float SURVIVED_TIME = 0; private static float BEST_SURVIVED_TIME = 0; private static int FPS = 0; private static Vector2 PLAYER_POSITION = new Vector2(-1, -1); private static List Tiles = new List(); private static PlayerTile PLAYER_TILE = new PlayerTile( new Vector2(10, CANVAS_HEIGHT / 3) ); private static TileGenerationAlgorithm TGAlgorithm; private static Random RANDOM = new Random(); private static Ability Ability1 = new Ability(); private static Ability Ability2 = new Ability(); private static EActiveScene ActiveScene = EActiveScene.StartScene; private enum EActiveScene { GameScene, StartScene, AbilityScene, GameOverScene } // Clamp functions is manually implemented due to C# version 7 limitation private static int Clamp(int Value, int Min, int Max) { // If value is less than min, then return min, if its larger than max, return max, else return value return Value < Min ? Min : Value > Max ? Max : Value; } // Rendering private static class Canvas { public static char[] RenderBuffer = new char[(CANVAS_WIDTH * CANVAS_HEIGHT) + CANVAS_WIDTH + CANVAS_HEIGHT]; public static void Render() { switch (ActiveScene) { case EActiveScene.GameScene: Console.SetCursorPosition(0, 0); // Canvas rendering for (int i = 0; i < RenderBuffer.Length; i++) RenderBuffer[i] = ' '; foreach (Tile tile in Tiles) { if (tile.Position.X > CANVAS_WIDTH || tile.Position.X < 0) continue; if (tile.Position.Y > CANVAS_HEIGHT || tile.Position.Y < 0) continue; int x = Game.Clamp((int)tile.Position.X, 0, CANVAS_WIDTH); int y = Game.Clamp((int)tile.Position.Y, 0, CANVAS_HEIGHT); RenderBuffer[y * CANVAS_WIDTH + x] = tile.Symbol; } foreach (Tile tile in Tiles) { if (tile is PlayerTile player_tile) { int x = Game.Clamp((int)player_tile.Position.X, 0, CANVAS_WIDTH); int y = Game.Clamp((int)player_tile.Position.Y, 0, CANVAS_HEIGHT - 1); RenderBuffer[y * CANVAS_WIDTH + x] = player_tile.Symbol; } } StringBuilder render_string = new StringBuilder(CANVAS_HEIGHT * CANVAS_WIDTH); for (int y = 0; y < CANVAS_HEIGHT; y++) { for (int x = 0; x < CANVAS_WIDTH; x++) { render_string.Append(RenderBuffer[(y * CANVAS_WIDTH) + x]); } render_string.Append("\n"); } // Clearing area for text UI to not overlap for (int k = 0; k < CANVAS_WIDTH; k++) { render_string.Append(' '); } render_string.Append('\n'); for (int k = 0; k < CANVAS_WIDTH; k++) { render_string.Append(' '); } render_string.Append('\n'); for (int k = 0; k < CANVAS_WIDTH; k++) { render_string.Append(' '); } render_string.Append('\n'); Console.Write(render_string); (int Top, int Left) cursor_position = (Console.CursorTop, Console.CursorLeft); Console.SetCursorPosition(0, cursor_position.Top - 3); Console.Write($"Ability 1 - {Ability1.Name} {Ability1.GetCooldown()}s"); Console.SetCursorPosition((int)(CANVAS_WIDTH / 2), cursor_position.Top - 3); Console.Write($"Ability 2 - {Ability2.Name} {Ability2.GetCooldown()}s"); Console.SetCursorPosition(0, cursor_position.Top - 2); Console.Write($"Survied: {SURVIVED_TIME}s\t\t"); Console.SetCursorPosition((int)(CANVAS_WIDTH / 2), cursor_position.Top - 2); Console.Write($"Best Survived Time: {BEST_SURVIVED_TIME}s\n"); Console.SetCursorPosition(0, cursor_position.Top - 1); Console.Write($"FPS: {FPS}\t\t\t"); Console.SetCursorPosition((int)(CANVAS_WIDTH / 2), cursor_position.Top - 1); Console.Write($"Lives Left: {PLAYER_TILE.HP}\n"); break; default: break; } } } // // Vectorial structs private struct Vector2 { // Position variables private float x; public float X { get { return x; } set { x = value; } } private float y; public float Y { get { return y; } set { y = value; } } // -- // Math operator logic implementation public static bool operator ==(Vector2 A, Vector2 B) { return (A.X == B.X && A.Y == B.Y); } public static bool operator !=(Vector2 A, Vector2 B) { return (A.X != B.X && A.Y != B.Y); } public static Vector2 operator +(Vector2 A, Vector2 B) { return new Vector2( A.X + B.X, A.Y + B.Y ); } public static Vector2 operator -(Vector2 A, Vector2 B) { return new Vector2( A.X - B.X, A.Y - B.Y ); } public static Vector2 operator *(Vector2 A, float B) { return new Vector2( A.X * B, A.Y * B ); } // -- // Rounding for position checks public Vector2 GetRounded() { return new Vector2((int)this.X, (int)this.Y); } // -- // Console.Write override public override string ToString() { return $"x:{this.X};y:{this.Y}"; } // -- public Vector2(float x, float y) { this.x = 0; this.y = 0; this.X = x; this.Y = y; } } // // ----- // // // TILES // // // ----- // // private enum EGenerationDirection { Top, Bottom } private static class TileGenerator { private static Vector2 SpawnOrigin = new Vector2(CANVAS_WIDTH + 1, 0); // generate pillar // from : top/bottom // height : int // length : int public static void GeneratePillar(EGenerationDirection From, int Height, int Width, int XOffset) { Vector2 AdjustedOrigin = SpawnOrigin + new Vector2(XOffset, 0); if (From == EGenerationDirection.Top) { AdjustedOrigin.Y = 0; } else if (From == EGenerationDirection.Bottom) { AdjustedOrigin.Y = CANVAS_HEIGHT - Height; } for (int i = 0; i < Height; i++) { for (int j = 0; j < Width; j++) { GroundTile ground_tile = new GroundTile(AdjustedOrigin + new Vector2(j, i)); Tiles.Add(ground_tile); } } } // generate tunnel // height : int // length : int public static void GenerateTunnel(int Height, int Width, int XOffset) { TileGenerator.GeneratePillar(EGenerationDirection.Top, Height, Width, XOffset); TileGenerator.GeneratePillar(EGenerationDirection.Bottom, Height, Width, XOffset); } public static void GenerateTunnel(int TopHeight, int TopWidth, int BottomHeight, int BottomWidth, int XOffset) { TileGenerator.GeneratePillar(EGenerationDirection.Top, TopHeight, TopWidth, XOffset); TileGenerator.GeneratePillar(EGenerationDirection.Bottom, BottomHeight, BottomWidth, XOffset); } public static void GenerateHostile(Vector2 Offset) { HostileTile ability_tile = new HostileTile(SpawnOrigin + Offset); Tiles.Add(ability_tile); } public static void GenerateAbility(Vector2 Offset) { AbilityTile ability_tile = new AbilityTile(SpawnOrigin + Offset); Tiles.Add(ability_tile); } } private class TileGenerationAlgorithm { private bool CanGenerate() { for (int y = 0; y < CANVAS_HEIGHT; y++) { Tile tile = Tile.GetTileAtPosition(new Vector2(CANVAS_WIDTH, y), false); if (tile is Tile) { if (tile.Anchored == true) continue; return false; } } return true; } public void Update(float DeltaTime) { if (this.CanGenerate()) { int random_platform = RANDOM.Next(2); switch (random_platform) { case 0: // generate a pillar leaving a gap of atleast 4 spaces int random_direction = RANDOM.Next(2); int random_height = RANDOM.Next(CANVAS_HEIGHT - 5); int random_width = RANDOM.Next(6) + 3; EGenerationDirection generation_direction = EGenerationDirection.Bottom; if (random_direction == 0) generation_direction = EGenerationDirection.Top; TileGenerator.GeneratePillar(generation_direction, random_height, random_width, 0); break; case 1: // generate a tunnel, leavign a gap of atleast 4 spaces break; default: break; } } } } private abstract class Tile { public char Symbol = ' '; public Vector2 Position; public Vector2 Velocity = new Vector2(0, 0); public bool Destroyed = false; public bool Anchored = false; // Ground tile only, it's here for access in the game scene public static Tile GetTileAtPosition(Vector2 Position, bool OnlyGetPlayerTile) { Tile return_tile = null; Position.X = (int)Position.X; Position.Y = (int)Position.Y; foreach (Tile tile in Tiles) { if ((int)tile.Position.X == Position.X && (int)tile.Position.Y == Position.Y) { if (OnlyGetPlayerTile) if (tile is PlayerTile player_tile) return_tile = tile; else continue; return_tile = tile; break; } } return return_tile; } // Update functions public abstract void UpdateInternalLogic(float DeltaTime); // Default logic for non-player tiles public virtual void UpdateLogic(ConsoleKeyInfo CKeyInfo) { if (this.Position.GetRounded() == PLAYER_POSITION.GetRounded()) { PLAYER_TILE.HP -= 1; this.Destroyed = true; } } } private class GroundTile : Tile { public GroundTile(Vector2 Position) { this.Symbol = '█'; this.Position = Position; this.Velocity.X = -TILE_SPEED; } public override void UpdateInternalLogic(float DeltaTime) { if (Anchored == false) { this.Position += this.Velocity * DeltaTime; if (this.Position.X < 0) this.Destroyed = true; } } } private class HostileTile : Tile { public HostileTile(Vector2 Position) { this.Symbol = '░'; this.Position = Position; this.Velocity.X = -TILE_SPEED; } public override void UpdateInternalLogic(float DeltaTime) { this.Position += this.Velocity * DeltaTime; if (this.Position.X < 0) this.Destroyed = true; } } private class AbilityTile : Tile { public AbilityTile(Vector2 Position) { this.Symbol = '?'; this.Position = Position; this.Velocity.X = -TILE_SPEED; } public void CollectAbility() { ActiveScene = EActiveScene.AbilityScene; } public override void UpdateInternalLogic(float DeltaTime) { this.Position += this.Velocity * DeltaTime; if (this.Position.X < 0) this.Destroyed = true; } public override void UpdateLogic(ConsoleKeyInfo CKeyInfo) { if (this.Position.GetRounded() == PLAYER_POSITION.GetRounded()) { PLAYER_TILE.MakeInvincible(2); this.CollectAbility(); this.Destroyed = true; } } } private class PlayerTile : Tile { // Invincibility public bool Invincible = false; private float invincibility_timer = 0; private float blinking_timer = 0; private int hp; public int HP { get { return hp; } set { if (this.Invincible) if (value < this.hp) return; if (value <= 0) { this.hp = 0; ActiveScene = EActiveScene.GameOverScene; } else { // Make the player invincible if a life is lost if (value < this.hp) this.MakeInvincible(2); this.hp = value; } } } public PlayerTile(Vector2 Position) { this.HP = 3; this.Position = Position; this.Velocity = new Vector2(0, 0); this.Symbol = '#'; } public void MakeInvincible(float InvincibilityDuration) { this.invincibility_timer = InvincibilityDuration; this.blinking_timer = 0f; } // Updates public override void UpdateInternalLogic(float DeltaTime) { // Invincibility logic invincibility_timer -= DeltaTime; blinking_timer -= DeltaTime; if (invincibility_timer > 0) { this.Invincible = true; if (blinking_timer < 0) { blinking_timer = 0.1f; if (this.Symbol == '@') this.Symbol = '.'; else this.Symbol = '@'; } } else { Invincible = false; this.Symbol = '#'; } // Physics calculations Vector2 future_position = this.Position + this.Velocity * DeltaTime; Vector2 march_position = new Vector2(this.Position.X, this.Position.Y); float march_direction = future_position.Y > this.Position.Y ? 1 : -1; int march_magnitude = Math.Abs((int)future_position.Y - (int)this.Position.Y); bool collided = false; // Checks tile at every Y position between this and the future position // IF the tile is ground then the player gets stopped // IF the tile is ground and the player is invincible and the tile is not anchored it get's destroyed for (int i = 0; i < march_magnitude; i++) { march_position.Y += march_direction; Tile tile = Tile.GetTileAtPosition(march_position, false); if (tile != null) { if (tile is GroundTile ground_tile) { if (this.Invincible && ground_tile.Anchored == false) ground_tile.Destroyed = true; else { march_position.Y -= march_direction; collided = true; this.Position = march_position; this.Velocity.Y = 0; } } break; } } if (collided == false) this.Position += this.Velocity * DeltaTime; this.Velocity.Y += GRAVITY * (DeltaTime * 2); PLAYER_POSITION = this.Position; if (Math.Abs(this.Velocity.Y) > MAX_PLAYER_Y_VELOCITY) { this.Velocity.Y = MAX_PLAYER_Y_VELOCITY; } } public override void UpdateLogic(ConsoleKeyInfo CKey) { // Keyboard controls switch (CKey.Key) { case ConsoleKey.D1: Ability1.UseAbility(); break; case ConsoleKey.D2: Ability2.UseAbility(); break; case ConsoleKey.Spacebar: this.Velocity.Y = -26; break; case ConsoleKey.S: this.Velocity.Y = -2; break; default: break; } } } // // --------- // // // ABILITIES // // // --------- // // private class Ability { protected float cooldown_timer = 0f; public float Cooldown = 0f; public string Name = "None"; public string Description = ""; public virtual void UseAbility() { } public void InternalLogicUpdate(float DeltaTime) { this.cooldown_timer -= DeltaTime; if (this.cooldown_timer < 0) this.cooldown_timer = 0; } public float GetCooldown() { return (float)Math.Floor((float)this.cooldown_timer * 10) / 10; } } private class PhoenixAbility : Ability { public PhoenixAbility() { this.Name = "Phoenix"; this.Description = "Gain a life and 2 seconds of invincibility"; this.Cooldown = 60; } public override void UseAbility() { if (cooldown_timer <= 0) { PLAYER_TILE.HP += 1; PLAYER_TILE.MakeInvincible(2); cooldown_timer = Cooldown; } } } // // ---------- // // // Game logic // // // ---------- // // public static void Main() { Stopwatch game_timer = new Stopwatch(); List destroyed_tiles = new List(); long previous_time = 0; long current_time = 0; float delta_time = 0; Console.CursorVisible = false; game_timer.Start(); while (true) { // Delta time calculation current_time = game_timer.ElapsedMilliseconds; delta_time = (float)(current_time - previous_time) / 1000; previous_time = current_time; // -- // Game scene logic switch (ActiveScene) { case EActiveScene.StartScene: ActiveScene = EActiveScene.GameScene; // Reset dynamic variables and update best survived time BEST_SURVIVED_TIME = BEST_SURVIVED_TIME < SURVIVED_TIME ? SURVIVED_TIME : BEST_SURVIVED_TIME; SURVIVED_TIME = 0; FPS = 0; TGAlgorithm = new TileGenerationAlgorithm(); // Clearing all tiles and re-creating player tile Tiles.Clear(); PLAYER_TILE = new PlayerTile( new Vector2(10, CANVAS_HEIGHT / 3) ); Tiles.Add(PLAYER_TILE); // Game custimization Console.CursorVisible = true; bool customizing = true; while (customizing) { Console.Clear(); bool selecting_lives = true; while (selecting_lives) { Console.Write("(1) - 1 life\n(2) - 3 lives\n(3) - 10 lives\n(4) - 100 lives\nSelect the amount HP you will have"); try { switch (Console.ReadKey(true).Key) { case ConsoleKey.D1: PLAYER_TILE.HP = 1; break; case ConsoleKey.D2: PLAYER_TILE.HP = 3; break; case ConsoleKey.D3: PLAYER_TILE.HP = 10; break; case ConsoleKey.D4: PLAYER_TILE.HP = 100; break; default: throw new CannotUnloadAppDomainException(); } selecting_lives = false; } catch { Console.WriteLine("\nInvalid input"); } } customizing = false; Console.Clear(); Console.WriteLine("CONTROLS\n Spacebar - Jump\n S - Quick fall\n 1 - First ability\n 2 - Second ability\n\n"); Console.WriteLine("GAMEPLAY\n You are #\n Your goal is to survive as long as possible\n Crashing into a █ tile from the side or ░ tile will take away a life and make you invincible for 2 seconds\n You can stand and bump your head on █ (ground) tiles\n If you crash into a ? tile you will gain an ability"); Console.WriteLine("\nPress any key to continue..."); Console.ReadKey(true); } Console.CursorVisible = false; // Creating floor and ceiling tiles for (int j = 0; j < 2; j++) { for (int i = 0; i < CANVAS_WIDTH; i++) { GroundTile ground_tile = new GroundTile(new Vector2(i, j * (CANVAS_HEIGHT - 1))); ground_tile.Anchored = true; Tiles.Add( ground_tile ); } } // -- Console.Clear(); game_timer.Restart(); previous_time = 0; current_time = 0; delta_time = 0; break; case EActiveScene.GameScene: // Tile updates TGAlgorithm.Update(delta_time); // The player tile get's updated first for consistancy PLAYER_TILE.UpdateInternalLogic(delta_time); if (Console.KeyAvailable) PLAYER_TILE.UpdateLogic(Console.ReadKey(true)); foreach (Tile tile in Tiles) { if (tile == PLAYER_TILE) continue; if (tile.Anchored == false) tile.UpdateInternalLogic(delta_time); tile.UpdateLogic(new ConsoleKeyInfo()); if (tile.Destroyed) destroyed_tiles.Add(tile); } foreach (Tile tile in destroyed_tiles) Tiles.Remove(tile); destroyed_tiles.Clear(); // -- // UI Update SURVIVED_TIME = (float)Math.Floor((float)game_timer.ElapsedMilliseconds / 10) / 100; FPS = (int)Math.Floor(1000 / (1000 * delta_time)); Ability1.InternalLogicUpdate(delta_time); Ability2.InternalLogicUpdate(delta_time); // -- break; case EActiveScene.GameOverScene: ActiveScene = EActiveScene.StartScene; // Buffer time incase the user is spamming long target_time = game_timer.ElapsedMilliseconds + 777; while (game_timer.ElapsedMilliseconds < target_time) { Thread.Sleep(1); if (Console.KeyAvailable) Console.ReadKey(true); } Console.Clear(); Console.WriteLine($"END OF LIFE STATS\n Survived: {SURVIVED_TIME}s\n Best time: {BEST_SURVIVED_TIME}s"); if (SURVIVED_TIME > BEST_SURVIVED_TIME) { Console.Write($" NEW RECORD!!! You beat your previous best by {Math.Floor((SURVIVED_TIME - BEST_SURVIVED_TIME) * 1000) / 1000}s!\n"); } Console.Write("\nPress any key to continue..."); Console.ReadKey(true); break; case EActiveScene.AbilityScene: ActiveScene = EActiveScene.GameScene; game_timer.Stop(); // Generates a random ability and gives the player a choice wether to keep it or not int random_number = new Random().Next(1); ConsoleKey selected_ability; Ability generated_ability; switch (random_number) { case 0: generated_ability = new PhoenixAbility(); break; default: generated_ability = new Ability(); break; } Console.Clear(); string line_seperator = "-"; string space_seperator = ""; string description_line_seperator = "-"; string description_space_seperator = " "; for (int i = 0; i < generated_ability.Name.Length; i++) { line_seperator += "-"; space_seperator += " "; } for (int i = 0; i < generated_ability.Description.Length; i++) { description_line_seperator += "-"; description_space_seperator += " "; } Console.Write($"{space_seperator} {description_space_seperator}\n{generated_ability.Name} |------| "); Console.Write($"{generated_ability.Description}\n{space_seperator} {description_space_seperator}\n"); Console.Write($"{space_seperator} Type:\n{space_seperator}{space_seperator}(1) - To replace your first ability '{Ability1.Name}' with {generated_ability.Name}\n{space_seperator}{space_seperator}(2) - To replace your second ability '{Ability2.Name}' with {generated_ability.Name}\n{space_seperator}{space_seperator}(d) - To ignore the ability\n"); bool selecting_ability = true; while (selecting_ability) { selected_ability = Console.ReadKey(true).Key; if (selected_ability == ConsoleKey.D1) { Ability1 = generated_ability; selecting_ability = false; } else if (selected_ability == ConsoleKey.D2) { Ability2 = generated_ability; selecting_ability = false; } else if (selected_ability == ConsoleKey.D) { selecting_ability = false; } } Console.Clear(); game_timer.Start(); break; default: break; } Canvas.Render(); //Console.Write(delta_time); Thread.Sleep(1); } } } }