// META DATI /* Autors: Adrians Zemturis Grupa: 110 Projekts: Flappy Blyord Apraksts: Flappy Blyord ir maisijums starp Flappy Bird un platofrmeriem ar spējas mehānismu un dinamiski ģenerētu mapi. Veidots: 04.05.2026 IDE: Visual Studio Community 2026 ar .NET 8.0 Noformējums: Klases un struktūras nosaukumi, klases un struktūras biedru nosaukumi, enumerātoru nosaukumi, enumerātoru biedru nosaukumi, funkcijas nosaukumi, funkcijas parametru nosaukumi lieto lielo burtu katra vārda sākumā un bez atstarpēm. Piemērs: TileGenerationAlgorithm. Globālo mainīgo nosaukumi lieto visus lielos burtus un pārmaina atstarpes pret pasvītrojumu. Piemērs: MAX_PLAYER_Y_VELOCITY. Mainīgo nosaukumi, kas definēti koda blokos lieto visus mazos burtus un pārmaina atstarpes pret pasvītrojumu. Piemērs: cursor_position. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Threading; 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 long TILE_MOVE_TIME = 75; 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(0, 0); private static List TILES = new List(); private static PlayerTile PLAYER_TILE; private static TileGenerationAlgorithm TILE_GENERATION_ALGORITHM; private static Random RANDOM = new Random(); private static Ability ABILITY_1 = new Ability(); private static Ability ABILITY_2 = new Ability(); private static EActiveScene ACTIVE_SCENE = EActiveScene.StartScene; private enum EActiveScene { GameScene, StartScene, AbilityScene, GameOverScene } // Rendering private static class Canvas { public static char[] RenderBuffer = new char[(CANVAS_WIDTH * CANVAS_HEIGHT) + CANVAS_WIDTH + CANVAS_HEIGHT]; public static void Render() { if (ACTIVE_SCENE != EActiveScene.GameScene) return; /* Rendering is done by firstly setting all the arrays values to a space. Then, secondly, mapping every tile's draw symbol to the RenderBuffer based on the tiles position. Then, thirdly, looping through the RenderBuffer and appending the RenderBuffer's character to a StringBuilder class to prevent constant string duplication every time a new value is appended. Then, fourthly, some free space is added for the UI element's so there arent any left over characters if the UI elements lengrh changes, because the screen isn't being cleared with the Console.Clear() method. Then, finally, the StringBuilder is printed, and the UI elements are drawn. */ // Clearing the RenderBuffer by changing all it's values to a space Console.SetCursorPosition(0, 0); for (int i = 0; i < RenderBuffer.Length; i++) RenderBuffer[i] = ' '; // Every tiles draw symbol is mapped to the RenderBuffer by first rounding the float X and float Y values to ints, then checking if they're out of bounds, then assigning them to the RenderBuffer via a math formula foreach (Tile tile in TILES) { int x = (int)tile.Position.X; int y = (int)tile.Position.Y; if (x >= CANVAS_WIDTH || x < 0) continue; if (y >= CANVAS_HEIGHT || y < 0) continue; RenderBuffer[y * CANVAS_WIDTH + x] = tile.Symbol; } // The same process applies to the player minus the bounds check because the player is never gonna be there. The player is rendered after all the tiles are rendered so the player is always on top. foreach (Tile tile in TILES) { if (tile is PlayerTile player_tile) { int x = (int)player_tile.Position.X; int y = (int)player_tile.Position.Y; RenderBuffer[y * CANVAS_WIDTH + x] = player_tile.Symbol; } } StringBuilder render_string = new StringBuilder(CANVAS_HEIGHT * CANVAS_WIDTH); // The draw symbols from the RenderBuffer are added to the render string via the math formula 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"); } // 3 rows of empty space is added to the render string to clear the UI elements render_string.Append('\n'); for (int y = 0; y < 3; y++) { for (int x = 0; x < CANVAS_WIDTH; x++) { render_string.Append(' '); } } render_string.Append('\n'); Console.Write(render_string); // Rendering UI (int Top, int Left) cursor_position = (Console.CursorTop, Console.CursorLeft); Console.SetCursorPosition(0, cursor_position.Top - 3); Console.Write($"Ability 1 - {ABILITY_1.Name} {ABILITY_1.GetCooldown()}s"); Console.SetCursorPosition((int)(CANVAS_WIDTH / 2), cursor_position.Top - 3); Console.Write($"Ability 2 - {ABILITY_2.Name} {ABILITY_2.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"); } } private struct Vector2 { private float x; public float X { get { return x; } set { x = value; } } private float y; public float Y { get { return y; } set { y = value; } } // Custom math operator definitions 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 ); } public Vector2 GetRounded() { return new Vector2((int)this.X, (int)this.Y); } public override string ToString() { return $"x:{this.X};y:{this.Y}"; } public Vector2(float x, float y) { 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); /* Pillars are generated by looping through the pillars height and width and creating a new tile at that position + XOffset + SpawnOrigin (which is top right of the canvas). The pillars are generated from TOP to BOTTOM, so if the generation direction is TOP nothing need to be done, but if the generation direction is bottom, then the Origins Y position is set to the CANVAS_HEIGHT - pillar height, so the pillars can generate on the bottom. */ public static void GeneratePillar(EGenerationDirection From, int Height, int Width, int XOffset) { Vector2 adjusted_origin = SpawnOrigin + new Vector2(XOffset, 0); if (From == EGenerationDirection.Bottom) { adjusted_origin.Y = CANVAS_HEIGHT - Height; } for (int y = 0; y < Height; y++) { for (int x = 0; x < Width; x++) { GroundTile ground_tile = new GroundTile(adjusted_origin + new Vector2(x, y)); TILES.Add(ground_tile); } } } 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); } } // -- NOTE! /* The algorithms algorithm part was made by ChatGPT. Also the algorithm can generate an impossible pillar combinations, but it's fine because the player has abilities. */ private class TileGenerationAlgorithm { private const int MIN_GAP = 6; // Checks every Y position for a tile at the canvases right side, if there is an anchored tile then continue searching, otherwise if there is a tile, then return false, else return true private bool CanGenerate() { for (int y = 0; y < CANVAS_HEIGHT; y++) { Tile tile = Tile.GetTileAtPosition(new Vector2(CANVAS_WIDTH + 1, y)); if (tile != null) { if (tile.Anchored) continue; return false; } } return true; } public void Update() { if (CanGenerate() == false) return; int safe_top; int safe_bottom; int width = RANDOM.Next(3, 8); switch (RANDOM.Next(2)) { // PILLAR case 0: { bool from_top = RANDOM.Next(2) == 0; int height = RANDOM.Next(4, CANVAS_HEIGHT - MIN_GAP - 2); if (from_top) { TileGenerator.GeneratePillar( EGenerationDirection.Top, height, width, 0 ); safe_top = height; safe_bottom = CANVAS_HEIGHT - 2; } else { TileGenerator.GeneratePillar( EGenerationDirection.Bottom, height, width, 0 ); safe_top = 1; safe_bottom = CANVAS_HEIGHT - height; } break; } // TUNNEL default: { int top_height = RANDOM.Next(2, CANVAS_HEIGHT / 3); int bottom_height = RANDOM.Next(2, CANVAS_HEIGHT / 3); // Ensure minimum gap while (CANVAS_HEIGHT - top_height - bottom_height < MIN_GAP) { if (top_height > bottom_height) top_height--; else bottom_height--; } TileGenerator.GenerateTunnel( top_height, width, bottom_height, width, 0 ); safe_top = top_height; safe_bottom = CANVAS_HEIGHT - bottom_height; break; } } // // Hostile generation // // ~35% chance if (RANDOM.NextDouble() < 0.35) { int hostile_y = RANDOM.Next( safe_top + 1, safe_bottom - 1 ); int hostile_x = RANDOM.Next(width + 2, width + 10); TileGenerator.GenerateHostile( new Vector2(hostile_x, hostile_y) ); } // // Ability generation // // ~4% chance if (RANDOM.NextDouble() < 0.04) { int ability_y = RANDOM.Next( safe_top + 1, safe_bottom - 1 ); int ability_x = RANDOM.Next(width + 3, width + 8); TileGenerator.GenerateAbility( new Vector2(ability_x, ability_y) ); } } } /* The tile class is the base class for the GroundTile, PlayerTile, AbilityTile and HostileTile. */ private abstract class Tile { public char Symbol = ' '; public Vector2 Position; public bool Destroyed = false; public bool Anchored = false; // Ground tile only, it's here for access in the game scene without having to check if it's a ground tile. This variable is used to mark floor or ceiling tiles so they don't get destroyed. /* The GetTileAtPosition functions searches through all the tiles, comparing their rounded positions to the parameters rounded position, if they match then return the tile, otherwise return null. */ public static Tile GetTileAtPosition(Vector2 Position) { 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) { return_tile = tile; break; } } return return_tile; } // Default internal logic for non-player tiles public virtual void UpdateInternalLogic(float DeltaTime) { this.Position += new Vector2(-1, 0); if (this.Position.X < 0) this.Destroyed = true; } // Default update logic for non-player tiles public virtual void UpdateLogic(ConsoleKeyInfo CKeyInfo) { // If the player is inside of the tile, then take away a life and destroy the tile 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; } public override void UpdateInternalLogic(float DeltaTime) { if (Anchored == false) { this.Position += new Vector2(-1, 0); if (this.Position.X < 0) this.Destroyed = true; } } } private class HostileTile : Tile { public HostileTile(Vector2 Position) { this.Symbol = '░'; this.Position = Position; } } private class AbilityTile : Tile { public AbilityTile(Vector2 Position) { this.Symbol = '?'; this.Position = Position; } public void CollectAbility() { ACTIVE_SCENE = EActiveScene.AbilityScene; } 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; // If this timer is above 0, then the Invincible variable is set to true private float blinking_timer = 0; // This timer is responsible for changing the player draw symbol to indicate that the player is invincible public Vector2 Velocity = new Vector2(0, 0); private int hp; public int HP { get { return hp; } set { /* If the player is invincible and the player is not getting more hp, then ignore the value change */ if (this.Invincible) if (value < this.hp) return; /* If the player's going to die, then set the ACTIVE_SCENE to the GameOverScreen */ if (value <= 0) { this.hp = 0; ACTIVE_SCENE = EActiveScene.GameOverScene; } /* Otherwise, if the player loses a life, then make the player invincible for 2 seconds */ 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; } // Internal logic update public override void UpdateInternalLogic(float DeltaTime) { /* If the invincibility_timer is above 0, then set Invincible to true, and constantly change the players draw symbol to indicate that the player is invincible, otherwise set Invincible to false, and reset the players draw symbol. */ 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 the player is going to be at. // 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 then it get's destroyed. for (int i = 0; i < march_magnitude; i++) { march_position.Y += march_direction; Tile tile = Tile.GetTileAtPosition(march_position); 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 * 1.5f); PLAYER_POSITION = this.Position; // Limit the player's max Y velocity if (Math.Abs(this.Velocity.Y) > MAX_PLAYER_Y_VELOCITY) { this.Velocity.Y = MAX_PLAYER_Y_VELOCITY; } } public override void UpdateLogic(ConsoleKeyInfo CKey) { switch (CKey.Key) { case ConsoleKey.D1: ABILITY_1.UseAbility(); break; case ConsoleKey.D2: ABILITY_2.UseAbility(); break; case ConsoleKey.Spacebar: this.Velocity.Y = -22; // Velocity upwards break; case ConsoleKey.S: this.Velocity.Y = 10; // Velocity downwards break; default: break; } } } // // --------- // // // ABILITIES // // // --------- // // private class Ability { public float cooldown_timer = 0f; // Variable would have been a protected variable if not for the "ONLY 2 modifiers" restriction public float Cooldown = 0f; // The amount of seconds between each use of an ability // The name and description are used in the AbilityScene for printing public string Name = "None"; public string Description = ""; public virtual void UseAbility() { } // Custom ability implementation 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; // Round the cooldown to a 0.xx decimal } } 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; long last_tile_move_time = 0; bool update_tile_positions = false; 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; // -- // If the last time all the tiles (except the player) moved is above TILE_MOVE_TIME, then set update_tile_positions to true update_tile_positions = false; if (game_timer.ElapsedMilliseconds - last_tile_move_time >= 0) { last_tile_move_time = game_timer.ElapsedMilliseconds + TILE_MOVE_TIME; update_tile_positions = true; } // Game scene logic switch (ACTIVE_SCENE) { case EActiveScene.StartScene: ACTIVE_SCENE = EActiveScene.GameScene; // Reset all the dynamic variables required for a new round BEST_SURVIVED_TIME = BEST_SURVIVED_TIME < SURVIVED_TIME ? SURVIVED_TIME : BEST_SURVIVED_TIME; SURVIVED_TIME = 0; FPS = 0; TILE_GENERATION_ALGORITHM = new TileGenerationAlgorithm(); last_tile_move_time = 0; // Clearing all tiles and re-creating player tile TILES.Clear(); PLAYER_TILE = new PlayerTile(new Vector2(10, CANVAS_HEIGHT / 3)); TILES.Add(PLAYER_TILE); // 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 ); } } // Player life selection screen and a quick tutorial screen 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 AccessViolationException(); // scary exception to scare the user for not following instructions } selecting_lives = false; } catch(Exception e) { Console.WriteLine($"\n{e.StackTrace}: {e.Message}"); // scary message from the scary exception to scare the user for not following instructions } } 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 a ░ tile from any side will take away a life and make you invincible for 2 seconds\n You can stand and bump your head on █ tiles\n If you crash into a ? tile you will gain an ability and become invincible for 2 seconds\n\n"); Console.WriteLine("NOTES\n Spamming quick fall will cause you to fall slower than letting gravity take over\n Impossible to survive map generation is possible without an extra life or an ability"); Console.WriteLine("\nPress any key to continue..."); Console.ReadKey(true); } Console.CursorVisible = false; // -- Console.Clear(); game_timer.Restart(); previous_time = 0; current_time = 0; delta_time = 0; break; case EActiveScene.GameScene: // Tile updates TILE_GENERATION_ALGORITHM.Update(); // Player tile updates PLAYER_TILE.UpdateInternalLogic(delta_time); if (Console.KeyAvailable) PLAYER_TILE.UpdateLogic(Console.ReadKey(true)); // Tile updates foreach (Tile tile in TILES) { if (tile == PLAYER_TILE) continue; if (tile.Anchored == false && update_tile_positions) // Only update internal logic if the tile is not a ground or ceiling tile and update_tile_positions is true 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 variable update SURVIVED_TIME = (float)Math.Floor((float)game_timer.ElapsedMilliseconds / 10) / 100; FPS = (int)Math.Floor(1000 / (1000 * delta_time)); // -- ABILITY_1.InternalLogicUpdate(delta_time); ABILITY_2.InternalLogicUpdate(delta_time); break; case EActiveScene.GameOverScene: ACTIVE_SCENE = EActiveScene.StartScene; // Buffer of 777ms incase the user is spamming when they die, so the game doesn't skip the GameOverScene. 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: ACTIVE_SCENE = EActiveScene.GameScene; game_timer.Stop(); // Generates a random ability and gives the player a choice wether to replace one of their current abilities with this one, or just skip it. 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 '{ABILITY_1.Name}' with {generated_ability.Name}\n{space_seperator}{space_seperator}(2) - To replace your second ability '{ABILITY_2.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) { ABILITY_1 = generated_ability; selecting_ability = false; } else if (selected_ability == ConsoleKey.D2) { ABILITY_2 = generated_ability; selecting_ability = false; } else if (selected_ability == ConsoleKey.D) { selecting_ability = false; } } Console.Clear(); game_timer.Start(); break; default: break; } Canvas.Render(); Thread.Sleep(1); } } } }