From 051d0233502eeb6f27bc457c6311d1f49dc8cc3c Mon Sep 17 00:00:00 2001 From: Jahg Date: Thu, 26 Oct 2023 22:17:07 -0700 Subject: [PATCH 01/24] Add Tetris --- .github/workflows/Tetris Build.yml | 20 + .vscode/launch.json | 10 + .vscode/tasks.json | 13 + Projects/Tetris/Program.cs | 768 ++++++++++++++++++++++++ Projects/Tetris/README.md | 36 ++ Projects/Tetris/Tetris.csproj | 10 + Projects/Website/.vscode/launch.json | 11 + Projects/Website/.vscode/tasks.json | 41 ++ Projects/Website/Games/Tetris/Tetris.cs | 733 ++++++++++++++++++++++ Projects/Website/Pages/Tetris.razor | 54 ++ Projects/Website/Shared/NavMenu.razor | 5 + README.md | 1 + dotnet-console-games.sln | 6 + dotnet-console-games.slnf | 3 +- 14 files changed, 1710 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/Tetris Build.yml create mode 100644 Projects/Tetris/Program.cs create mode 100644 Projects/Tetris/README.md create mode 100644 Projects/Tetris/Tetris.csproj create mode 100644 Projects/Website/.vscode/launch.json create mode 100644 Projects/Website/.vscode/tasks.json create mode 100644 Projects/Website/Games/Tetris/Tetris.cs create mode 100644 Projects/Website/Pages/Tetris.razor diff --git a/.github/workflows/Tetris Build.yml b/.github/workflows/Tetris Build.yml new file mode 100644 index 00000000..a2ced14f --- /dev/null +++ b/.github/workflows/Tetris Build.yml @@ -0,0 +1,20 @@ +name: Tetris Build +on: + push: + paths: + - 'Projects/Tetris/**' + - '!**.md' + pull_request: + paths: + - 'Projects/Tetris/**' + - '!**.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\Tetris\Tetris.csproj" --configuration Release diff --git a/.vscode/launch.json b/.vscode/launch.json index 476816fe..9d48e2c0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -492,5 +492,15 @@ "console": "externalTerminal", "stopAtEntry": false, }, + { + "name": "Tetris", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build Tetris", + "program": "${workspaceFolder}/Projects/Tetris/bin/Debug/Tetris.dll", + "cwd": "${workspaceFolder}/Projects/Tetris/bin/Debug", + "console": "externalTerminal", + "stopAtEntry": false, + }, ], } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2f312b30..8e3d42b3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -665,5 +665,18 @@ ], "problemMatcher": "$msCompile", }, + { + "label": "Build Tetris", + "command": "dotnet", + "type": "process", + "args": + [ + "build", + "${workspaceFolder}/Projects/Tetris/Tetris.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + ], + "problemMatcher": "$msCompile", + }, ], } diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs new file mode 100644 index 00000000..6c71c532 --- /dev/null +++ b/Projects/Tetris/Program.cs @@ -0,0 +1,768 @@ +using System.Diagnostics; +using System.Text; + +Console.OutputEncoding = Encoding.UTF8; +Console.CursorVisible = false; +Stopwatch Stopwatch = Stopwatch.StartNew(); + +bool DEBUGCONTROLS = false; + +string[] FIELD = new[] +{ + "╭──────────────────────────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯" +}; + +string[] NEXTTETROMINO = new[] +{ + "╭─────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰─────────╯" +}; + +string[] SCORE = new[]{ + "╭─────────╮", + "│ │", + "╰─────────╯" +}; + +string[] PAUSE = new[]{ + "█████╗ ███╗ ██╗██╗█████╗█████╗", + "██╔██║██╔██╗██║██║██╔══╝██╔══╝", + "█████║█████║██║██║ ███╗ █████╗", + "██╔══╝██╔██║██║██║ ██╗██╔══╝", + "██║ ██║██║█████║█████║█████╗", + "╚═╝ ╚═╝╚═╝╚════╝╚════╝╚════╝", +}; + +string[][] TETROMINOS = new[] +{ + new[]{ + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯" + }, + new[]{ + "╭─╮ ", + "╰─╯ ", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + " ╭─╮", + " ╰─╯", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + "╭─╮╭─╮", + "╰─╯╰─╯", + "╭─╮╭─╮", + "╰─╯╰─╯" + }, + new[]{ + " ╭─╮╭─╮", + " ╰─╯╰─╯", + "╭─╮╭─╮ ", + "╰─╯╰─╯ " + }, + new[]{ + " ╭─╮ ", + " ╰─╯ ", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + "╭─╮╭─╮ ", + "╰─╯╰─╯ ", + " ╭─╮╭─╮", + " ╰─╯╰─╯" + }, +}; + +string[] PLAYFIELD = (string[])FIELD.Clone(); +const int BORDER = 1; +int FallSpeedMilliSeconds = 1000; +bool CloseGame = false; +int Score = 0; +int PauseCount = 0; +int TextColor = 0; +GameStatus GameStatus = GameStatus.Gameover; + +Random RamdomGenerator = new(); + +int INITIALTETROMINOX = Convert.ToInt16(PLAYFIELD[0].Length / 2) - 3; +int INITIALTETROMINOY = 1; +Tetromino TETROMINO = new() +{ + Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + X = INITIALTETROMINOX, + Y = INITIALTETROMINOY +}; + +char[][]? LastFrame = null; + +AutoResetEvent AutoEvent = new AutoResetEvent(false); +Timer? FallTimer = null; +GameStatus = GameStatus.Playing; + +Console.WriteLine(); +Console.WriteLine(" ██████╗█████╗██████╗█████╗ ██╗█████╗"); +Console.WriteLine(" ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝"); +Console.WriteLine(" ██║ █████╗ ██║ █████╔╝██║ ███╗ "); +Console.WriteLine(" ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗"); +Console.WriteLine(" ██║ █████╗ ██║ ██║ ██║██║█████║"); +Console.WriteLine(" ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝"); + +Console.WriteLine(); +Console.WriteLine(" Controls:"); +Console.WriteLine(" WASD or ARROW to move"); +Console.WriteLine(" Q or E to spin left or right"); +Console.WriteLine(" P to paused the game, press enter"); +Console.WriteLine(" key to resume"); +Console.WriteLine(" R to change Text color"); +Console.WriteLine(); +Console.WriteLine(" Press escape to close the game at any time."); +Console.WriteLine(); +Console.Write(" Press enter to start tetris..."); +Console.CursorVisible = false; +StartGame(); +Console.Clear(); + +FallTimer = new Timer(TetrominoFall, AutoEvent, 1000, FallSpeedMilliSeconds); + +while (!CloseGame) +{ + if (CloseGame) + { + break; + } + + PlayerControl(); + if (GameStatus == GameStatus.Playing) + { + DrawFrame(); + SleepAfterRender(); + } +} + +void PlayerControl() +{ + while (Console.KeyAvailable && GameStatus == GameStatus.Playing) + { + switch (Console.ReadKey(true).Key) + { + case ConsoleKey.A or ConsoleKey.LeftArrow: + if (Collision(Direction.Left)) break; + TETROMINO.X -= 3; + break; + case ConsoleKey.D or ConsoleKey.RightArrow: + if (Collision(Direction.Right)) break; + TETROMINO.X += 3; + break; + case ConsoleKey.S or ConsoleKey.DownArrow: + FallTimer.Change(0, FallSpeedMilliSeconds); + break; + case ConsoleKey.E: + TetrominoSpin(Direction.Right); + break; + case ConsoleKey.Q: + TetrominoSpin(Direction.Left); + break; + case ConsoleKey.P: + PauseGame(); + break; + case ConsoleKey.R: + TextColor++; + if (TextColor == 16) TextColor = 1; + Console.ForegroundColor = (ConsoleColor)TextColor; + break; + + //DEBUG + case ConsoleKey.Spacebar: + if (!DEBUGCONTROLS) return; + PLAYFIELD = (string[])FIELD.Clone(); + break; + case ConsoleKey.I: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[0]; + break; + case ConsoleKey.J: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[1]; + break; + case ConsoleKey.L: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[2]; + break; + case ConsoleKey.O: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[3]; + break; + case ConsoleKey.C: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[4]; + break; + case ConsoleKey.T: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[5]; + break; + case ConsoleKey.Z: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[6]; + break; + case ConsoleKey.X: + if (!DEBUGCONTROLS) return; + Score += 10; + break; + } + } +} + +void DrawFrame() +{ + bool collision = false; + int yScope = TETROMINO.Y; + string[] shapeScope = TETROMINO.Shape; + char[][] frame = new char[PLAYFIELD.Length][]; + + //Field + for (int y = 0; y < PLAYFIELD.Length; y++) + { + frame[y] = PLAYFIELD[y].ToCharArray(); + } + + //Draw Tetromino + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = TETROMINO.X + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = charTetromino; + } + } + + //Save Frame + if (collision && LastFrame != null) frame = LastFrame; + LastFrame = (char[][])frame.Clone(); + for (int y = 0; y < LastFrame.Length; y++) + { + LastFrame[y] = (char[])frame[y].Clone(); + } + + //Draw Preview + for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) + { + if (CollisionPreview(yField, yScope, shapeScope)) continue; + + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yField + y; + + if (yScope + shapeScope.Length > tY) continue; + + int tX = TETROMINO.X + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = '•'; + } + } + + break; + } + + //Next Square + for (int y = 0; y < NEXTTETROMINO.Length; y++) + { + frame[y] = frame[y].Concat(NEXTTETROMINO[y]).ToArray(); + } + + //Score Square + for (int y = 0; y < SCORE.Length; y++) + { + int sY = NEXTTETROMINO.Length + y; + frame[sY] = frame[sY].Concat(SCORE[y]).ToArray(); + } + + //Draw Next + for (int y = 0; y < TETROMINO.Next.Length; y++) + { + for (int x = 0; x < TETROMINO.Next[y].Length; x++) + { + int tY = y + BORDER; + int tX = PLAYFIELD[y].Length + x + BORDER; + char charTetromino = TETROMINO.Next[y][x]; + frame[tY][tX] = charTetromino; + } + } + + //Draw Score + char[] score = Score.ToString().ToCharArray(); + for (int scoreX = score.Length - 1; scoreX >= 0; scoreX--) + { + int sY = NEXTTETROMINO.Length + BORDER; + int sX = frame[sY].Length - (score.Length - scoreX) - BORDER; + frame[sY][sX] = score[scoreX]; + } + + //Draw Pause + if (GameStatus == GameStatus.Paused) + { + for (int y = 0; y < PAUSE.Length; y++) + { + int fY = (PLAYFIELD.Length / 2) + y - PAUSE.Length; + for (int x = 0; x < PAUSE[y].Length; x++) + { + int fX = x + BORDER; + + if (x >= PLAYFIELD[fY].Length) break; + + frame[fY][fX] = PAUSE[y][x]; + } + } + } + + //Create Render + StringBuilder render = new(); + for (int y = 0; y < frame.Length; y++) + { + render.AppendLine(new string(frame[y])); + } + + Console.Clear(); + Console.Write(render); + Console.CursorVisible = false; +} + +bool Collision(Direction direction) +{ + int xNew = TETROMINO.X; + bool collision = false; + + switch (direction) + { + case Direction.Right: + xNew += 3; + if (xNew + TETROMINO.Shape[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; + break; + case Direction.Left: + xNew -= 3; + if (xNew < BORDER) collision = true; + break; + case Direction.None: + break; + } + + if (collision) return collision; + + for (int y = 0; y < TETROMINO.Shape.Length && !collision; y++) + { + for (int x = 0; x < TETROMINO.Shape[y].Length; x++) + { + int tY = TETROMINO.Y + y; + int tX = xNew + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = TETROMINO.Shape[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + } + } + + return collision; +} + +bool CollisionPreview(int initY, int yScope, string[] shape) +{ + int xNew = TETROMINO.X; + + for (int yUpper = initY; yUpper >= yScope; yUpper -= 2) + { + for (int y = shape.Length - 1; y >= 0; y -= 2) + { + for (int x = 0; x < shape[y].Length; x++) + { + int tY = yUpper + y; + int tX = xNew + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shape[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + return true; + } + } + } + } + + return false; +} + +void Gameover() +{ + GameStatus = GameStatus.Gameover; + AutoEvent.Dispose(); + FallTimer.Dispose(); + + SleepAfterRender(); + + Console.Clear(); + Console.WriteLine(); + Console.WriteLine(" ██████╗ █████╗ ██ ██╗█████╗"); + Console.WriteLine(" ██╔════╝ ██╔══██╗███ ███║██╔══╝"); + Console.WriteLine(" ██║ ███╗███████║██╔██═██║█████╗"); + Console.WriteLine(" ██║ ██║██╔══██║██║ ██║██╔══╝"); + Console.WriteLine(" ╚██████╔╝██║ ██║██║ ██║█████╗"); + Console.WriteLine(" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝"); + Console.WriteLine(" ██████╗██╗ ██╗█████╗█████╗ "); + Console.WriteLine(" ██ ██║██║ ██║██╔══╝██╔═██╗ "); + Console.WriteLine(" ██ ██║██║ ██║█████╗█████╔╝ "); + Console.WriteLine(" ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ "); + Console.WriteLine(" ██████║ ╚███╔╝ █████╗██║ ██║ "); + Console.WriteLine(" ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ "); + + Console.WriteLine(); + Console.WriteLine($" Final Score: {Score}"); + Console.WriteLine($" Pause Count: {PauseCount}"); + Console.WriteLine(); + Console.WriteLine(" Press enter to play again"); + Console.WriteLine(" Press escape to close the game"); + Console.CursorVisible = false; + Console.ReadKey(); + StartGame(); + RestartGame(); +} + +void StartGame(ConsoleKey key = ConsoleKey.Enter) +{ + ConsoleKey input = default; + while (input != key && !CloseGame) + { + input = Console.ReadKey(true).Key; + if (input is ConsoleKey.Escape) + { + CloseGame = true; + return; + } + } +} + +void RestartGame() +{ + PLAYFIELD = (string[])FIELD.Clone(); + FallSpeedMilliSeconds = 1000; + Score = 0; + TETROMINO = new() + { + Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + X = INITIALTETROMINOX, + Y = INITIALTETROMINOY + }; + + LastFrame = null; + AutoEvent = new AutoResetEvent(false); + FallTimer = new Timer(TetrominoFall, AutoEvent, 1000, FallSpeedMilliSeconds); + GameStatus = GameStatus.Playing; +} + +void PauseGame() +{ + PauseCount++; + FallTimer.Change(Timeout.Infinite, Timeout.Infinite); + GameStatus = GameStatus.Paused; + DrawFrame(); + + ResumeGame(); +} + +void ResumeGame(ConsoleKey key = ConsoleKey.Enter) +{ + ConsoleKey input = default; + while (input != key && !CloseGame) + { + input = Console.ReadKey(true).Key; + if (input is ConsoleKey.Enter && GameStatus == GameStatus.Paused && FallTimer != null) + { + FallTimer.Change(0, FallSpeedMilliSeconds); + GameStatus = GameStatus.Playing; + return; + } + } +} + +void TetrominoFall(object? e) +{ + if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) TETROMINO.Y = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; + else TETROMINO.Y += 2; + + //Y Collision + for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) + { + for (int yCollision = TETROMINO.Shape.Length - 1; yCollision >= 0; yCollision -= 2) + { + char exist = TETROMINO.Shape[yCollision][xCollision]; + + if (exist == ' ') continue; + + char[] lineYC = PLAYFIELD[TETROMINO.Y + yCollision - 1].ToCharArray(); + + if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; + + if + ( + lineYC[TETROMINO.X + xCollision] != ' ' && + lineYC[TETROMINO.X + xCollision] != '│' && + LastFrame != null + ) + { + for (int y = 0; y < LastFrame.Length; y++) + { + PLAYFIELD[y] = new string(LastFrame[y]); + } + + TETROMINO.X = INITIALTETROMINOX; + TETROMINO.Y = INITIALTETROMINOY; + TETROMINO.Shape = TETROMINO.Next; + TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; + + xCollision = TETROMINO.Shape[0].Length; + break; + } + } + + xCollision += 3; + } + + //Clean Lines + for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) + { + string line = PLAYFIELD[lineIndex]; + bool notCompleted = line.Any(e => e == ' '); + + if (lineIndex == 0 || lineIndex == PLAYFIELD.Length - 1) continue; + + if (!notCompleted) + { + PLAYFIELD[lineIndex] = "│ │"; + Score++; + + for (int lineM = lineIndex; lineM >= 1; lineM--) + { + if (PLAYFIELD[lineM - 1] == "╭──────────────────────────────╮") + { + PLAYFIELD[lineM] = "│ │"; + continue; + } + + PLAYFIELD[lineM] = PLAYFIELD[lineM - 1]; + } + + lineIndex++; + } + } + + //VerifiedCollision + if (Collision(Direction.None) && FallTimer != null) Gameover(); + + //Change Speed + if (Score > 100) return; + if (Score < 10) FallSpeedMilliSeconds = 1000; + else if (Score < 20) FallSpeedMilliSeconds = 800; + else if (Score < 30) FallSpeedMilliSeconds = 800; + else if (Score < 40) FallSpeedMilliSeconds = 600; + else if (Score < 50) FallSpeedMilliSeconds = 400; + else if (Score < 60) FallSpeedMilliSeconds = 200; + else if (Score < 70) FallSpeedMilliSeconds = 100; + else if (Score < 99) FallSpeedMilliSeconds = 50; +} + +void TetrominoSpin(Direction spinDirection) +{ + string[] newShape = new string[TETROMINO.Shape[0].Length / 3 * 2]; + int newY = 0; + int rowEven = 0; + int rowOdd = 1; + + //Turn + for (int y = 0; y < TETROMINO.Shape.Length;) + { + switch (spinDirection) + { + case Direction.Right: + SpinRight(newShape, ref newY, rowEven, rowOdd, y); + break; + case Direction.Left: + SpinLeft(newShape, ref newY, rowEven, rowOdd, y); + break; + } + + newY = 0; + rowEven += 2; + rowOdd += 2; + y += 2; + } + + //Verified Collision + for (int y = 0; y < newShape.Length - 1; y++) + { + for (int x = 0; x < newShape[y].Length; x++) + { + if (newShape[y][x] == ' ') continue; + + char c = PLAYFIELD[TETROMINO.Y + y][TETROMINO.X + x]; + if (c != ' ') return; + } + } + + TETROMINO.Shape = newShape; +} + +void SpinLeft(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) +{ + for (int x = TETROMINO.Shape[y].Length - 1; x >= 0; x -= 3) + { + for (int xS = 2; xS >= 0; xS--) + { + newShape[newY] += TETROMINO.Shape[rowEven][x - xS]; + newShape[newY + 1] += TETROMINO.Shape[rowOdd][x - xS]; + } + + newY += 2; + } +} + +void SpinRight(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) +{ + for (int x = 2; x < TETROMINO.Shape[y].Length; x += 3) + { + if (newShape[newY] == null) + { + newShape[newY] = ""; + newShape[newY + 1] = ""; + } + + for (int xS = 0; xS <= 2; xS++) + { + newShape[newY] = newShape[newY].Insert(0, TETROMINO.Shape[rowEven][x - xS].ToString()); + newShape[newY + 1] = newShape[newY + 1].Insert(0, TETROMINO.Shape[rowOdd][x - xS].ToString()); + } + + newY += 2; + } +} + +void SleepAfterRender() +{ + TimeSpan sleep = TimeSpan.FromSeconds(1d / 120d) - Stopwatch.Elapsed; + if (sleep > TimeSpan.Zero) + { + Thread.Sleep(sleep); + } + Stopwatch.Restart(); +} + +class Tetromino +{ + public required string[] Shape { get; set; } + public required string[] Next { get; set; } + public int X { get; set; } + public int Y { get; set; } +} + +enum Direction +{ + Right, + Left, + None +} + +enum GameStatus +{ + Gameover, + Playing, + Paused +} \ No newline at end of file diff --git a/Projects/Tetris/README.md b/Projects/Tetris/README.md new file mode 100644 index 00000000..3b4196db --- /dev/null +++ b/Projects/Tetris/README.md @@ -0,0 +1,36 @@ +

+ Tetris +

+ +

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

+ +Well, just tetris! + +## Inputs + +|Key|Action| +|---|---| +|`↓`, `S`, `←`, `A`, `→`, `D` |Move | +|`Q` |Spin Left | +|`E` |Spin Right | +|`P` |Pause, enter to resume | +|`R` |Change Text Color | + +Debug controls (only if debug mode its active, it may break the game): +|Key|Action| +|---|---| +|`Spacebar` |Clean Field | +|`X` |Add 10 score points | +|`I` |change active tetromino to I| +|`J` |change active tetromino to J| +|`L` |change active tetromino to L| +|`O` |change active tetromino to O| +|`C` |change active tetromino to S| +|`T` |change active tetromino to T| +|`Z` |change active tetromino to Z| \ No newline at end of file diff --git a/Projects/Tetris/Tetris.csproj b/Projects/Tetris/Tetris.csproj new file mode 100644 index 00000000..f02677bf --- /dev/null +++ b/Projects/Tetris/Tetris.csproj @@ -0,0 +1,10 @@ + + + + Exe + net7.0 + enable + enable + + + diff --git a/Projects/Website/.vscode/launch.json b/Projects/Website/.vscode/launch.json new file mode 100644 index 00000000..b0726229 --- /dev/null +++ b/Projects/Website/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch and Debug Standalone Blazor WebAssembly App", + "type": "blazorwasm", + "request": "launch", + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Projects/Website/.vscode/tasks.json b/Projects/Website/.vscode/tasks.json new file mode 100644 index 00000000..46740de4 --- /dev/null +++ b/Projects/Website/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Website.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Website.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/Website.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Projects/Website/Games/Tetris/Tetris.cs b/Projects/Website/Games/Tetris/Tetris.cs new file mode 100644 index 00000000..acfccec7 --- /dev/null +++ b/Projects/Website/Games/Tetris/Tetris.cs @@ -0,0 +1,733 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Text; +using System.Linq; + +namespace Website.Games.Tetris; + +public class Tetris +{ + public readonly BlazorConsole Console = new(); + + public async Task Run() + { + Console.OutputEncoding = Encoding.UTF8; + Console.CursorVisible = false; + Stopwatch Stopwatch = Stopwatch.StartNew(); + + string[] FIELD = new[] + { + "╭──────────────────────────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯" + }; + + string[] NEXTTETROMINO = new[] + { + "╭─────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰─────────╯" + }; + + string[] SCORE = new[]{ + "╭─────────╮", + "│ │", + "╰─────────╯" + }; + + string[] PAUSE = new[]{ + "█████╗ ███╗ ██╗██╗█████╗█████╗", + "██╔██║██╔██╗██║██║██╔══╝██╔══╝", + "█████║█████║██║██║ ███╗ █████╗", + "██╔══╝██╔██║██║██║ ██╗██╔══╝", + "██║ ██║██║█████║█████║█████╗", + "╚═╝ ╚═╝╚═╝╚════╝╚════╝╚════╝", + }; + + string[][] TETROMINOS = new[] + { + new[]{ + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯" + }, + new[]{ + "╭─╮ ", + "╰─╯ ", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + " ╭─╮", + " ╰─╯", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + "╭─╮╭─╮", + "╰─╯╰─╯", + "╭─╮╭─╮", + "╰─╯╰─╯" + }, + new[]{ + " ╭─╮╭─╮", + " ╰─╯╰─╯", + "╭─╮╭─╮ ", + "╰─╯╰─╯ " + }, + new[]{ + " ╭─╮ ", + " ╰─╯ ", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + "╭─╮╭─╮ ", + "╰─╯╰─╯ ", + " ╭─╮╭─╮", + " ╰─╯╰─╯" + }, + }; + + string[] PLAYFIELD = (string[])FIELD.Clone(); + const int BORDER = 1; + int FallSpeedMilliSeconds = 1000; + bool CloseGame = false; + int Score = 0; + int PauseCount = 0; + GameStatus GameStatus = GameStatus.Gameover; + + Random RamdomGenerator = new(); + + int INITIALTETROMINOX = Convert.ToInt16(PLAYFIELD[0].Length / 2) - 3; + int INITIALTETROMINOY = 1; + Tetromino TETROMINO = new() + { + Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + X = INITIALTETROMINOX, + Y = INITIALTETROMINOY + }; + + char[][]? LastFrame = null; + + AutoResetEvent AutoEvent = new AutoResetEvent(false); + Timer? FallTimer = null; + GameStatus = GameStatus.Playing; + + await Console.WriteLine(); + await Console.WriteLine(" ██████╗█████╗██████╗█████╗ ██╗█████╗"); + await Console.WriteLine(" ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝"); + await Console.WriteLine(" ██║ █████╗ ██║ █████╔╝██║ ███╗ "); + await Console.WriteLine(" ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗"); + await Console.WriteLine(" ██║ █████╗ ██║ ██║ ██║██║█████║"); + await Console.WriteLine(" ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝"); + + await Console.WriteLine(); + await Console.WriteLine(" Controls:"); + await Console.WriteLine(" WASD or ARROW to move"); + await Console.WriteLine(" Q or E to spin left or right"); + await Console.WriteLine(" P to paused the game, press enter"); + await Console.WriteLine(" key to resume"); + await Console.WriteLine(); + await Console.Write(" Press enter to start tetris..."); + Console.CursorVisible = false; + await StartGame(); + await Console.Clear(); + + FallTimer = new Timer(TetrominoFall, AutoEvent, 1000, FallSpeedMilliSeconds); + + while (!CloseGame) + { + if (CloseGame) + { + break; + } + + await PlayerControl(); + if (GameStatus == GameStatus.Playing) + { + await DrawFrame(); + await SleepAfterRender(); + } + } + + async Task PlayerControl() + { + while (await Console.KeyAvailable() && GameStatus == GameStatus.Playing) + { + switch ((await Console.ReadKey(true)).Key) + { + case ConsoleKey.A or ConsoleKey.LeftArrow: + if (Collision(Direction.Left)) break; + TETROMINO.X -= 3; + break; + case ConsoleKey.D or ConsoleKey.RightArrow: + if (Collision(Direction.Right)) break; + TETROMINO.X += 3; + break; + case ConsoleKey.S or ConsoleKey.DownArrow: + FallTimer.Change(0, FallSpeedMilliSeconds); + break; + case ConsoleKey.E: + TetrominoSpin(Direction.Right); + break; + case ConsoleKey.Q: + TetrominoSpin(Direction.Left); + break; + case ConsoleKey.P: + PauseGame(); + break; + } + } + } + + async Task DrawFrame() + { + bool collision = false; + int yScope = TETROMINO.Y; + string[] shapeScope = TETROMINO.Shape; + char[][] frame = new char[PLAYFIELD.Length][]; + + //Field + for (int y = 0; y < PLAYFIELD.Length; y++) + { + frame[y] = PLAYFIELD[y].ToCharArray(); + } + + //Draw Tetromino + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = TETROMINO.X + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = charTetromino; + } + } + + //Save Frame + if (collision && LastFrame != null) frame = LastFrame; + LastFrame = (char[][])frame.Clone(); + for (int y = 0; y < LastFrame.Length; y++) + { + LastFrame[y] = (char[])frame[y].Clone(); + } + + //Draw Preview + for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) + { + if (CollisionPreview(yField, yScope, shapeScope)) continue; + + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yField + y; + + if (yScope + shapeScope.Length > tY) continue; + + int tX = TETROMINO.X + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = '•'; + } + } + + break; + } + + //Next Square + for (int y = 0; y < NEXTTETROMINO.Length; y++) + { + frame[y] = frame[y].Concat(NEXTTETROMINO[y]).ToArray(); + } + + //Score Square + for (int y = 0; y < SCORE.Length; y++) + { + int sY = NEXTTETROMINO.Length + y; + frame[sY] = frame[sY].Concat(SCORE[y]).ToArray(); + } + + //Draw Next + for (int y = 0; y < TETROMINO.Next.Length; y++) + { + for (int x = 0; x < TETROMINO.Next[y].Length; x++) + { + int tY = y + BORDER; + int tX = PLAYFIELD[y].Length + x + BORDER; + char charTetromino = TETROMINO.Next[y][x]; + frame[tY][tX] = charTetromino; + } + } + + //Draw Score + char[] score = Score.ToString().ToCharArray(); + for (int scoreX = score.Length - 1; scoreX >= 0; scoreX--) + { + int sY = NEXTTETROMINO.Length + BORDER; + int sX = frame[sY].Length - (score.Length - scoreX) - BORDER; + frame[sY][sX] = score[scoreX]; + } + + //Draw Pause + if (GameStatus == GameStatus.Paused) + { + for (int y = 0; y < PAUSE.Length; y++) + { + int fY = (PLAYFIELD.Length / 2) + y - PAUSE.Length; + for (int x = 0; x < PAUSE[y].Length; x++) + { + int fX = x + BORDER; + + if (x >= PLAYFIELD[fY].Length) break; + + frame[fY][fX] = PAUSE[y][x]; + } + } + } + + //Create Render + StringBuilder render = new(); + for (int y = 0; y < frame.Length; y++) + { + render.AppendLine(new string(frame[y])); + } + + await Console.Clear(); + await Console.Write(render); + Console.CursorVisible = false; + } + + bool Collision(Direction direction) + { + int xNew = TETROMINO.X; + bool collision = false; + + switch (direction) + { + case Direction.Right: + xNew += 3; + if (xNew + TETROMINO.Shape[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; + break; + case Direction.Left: + xNew -= 3; + if (xNew < BORDER) collision = true; + break; + case Direction.None: + break; + } + + if (collision) return collision; + + for (int y = 0; y < TETROMINO.Shape.Length && !collision; y++) + { + for (int x = 0; x < TETROMINO.Shape[y].Length; x++) + { + int tY = TETROMINO.Y + y; + int tX = xNew + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = TETROMINO.Shape[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + } + } + + return collision; + } + + bool CollisionPreview(int initY, int yScope, string[] shape) + { + int xNew = TETROMINO.X; + + for (int yUpper = initY; yUpper >= yScope; yUpper -= 2) + { + for (int y = shape.Length - 1; y >= 0; y -= 2) + { + for (int x = 0; x < shape[y].Length; x++) + { + int tY = yUpper + y; + int tX = xNew + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shape[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + return true; + } + } + } + } + + return false; + } + + async void Gameover() + { + GameStatus = GameStatus.Gameover; + AutoEvent.Dispose(); + FallTimer.Dispose(); + + await SleepAfterRender(); + + await Console.Clear(); + await Console.WriteLine(); + await Console.WriteLine(" ██████╗ █████╗ ██ ██╗█████╗"); + await Console.WriteLine(" ██╔════╝ ██╔══██╗███ ███║██╔══╝"); + await Console.WriteLine(" ██║ ███╗███████║██╔██═██║█████╗"); + await Console.WriteLine(" ██║ ██║██╔══██║██║ ██║██╔══╝"); + await Console.WriteLine(" ╚██████╔╝██║ ██║██║ ██║█████╗"); + await Console.WriteLine(" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝"); + await Console.WriteLine(" ██████╗██╗ ██╗█████╗█████╗ "); + await Console.WriteLine(" ██ ██║██║ ██║██╔══╝██╔═██╗ "); + await Console.WriteLine(" ██ ██║██║ ██║█████╗█████╔╝ "); + await Console.WriteLine(" ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ "); + await Console.WriteLine(" ██████║ ╚███╔╝ █████╗██║ ██║ "); + await Console.WriteLine(" ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ "); + + await Console.WriteLine(); + await Console.WriteLine($" Final Score: {Score}"); + await Console.WriteLine($" Pause Count: {PauseCount}"); + await Console.WriteLine(); + await Console.WriteLine(" Press enter to play again"); + await Console.WriteLine(" Press escape to close the game"); + Console.CursorVisible = false; + await StartGame(); + RestartGame(); + } + + async Task StartGame(ConsoleKey key = ConsoleKey.Enter) + { + ConsoleKey input = default; + while (input != key && !CloseGame) + { + input = (await Console.ReadKey(true)).Key; + if (input is ConsoleKey.Escape) + { + CloseGame = true; + return; + } + } + } + + void RestartGame() + { + PLAYFIELD = (string[])FIELD.Clone(); + FallSpeedMilliSeconds = 1000; + Score = 0; + TETROMINO = new() + { + Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + X = INITIALTETROMINOX, + Y = INITIALTETROMINOY + }; + + LastFrame = null; + AutoEvent = new AutoResetEvent(false); + FallTimer = new Timer(TetrominoFall, AutoEvent, 1000, FallSpeedMilliSeconds); + GameStatus = GameStatus.Playing; + } + + async void PauseGame() + { + PauseCount++; + FallTimer.Change(Timeout.Infinite, Timeout.Infinite); + GameStatus = GameStatus.Paused; + await DrawFrame(); + + await ResumeGame(); + } + + async Task ResumeGame(ConsoleKey key = ConsoleKey.Enter) + { + ConsoleKey input = default; + while (input != key && !CloseGame) + { + input = (await Console.ReadKey(true)).Key; + if (input is ConsoleKey.Enter && GameStatus == GameStatus.Paused && FallTimer != null) + { + FallTimer.Change(0, FallSpeedMilliSeconds); + GameStatus = GameStatus.Playing; + return; + } + } + } + + void TetrominoFall(object? e) + { + if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) TETROMINO.Y = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; + else TETROMINO.Y += 2; + + //Y Collision + for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) + { + for (int yCollision = TETROMINO.Shape.Length - 1; yCollision >= 0; yCollision -= 2) + { + char exist = TETROMINO.Shape[yCollision][xCollision]; + + if (exist == ' ') continue; + + char[] lineYC = PLAYFIELD[TETROMINO.Y + yCollision - 1].ToCharArray(); + + if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; + + if + ( + lineYC[TETROMINO.X + xCollision] != ' ' && + lineYC[TETROMINO.X + xCollision] != '│' && + LastFrame != null + ) + { + for (int y = 0; y < LastFrame.Length; y++) + { + PLAYFIELD[y] = new string(LastFrame[y]); + } + + TETROMINO.X = INITIALTETROMINOX; + TETROMINO.Y = INITIALTETROMINOY; + TETROMINO.Shape = TETROMINO.Next; + TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; + + xCollision = TETROMINO.Shape[0].Length; + break; + } + } + + xCollision += 3; + } + + //Clean Lines + for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) + { + string line = PLAYFIELD[lineIndex]; + bool notCompleted = line.Any(e => e == ' '); + + if (lineIndex == 0 || lineIndex == PLAYFIELD.Length - 1) continue; + + if (!notCompleted) + { + PLAYFIELD[lineIndex] = "│ │"; + Score++; + + for (int lineM = lineIndex; lineM >= 1; lineM--) + { + if (PLAYFIELD[lineM - 1] == "╭──────────────────────────────╮") + { + PLAYFIELD[lineM] = "│ │"; + continue; + } + + PLAYFIELD[lineM] = PLAYFIELD[lineM - 1]; + } + + lineIndex++; + } + } + + //VerifiedCollision + if (Collision(Direction.None) && FallTimer != null) Gameover(); + + //Change Speed + if (Score > 100) return; + if (Score < 10) FallSpeedMilliSeconds = 1000; + else if (Score < 20) FallSpeedMilliSeconds = 800; + else if (Score < 30) FallSpeedMilliSeconds = 800; + else if (Score < 40) FallSpeedMilliSeconds = 600; + else if (Score < 50) FallSpeedMilliSeconds = 400; + else if (Score < 60) FallSpeedMilliSeconds = 200; + else if (Score < 70) FallSpeedMilliSeconds = 100; + else if (Score < 99) FallSpeedMilliSeconds = 50; + } + + void TetrominoSpin(Direction spinDirection) + { + string[] newShape = new string[TETROMINO.Shape[0].Length / 3 * 2]; + int newY = 0; + int rowEven = 0; + int rowOdd = 1; + + //Turn + for (int y = 0; y < TETROMINO.Shape.Length;) + { + switch (spinDirection) + { + case Direction.Right: + SpinRight(newShape, ref newY, rowEven, rowOdd, y); + break; + case Direction.Left: + SpinLeft(newShape, ref newY, rowEven, rowOdd, y); + break; + } + + newY = 0; + rowEven += 2; + rowOdd += 2; + y += 2; + } + + //Verified Collision + for (int y = 0; y < newShape.Length - 1; y++) + { + for (int x = 0; x < newShape[y].Length; x++) + { + if (newShape[y][x] == ' ') continue; + + char c = PLAYFIELD[TETROMINO.Y + y][TETROMINO.X + x]; + if (c != ' ') return; + } + } + + TETROMINO.Shape = newShape; + } + + void SpinLeft(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) + { + for (int x = TETROMINO.Shape[y].Length - 1; x >= 0; x -= 3) + { + for (int xS = 2; xS >= 0; xS--) + { + newShape[newY] += TETROMINO.Shape[rowEven][x - xS]; + newShape[newY + 1] += TETROMINO.Shape[rowOdd][x - xS]; + } + + newY += 2; + } + } + + void SpinRight(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) + { + for (int x = 2; x < TETROMINO.Shape[y].Length; x += 3) + { + if (newShape[newY] == null) + { + newShape[newY] = ""; + newShape[newY + 1] = ""; + } + + for (int xS = 0; xS <= 2; xS++) + { + newShape[newY] = newShape[newY].Insert(0, TETROMINO.Shape[rowEven][x - xS].ToString()); + newShape[newY + 1] = newShape[newY + 1].Insert(0, TETROMINO.Shape[rowOdd][x - xS].ToString()); + } + + newY += 2; + } + } + + async Task SleepAfterRender() + { + TimeSpan sleep = TimeSpan.FromSeconds(1d / 120d) - Stopwatch.Elapsed; + if (sleep > TimeSpan.Zero) + { + await Console.RefreshAndDelay(sleep); + } + Stopwatch.Restart(); + } + } + + class Tetromino + { + public required string[] Shape { get; set; } + public required string[] Next { get; set; } + public int X { get; set; } + public int Y { get; set; } + } + + enum Direction + { + Right, + Left, + None + } + + enum GameStatus + { + Gameover, + Playing, + Paused + } + +} diff --git a/Projects/Website/Pages/Tetris.razor b/Projects/Website/Pages/Tetris.razor new file mode 100644 index 00000000..4bc7b4f0 --- /dev/null +++ b/Projects/Website/Pages/Tetris.razor @@ -0,0 +1,54 @@ +@using System + +@page "/Tetris" + +Tetris + +

Tetris

+ + + Go To Readme + + +
+
+
+			@Console.State
+		
+
+
+ + + + + + + + +
+
+ + + + + +@code +{ + Games.Tetris.Tetris Game; + BlazorConsole Console; + + public Tetris() + { + Game = new(); + Console = Game.Console; + Console.WindowWidth = 43; + Console.WindowHeight = 42; + Console.TriggerRefresh = StateHasChanged; + } + + protected override void OnInitialized() => InvokeAsync(Game.Run); +} diff --git a/Projects/Website/Shared/NavMenu.razor b/Projects/Website/Shared/NavMenu.razor index b2c080a1..4f3b0348 100644 --- a/Projects/Website/Shared/NavMenu.razor +++ b/Projects/Website/Shared/NavMenu.razor @@ -248,6 +248,11 @@ Shmup + diff --git a/README.md b/README.md index 05a836ef..4f6ddc4a 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ |[Role Playing Game](Projects/Role%20Playing%20Game)|6|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Role%20Playing%20Game) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Role%20Playing%20Game%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[Console Monsters](Projects/Console%20Monsters)|7|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Console%20Monsters) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Console%20Monsters%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_Community Collaboration_
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Work In Progress_| |[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)_| +|[Tetris](Projects/Tetris)|?|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Tetris) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Tetris%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_[Community Contribution]()_| \*_**Weight**: A relative rating for how advanced the source code is._
diff --git a/dotnet-console-games.sln b/dotnet-console-games.sln index 1cd68ef3..92b38d84 100644 --- a/dotnet-console-games.sln +++ b/dotnet-console-games.sln @@ -103,6 +103,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shmup", "Projects\Shmup\Shm EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clicker", "Projects\Clicker\Clicker.csproj", "{C408B9C3-5F16-4F0A-B0D0-F39A6F7F0B72}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tetris", "Projects\Tetris\Tetris.csproj", "{4E9F6AA3-7E12-4555-95C2-6D90C8CD3DBB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -309,6 +311,10 @@ Global {C408B9C3-5F16-4F0A-B0D0-F39A6F7F0B72}.Debug|Any CPU.Build.0 = Debug|Any CPU {C408B9C3-5F16-4F0A-B0D0-F39A6F7F0B72}.Release|Any CPU.ActiveCfg = Release|Any CPU {C408B9C3-5F16-4F0A-B0D0-F39A6F7F0B72}.Release|Any CPU.Build.0 = Release|Any CPU + {4E9F6AA3-7E12-4555-95C2-6D90C8CD3DBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E9F6AA3-7E12-4555-95C2-6D90C8CD3DBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E9F6AA3-7E12-4555-95C2-6D90C8CD3DBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E9F6AA3-7E12-4555-95C2-6D90C8CD3DBB}.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 4cfb281f..57683bef 100644 --- a/dotnet-console-games.slnf +++ b/dotnet-console-games.slnf @@ -50,7 +50,8 @@ "Projects\\Whack A Mole\\Whack A Mole.csproj", "Projects\\Wordle\\Wordle.csproj", "Projects\\Wumpus World\\Wumpus World.csproj", - "Projects\\Yahtzee\\Yahtzee.csproj" + "Projects\\Yahtzee\\Yahtzee.csproj", + "Projects\\Tetris\\Tetris.csproj" ] } } \ No newline at end of file From 89ec8e9879fc934f3384c1c3f700290d64d6bff3 Mon Sep 17 00:00:00 2001 From: Jahg Date: Fri, 27 Oct 2023 10:24:59 -0700 Subject: [PATCH 02/24] Improvements, inmutability and fall logic fix --- Projects/Tetris/Program.cs | 175 ++++++++++++++++-------- Projects/Website/Games/Tetris/Tetris.cs | 175 ++++++++++++++++-------- README.md | 2 +- 3 files changed, 240 insertions(+), 112 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index 6c71c532..151efe53 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -153,8 +153,6 @@ Y = INITIALTETROMINOY }; -char[][]? LastFrame = null; - AutoResetEvent AutoEvent = new AutoResetEvent(false); Timer? FallTimer = null; GameStatus = GameStatus.Playing; @@ -182,7 +180,7 @@ StartGame(); Console.Clear(); -FallTimer = new Timer(TetrominoFall, AutoEvent, 1000, FallSpeedMilliSeconds); +FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); while (!CloseGame) { @@ -276,7 +274,8 @@ void DrawFrame() { bool collision = false; int yScope = TETROMINO.Y; - string[] shapeScope = TETROMINO.Shape; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); char[][] frame = new char[PLAYFIELD.Length][]; //Field @@ -307,14 +306,6 @@ void DrawFrame() } } - //Save Frame - if (collision && LastFrame != null) frame = LastFrame; - LastFrame = (char[][])frame.Clone(); - for (int y = 0; y < LastFrame.Length; y++) - { - LastFrame[y] = (char[])frame[y].Clone(); - } - //Draw Preview for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) { @@ -361,13 +352,13 @@ void DrawFrame() } //Draw Next - for (int y = 0; y < TETROMINO.Next.Length; y++) + for (int y = 0; y < nextShapeScope.Length; y++) { - for (int x = 0; x < TETROMINO.Next[y].Length; x++) + for (int x = 0; x < nextShapeScope[y].Length; x++) { int tY = y + BORDER; int tX = PLAYFIELD[y].Length + x + BORDER; - char charTetromino = TETROMINO.Next[y][x]; + char charTetromino = nextShapeScope[y][x]; frame[tY][tX] = charTetromino; } } @@ -410,16 +401,58 @@ void DrawFrame() Console.CursorVisible = false; } +char[][] DrawLastFrame(int yS) +{ + bool collision = false; + int yScope = yS - 2; + int xScope = TETROMINO.X; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); + char[][] frame = new char[PLAYFIELD.Length][]; + + //Field + for (int y = 0; y < PLAYFIELD.Length; y++) + { + frame[y] = PLAYFIELD[y].ToCharArray(); + } + + //Draw Tetromino + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = xScope + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = charTetromino; + } + } + + return frame; +} + bool Collision(Direction direction) { int xNew = TETROMINO.X; + int yScope = TETROMINO.Y; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); bool collision = false; switch (direction) { case Direction.Right: xNew += 3; - if (xNew + TETROMINO.Shape[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; + if (xNew + shapeScope[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; break; case Direction.Left: xNew -= 3; @@ -431,14 +464,14 @@ bool Collision(Direction direction) if (collision) return collision; - for (int y = 0; y < TETROMINO.Shape.Length && !collision; y++) + for (int y = 0; y < shapeScope.Length && !collision; y++) { - for (int x = 0; x < TETROMINO.Shape[y].Length; x++) + for (int x = 0; x < shapeScope[y].Length; x++) { - int tY = TETROMINO.Y + y; + int tY = yScope + y; int tX = xNew + x; char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = TETROMINO.Shape[y][x]; + char charTetromino = shapeScope[y][x]; if (charTetromino == ' ') continue; @@ -511,7 +544,6 @@ void Gameover() Console.WriteLine(" Press enter to play again"); Console.WriteLine(" Press escape to close the game"); Console.CursorVisible = false; - Console.ReadKey(); StartGame(); RestartGame(); } @@ -543,9 +575,8 @@ void RestartGame() Y = INITIALTETROMINOY }; - LastFrame = null; AutoEvent = new AutoResetEvent(false); - FallTimer = new Timer(TetrominoFall, AutoEvent, 1000, FallSpeedMilliSeconds); + FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); GameStatus = GameStatus.Playing; } @@ -574,10 +605,48 @@ void ResumeGame(ConsoleKey key = ConsoleKey.Enter) } } +void AddScoreChangeSpeed(int value) +{ + Score += value; + + if (Score > 100) return; + + switch (Score) + { + case 10: + FallSpeedMilliSeconds = 900; + break; + case 20: + FallSpeedMilliSeconds = 800; + break; + case 30: + FallSpeedMilliSeconds = 700; + break; + case 40: + FallSpeedMilliSeconds = 500; + break; + case 50: + FallSpeedMilliSeconds = 300; + break; + case 60: + FallSpeedMilliSeconds = 200; + break; + case 70: + FallSpeedMilliSeconds = 100; + break; + case 100: + FallSpeedMilliSeconds = 50; + break; + } +} + void TetrominoFall(object? e) { - if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) TETROMINO.Y = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; - else TETROMINO.Y += 2; + int yAfterFall = TETROMINO.Y; + bool collision = false; + + if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) yAfterFall = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; + else yAfterFall += 2; //Y Collision for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) @@ -588,20 +657,20 @@ void TetrominoFall(object? e) if (exist == ' ') continue; - char[] lineYC = PLAYFIELD[TETROMINO.Y + yCollision - 1].ToCharArray(); + char[] lineYC = PLAYFIELD[yAfterFall + yCollision - 1].ToCharArray(); if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; if ( lineYC[TETROMINO.X + xCollision] != ' ' && - lineYC[TETROMINO.X + xCollision] != '│' && - LastFrame != null + lineYC[TETROMINO.X + xCollision] != '│' ) { - for (int y = 0; y < LastFrame.Length; y++) + char[][] lastFrame = DrawLastFrame(yAfterFall); + for (int y = 0; y < lastFrame.Length; y++) { - PLAYFIELD[y] = new string(LastFrame[y]); + PLAYFIELD[y] = new string(lastFrame[y]); } TETROMINO.X = INITIALTETROMINOX; @@ -610,6 +679,7 @@ void TetrominoFall(object? e) TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; xCollision = TETROMINO.Shape[0].Length; + collision = true; break; } } @@ -617,6 +687,8 @@ void TetrominoFall(object? e) xCollision += 3; } + if (!collision) TETROMINO.Y = yAfterFall; + //Clean Lines for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) { @@ -628,7 +700,7 @@ void TetrominoFall(object? e) if (!notCompleted) { PLAYFIELD[lineIndex] = "│ │"; - Score++; + AddScoreChangeSpeed(1); for (int lineM = lineIndex; lineM >= 1; lineM--) { @@ -647,36 +719,27 @@ void TetrominoFall(object? e) //VerifiedCollision if (Collision(Direction.None) && FallTimer != null) Gameover(); - - //Change Speed - if (Score > 100) return; - if (Score < 10) FallSpeedMilliSeconds = 1000; - else if (Score < 20) FallSpeedMilliSeconds = 800; - else if (Score < 30) FallSpeedMilliSeconds = 800; - else if (Score < 40) FallSpeedMilliSeconds = 600; - else if (Score < 50) FallSpeedMilliSeconds = 400; - else if (Score < 60) FallSpeedMilliSeconds = 200; - else if (Score < 70) FallSpeedMilliSeconds = 100; - else if (Score < 99) FallSpeedMilliSeconds = 50; } void TetrominoSpin(Direction spinDirection) { - string[] newShape = new string[TETROMINO.Shape[0].Length / 3 * 2]; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + int yScope = TETROMINO.Y; + string[] newShape = new string[shapeScope[0].Length / 3 * 2]; int newY = 0; int rowEven = 0; int rowOdd = 1; //Turn - for (int y = 0; y < TETROMINO.Shape.Length;) + for (int y = 0; y < shapeScope.Length;) { switch (spinDirection) { case Direction.Right: - SpinRight(newShape, ref newY, rowEven, rowOdd, y); + SpinRight(newShape, shapeScope, ref newY, rowEven, rowOdd, y); break; case Direction.Left: - SpinLeft(newShape, ref newY, rowEven, rowOdd, y); + SpinLeft(newShape, shapeScope, ref newY, rowEven, rowOdd, y); break; } @@ -693,7 +756,7 @@ void TetrominoSpin(Direction spinDirection) { if (newShape[y][x] == ' ') continue; - char c = PLAYFIELD[TETROMINO.Y + y][TETROMINO.X + x]; + char c = PLAYFIELD[yScope + y][TETROMINO.X + x]; if (c != ' ') return; } } @@ -701,23 +764,23 @@ void TetrominoSpin(Direction spinDirection) TETROMINO.Shape = newShape; } -void SpinLeft(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) +void SpinLeft(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) { - for (int x = TETROMINO.Shape[y].Length - 1; x >= 0; x -= 3) + for (int x = shape[y].Length - 1; x >= 0; x -= 3) { for (int xS = 2; xS >= 0; xS--) { - newShape[newY] += TETROMINO.Shape[rowEven][x - xS]; - newShape[newY + 1] += TETROMINO.Shape[rowOdd][x - xS]; + newShape[newY] += shape[rowEven][x - xS]; + newShape[newY + 1] += shape[rowOdd][x - xS]; } newY += 2; } } -void SpinRight(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) +void SpinRight(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) { - for (int x = 2; x < TETROMINO.Shape[y].Length; x += 3) + for (int x = 2; x < shape[y].Length; x += 3) { if (newShape[newY] == null) { @@ -727,8 +790,8 @@ void SpinRight(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) for (int xS = 0; xS <= 2; xS++) { - newShape[newY] = newShape[newY].Insert(0, TETROMINO.Shape[rowEven][x - xS].ToString()); - newShape[newY + 1] = newShape[newY + 1].Insert(0, TETROMINO.Shape[rowOdd][x - xS].ToString()); + newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString()); + newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString()); } newY += 2; @@ -737,7 +800,7 @@ void SpinRight(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) void SleepAfterRender() { - TimeSpan sleep = TimeSpan.FromSeconds(1d / 120d) - Stopwatch.Elapsed; + TimeSpan sleep = TimeSpan.FromSeconds(1d / 60d) - Stopwatch.Elapsed; if (sleep > TimeSpan.Zero) { Thread.Sleep(sleep); diff --git a/Projects/Website/Games/Tetris/Tetris.cs b/Projects/Website/Games/Tetris/Tetris.cs index acfccec7..a973393e 100644 --- a/Projects/Website/Games/Tetris/Tetris.cs +++ b/Projects/Website/Games/Tetris/Tetris.cs @@ -162,8 +162,6 @@ public async Task Run() Y = INITIALTETROMINOY }; - char[][]? LastFrame = null; - AutoResetEvent AutoEvent = new AutoResetEvent(false); Timer? FallTimer = null; GameStatus = GameStatus.Playing; @@ -188,7 +186,7 @@ public async Task Run() await StartGame(); await Console.Clear(); - FallTimer = new Timer(TetrominoFall, AutoEvent, 1000, FallSpeedMilliSeconds); + FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); while (!CloseGame) { @@ -239,7 +237,8 @@ async Task DrawFrame() { bool collision = false; int yScope = TETROMINO.Y; - string[] shapeScope = TETROMINO.Shape; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); char[][] frame = new char[PLAYFIELD.Length][]; //Field @@ -270,14 +269,6 @@ async Task DrawFrame() } } - //Save Frame - if (collision && LastFrame != null) frame = LastFrame; - LastFrame = (char[][])frame.Clone(); - for (int y = 0; y < LastFrame.Length; y++) - { - LastFrame[y] = (char[])frame[y].Clone(); - } - //Draw Preview for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) { @@ -324,13 +315,13 @@ async Task DrawFrame() } //Draw Next - for (int y = 0; y < TETROMINO.Next.Length; y++) + for (int y = 0; y < nextShapeScope.Length; y++) { - for (int x = 0; x < TETROMINO.Next[y].Length; x++) + for (int x = 0; x < nextShapeScope[y].Length; x++) { int tY = y + BORDER; int tX = PLAYFIELD[y].Length + x + BORDER; - char charTetromino = TETROMINO.Next[y][x]; + char charTetromino = nextShapeScope[y][x]; frame[tY][tX] = charTetromino; } } @@ -373,16 +364,58 @@ async Task DrawFrame() Console.CursorVisible = false; } + char[][] DrawLastFrame(int yS) + { + bool collision = false; + int yScope = yS - 2; + int xScope = TETROMINO.X; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); + char[][] frame = new char[PLAYFIELD.Length][]; + + //Field + for (int y = 0; y < PLAYFIELD.Length; y++) + { + frame[y] = PLAYFIELD[y].ToCharArray(); + } + + //Draw Tetromino + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = xScope + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = charTetromino; + } + } + + return frame; + } + bool Collision(Direction direction) { int xNew = TETROMINO.X; + int yScope = TETROMINO.Y; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); bool collision = false; switch (direction) { case Direction.Right: xNew += 3; - if (xNew + TETROMINO.Shape[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; + if (xNew + shapeScope[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; break; case Direction.Left: xNew -= 3; @@ -394,14 +427,14 @@ bool Collision(Direction direction) if (collision) return collision; - for (int y = 0; y < TETROMINO.Shape.Length && !collision; y++) + for (int y = 0; y < shapeScope.Length && !collision; y++) { - for (int x = 0; x < TETROMINO.Shape[y].Length; x++) + for (int x = 0; x < shapeScope[y].Length; x++) { - int tY = TETROMINO.Y + y; + int tY = yScope + y; int tX = xNew + x; char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = TETROMINO.Shape[y][x]; + char charTetromino = shapeScope[y][x]; if (charTetromino == ' ') continue; @@ -505,9 +538,8 @@ void RestartGame() Y = INITIALTETROMINOY }; - LastFrame = null; AutoEvent = new AutoResetEvent(false); - FallTimer = new Timer(TetrominoFall, AutoEvent, 1000, FallSpeedMilliSeconds); + FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); GameStatus = GameStatus.Playing; } @@ -536,10 +568,48 @@ async Task ResumeGame(ConsoleKey key = ConsoleKey.Enter) } } + void AddScoreChangeSpeed(int value) + { + Score += value; + + if (Score > 100) return; + + switch (Score) + { + case 10: + FallSpeedMilliSeconds = 900; + break; + case 20: + FallSpeedMilliSeconds = 800; + break; + case 30: + FallSpeedMilliSeconds = 700; + break; + case 40: + FallSpeedMilliSeconds = 500; + break; + case 50: + FallSpeedMilliSeconds = 300; + break; + case 60: + FallSpeedMilliSeconds = 200; + break; + case 70: + FallSpeedMilliSeconds = 100; + break; + case 100: + FallSpeedMilliSeconds = 50; + break; + } + } + void TetrominoFall(object? e) { - if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) TETROMINO.Y = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; - else TETROMINO.Y += 2; + int yAfterFall = TETROMINO.Y; + bool collision = false; + + if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) yAfterFall = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; + else yAfterFall += 2; //Y Collision for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) @@ -550,20 +620,20 @@ void TetrominoFall(object? e) if (exist == ' ') continue; - char[] lineYC = PLAYFIELD[TETROMINO.Y + yCollision - 1].ToCharArray(); + char[] lineYC = PLAYFIELD[yAfterFall + yCollision - 1].ToCharArray(); if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; if ( lineYC[TETROMINO.X + xCollision] != ' ' && - lineYC[TETROMINO.X + xCollision] != '│' && - LastFrame != null + lineYC[TETROMINO.X + xCollision] != '│' ) { - for (int y = 0; y < LastFrame.Length; y++) + char[][] lastFrame = DrawLastFrame(yAfterFall); + for (int y = 0; y < lastFrame.Length; y++) { - PLAYFIELD[y] = new string(LastFrame[y]); + PLAYFIELD[y] = new string(lastFrame[y]); } TETROMINO.X = INITIALTETROMINOX; @@ -572,6 +642,7 @@ void TetrominoFall(object? e) TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; xCollision = TETROMINO.Shape[0].Length; + collision = true; break; } } @@ -579,6 +650,8 @@ void TetrominoFall(object? e) xCollision += 3; } + if (!collision) TETROMINO.Y = yAfterFall; + //Clean Lines for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) { @@ -590,7 +663,7 @@ void TetrominoFall(object? e) if (!notCompleted) { PLAYFIELD[lineIndex] = "│ │"; - Score++; + AddScoreChangeSpeed(1); for (int lineM = lineIndex; lineM >= 1; lineM--) { @@ -609,36 +682,27 @@ void TetrominoFall(object? e) //VerifiedCollision if (Collision(Direction.None) && FallTimer != null) Gameover(); - - //Change Speed - if (Score > 100) return; - if (Score < 10) FallSpeedMilliSeconds = 1000; - else if (Score < 20) FallSpeedMilliSeconds = 800; - else if (Score < 30) FallSpeedMilliSeconds = 800; - else if (Score < 40) FallSpeedMilliSeconds = 600; - else if (Score < 50) FallSpeedMilliSeconds = 400; - else if (Score < 60) FallSpeedMilliSeconds = 200; - else if (Score < 70) FallSpeedMilliSeconds = 100; - else if (Score < 99) FallSpeedMilliSeconds = 50; } void TetrominoSpin(Direction spinDirection) { - string[] newShape = new string[TETROMINO.Shape[0].Length / 3 * 2]; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + int yScope = TETROMINO.Y; + string[] newShape = new string[shapeScope[0].Length / 3 * 2]; int newY = 0; int rowEven = 0; int rowOdd = 1; //Turn - for (int y = 0; y < TETROMINO.Shape.Length;) + for (int y = 0; y < shapeScope.Length;) { switch (spinDirection) { case Direction.Right: - SpinRight(newShape, ref newY, rowEven, rowOdd, y); + SpinRight(newShape, shapeScope, ref newY, rowEven, rowOdd, y); break; case Direction.Left: - SpinLeft(newShape, ref newY, rowEven, rowOdd, y); + SpinLeft(newShape, shapeScope, ref newY, rowEven, rowOdd, y); break; } @@ -655,7 +719,7 @@ void TetrominoSpin(Direction spinDirection) { if (newShape[y][x] == ' ') continue; - char c = PLAYFIELD[TETROMINO.Y + y][TETROMINO.X + x]; + char c = PLAYFIELD[yScope + y][TETROMINO.X + x]; if (c != ' ') return; } } @@ -663,23 +727,23 @@ void TetrominoSpin(Direction spinDirection) TETROMINO.Shape = newShape; } - void SpinLeft(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) + void SpinLeft(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) { - for (int x = TETROMINO.Shape[y].Length - 1; x >= 0; x -= 3) + for (int x = shape[y].Length - 1; x >= 0; x -= 3) { for (int xS = 2; xS >= 0; xS--) { - newShape[newY] += TETROMINO.Shape[rowEven][x - xS]; - newShape[newY + 1] += TETROMINO.Shape[rowOdd][x - xS]; + newShape[newY] += shape[rowEven][x - xS]; + newShape[newY + 1] += shape[rowOdd][x - xS]; } newY += 2; } } - void SpinRight(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) + void SpinRight(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) { - for (int x = 2; x < TETROMINO.Shape[y].Length; x += 3) + for (int x = 2; x < shape[y].Length; x += 3) { if (newShape[newY] == null) { @@ -689,8 +753,8 @@ void SpinRight(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) for (int xS = 0; xS <= 2; xS++) { - newShape[newY] = newShape[newY].Insert(0, TETROMINO.Shape[rowEven][x - xS].ToString()); - newShape[newY + 1] = newShape[newY + 1].Insert(0, TETROMINO.Shape[rowOdd][x - xS].ToString()); + newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString()); + newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString()); } newY += 2; @@ -699,13 +763,14 @@ void SpinRight(string[] newShape, ref int newY, int rowEven, int rowOdd, int y) async Task SleepAfterRender() { - TimeSpan sleep = TimeSpan.FromSeconds(1d / 120d) - Stopwatch.Elapsed; + TimeSpan sleep = TimeSpan.FromSeconds(1d / 60d) - Stopwatch.Elapsed; if (sleep > TimeSpan.Zero) { await Console.RefreshAndDelay(sleep); } Stopwatch.Restart(); } + } class Tetromino diff --git a/README.md b/README.md index 4f6ddc4a..ea5d9322 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ |[Role Playing Game](Projects/Role%20Playing%20Game)|6|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Role%20Playing%20Game) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Role%20Playing%20Game%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[Console Monsters](Projects/Console%20Monsters)|7|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Console%20Monsters) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Console%20Monsters%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_Community Collaboration_
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Work In Progress_| |[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)_| -|[Tetris](Projects/Tetris)|?|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Tetris) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Tetris%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_[Community Contribution]()_| +|[Tetris](Projects/Tetris)|?|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Tetris) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Tetris%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_[Community Contribution](https://github.com/dotnet/dotnet-console-games/pull/89)_| \*_**Weight**: A relative rating for how advanced the source code is._
From a5ce87f234c51f897c8d2cb17d4f87a4e509306d Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 12:29:32 -0500 Subject: [PATCH 03/24] slnf alphabetical order --- dotnet-console-games.slnf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet-console-games.slnf b/dotnet-console-games.slnf index 57683bef..5b642937 100644 --- a/dotnet-console-games.slnf +++ b/dotnet-console-games.slnf @@ -42,6 +42,7 @@ "Projects\\Snake\\Snake.csproj", "Projects\\Sudoku\\Sudoku.csproj", "Projects\\Tanks\\Tanks.csproj", + "Projects\\Tetris\\Tetris.csproj" "Projects\\Tic Tac Toe\\Tic Tac Toe.csproj", "Projects\\Tents\\Tents.csproj", "Projects\\Tower Of Hanoi\\Tower Of Hanoi.csproj", @@ -51,7 +52,6 @@ "Projects\\Wordle\\Wordle.csproj", "Projects\\Wumpus World\\Wumpus World.csproj", "Projects\\Yahtzee\\Yahtzee.csproj", - "Projects\\Tetris\\Tetris.csproj" ] } } \ No newline at end of file From 6895b51c52ec4e3e045552d4b0f01d1daaad9e49 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 12:34:15 -0500 Subject: [PATCH 04/24] format documents --- Projects/Tetris/Program.cs | 1356 ++++++++++---------- Projects/Tetris/Tetris.csproj | 14 +- Projects/Website/Games/Tetris/Tetris.cs | 1570 +++++++++++------------ 3 files changed, 1469 insertions(+), 1471 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index 151efe53..b052e062 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -9,127 +9,127 @@ string[] FIELD = new[] { - "╭──────────────────────────────╮", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰──────────────────────────────╯" + "╭──────────────────────────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯" }; string[] NEXTTETROMINO = new[] { - "╭─────────╮", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰─────────╯" + "╭─────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰─────────╯" }; string[] SCORE = new[]{ - "╭─────────╮", - "│ │", - "╰─────────╯" + "╭─────────╮", + "│ │", + "╰─────────╯" }; string[] PAUSE = new[]{ - "█████╗ ███╗ ██╗██╗█████╗█████╗", - "██╔██║██╔██╗██║██║██╔══╝██╔══╝", - "█████║█████║██║██║ ███╗ █████╗", - "██╔══╝██╔██║██║██║ ██╗██╔══╝", - "██║ ██║██║█████║█████║█████╗", - "╚═╝ ╚═╝╚═╝╚════╝╚════╝╚════╝", + "█████╗ ███╗ ██╗██╗█████╗█████╗", + "██╔██║██╔██╗██║██║██╔══╝██╔══╝", + "█████║█████║██║██║ ███╗ █████╗", + "██╔══╝██╔██║██║██║ ██╗██╔══╝", + "██║ ██║██║█████║█████║█████╗", + "╚═╝ ╚═╝╚═╝╚════╝╚════╝╚════╝", }; string[][] TETROMINOS = new[] { - new[]{ - "╭─╮", - "╰─╯", - "╭─╮", - "╰─╯", - "╭─╮", - "╰─╯", - "╭─╮", - "╰─╯" - }, - new[]{ - "╭─╮ ", - "╰─╯ ", - "╭─╮╭─╮╭─╮", - "╰─╯╰─╯╰─╯" - }, - new[]{ - " ╭─╮", - " ╰─╯", - "╭─╮╭─╮╭─╮", - "╰─╯╰─╯╰─╯" - }, - new[]{ - "╭─╮╭─╮", - "╰─╯╰─╯", - "╭─╮╭─╮", - "╰─╯╰─╯" - }, - new[]{ - " ╭─╮╭─╮", - " ╰─╯╰─╯", - "╭─╮╭─╮ ", - "╰─╯╰─╯ " - }, - new[]{ - " ╭─╮ ", - " ╰─╯ ", - "╭─╮╭─╮╭─╮", - "╰─╯╰─╯╰─╯" - }, - new[]{ - "╭─╮╭─╮ ", - "╰─╯╰─╯ ", - " ╭─╮╭─╮", - " ╰─╯╰─╯" - }, + new[]{ + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯" + }, + new[]{ + "╭─╮ ", + "╰─╯ ", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + " ╭─╮", + " ╰─╯", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + "╭─╮╭─╮", + "╰─╯╰─╯", + "╭─╮╭─╮", + "╰─╯╰─╯" + }, + new[]{ + " ╭─╮╭─╮", + " ╰─╯╰─╯", + "╭─╮╭─╮ ", + "╰─╯╰─╯ " + }, + new[]{ + " ╭─╮ ", + " ╰─╯ ", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + "╭─╮╭─╮ ", + "╰─╯╰─╯ ", + " ╭─╮╭─╮", + " ╰─╯╰─╯" + }, }; string[] PLAYFIELD = (string[])FIELD.Clone(); @@ -147,10 +147,10 @@ int INITIALTETROMINOY = 1; Tetromino TETROMINO = new() { - Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - X = INITIALTETROMINOX, - Y = INITIALTETROMINOY + Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + X = INITIALTETROMINOX, + Y = INITIALTETROMINOY }; AutoResetEvent AutoEvent = new AutoResetEvent(false); @@ -184,648 +184,648 @@ while (!CloseGame) { - if (CloseGame) - { - break; - } - - PlayerControl(); - if (GameStatus == GameStatus.Playing) - { - DrawFrame(); - SleepAfterRender(); - } + if (CloseGame) + { + break; + } + + PlayerControl(); + if (GameStatus == GameStatus.Playing) + { + DrawFrame(); + SleepAfterRender(); + } } void PlayerControl() { - while (Console.KeyAvailable && GameStatus == GameStatus.Playing) - { - switch (Console.ReadKey(true).Key) - { - case ConsoleKey.A or ConsoleKey.LeftArrow: - if (Collision(Direction.Left)) break; - TETROMINO.X -= 3; - break; - case ConsoleKey.D or ConsoleKey.RightArrow: - if (Collision(Direction.Right)) break; - TETROMINO.X += 3; - break; - case ConsoleKey.S or ConsoleKey.DownArrow: - FallTimer.Change(0, FallSpeedMilliSeconds); - break; - case ConsoleKey.E: - TetrominoSpin(Direction.Right); - break; - case ConsoleKey.Q: - TetrominoSpin(Direction.Left); - break; - case ConsoleKey.P: - PauseGame(); - break; - case ConsoleKey.R: - TextColor++; - if (TextColor == 16) TextColor = 1; - Console.ForegroundColor = (ConsoleColor)TextColor; - break; - - //DEBUG - case ConsoleKey.Spacebar: - if (!DEBUGCONTROLS) return; - PLAYFIELD = (string[])FIELD.Clone(); - break; - case ConsoleKey.I: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[0]; - break; - case ConsoleKey.J: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[1]; - break; - case ConsoleKey.L: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[2]; - break; - case ConsoleKey.O: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[3]; - break; - case ConsoleKey.C: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[4]; - break; - case ConsoleKey.T: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[5]; - break; - case ConsoleKey.Z: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[6]; - break; - case ConsoleKey.X: - if (!DEBUGCONTROLS) return; - Score += 10; - break; - } - } + while (Console.KeyAvailable && GameStatus == GameStatus.Playing) + { + switch (Console.ReadKey(true).Key) + { + case ConsoleKey.A or ConsoleKey.LeftArrow: + if (Collision(Direction.Left)) break; + TETROMINO.X -= 3; + break; + case ConsoleKey.D or ConsoleKey.RightArrow: + if (Collision(Direction.Right)) break; + TETROMINO.X += 3; + break; + case ConsoleKey.S or ConsoleKey.DownArrow: + FallTimer.Change(0, FallSpeedMilliSeconds); + break; + case ConsoleKey.E: + TetrominoSpin(Direction.Right); + break; + case ConsoleKey.Q: + TetrominoSpin(Direction.Left); + break; + case ConsoleKey.P: + PauseGame(); + break; + case ConsoleKey.R: + TextColor++; + if (TextColor == 16) TextColor = 1; + Console.ForegroundColor = (ConsoleColor)TextColor; + break; + + //DEBUG + case ConsoleKey.Spacebar: + if (!DEBUGCONTROLS) return; + PLAYFIELD = (string[])FIELD.Clone(); + break; + case ConsoleKey.I: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[0]; + break; + case ConsoleKey.J: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[1]; + break; + case ConsoleKey.L: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[2]; + break; + case ConsoleKey.O: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[3]; + break; + case ConsoleKey.C: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[4]; + break; + case ConsoleKey.T: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[5]; + break; + case ConsoleKey.Z: + if (!DEBUGCONTROLS) return; + TETROMINO.Shape = TETROMINOS[6]; + break; + case ConsoleKey.X: + if (!DEBUGCONTROLS) return; + Score += 10; + break; + } + } } void DrawFrame() { - bool collision = false; - int yScope = TETROMINO.Y; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); - char[][] frame = new char[PLAYFIELD.Length][]; - - //Field - for (int y = 0; y < PLAYFIELD.Length; y++) - { - frame[y] = PLAYFIELD[y].ToCharArray(); - } - - //Draw Tetromino - for (int y = 0; y < shapeScope.Length && !collision; y++) - { - for (int x = 0; x < shapeScope[y].Length; x++) - { - int tY = yScope + y; - int tX = TETROMINO.X + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - collision = true; - break; - } - - frame[tY][tX] = charTetromino; - } - } - - //Draw Preview - for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) - { - if (CollisionPreview(yField, yScope, shapeScope)) continue; - - for (int y = 0; y < shapeScope.Length && !collision; y++) - { - for (int x = 0; x < shapeScope[y].Length; x++) - { - int tY = yField + y; - - if (yScope + shapeScope.Length > tY) continue; - - int tX = TETROMINO.X + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - collision = true; - break; - } - - frame[tY][tX] = '•'; - } - } - - break; - } - - //Next Square - for (int y = 0; y < NEXTTETROMINO.Length; y++) - { - frame[y] = frame[y].Concat(NEXTTETROMINO[y]).ToArray(); - } - - //Score Square - for (int y = 0; y < SCORE.Length; y++) - { - int sY = NEXTTETROMINO.Length + y; - frame[sY] = frame[sY].Concat(SCORE[y]).ToArray(); - } - - //Draw Next - for (int y = 0; y < nextShapeScope.Length; y++) - { - for (int x = 0; x < nextShapeScope[y].Length; x++) - { - int tY = y + BORDER; - int tX = PLAYFIELD[y].Length + x + BORDER; - char charTetromino = nextShapeScope[y][x]; - frame[tY][tX] = charTetromino; - } - } - - //Draw Score - char[] score = Score.ToString().ToCharArray(); - for (int scoreX = score.Length - 1; scoreX >= 0; scoreX--) - { - int sY = NEXTTETROMINO.Length + BORDER; - int sX = frame[sY].Length - (score.Length - scoreX) - BORDER; - frame[sY][sX] = score[scoreX]; - } - - //Draw Pause - if (GameStatus == GameStatus.Paused) - { - for (int y = 0; y < PAUSE.Length; y++) - { - int fY = (PLAYFIELD.Length / 2) + y - PAUSE.Length; - for (int x = 0; x < PAUSE[y].Length; x++) - { - int fX = x + BORDER; - - if (x >= PLAYFIELD[fY].Length) break; - - frame[fY][fX] = PAUSE[y][x]; - } - } - } - - //Create Render - StringBuilder render = new(); - for (int y = 0; y < frame.Length; y++) - { - render.AppendLine(new string(frame[y])); - } - - Console.Clear(); - Console.Write(render); - Console.CursorVisible = false; + bool collision = false; + int yScope = TETROMINO.Y; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); + char[][] frame = new char[PLAYFIELD.Length][]; + + //Field + for (int y = 0; y < PLAYFIELD.Length; y++) + { + frame[y] = PLAYFIELD[y].ToCharArray(); + } + + //Draw Tetromino + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = TETROMINO.X + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = charTetromino; + } + } + + //Draw Preview + for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) + { + if (CollisionPreview(yField, yScope, shapeScope)) continue; + + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yField + y; + + if (yScope + shapeScope.Length > tY) continue; + + int tX = TETROMINO.X + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = '•'; + } + } + + break; + } + + //Next Square + for (int y = 0; y < NEXTTETROMINO.Length; y++) + { + frame[y] = frame[y].Concat(NEXTTETROMINO[y]).ToArray(); + } + + //Score Square + for (int y = 0; y < SCORE.Length; y++) + { + int sY = NEXTTETROMINO.Length + y; + frame[sY] = frame[sY].Concat(SCORE[y]).ToArray(); + } + + //Draw Next + for (int y = 0; y < nextShapeScope.Length; y++) + { + for (int x = 0; x < nextShapeScope[y].Length; x++) + { + int tY = y + BORDER; + int tX = PLAYFIELD[y].Length + x + BORDER; + char charTetromino = nextShapeScope[y][x]; + frame[tY][tX] = charTetromino; + } + } + + //Draw Score + char[] score = Score.ToString().ToCharArray(); + for (int scoreX = score.Length - 1; scoreX >= 0; scoreX--) + { + int sY = NEXTTETROMINO.Length + BORDER; + int sX = frame[sY].Length - (score.Length - scoreX) - BORDER; + frame[sY][sX] = score[scoreX]; + } + + //Draw Pause + if (GameStatus == GameStatus.Paused) + { + for (int y = 0; y < PAUSE.Length; y++) + { + int fY = (PLAYFIELD.Length / 2) + y - PAUSE.Length; + for (int x = 0; x < PAUSE[y].Length; x++) + { + int fX = x + BORDER; + + if (x >= PLAYFIELD[fY].Length) break; + + frame[fY][fX] = PAUSE[y][x]; + } + } + } + + //Create Render + StringBuilder render = new(); + for (int y = 0; y < frame.Length; y++) + { + render.AppendLine(new string(frame[y])); + } + + Console.Clear(); + Console.Write(render); + Console.CursorVisible = false; } char[][] DrawLastFrame(int yS) { - bool collision = false; - int yScope = yS - 2; - int xScope = TETROMINO.X; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); - char[][] frame = new char[PLAYFIELD.Length][]; - - //Field - for (int y = 0; y < PLAYFIELD.Length; y++) - { - frame[y] = PLAYFIELD[y].ToCharArray(); - } - - //Draw Tetromino - for (int y = 0; y < shapeScope.Length && !collision; y++) - { - for (int x = 0; x < shapeScope[y].Length; x++) - { - int tY = yScope + y; - int tX = xScope + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - collision = true; - break; - } - - frame[tY][tX] = charTetromino; - } - } - - return frame; + bool collision = false; + int yScope = yS - 2; + int xScope = TETROMINO.X; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); + char[][] frame = new char[PLAYFIELD.Length][]; + + //Field + for (int y = 0; y < PLAYFIELD.Length; y++) + { + frame[y] = PLAYFIELD[y].ToCharArray(); + } + + //Draw Tetromino + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = xScope + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = charTetromino; + } + } + + return frame; } bool Collision(Direction direction) { - int xNew = TETROMINO.X; - int yScope = TETROMINO.Y; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - bool collision = false; - - switch (direction) - { - case Direction.Right: - xNew += 3; - if (xNew + shapeScope[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; - break; - case Direction.Left: - xNew -= 3; - if (xNew < BORDER) collision = true; - break; - case Direction.None: - break; - } - - if (collision) return collision; - - for (int y = 0; y < shapeScope.Length && !collision; y++) - { - for (int x = 0; x < shapeScope[y].Length; x++) - { - int tY = yScope + y; - int tX = xNew + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - collision = true; - break; - } - } - } - - return collision; + int xNew = TETROMINO.X; + int yScope = TETROMINO.Y; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + bool collision = false; + + switch (direction) + { + case Direction.Right: + xNew += 3; + if (xNew + shapeScope[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; + break; + case Direction.Left: + xNew -= 3; + if (xNew < BORDER) collision = true; + break; + case Direction.None: + break; + } + + if (collision) return collision; + + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = xNew + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + } + } + + return collision; } bool CollisionPreview(int initY, int yScope, string[] shape) { - int xNew = TETROMINO.X; - - for (int yUpper = initY; yUpper >= yScope; yUpper -= 2) - { - for (int y = shape.Length - 1; y >= 0; y -= 2) - { - for (int x = 0; x < shape[y].Length; x++) - { - int tY = yUpper + y; - int tX = xNew + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shape[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - return true; - } - } - } - } - - return false; + int xNew = TETROMINO.X; + + for (int yUpper = initY; yUpper >= yScope; yUpper -= 2) + { + for (int y = shape.Length - 1; y >= 0; y -= 2) + { + for (int x = 0; x < shape[y].Length; x++) + { + int tY = yUpper + y; + int tX = xNew + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shape[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + return true; + } + } + } + } + + return false; } void Gameover() { - GameStatus = GameStatus.Gameover; - AutoEvent.Dispose(); - FallTimer.Dispose(); - - SleepAfterRender(); - - Console.Clear(); - Console.WriteLine(); - Console.WriteLine(" ██████╗ █████╗ ██ ██╗█████╗"); - Console.WriteLine(" ██╔════╝ ██╔══██╗███ ███║██╔══╝"); - Console.WriteLine(" ██║ ███╗███████║██╔██═██║█████╗"); - Console.WriteLine(" ██║ ██║██╔══██║██║ ██║██╔══╝"); - Console.WriteLine(" ╚██████╔╝██║ ██║██║ ██║█████╗"); - Console.WriteLine(" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝"); - Console.WriteLine(" ██████╗██╗ ██╗█████╗█████╗ "); - Console.WriteLine(" ██ ██║██║ ██║██╔══╝██╔═██╗ "); - Console.WriteLine(" ██ ██║██║ ██║█████╗█████╔╝ "); - Console.WriteLine(" ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ "); - Console.WriteLine(" ██████║ ╚███╔╝ █████╗██║ ██║ "); - Console.WriteLine(" ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ "); - - Console.WriteLine(); - Console.WriteLine($" Final Score: {Score}"); - Console.WriteLine($" Pause Count: {PauseCount}"); - Console.WriteLine(); - Console.WriteLine(" Press enter to play again"); - Console.WriteLine(" Press escape to close the game"); - Console.CursorVisible = false; - StartGame(); - RestartGame(); + GameStatus = GameStatus.Gameover; + AutoEvent.Dispose(); + FallTimer.Dispose(); + + SleepAfterRender(); + + Console.Clear(); + Console.WriteLine(); + Console.WriteLine(" ██████╗ █████╗ ██ ██╗█████╗"); + Console.WriteLine(" ██╔════╝ ██╔══██╗███ ███║██╔══╝"); + Console.WriteLine(" ██║ ███╗███████║██╔██═██║█████╗"); + Console.WriteLine(" ██║ ██║██╔══██║██║ ██║██╔══╝"); + Console.WriteLine(" ╚██████╔╝██║ ██║██║ ██║█████╗"); + Console.WriteLine(" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝"); + Console.WriteLine(" ██████╗██╗ ██╗█████╗█████╗ "); + Console.WriteLine(" ██ ██║██║ ██║██╔══╝██╔═██╗ "); + Console.WriteLine(" ██ ██║██║ ██║█████╗█████╔╝ "); + Console.WriteLine(" ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ "); + Console.WriteLine(" ██████║ ╚███╔╝ █████╗██║ ██║ "); + Console.WriteLine(" ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ "); + + Console.WriteLine(); + Console.WriteLine($" Final Score: {Score}"); + Console.WriteLine($" Pause Count: {PauseCount}"); + Console.WriteLine(); + Console.WriteLine(" Press enter to play again"); + Console.WriteLine(" Press escape to close the game"); + Console.CursorVisible = false; + StartGame(); + RestartGame(); } void StartGame(ConsoleKey key = ConsoleKey.Enter) { - ConsoleKey input = default; - while (input != key && !CloseGame) - { - input = Console.ReadKey(true).Key; - if (input is ConsoleKey.Escape) - { - CloseGame = true; - return; - } - } + ConsoleKey input = default; + while (input != key && !CloseGame) + { + input = Console.ReadKey(true).Key; + if (input is ConsoleKey.Escape) + { + CloseGame = true; + return; + } + } } void RestartGame() { - PLAYFIELD = (string[])FIELD.Clone(); - FallSpeedMilliSeconds = 1000; - Score = 0; - TETROMINO = new() - { - Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - X = INITIALTETROMINOX, - Y = INITIALTETROMINOY - }; - - AutoEvent = new AutoResetEvent(false); - FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); - GameStatus = GameStatus.Playing; + PLAYFIELD = (string[])FIELD.Clone(); + FallSpeedMilliSeconds = 1000; + Score = 0; + TETROMINO = new() + { + Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + X = INITIALTETROMINOX, + Y = INITIALTETROMINOY + }; + + AutoEvent = new AutoResetEvent(false); + FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); + GameStatus = GameStatus.Playing; } void PauseGame() { - PauseCount++; - FallTimer.Change(Timeout.Infinite, Timeout.Infinite); - GameStatus = GameStatus.Paused; - DrawFrame(); + PauseCount++; + FallTimer.Change(Timeout.Infinite, Timeout.Infinite); + GameStatus = GameStatus.Paused; + DrawFrame(); - ResumeGame(); + ResumeGame(); } void ResumeGame(ConsoleKey key = ConsoleKey.Enter) { - ConsoleKey input = default; - while (input != key && !CloseGame) - { - input = Console.ReadKey(true).Key; - if (input is ConsoleKey.Enter && GameStatus == GameStatus.Paused && FallTimer != null) - { - FallTimer.Change(0, FallSpeedMilliSeconds); - GameStatus = GameStatus.Playing; - return; - } - } + ConsoleKey input = default; + while (input != key && !CloseGame) + { + input = Console.ReadKey(true).Key; + if (input is ConsoleKey.Enter && GameStatus == GameStatus.Paused && FallTimer != null) + { + FallTimer.Change(0, FallSpeedMilliSeconds); + GameStatus = GameStatus.Playing; + return; + } + } } void AddScoreChangeSpeed(int value) { - Score += value; - - if (Score > 100) return; - - switch (Score) - { - case 10: - FallSpeedMilliSeconds = 900; - break; - case 20: - FallSpeedMilliSeconds = 800; - break; - case 30: - FallSpeedMilliSeconds = 700; - break; - case 40: - FallSpeedMilliSeconds = 500; - break; - case 50: - FallSpeedMilliSeconds = 300; - break; - case 60: - FallSpeedMilliSeconds = 200; - break; - case 70: - FallSpeedMilliSeconds = 100; - break; - case 100: - FallSpeedMilliSeconds = 50; - break; - } + Score += value; + + if (Score > 100) return; + + switch (Score) + { + case 10: + FallSpeedMilliSeconds = 900; + break; + case 20: + FallSpeedMilliSeconds = 800; + break; + case 30: + FallSpeedMilliSeconds = 700; + break; + case 40: + FallSpeedMilliSeconds = 500; + break; + case 50: + FallSpeedMilliSeconds = 300; + break; + case 60: + FallSpeedMilliSeconds = 200; + break; + case 70: + FallSpeedMilliSeconds = 100; + break; + case 100: + FallSpeedMilliSeconds = 50; + break; + } } void TetrominoFall(object? e) { - int yAfterFall = TETROMINO.Y; - bool collision = false; - - if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) yAfterFall = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; - else yAfterFall += 2; - - //Y Collision - for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) - { - for (int yCollision = TETROMINO.Shape.Length - 1; yCollision >= 0; yCollision -= 2) - { - char exist = TETROMINO.Shape[yCollision][xCollision]; - - if (exist == ' ') continue; - - char[] lineYC = PLAYFIELD[yAfterFall + yCollision - 1].ToCharArray(); - - if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; - - if - ( - lineYC[TETROMINO.X + xCollision] != ' ' && - lineYC[TETROMINO.X + xCollision] != '│' - ) - { - char[][] lastFrame = DrawLastFrame(yAfterFall); - for (int y = 0; y < lastFrame.Length; y++) - { - PLAYFIELD[y] = new string(lastFrame[y]); - } - - TETROMINO.X = INITIALTETROMINOX; - TETROMINO.Y = INITIALTETROMINOY; - TETROMINO.Shape = TETROMINO.Next; - TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; - - xCollision = TETROMINO.Shape[0].Length; - collision = true; - break; - } - } - - xCollision += 3; - } - - if (!collision) TETROMINO.Y = yAfterFall; - - //Clean Lines - for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) - { - string line = PLAYFIELD[lineIndex]; - bool notCompleted = line.Any(e => e == ' '); - - if (lineIndex == 0 || lineIndex == PLAYFIELD.Length - 1) continue; - - if (!notCompleted) - { - PLAYFIELD[lineIndex] = "│ │"; - AddScoreChangeSpeed(1); - - for (int lineM = lineIndex; lineM >= 1; lineM--) - { - if (PLAYFIELD[lineM - 1] == "╭──────────────────────────────╮") - { - PLAYFIELD[lineM] = "│ │"; - continue; - } - - PLAYFIELD[lineM] = PLAYFIELD[lineM - 1]; - } - - lineIndex++; - } - } - - //VerifiedCollision - if (Collision(Direction.None) && FallTimer != null) Gameover(); + int yAfterFall = TETROMINO.Y; + bool collision = false; + + if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) yAfterFall = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; + else yAfterFall += 2; + + //Y Collision + for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) + { + for (int yCollision = TETROMINO.Shape.Length - 1; yCollision >= 0; yCollision -= 2) + { + char exist = TETROMINO.Shape[yCollision][xCollision]; + + if (exist == ' ') continue; + + char[] lineYC = PLAYFIELD[yAfterFall + yCollision - 1].ToCharArray(); + + if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; + + if + ( + lineYC[TETROMINO.X + xCollision] != ' ' && + lineYC[TETROMINO.X + xCollision] != '│' + ) + { + char[][] lastFrame = DrawLastFrame(yAfterFall); + for (int y = 0; y < lastFrame.Length; y++) + { + PLAYFIELD[y] = new string(lastFrame[y]); + } + + TETROMINO.X = INITIALTETROMINOX; + TETROMINO.Y = INITIALTETROMINOY; + TETROMINO.Shape = TETROMINO.Next; + TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; + + xCollision = TETROMINO.Shape[0].Length; + collision = true; + break; + } + } + + xCollision += 3; + } + + if (!collision) TETROMINO.Y = yAfterFall; + + //Clean Lines + for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) + { + string line = PLAYFIELD[lineIndex]; + bool notCompleted = line.Any(e => e == ' '); + + if (lineIndex == 0 || lineIndex == PLAYFIELD.Length - 1) continue; + + if (!notCompleted) + { + PLAYFIELD[lineIndex] = "│ │"; + AddScoreChangeSpeed(1); + + for (int lineM = lineIndex; lineM >= 1; lineM--) + { + if (PLAYFIELD[lineM - 1] == "╭──────────────────────────────╮") + { + PLAYFIELD[lineM] = "│ │"; + continue; + } + + PLAYFIELD[lineM] = PLAYFIELD[lineM - 1]; + } + + lineIndex++; + } + } + + //VerifiedCollision + if (Collision(Direction.None) && FallTimer != null) Gameover(); } void TetrominoSpin(Direction spinDirection) { - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - int yScope = TETROMINO.Y; - string[] newShape = new string[shapeScope[0].Length / 3 * 2]; - int newY = 0; - int rowEven = 0; - int rowOdd = 1; - - //Turn - for (int y = 0; y < shapeScope.Length;) - { - switch (spinDirection) - { - case Direction.Right: - SpinRight(newShape, shapeScope, ref newY, rowEven, rowOdd, y); - break; - case Direction.Left: - SpinLeft(newShape, shapeScope, ref newY, rowEven, rowOdd, y); - break; - } - - newY = 0; - rowEven += 2; - rowOdd += 2; - y += 2; - } - - //Verified Collision - for (int y = 0; y < newShape.Length - 1; y++) - { - for (int x = 0; x < newShape[y].Length; x++) - { - if (newShape[y][x] == ' ') continue; - - char c = PLAYFIELD[yScope + y][TETROMINO.X + x]; - if (c != ' ') return; - } - } - - TETROMINO.Shape = newShape; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + int yScope = TETROMINO.Y; + string[] newShape = new string[shapeScope[0].Length / 3 * 2]; + int newY = 0; + int rowEven = 0; + int rowOdd = 1; + + //Turn + for (int y = 0; y < shapeScope.Length;) + { + switch (spinDirection) + { + case Direction.Right: + SpinRight(newShape, shapeScope, ref newY, rowEven, rowOdd, y); + break; + case Direction.Left: + SpinLeft(newShape, shapeScope, ref newY, rowEven, rowOdd, y); + break; + } + + newY = 0; + rowEven += 2; + rowOdd += 2; + y += 2; + } + + //Verified Collision + for (int y = 0; y < newShape.Length - 1; y++) + { + for (int x = 0; x < newShape[y].Length; x++) + { + if (newShape[y][x] == ' ') continue; + + char c = PLAYFIELD[yScope + y][TETROMINO.X + x]; + if (c != ' ') return; + } + } + + TETROMINO.Shape = newShape; } void SpinLeft(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) { - for (int x = shape[y].Length - 1; x >= 0; x -= 3) - { - for (int xS = 2; xS >= 0; xS--) - { - newShape[newY] += shape[rowEven][x - xS]; - newShape[newY + 1] += shape[rowOdd][x - xS]; - } - - newY += 2; - } + for (int x = shape[y].Length - 1; x >= 0; x -= 3) + { + for (int xS = 2; xS >= 0; xS--) + { + newShape[newY] += shape[rowEven][x - xS]; + newShape[newY + 1] += shape[rowOdd][x - xS]; + } + + newY += 2; + } } void SpinRight(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) { - for (int x = 2; x < shape[y].Length; x += 3) - { - if (newShape[newY] == null) - { - newShape[newY] = ""; - newShape[newY + 1] = ""; - } - - for (int xS = 0; xS <= 2; xS++) - { - newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString()); - newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString()); - } - - newY += 2; - } + for (int x = 2; x < shape[y].Length; x += 3) + { + if (newShape[newY] == null) + { + newShape[newY] = ""; + newShape[newY + 1] = ""; + } + + for (int xS = 0; xS <= 2; xS++) + { + newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString()); + newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString()); + } + + newY += 2; + } } void SleepAfterRender() { - TimeSpan sleep = TimeSpan.FromSeconds(1d / 60d) - Stopwatch.Elapsed; - if (sleep > TimeSpan.Zero) - { - Thread.Sleep(sleep); - } - Stopwatch.Restart(); + TimeSpan sleep = TimeSpan.FromSeconds(1d / 60d) - Stopwatch.Elapsed; + if (sleep > TimeSpan.Zero) + { + Thread.Sleep(sleep); + } + Stopwatch.Restart(); } class Tetromino { - public required string[] Shape { get; set; } - public required string[] Next { get; set; } - public int X { get; set; } - public int Y { get; set; } + public required string[] Shape { get; set; } + public required string[] Next { get; set; } + public int X { get; set; } + public int Y { get; set; } } enum Direction { - Right, - Left, - None + Right, + Left, + None } enum GameStatus { - Gameover, - Playing, - Paused + Gameover, + Playing, + Paused } \ No newline at end of file diff --git a/Projects/Tetris/Tetris.csproj b/Projects/Tetris/Tetris.csproj index f02677bf..1a40ec66 100644 --- a/Projects/Tetris/Tetris.csproj +++ b/Projects/Tetris/Tetris.csproj @@ -1,10 +1,8 @@ - - - Exe - net7.0 - enable - enable - - + + Exe + net7.0 + enable + enable + diff --git a/Projects/Website/Games/Tetris/Tetris.cs b/Projects/Website/Games/Tetris/Tetris.cs index a973393e..00563e63 100644 --- a/Projects/Website/Games/Tetris/Tetris.cs +++ b/Projects/Website/Games/Tetris/Tetris.cs @@ -9,790 +9,790 @@ namespace Website.Games.Tetris; public class Tetris { - public readonly BlazorConsole Console = new(); - - public async Task Run() - { - Console.OutputEncoding = Encoding.UTF8; - Console.CursorVisible = false; - Stopwatch Stopwatch = Stopwatch.StartNew(); - - string[] FIELD = new[] - { - "╭──────────────────────────────╮", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰──────────────────────────────╯" - }; - - string[] NEXTTETROMINO = new[] - { - "╭─────────╮", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰─────────╯" - }; - - string[] SCORE = new[]{ - "╭─────────╮", - "│ │", - "╰─────────╯" - }; - - string[] PAUSE = new[]{ - "█████╗ ███╗ ██╗██╗█████╗█████╗", - "██╔██║██╔██╗██║██║██╔══╝██╔══╝", - "█████║█████║██║██║ ███╗ █████╗", - "██╔══╝██╔██║██║██║ ██╗██╔══╝", - "██║ ██║██║█████║█████║█████╗", - "╚═╝ ╚═╝╚═╝╚════╝╚════╝╚════╝", - }; - - string[][] TETROMINOS = new[] - { - new[]{ - "╭─╮", - "╰─╯", - "╭─╮", - "╰─╯", - "╭─╮", - "╰─╯", - "╭─╮", - "╰─╯" - }, - new[]{ - "╭─╮ ", - "╰─╯ ", - "╭─╮╭─╮╭─╮", - "╰─╯╰─╯╰─╯" - }, - new[]{ - " ╭─╮", - " ╰─╯", - "╭─╮╭─╮╭─╮", - "╰─╯╰─╯╰─╯" - }, - new[]{ - "╭─╮╭─╮", - "╰─╯╰─╯", - "╭─╮╭─╮", - "╰─╯╰─╯" - }, - new[]{ - " ╭─╮╭─╮", - " ╰─╯╰─╯", - "╭─╮╭─╮ ", - "╰─╯╰─╯ " - }, - new[]{ - " ╭─╮ ", - " ╰─╯ ", - "╭─╮╭─╮╭─╮", - "╰─╯╰─╯╰─╯" - }, - new[]{ - "╭─╮╭─╮ ", - "╰─╯╰─╯ ", - " ╭─╮╭─╮", - " ╰─╯╰─╯" - }, - }; - - string[] PLAYFIELD = (string[])FIELD.Clone(); - const int BORDER = 1; - int FallSpeedMilliSeconds = 1000; - bool CloseGame = false; - int Score = 0; - int PauseCount = 0; - GameStatus GameStatus = GameStatus.Gameover; - - Random RamdomGenerator = new(); - - int INITIALTETROMINOX = Convert.ToInt16(PLAYFIELD[0].Length / 2) - 3; - int INITIALTETROMINOY = 1; - Tetromino TETROMINO = new() - { - Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - X = INITIALTETROMINOX, - Y = INITIALTETROMINOY - }; - - AutoResetEvent AutoEvent = new AutoResetEvent(false); - Timer? FallTimer = null; - GameStatus = GameStatus.Playing; - - await Console.WriteLine(); - await Console.WriteLine(" ██████╗█████╗██████╗█████╗ ██╗█████╗"); - await Console.WriteLine(" ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝"); - await Console.WriteLine(" ██║ █████╗ ██║ █████╔╝██║ ███╗ "); - await Console.WriteLine(" ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗"); - await Console.WriteLine(" ██║ █████╗ ██║ ██║ ██║██║█████║"); - await Console.WriteLine(" ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝"); - - await Console.WriteLine(); - await Console.WriteLine(" Controls:"); - await Console.WriteLine(" WASD or ARROW to move"); - await Console.WriteLine(" Q or E to spin left or right"); - await Console.WriteLine(" P to paused the game, press enter"); - await Console.WriteLine(" key to resume"); - await Console.WriteLine(); - await Console.Write(" Press enter to start tetris..."); - Console.CursorVisible = false; - await StartGame(); - await Console.Clear(); - - FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); - - while (!CloseGame) - { - if (CloseGame) - { - break; - } - - await PlayerControl(); - if (GameStatus == GameStatus.Playing) - { - await DrawFrame(); - await SleepAfterRender(); - } - } - - async Task PlayerControl() - { - while (await Console.KeyAvailable() && GameStatus == GameStatus.Playing) - { - switch ((await Console.ReadKey(true)).Key) - { - case ConsoleKey.A or ConsoleKey.LeftArrow: - if (Collision(Direction.Left)) break; - TETROMINO.X -= 3; - break; - case ConsoleKey.D or ConsoleKey.RightArrow: - if (Collision(Direction.Right)) break; - TETROMINO.X += 3; - break; - case ConsoleKey.S or ConsoleKey.DownArrow: - FallTimer.Change(0, FallSpeedMilliSeconds); - break; - case ConsoleKey.E: - TetrominoSpin(Direction.Right); - break; - case ConsoleKey.Q: - TetrominoSpin(Direction.Left); - break; - case ConsoleKey.P: - PauseGame(); - break; - } - } - } - - async Task DrawFrame() - { - bool collision = false; - int yScope = TETROMINO.Y; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); - char[][] frame = new char[PLAYFIELD.Length][]; - - //Field - for (int y = 0; y < PLAYFIELD.Length; y++) - { - frame[y] = PLAYFIELD[y].ToCharArray(); - } - - //Draw Tetromino - for (int y = 0; y < shapeScope.Length && !collision; y++) - { - for (int x = 0; x < shapeScope[y].Length; x++) - { - int tY = yScope + y; - int tX = TETROMINO.X + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - collision = true; - break; - } - - frame[tY][tX] = charTetromino; - } - } - - //Draw Preview - for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) - { - if (CollisionPreview(yField, yScope, shapeScope)) continue; - - for (int y = 0; y < shapeScope.Length && !collision; y++) - { - for (int x = 0; x < shapeScope[y].Length; x++) - { - int tY = yField + y; - - if (yScope + shapeScope.Length > tY) continue; - - int tX = TETROMINO.X + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - collision = true; - break; - } - - frame[tY][tX] = '•'; - } - } - - break; - } - - //Next Square - for (int y = 0; y < NEXTTETROMINO.Length; y++) - { - frame[y] = frame[y].Concat(NEXTTETROMINO[y]).ToArray(); - } - - //Score Square - for (int y = 0; y < SCORE.Length; y++) - { - int sY = NEXTTETROMINO.Length + y; - frame[sY] = frame[sY].Concat(SCORE[y]).ToArray(); - } - - //Draw Next - for (int y = 0; y < nextShapeScope.Length; y++) - { - for (int x = 0; x < nextShapeScope[y].Length; x++) - { - int tY = y + BORDER; - int tX = PLAYFIELD[y].Length + x + BORDER; - char charTetromino = nextShapeScope[y][x]; - frame[tY][tX] = charTetromino; - } - } - - //Draw Score - char[] score = Score.ToString().ToCharArray(); - for (int scoreX = score.Length - 1; scoreX >= 0; scoreX--) - { - int sY = NEXTTETROMINO.Length + BORDER; - int sX = frame[sY].Length - (score.Length - scoreX) - BORDER; - frame[sY][sX] = score[scoreX]; - } - - //Draw Pause - if (GameStatus == GameStatus.Paused) - { - for (int y = 0; y < PAUSE.Length; y++) - { - int fY = (PLAYFIELD.Length / 2) + y - PAUSE.Length; - for (int x = 0; x < PAUSE[y].Length; x++) - { - int fX = x + BORDER; - - if (x >= PLAYFIELD[fY].Length) break; - - frame[fY][fX] = PAUSE[y][x]; - } - } - } - - //Create Render - StringBuilder render = new(); - for (int y = 0; y < frame.Length; y++) - { - render.AppendLine(new string(frame[y])); - } - - await Console.Clear(); - await Console.Write(render); - Console.CursorVisible = false; - } - - char[][] DrawLastFrame(int yS) - { - bool collision = false; - int yScope = yS - 2; - int xScope = TETROMINO.X; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); - char[][] frame = new char[PLAYFIELD.Length][]; - - //Field - for (int y = 0; y < PLAYFIELD.Length; y++) - { - frame[y] = PLAYFIELD[y].ToCharArray(); - } - - //Draw Tetromino - for (int y = 0; y < shapeScope.Length && !collision; y++) - { - for (int x = 0; x < shapeScope[y].Length; x++) - { - int tY = yScope + y; - int tX = xScope + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - collision = true; - break; - } - - frame[tY][tX] = charTetromino; - } - } - - return frame; - } - - bool Collision(Direction direction) - { - int xNew = TETROMINO.X; - int yScope = TETROMINO.Y; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - bool collision = false; - - switch (direction) - { - case Direction.Right: - xNew += 3; - if (xNew + shapeScope[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; - break; - case Direction.Left: - xNew -= 3; - if (xNew < BORDER) collision = true; - break; - case Direction.None: - break; - } - - if (collision) return collision; - - for (int y = 0; y < shapeScope.Length && !collision; y++) - { - for (int x = 0; x < shapeScope[y].Length; x++) - { - int tY = yScope + y; - int tX = xNew + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - collision = true; - break; - } - } - } - - return collision; - } - - bool CollisionPreview(int initY, int yScope, string[] shape) - { - int xNew = TETROMINO.X; - - for (int yUpper = initY; yUpper >= yScope; yUpper -= 2) - { - for (int y = shape.Length - 1; y >= 0; y -= 2) - { - for (int x = 0; x < shape[y].Length; x++) - { - int tY = yUpper + y; - int tX = xNew + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shape[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') - { - return true; - } - } - } - } - - return false; - } - - async void Gameover() - { - GameStatus = GameStatus.Gameover; - AutoEvent.Dispose(); - FallTimer.Dispose(); - - await SleepAfterRender(); - - await Console.Clear(); - await Console.WriteLine(); - await Console.WriteLine(" ██████╗ █████╗ ██ ██╗█████╗"); - await Console.WriteLine(" ██╔════╝ ██╔══██╗███ ███║██╔══╝"); - await Console.WriteLine(" ██║ ███╗███████║██╔██═██║█████╗"); - await Console.WriteLine(" ██║ ██║██╔══██║██║ ██║██╔══╝"); - await Console.WriteLine(" ╚██████╔╝██║ ██║██║ ██║█████╗"); - await Console.WriteLine(" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝"); - await Console.WriteLine(" ██████╗██╗ ██╗█████╗█████╗ "); - await Console.WriteLine(" ██ ██║██║ ██║██╔══╝██╔═██╗ "); - await Console.WriteLine(" ██ ██║██║ ██║█████╗█████╔╝ "); - await Console.WriteLine(" ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ "); - await Console.WriteLine(" ██████║ ╚███╔╝ █████╗██║ ██║ "); - await Console.WriteLine(" ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ "); - - await Console.WriteLine(); - await Console.WriteLine($" Final Score: {Score}"); - await Console.WriteLine($" Pause Count: {PauseCount}"); - await Console.WriteLine(); - await Console.WriteLine(" Press enter to play again"); - await Console.WriteLine(" Press escape to close the game"); - Console.CursorVisible = false; - await StartGame(); - RestartGame(); - } - - async Task StartGame(ConsoleKey key = ConsoleKey.Enter) - { - ConsoleKey input = default; - while (input != key && !CloseGame) - { - input = (await Console.ReadKey(true)).Key; - if (input is ConsoleKey.Escape) - { - CloseGame = true; - return; - } - } - } - - void RestartGame() - { - PLAYFIELD = (string[])FIELD.Clone(); - FallSpeedMilliSeconds = 1000; - Score = 0; - TETROMINO = new() - { - Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - X = INITIALTETROMINOX, - Y = INITIALTETROMINOY - }; - - AutoEvent = new AutoResetEvent(false); - FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); - GameStatus = GameStatus.Playing; - } - - async void PauseGame() - { - PauseCount++; - FallTimer.Change(Timeout.Infinite, Timeout.Infinite); - GameStatus = GameStatus.Paused; - await DrawFrame(); - - await ResumeGame(); - } - - async Task ResumeGame(ConsoleKey key = ConsoleKey.Enter) - { - ConsoleKey input = default; - while (input != key && !CloseGame) - { - input = (await Console.ReadKey(true)).Key; - if (input is ConsoleKey.Enter && GameStatus == GameStatus.Paused && FallTimer != null) - { - FallTimer.Change(0, FallSpeedMilliSeconds); - GameStatus = GameStatus.Playing; - return; - } - } - } - - void AddScoreChangeSpeed(int value) - { - Score += value; - - if (Score > 100) return; - - switch (Score) - { - case 10: - FallSpeedMilliSeconds = 900; - break; - case 20: - FallSpeedMilliSeconds = 800; - break; - case 30: - FallSpeedMilliSeconds = 700; - break; - case 40: - FallSpeedMilliSeconds = 500; - break; - case 50: - FallSpeedMilliSeconds = 300; - break; - case 60: - FallSpeedMilliSeconds = 200; - break; - case 70: - FallSpeedMilliSeconds = 100; - break; - case 100: - FallSpeedMilliSeconds = 50; - break; - } - } - - void TetrominoFall(object? e) - { - int yAfterFall = TETROMINO.Y; - bool collision = false; - - if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) yAfterFall = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; - else yAfterFall += 2; - - //Y Collision - for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) - { - for (int yCollision = TETROMINO.Shape.Length - 1; yCollision >= 0; yCollision -= 2) - { - char exist = TETROMINO.Shape[yCollision][xCollision]; - - if (exist == ' ') continue; - - char[] lineYC = PLAYFIELD[yAfterFall + yCollision - 1].ToCharArray(); - - if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; - - if - ( - lineYC[TETROMINO.X + xCollision] != ' ' && - lineYC[TETROMINO.X + xCollision] != '│' - ) - { - char[][] lastFrame = DrawLastFrame(yAfterFall); - for (int y = 0; y < lastFrame.Length; y++) - { - PLAYFIELD[y] = new string(lastFrame[y]); - } - - TETROMINO.X = INITIALTETROMINOX; - TETROMINO.Y = INITIALTETROMINOY; - TETROMINO.Shape = TETROMINO.Next; - TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; - - xCollision = TETROMINO.Shape[0].Length; - collision = true; - break; - } - } - - xCollision += 3; - } - - if (!collision) TETROMINO.Y = yAfterFall; - - //Clean Lines - for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) - { - string line = PLAYFIELD[lineIndex]; - bool notCompleted = line.Any(e => e == ' '); - - if (lineIndex == 0 || lineIndex == PLAYFIELD.Length - 1) continue; - - if (!notCompleted) - { - PLAYFIELD[lineIndex] = "│ │"; - AddScoreChangeSpeed(1); - - for (int lineM = lineIndex; lineM >= 1; lineM--) - { - if (PLAYFIELD[lineM - 1] == "╭──────────────────────────────╮") - { - PLAYFIELD[lineM] = "│ │"; - continue; - } - - PLAYFIELD[lineM] = PLAYFIELD[lineM - 1]; - } - - lineIndex++; - } - } - - //VerifiedCollision - if (Collision(Direction.None) && FallTimer != null) Gameover(); - } - - void TetrominoSpin(Direction spinDirection) - { - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - int yScope = TETROMINO.Y; - string[] newShape = new string[shapeScope[0].Length / 3 * 2]; - int newY = 0; - int rowEven = 0; - int rowOdd = 1; - - //Turn - for (int y = 0; y < shapeScope.Length;) - { - switch (spinDirection) - { - case Direction.Right: - SpinRight(newShape, shapeScope, ref newY, rowEven, rowOdd, y); - break; - case Direction.Left: - SpinLeft(newShape, shapeScope, ref newY, rowEven, rowOdd, y); - break; - } - - newY = 0; - rowEven += 2; - rowOdd += 2; - y += 2; - } - - //Verified Collision - for (int y = 0; y < newShape.Length - 1; y++) - { - for (int x = 0; x < newShape[y].Length; x++) - { - if (newShape[y][x] == ' ') continue; - - char c = PLAYFIELD[yScope + y][TETROMINO.X + x]; - if (c != ' ') return; - } - } - - TETROMINO.Shape = newShape; - } - - void SpinLeft(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) - { - for (int x = shape[y].Length - 1; x >= 0; x -= 3) - { - for (int xS = 2; xS >= 0; xS--) - { - newShape[newY] += shape[rowEven][x - xS]; - newShape[newY + 1] += shape[rowOdd][x - xS]; - } - - newY += 2; - } - } - - void SpinRight(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) - { - for (int x = 2; x < shape[y].Length; x += 3) - { - if (newShape[newY] == null) - { - newShape[newY] = ""; - newShape[newY + 1] = ""; - } - - for (int xS = 0; xS <= 2; xS++) - { - newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString()); - newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString()); - } - - newY += 2; - } - } - - async Task SleepAfterRender() - { - TimeSpan sleep = TimeSpan.FromSeconds(1d / 60d) - Stopwatch.Elapsed; - if (sleep > TimeSpan.Zero) - { - await Console.RefreshAndDelay(sleep); - } - Stopwatch.Restart(); - } - - } - - class Tetromino - { - public required string[] Shape { get; set; } - public required string[] Next { get; set; } - public int X { get; set; } - public int Y { get; set; } - } - - enum Direction - { - Right, - Left, - None - } - - enum GameStatus - { - Gameover, - Playing, - Paused - } + public readonly BlazorConsole Console = new(); + + public async Task Run() + { + Console.OutputEncoding = Encoding.UTF8; + Console.CursorVisible = false; + Stopwatch Stopwatch = Stopwatch.StartNew(); + + string[] FIELD = new[] + { + "╭──────────────────────────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯" + }; + + string[] NEXTTETROMINO = new[] + { + "╭─────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰─────────╯" + }; + + string[] SCORE = new[]{ + "╭─────────╮", + "│ │", + "╰─────────╯" + }; + + string[] PAUSE = new[]{ + "█████╗ ███╗ ██╗██╗█████╗█████╗", + "██╔██║██╔██╗██║██║██╔══╝██╔══╝", + "█████║█████║██║██║ ███╗ █████╗", + "██╔══╝██╔██║██║██║ ██╗██╔══╝", + "██║ ██║██║█████║█████║█████╗", + "╚═╝ ╚═╝╚═╝╚════╝╚════╝╚════╝", + }; + + string[][] TETROMINOS = new[] + { + new[]{ + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯", + "╭─╮", + "╰─╯" + }, + new[]{ + "╭─╮ ", + "╰─╯ ", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + " ╭─╮", + " ╰─╯", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + "╭─╮╭─╮", + "╰─╯╰─╯", + "╭─╮╭─╮", + "╰─╯╰─╯" + }, + new[]{ + " ╭─╮╭─╮", + " ╰─╯╰─╯", + "╭─╮╭─╮ ", + "╰─╯╰─╯ " + }, + new[]{ + " ╭─╮ ", + " ╰─╯ ", + "╭─╮╭─╮╭─╮", + "╰─╯╰─╯╰─╯" + }, + new[]{ + "╭─╮╭─╮ ", + "╰─╯╰─╯ ", + " ╭─╮╭─╮", + " ╰─╯╰─╯" + }, + }; + + string[] PLAYFIELD = (string[])FIELD.Clone(); + const int BORDER = 1; + int FallSpeedMilliSeconds = 1000; + bool CloseGame = false; + int Score = 0; + int PauseCount = 0; + GameStatus GameStatus = GameStatus.Gameover; + + Random RamdomGenerator = new(); + + int INITIALTETROMINOX = Convert.ToInt16(PLAYFIELD[0].Length / 2) - 3; + int INITIALTETROMINOY = 1; + Tetromino TETROMINO = new() + { + Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + X = INITIALTETROMINOX, + Y = INITIALTETROMINOY + }; + + AutoResetEvent AutoEvent = new AutoResetEvent(false); + Timer? FallTimer = null; + GameStatus = GameStatus.Playing; + + await Console.WriteLine(); + await Console.WriteLine(" ██████╗█████╗██████╗█████╗ ██╗█████╗"); + await Console.WriteLine(" ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝"); + await Console.WriteLine(" ██║ █████╗ ██║ █████╔╝██║ ███╗ "); + await Console.WriteLine(" ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗"); + await Console.WriteLine(" ██║ █████╗ ██║ ██║ ██║██║█████║"); + await Console.WriteLine(" ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝"); + + await Console.WriteLine(); + await Console.WriteLine(" Controls:"); + await Console.WriteLine(" WASD or ARROW to move"); + await Console.WriteLine(" Q or E to spin left or right"); + await Console.WriteLine(" P to paused the game, press enter"); + await Console.WriteLine(" key to resume"); + await Console.WriteLine(); + await Console.Write(" Press enter to start tetris..."); + Console.CursorVisible = false; + await StartGame(); + await Console.Clear(); + + FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); + + while (!CloseGame) + { + if (CloseGame) + { + break; + } + + await PlayerControl(); + if (GameStatus == GameStatus.Playing) + { + await DrawFrame(); + await SleepAfterRender(); + } + } + + async Task PlayerControl() + { + while (await Console.KeyAvailable() && GameStatus == GameStatus.Playing) + { + switch ((await Console.ReadKey(true)).Key) + { + case ConsoleKey.A or ConsoleKey.LeftArrow: + if (Collision(Direction.Left)) break; + TETROMINO.X -= 3; + break; + case ConsoleKey.D or ConsoleKey.RightArrow: + if (Collision(Direction.Right)) break; + TETROMINO.X += 3; + break; + case ConsoleKey.S or ConsoleKey.DownArrow: + FallTimer.Change(0, FallSpeedMilliSeconds); + break; + case ConsoleKey.E: + TetrominoSpin(Direction.Right); + break; + case ConsoleKey.Q: + TetrominoSpin(Direction.Left); + break; + case ConsoleKey.P: + PauseGame(); + break; + } + } + } + + async Task DrawFrame() + { + bool collision = false; + int yScope = TETROMINO.Y; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); + char[][] frame = new char[PLAYFIELD.Length][]; + + //Field + for (int y = 0; y < PLAYFIELD.Length; y++) + { + frame[y] = PLAYFIELD[y].ToCharArray(); + } + + //Draw Tetromino + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = TETROMINO.X + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = charTetromino; + } + } + + //Draw Preview + for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) + { + if (CollisionPreview(yField, yScope, shapeScope)) continue; + + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yField + y; + + if (yScope + shapeScope.Length > tY) continue; + + int tX = TETROMINO.X + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = '•'; + } + } + + break; + } + + //Next Square + for (int y = 0; y < NEXTTETROMINO.Length; y++) + { + frame[y] = frame[y].Concat(NEXTTETROMINO[y]).ToArray(); + } + + //Score Square + for (int y = 0; y < SCORE.Length; y++) + { + int sY = NEXTTETROMINO.Length + y; + frame[sY] = frame[sY].Concat(SCORE[y]).ToArray(); + } + + //Draw Next + for (int y = 0; y < nextShapeScope.Length; y++) + { + for (int x = 0; x < nextShapeScope[y].Length; x++) + { + int tY = y + BORDER; + int tX = PLAYFIELD[y].Length + x + BORDER; + char charTetromino = nextShapeScope[y][x]; + frame[tY][tX] = charTetromino; + } + } + + //Draw Score + char[] score = Score.ToString().ToCharArray(); + for (int scoreX = score.Length - 1; scoreX >= 0; scoreX--) + { + int sY = NEXTTETROMINO.Length + BORDER; + int sX = frame[sY].Length - (score.Length - scoreX) - BORDER; + frame[sY][sX] = score[scoreX]; + } + + //Draw Pause + if (GameStatus == GameStatus.Paused) + { + for (int y = 0; y < PAUSE.Length; y++) + { + int fY = (PLAYFIELD.Length / 2) + y - PAUSE.Length; + for (int x = 0; x < PAUSE[y].Length; x++) + { + int fX = x + BORDER; + + if (x >= PLAYFIELD[fY].Length) break; + + frame[fY][fX] = PAUSE[y][x]; + } + } + } + + //Create Render + StringBuilder render = new(); + for (int y = 0; y < frame.Length; y++) + { + render.AppendLine(new string(frame[y])); + } + + await Console.Clear(); + await Console.Write(render); + Console.CursorVisible = false; + } + + char[][] DrawLastFrame(int yS) + { + bool collision = false; + int yScope = yS - 2; + int xScope = TETROMINO.X; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); + char[][] frame = new char[PLAYFIELD.Length][]; + + //Field + for (int y = 0; y < PLAYFIELD.Length; y++) + { + frame[y] = PLAYFIELD[y].ToCharArray(); + } + + //Draw Tetromino + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = xScope + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + + frame[tY][tX] = charTetromino; + } + } + + return frame; + } + + bool Collision(Direction direction) + { + int xNew = TETROMINO.X; + int yScope = TETROMINO.Y; + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + bool collision = false; + + switch (direction) + { + case Direction.Right: + xNew += 3; + if (xNew + shapeScope[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; + break; + case Direction.Left: + xNew -= 3; + if (xNew < BORDER) collision = true; + break; + case Direction.None: + break; + } + + if (collision) return collision; + + for (int y = 0; y < shapeScope.Length && !collision; y++) + { + for (int x = 0; x < shapeScope[y].Length; x++) + { + int tY = yScope + y; + int tX = xNew + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shapeScope[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + collision = true; + break; + } + } + } + + return collision; + } + + bool CollisionPreview(int initY, int yScope, string[] shape) + { + int xNew = TETROMINO.X; + + for (int yUpper = initY; yUpper >= yScope; yUpper -= 2) + { + for (int y = shape.Length - 1; y >= 0; y -= 2) + { + for (int x = 0; x < shape[y].Length; x++) + { + int tY = yUpper + y; + int tX = xNew + x; + char charToReplace = PLAYFIELD[tY][tX]; + char charTetromino = shape[y][x]; + + if (charTetromino == ' ') continue; + + if (charToReplace != ' ') + { + return true; + } + } + } + } + + return false; + } + + async void Gameover() + { + GameStatus = GameStatus.Gameover; + AutoEvent.Dispose(); + FallTimer.Dispose(); + + await SleepAfterRender(); + + await Console.Clear(); + await Console.WriteLine(); + await Console.WriteLine(" ██████╗ █████╗ ██ ██╗█████╗"); + await Console.WriteLine(" ██╔════╝ ██╔══██╗███ ███║██╔══╝"); + await Console.WriteLine(" ██║ ███╗███████║██╔██═██║█████╗"); + await Console.WriteLine(" ██║ ██║██╔══██║██║ ██║██╔══╝"); + await Console.WriteLine(" ╚██████╔╝██║ ██║██║ ██║█████╗"); + await Console.WriteLine(" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝"); + await Console.WriteLine(" ██████╗██╗ ██╗█████╗█████╗ "); + await Console.WriteLine(" ██ ██║██║ ██║██╔══╝██╔═██╗ "); + await Console.WriteLine(" ██ ██║██║ ██║█████╗█████╔╝ "); + await Console.WriteLine(" ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ "); + await Console.WriteLine(" ██████║ ╚███╔╝ █████╗██║ ██║ "); + await Console.WriteLine(" ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ "); + + await Console.WriteLine(); + await Console.WriteLine($" Final Score: {Score}"); + await Console.WriteLine($" Pause Count: {PauseCount}"); + await Console.WriteLine(); + await Console.WriteLine(" Press enter to play again"); + await Console.WriteLine(" Press escape to close the game"); + Console.CursorVisible = false; + await StartGame(); + RestartGame(); + } + + async Task StartGame(ConsoleKey key = ConsoleKey.Enter) + { + ConsoleKey input = default; + while (input != key && !CloseGame) + { + input = (await Console.ReadKey(true)).Key; + if (input is ConsoleKey.Escape) + { + CloseGame = true; + return; + } + } + } + + void RestartGame() + { + PLAYFIELD = (string[])FIELD.Clone(); + FallSpeedMilliSeconds = 1000; + Score = 0; + TETROMINO = new() + { + Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + X = INITIALTETROMINOX, + Y = INITIALTETROMINOY + }; + + AutoEvent = new AutoResetEvent(false); + FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); + GameStatus = GameStatus.Playing; + } + + async void PauseGame() + { + PauseCount++; + FallTimer.Change(Timeout.Infinite, Timeout.Infinite); + GameStatus = GameStatus.Paused; + await DrawFrame(); + + await ResumeGame(); + } + + async Task ResumeGame(ConsoleKey key = ConsoleKey.Enter) + { + ConsoleKey input = default; + while (input != key && !CloseGame) + { + input = (await Console.ReadKey(true)).Key; + if (input is ConsoleKey.Enter && GameStatus == GameStatus.Paused && FallTimer != null) + { + FallTimer.Change(0, FallSpeedMilliSeconds); + GameStatus = GameStatus.Playing; + return; + } + } + } + + void AddScoreChangeSpeed(int value) + { + Score += value; + + if (Score > 100) return; + + switch (Score) + { + case 10: + FallSpeedMilliSeconds = 900; + break; + case 20: + FallSpeedMilliSeconds = 800; + break; + case 30: + FallSpeedMilliSeconds = 700; + break; + case 40: + FallSpeedMilliSeconds = 500; + break; + case 50: + FallSpeedMilliSeconds = 300; + break; + case 60: + FallSpeedMilliSeconds = 200; + break; + case 70: + FallSpeedMilliSeconds = 100; + break; + case 100: + FallSpeedMilliSeconds = 50; + break; + } + } + + void TetrominoFall(object? e) + { + int yAfterFall = TETROMINO.Y; + bool collision = false; + + if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) yAfterFall = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; + else yAfterFall += 2; + + //Y Collision + for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) + { + for (int yCollision = TETROMINO.Shape.Length - 1; yCollision >= 0; yCollision -= 2) + { + char exist = TETROMINO.Shape[yCollision][xCollision]; + + if (exist == ' ') continue; + + char[] lineYC = PLAYFIELD[yAfterFall + yCollision - 1].ToCharArray(); + + if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; + + if + ( + lineYC[TETROMINO.X + xCollision] != ' ' && + lineYC[TETROMINO.X + xCollision] != '│' + ) + { + char[][] lastFrame = DrawLastFrame(yAfterFall); + for (int y = 0; y < lastFrame.Length; y++) + { + PLAYFIELD[y] = new string(lastFrame[y]); + } + + TETROMINO.X = INITIALTETROMINOX; + TETROMINO.Y = INITIALTETROMINOY; + TETROMINO.Shape = TETROMINO.Next; + TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; + + xCollision = TETROMINO.Shape[0].Length; + collision = true; + break; + } + } + + xCollision += 3; + } + + if (!collision) TETROMINO.Y = yAfterFall; + + //Clean Lines + for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) + { + string line = PLAYFIELD[lineIndex]; + bool notCompleted = line.Any(e => e == ' '); + + if (lineIndex == 0 || lineIndex == PLAYFIELD.Length - 1) continue; + + if (!notCompleted) + { + PLAYFIELD[lineIndex] = "│ │"; + AddScoreChangeSpeed(1); + + for (int lineM = lineIndex; lineM >= 1; lineM--) + { + if (PLAYFIELD[lineM - 1] == "╭──────────────────────────────╮") + { + PLAYFIELD[lineM] = "│ │"; + continue; + } + + PLAYFIELD[lineM] = PLAYFIELD[lineM - 1]; + } + + lineIndex++; + } + } + + //VerifiedCollision + if (Collision(Direction.None) && FallTimer != null) Gameover(); + } + + void TetrominoSpin(Direction spinDirection) + { + string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + int yScope = TETROMINO.Y; + string[] newShape = new string[shapeScope[0].Length / 3 * 2]; + int newY = 0; + int rowEven = 0; + int rowOdd = 1; + + //Turn + for (int y = 0; y < shapeScope.Length;) + { + switch (spinDirection) + { + case Direction.Right: + SpinRight(newShape, shapeScope, ref newY, rowEven, rowOdd, y); + break; + case Direction.Left: + SpinLeft(newShape, shapeScope, ref newY, rowEven, rowOdd, y); + break; + } + + newY = 0; + rowEven += 2; + rowOdd += 2; + y += 2; + } + + //Verified Collision + for (int y = 0; y < newShape.Length - 1; y++) + { + for (int x = 0; x < newShape[y].Length; x++) + { + if (newShape[y][x] == ' ') continue; + + char c = PLAYFIELD[yScope + y][TETROMINO.X + x]; + if (c != ' ') return; + } + } + + TETROMINO.Shape = newShape; + } + + void SpinLeft(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) + { + for (int x = shape[y].Length - 1; x >= 0; x -= 3) + { + for (int xS = 2; xS >= 0; xS--) + { + newShape[newY] += shape[rowEven][x - xS]; + newShape[newY + 1] += shape[rowOdd][x - xS]; + } + + newY += 2; + } + } + + void SpinRight(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) + { + for (int x = 2; x < shape[y].Length; x += 3) + { + if (newShape[newY] == null) + { + newShape[newY] = ""; + newShape[newY + 1] = ""; + } + + for (int xS = 0; xS <= 2; xS++) + { + newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString()); + newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString()); + } + + newY += 2; + } + } + + async Task SleepAfterRender() + { + TimeSpan sleep = TimeSpan.FromSeconds(1d / 60d) - Stopwatch.Elapsed; + if (sleep > TimeSpan.Zero) + { + await Console.RefreshAndDelay(sleep); + } + Stopwatch.Restart(); + } + + } + + class Tetromino + { + public required string[] Shape { get; set; } + public required string[] Next { get; set; } + public int X { get; set; } + public int Y { get; set; } + } + + enum Direction + { + Right, + Left, + None + } + + enum GameStatus + { + Gameover, + Playing, + Paused + } } From 53ddb3c90cb5506230618add220156698251d17c Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 12:37:40 -0500 Subject: [PATCH 05/24] readme updates --- Projects/Tetris/README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Projects/Tetris/README.md b/Projects/Tetris/README.md index 3b4196db..1d91366e 100644 --- a/Projects/Tetris/README.md +++ b/Projects/Tetris/README.md @@ -6,13 +6,26 @@ GitHub repo Language C# Target Framework + Build Discord License

+

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

+ +> **Note** This game was a *[Community Contribution](https://github.com/dotnet/dotnet-console-games/pull/89)! + Well, just tetris! -## Inputs +## Input |Key|Action| |---|---| @@ -33,4 +46,12 @@ Debug controls (only if debug mode its active, it may break the game): |`O` |change active tetromino to O| |`C` |change active tetromino to S| |`T` |change active tetromino to T| -|`Z` |change active tetromino to Z| \ No newline at end of file +|`Z` |change active tetromino to Z| + +## Downloads + +[win-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/win-x64/Tetris.exe) + +[linux-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/linux-x64/Tetris) + +[osx-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/osx-x64/Tetris) From 2c8fe718bc2d62fedb166821573b2b60aa40c7bd Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 13:03:13 -0500 Subject: [PATCH 06/24] == -> is && != -> is not --- Projects/Tetris/Program.cs | 54 ++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index b052e062..fa81e6e4 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -190,7 +190,7 @@ } PlayerControl(); - if (GameStatus == GameStatus.Playing) + if (GameStatus is GameStatus.Playing) { DrawFrame(); SleepAfterRender(); @@ -199,7 +199,7 @@ void PlayerControl() { - while (Console.KeyAvailable && GameStatus == GameStatus.Playing) + while (Console.KeyAvailable && GameStatus is GameStatus.Playing) { switch (Console.ReadKey(true).Key) { @@ -225,7 +225,7 @@ void PlayerControl() break; case ConsoleKey.R: TextColor++; - if (TextColor == 16) TextColor = 1; + if (TextColor is 16) TextColor = 1; Console.ForegroundColor = (ConsoleColor)TextColor; break; @@ -294,9 +294,9 @@ void DrawFrame() char charToReplace = PLAYFIELD[tY][tX]; char charTetromino = shapeScope[y][x]; - if (charTetromino == ' ') continue; + if (charTetromino is ' ') continue; - if (charToReplace != ' ') + if (charToReplace is not ' ') { collision = true; break; @@ -323,9 +323,9 @@ void DrawFrame() char charToReplace = PLAYFIELD[tY][tX]; char charTetromino = shapeScope[y][x]; - if (charTetromino == ' ') continue; + if (charTetromino is ' ') continue; - if (charToReplace != ' ') + if (charToReplace is not ' ') { collision = true; break; @@ -373,7 +373,7 @@ void DrawFrame() } //Draw Pause - if (GameStatus == GameStatus.Paused) + if (GameStatus is GameStatus.Paused) { for (int y = 0; y < PAUSE.Length; y++) { @@ -426,9 +426,9 @@ char[][] DrawLastFrame(int yS) char charToReplace = PLAYFIELD[tY][tX]; char charTetromino = shapeScope[y][x]; - if (charTetromino == ' ') continue; + if (charTetromino is ' ') continue; - if (charToReplace != ' ') + if (charToReplace is not ' ') { collision = true; break; @@ -473,9 +473,9 @@ bool Collision(Direction direction) char charToReplace = PLAYFIELD[tY][tX]; char charTetromino = shapeScope[y][x]; - if (charTetromino == ' ') continue; + if (charTetromino is ' ') continue; - if (charToReplace != ' ') + if (charToReplace is not ' ') { collision = true; break; @@ -501,9 +501,9 @@ bool CollisionPreview(int initY, int yScope, string[] shape) char charToReplace = PLAYFIELD[tY][tX]; char charTetromino = shape[y][x]; - if (charTetromino == ' ') continue; + if (charTetromino is ' ') continue; - if (charToReplace != ' ') + if (charToReplace is not ' ') { return true; } @@ -596,7 +596,7 @@ void ResumeGame(ConsoleKey key = ConsoleKey.Enter) while (input != key && !CloseGame) { input = Console.ReadKey(true).Key; - if (input is ConsoleKey.Enter && GameStatus == GameStatus.Paused && FallTimer != null) + if (input is ConsoleKey.Enter && GameStatus is GameStatus.Paused && FallTimer != null) { FallTimer.Change(0, FallSpeedMilliSeconds); GameStatus = GameStatus.Playing; @@ -655,17 +655,13 @@ void TetrominoFall(object? e) { char exist = TETROMINO.Shape[yCollision][xCollision]; - if (exist == ' ') continue; + if (exist is ' ') continue; char[] lineYC = PLAYFIELD[yAfterFall + yCollision - 1].ToCharArray(); if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; - if - ( - lineYC[TETROMINO.X + xCollision] != ' ' && - lineYC[TETROMINO.X + xCollision] != '│' - ) + if (lineYC[TETROMINO.X + xCollision] is not ' ' or '│') { char[][] lastFrame = DrawLastFrame(yAfterFall); for (int y = 0; y < lastFrame.Length; y++) @@ -693,9 +689,9 @@ void TetrominoFall(object? e) for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) { string line = PLAYFIELD[lineIndex]; - bool notCompleted = line.Any(e => e == ' '); + bool notCompleted = line.Any(e => e is ' '); - if (lineIndex == 0 || lineIndex == PLAYFIELD.Length - 1) continue; + if (lineIndex is 0 || lineIndex == PLAYFIELD.Length - 1) continue; if (!notCompleted) { @@ -704,7 +700,7 @@ void TetrominoFall(object? e) for (int lineM = lineIndex; lineM >= 1; lineM--) { - if (PLAYFIELD[lineM - 1] == "╭──────────────────────────────╮") + if (PLAYFIELD[lineM - 1] is "╭──────────────────────────────╮") { PLAYFIELD[lineM] = "│ │"; continue; @@ -717,8 +713,8 @@ void TetrominoFall(object? e) } } - //VerifiedCollision - if (Collision(Direction.None) && FallTimer != null) Gameover(); + //VerifiedCollision + if (Collision(Direction.None) && FallTimer is not null) Gameover(); } void TetrominoSpin(Direction spinDirection) @@ -754,10 +750,10 @@ void TetrominoSpin(Direction spinDirection) { for (int x = 0; x < newShape[y].Length; x++) { - if (newShape[y][x] == ' ') continue; + if (newShape[y][x] is ' ') continue; char c = PLAYFIELD[yScope + y][TETROMINO.X + x]; - if (c != ' ') return; + if (c is not ' ') return; } } @@ -782,7 +778,7 @@ void SpinRight(string[] newShape, string[] shape, ref int newY, int rowEven, int { for (int x = 2; x < shape[y].Length; x += 3) { - if (newShape[newY] == null) + if (newShape[newY] is null) { newShape[newY] = ""; newShape[newY + 1] = ""; From 516121f3057bbe26b029e31575777ec9542f9968 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 13:38:15 -0500 Subject: [PATCH 07/24] Convert.ToInt16 is unnecessary here --- Projects/Tetris/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index fa81e6e4..b40ee133 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -143,7 +143,7 @@ Random RamdomGenerator = new(); -int INITIALTETROMINOX = Convert.ToInt16(PLAYFIELD[0].Length / 2) - 3; +int INITIALTETROMINOX = (PLAYFIELD[0].Length / 2) - 3; int INITIALTETROMINOY = 1; Tetromino TETROMINO = new() { From 97fdeb1c7c78bdb5b8691c6cc367f8356e127953 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 13:44:51 -0500 Subject: [PATCH 08/24] syntax sugar --- Projects/Tetris/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index b40ee133..49cdb5b0 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -153,7 +153,7 @@ Y = INITIALTETROMINOY }; -AutoResetEvent AutoEvent = new AutoResetEvent(false); +AutoResetEvent AutoEvent = new(false); Timer? FallTimer = null; GameStatus = GameStatus.Playing; From a1341181420ee3fedf6780f2f2930052686b63ea Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 13:54:44 -0500 Subject: [PATCH 09/24] switch statement -> switch expression --- Projects/Tetris/Program.cs | 39 +++++++++++--------------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index 49cdb5b0..fab242cb 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -609,35 +609,18 @@ void AddScoreChangeSpeed(int value) { Score += value; - if (Score > 100) return; - - switch (Score) + FallSpeedMilliSeconds = Score switch { - case 10: - FallSpeedMilliSeconds = 900; - break; - case 20: - FallSpeedMilliSeconds = 800; - break; - case 30: - FallSpeedMilliSeconds = 700; - break; - case 40: - FallSpeedMilliSeconds = 500; - break; - case 50: - FallSpeedMilliSeconds = 300; - break; - case 60: - FallSpeedMilliSeconds = 200; - break; - case 70: - FallSpeedMilliSeconds = 100; - break; - case 100: - FallSpeedMilliSeconds = 50; - break; - } + > 100 => FallSpeedMilliSeconds = 050, + > 070 => FallSpeedMilliSeconds = 100, + > 060 => FallSpeedMilliSeconds = 200, + > 050 => FallSpeedMilliSeconds = 300, + > 040 => FallSpeedMilliSeconds = 500, + > 030 => FallSpeedMilliSeconds = 700, + > 020 => FallSpeedMilliSeconds = 800, + > 010 => FallSpeedMilliSeconds = 900, + _ => 1000, + }; } void TetrominoFall(object? e) From 3aed2caedf83baf56d78bd3fe6f967f52cd3aae6 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 13:57:31 -0500 Subject: [PATCH 10/24] Random.Shared --- Projects/Tetris/Program.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index fab242cb..060140f8 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -141,14 +141,12 @@ int TextColor = 0; GameStatus GameStatus = GameStatus.Gameover; -Random RamdomGenerator = new(); - int INITIALTETROMINOX = (PLAYFIELD[0].Length / 2) - 3; int INITIALTETROMINOY = 1; Tetromino TETROMINO = new() { - Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Shape = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)], X = INITIALTETROMINOX, Y = INITIALTETROMINOY }; @@ -569,8 +567,8 @@ void RestartGame() Score = 0; TETROMINO = new() { - Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], + Shape = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)], + Next = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)], X = INITIALTETROMINOX, Y = INITIALTETROMINOY }; @@ -655,7 +653,7 @@ void TetrominoFall(object? e) TETROMINO.X = INITIALTETROMINOX; TETROMINO.Y = INITIALTETROMINOY; TETROMINO.Shape = TETROMINO.Next; - TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; + TETROMINO.Next = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)]; xCollision = TETROMINO.Shape[0].Length; collision = true; From ecd2a60480f7bafb84c7c7c6239389529bbd134f Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 14:13:35 -0500 Subject: [PATCH 11/24] raw string literals --- Projects/Tetris/Program.cs | 78 +++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index 060140f8..ed0ec9d9 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -156,24 +156,25 @@ GameStatus = GameStatus.Playing; Console.WriteLine(); -Console.WriteLine(" ██████╗█████╗██████╗█████╗ ██╗█████╗"); -Console.WriteLine(" ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝"); -Console.WriteLine(" ██║ █████╗ ██║ █████╔╝██║ ███╗ "); -Console.WriteLine(" ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗"); -Console.WriteLine(" ██║ █████╗ ██║ ██║ ██║██║█████║"); -Console.WriteLine(" ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝"); - -Console.WriteLine(); -Console.WriteLine(" Controls:"); -Console.WriteLine(" WASD or ARROW to move"); -Console.WriteLine(" Q or E to spin left or right"); -Console.WriteLine(" P to paused the game, press enter"); -Console.WriteLine(" key to resume"); -Console.WriteLine(" R to change Text color"); -Console.WriteLine(); -Console.WriteLine(" Press escape to close the game at any time."); -Console.WriteLine(); -Console.Write(" Press enter to start tetris..."); +Console.WriteLine(""" + ██████╗█████╗██████╗█████╗ ██╗█████╗ + ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝ + ██║ █████╗ ██║ █████╔╝██║ ███╗ + ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗ + ██║ █████╗ ██║ ██║ ██║██║█████║ + ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝ + + Controls: + WASD or ARROW to move + Q or E to spin left or right + P to paused the game, press enter + key to resume + R to change Text color + + Press escape to close the game at any time. + + Press enter to start tetris... + """); Console.CursorVisible = false; StartGame(); Console.Clear(); @@ -521,26 +522,27 @@ void Gameover() SleepAfterRender(); Console.Clear(); - Console.WriteLine(); - Console.WriteLine(" ██████╗ █████╗ ██ ██╗█████╗"); - Console.WriteLine(" ██╔════╝ ██╔══██╗███ ███║██╔══╝"); - Console.WriteLine(" ██║ ███╗███████║██╔██═██║█████╗"); - Console.WriteLine(" ██║ ██║██╔══██║██║ ██║██╔══╝"); - Console.WriteLine(" ╚██████╔╝██║ ██║██║ ██║█████╗"); - Console.WriteLine(" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝"); - Console.WriteLine(" ██████╗██╗ ██╗█████╗█████╗ "); - Console.WriteLine(" ██ ██║██║ ██║██╔══╝██╔═██╗ "); - Console.WriteLine(" ██ ██║██║ ██║█████╗█████╔╝ "); - Console.WriteLine(" ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ "); - Console.WriteLine(" ██████║ ╚███╔╝ █████╗██║ ██║ "); - Console.WriteLine(" ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ "); - - Console.WriteLine(); - Console.WriteLine($" Final Score: {Score}"); - Console.WriteLine($" Pause Count: {PauseCount}"); - Console.WriteLine(); - Console.WriteLine(" Press enter to play again"); - Console.WriteLine(" Press escape to close the game"); + Console.Write($""" + + ██████╗ █████╗ ██ ██╗█████╗ + ██╔════╝ ██╔══██╗███ ███║██╔══╝ + ██║ ███╗███████║██╔██═██║█████╗ + ██║ ██║██╔══██║██║ ██║██╔══╝ + ╚██████╔╝██║ ██║██║ ██║█████╗ + ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝ + ██████╗██╗ ██╗█████╗█████╗ + ██ ██║██║ ██║██╔══╝██╔═██╗ + ██ ██║██║ ██║█████╗█████╔╝ + ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ + ██████║ ╚███╔╝ █████╗██║ ██║ + ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ + + Final Score: {Score} + Pause Count: {PauseCount} + + Press enter to play again + Press escape to close the game + """); Console.CursorVisible = false; StartGame(); RestartGame(); From f61c0b039d3728115a3c1d52239d96cc8ef3a3d0 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 14:23:50 -0500 Subject: [PATCH 12/24] removed unnecessary clones --- Projects/Tetris/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index ed0ec9d9..80a719d3 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -273,8 +273,8 @@ void DrawFrame() { bool collision = false; int yScope = TETROMINO.Y; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); + string[] shapeScope = TETROMINO.Shape; + string[] nextShapeScope = TETROMINO.Next; char[][] frame = new char[PLAYFIELD.Length][]; //Field From 3c2b387598b4b91099d2e20520a829aa03109125 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Sat, 28 Oct 2023 14:49:32 -0500 Subject: [PATCH 13/24] disable implicit usings (I hate them with a passion) :P --- Projects/Tetris/Program.cs | 5 ++++- Projects/Tetris/Tetris.csproj | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index 80a719d3..e49af5ae 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -1,5 +1,8 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; +using System.Linq; using System.Text; +using System.Threading; Console.OutputEncoding = Encoding.UTF8; Console.CursorVisible = false; diff --git a/Projects/Tetris/Tetris.csproj b/Projects/Tetris/Tetris.csproj index 1a40ec66..0e17b8ef 100644 --- a/Projects/Tetris/Tetris.csproj +++ b/Projects/Tetris/Tetris.csproj @@ -1,8 +1,8 @@ - + Exe net7.0 - enable + disable enable From 0b2860b54652ba0762465dc4edaf893c3c8085df Mon Sep 17 00:00:00 2001 From: Jahg Date: Sat, 28 Oct 2023 20:57:43 -0700 Subject: [PATCH 14/24] Improved Tetrominos Spin --- Projects/Tetris/Program.cs | 73 +++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index e49af5ae..c1db059e 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -90,7 +90,7 @@ new[]{ "╭─╮", "╰─╯", - "╭─╮", + "x─╮", "╰─╯", "╭─╮", "╰─╯", @@ -100,37 +100,37 @@ new[]{ "╭─╮ ", "╰─╯ ", - "╭─╮╭─╮╭─╮", + "╭─╮x─╮╭─╮", "╰─╯╰─╯╰─╯" }, new[]{ " ╭─╮", " ╰─╯", - "╭─╮╭─╮╭─╮", + "╭─╮x─╮╭─╮", "╰─╯╰─╯╰─╯" }, new[]{ "╭─╮╭─╮", "╰─╯╰─╯", - "╭─╮╭─╮", + "x─╮╭─╮", "╰─╯╰─╯" }, new[]{ " ╭─╮╭─╮", " ╰─╯╰─╯", - "╭─╮╭─╮ ", + "╭─╮x─╮ ", "╰─╯╰─╯ " }, new[]{ " ╭─╮ ", " ╰─╯ ", - "╭─╮╭─╮╭─╮", + "╭─╮x─╮╭─╮", "╰─╯╰─╯╰─╯" }, new[]{ "╭─╮╭─╮ ", "╰─╯╰─╯ ", - " ╭─╮╭─╮", + " x─╮╭─╮", " ╰─╯╰─╯" }, }; @@ -304,6 +304,7 @@ void DrawFrame() break; } + if (charTetromino is 'x') charTetromino = '╭'; frame[tY][tX] = charTetromino; } } @@ -361,6 +362,7 @@ void DrawFrame() int tY = y + BORDER; int tX = PLAYFIELD[y].Length + x + BORDER; char charTetromino = nextShapeScope[y][x]; + if (charTetromino is 'x') charTetromino = '╭'; frame[tY][tX] = charTetromino; } } @@ -435,7 +437,8 @@ char[][] DrawLastFrame(int yS) collision = true; break; } - + + if (charTetromino is 'x') charTetromino = '╭'; frame[tY][tX] = charTetromino; } } @@ -707,6 +710,7 @@ void TetrominoSpin(Direction spinDirection) { string[] shapeScope = (string[])TETROMINO.Shape.Clone(); int yScope = TETROMINO.Y; + int xScope = TETROMINO.X; string[] newShape = new string[shapeScope[0].Length / 3 * 2]; int newY = 0; int rowEven = 0; @@ -731,6 +735,55 @@ void TetrominoSpin(Direction spinDirection) y += 2; } + //Old Pivot + (int y, int x) offsetOP = (0, 0); + for (int y = 0; y < shapeScope.Length; y += 2) + { + for (int x = 0; x < shapeScope[y].Length; x += 3) + { + if (shapeScope[y][x] is 'x') + { + offsetOP = (y / 2, x / 3); + y = shapeScope.Length; + break; + } + } + } + + //New Pivot + (int y, int x) offsetNP = (0, 0); + for (int y = 0; y < newShape.Length; y += 2) + { + for (int x = 0; x < newShape[y].Length; x += 3) + { + if (newShape[y][x] is 'x') + { + offsetNP = (y / 2, x / 3); + y = newShape.Length; + break; + } + } + } + + yScope += (offsetOP.y - offsetNP.y) * 2; + xScope += (offsetOP.x - offsetNP.x) * 3; + + //Tetromino Square(O) special case + if (newShape.Length / 2 == newShape[0].Length / 3) + { + yScope = TETROMINO.Y; + xScope = TETROMINO.X; + } + //Tetromino I special case + else if (newShape.Length is 8 && newShape[0].Length is 3 && offsetNP.y is 2) + { + newShape[2] = "x─╮"; + newShape[4] = "╭─╮"; + yScope += 2; + } + + if (xScope < 1 || yScope < 1) return; + //Verified Collision for (int y = 0; y < newShape.Length - 1; y++) { @@ -738,11 +791,13 @@ void TetrominoSpin(Direction spinDirection) { if (newShape[y][x] is ' ') continue; - char c = PLAYFIELD[yScope + y][TETROMINO.X + x]; + char c = PLAYFIELD[yScope + y][xScope + x]; if (c is not ' ') return; } } + TETROMINO.Y = yScope; + TETROMINO.X = xScope; TETROMINO.Shape = newShape; } From 05463dfdd1f5f9160112e9582731556d9b7cd37d Mon Sep 17 00:00:00 2001 From: Jahg Date: Sat, 28 Oct 2023 21:25:23 -0700 Subject: [PATCH 15/24] Implemented HardDrop --- Projects/Tetris/Program.cs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index c1db059e..d4c55f5f 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -170,6 +170,7 @@ Controls: WASD or ARROW to move Q or E to spin left or right + Spacebar to HardDrop P to paused the game, press enter key to resume R to change Text color @@ -230,9 +231,12 @@ void PlayerControl() if (TextColor is 16) TextColor = 1; Console.ForegroundColor = (ConsoleColor)TextColor; break; + case ConsoleKey.Spacebar: + HardDrop(); + break; //DEBUG - case ConsoleKey.Spacebar: + case ConsoleKey.Tab: if (!DEBUGCONTROLS) return; PLAYFIELD = (string[])FIELD.Clone(); break; @@ -312,7 +316,7 @@ void DrawFrame() //Draw Preview for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) { - if (CollisionPreview(yField, yScope, shapeScope)) continue; + if (CollisionBottom(yField, yScope, shapeScope)) continue; for (int y = 0; y < shapeScope.Length && !collision; y++) { @@ -437,7 +441,7 @@ char[][] DrawLastFrame(int yS) collision = true; break; } - + if (charTetromino is 'x') charTetromino = '╭'; frame[tY][tX] = charTetromino; } @@ -491,7 +495,7 @@ bool Collision(Direction direction) return collision; } -bool CollisionPreview(int initY, int yScope, string[] shape) +bool CollisionBottom(int initY, int yScope, string[] shape) { int xNew = TETROMINO.X; @@ -706,6 +710,19 @@ void TetrominoFall(object? e) if (Collision(Direction.None) && FallTimer is not null) Gameover(); } +void HardDrop() +{ + int y = TETROMINO.Y; + int x = TETROMINO.X; + var shapeScope = TETROMINO.Shape; + for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) + { + if (CollisionBottom(yField, y, shapeScope)) continue; + TETROMINO.Y = yField; + break; + } +} + void TetrominoSpin(Direction spinDirection) { string[] shapeScope = (string[])TETROMINO.Shape.Clone(); From 8d4cc2139c27f023084110e2ef659355c50f9d97 Mon Sep 17 00:00:00 2001 From: Jahg Date: Sat, 28 Oct 2023 21:40:39 -0700 Subject: [PATCH 16/24] Change score -> line clears --- Projects/Tetris/Program.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index d4c55f5f..728f41fe 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -617,6 +617,7 @@ void ResumeGame(ConsoleKey key = ConsoleKey.Enter) void AddScoreChangeSpeed(int value) { + if (value >= 4) value++; Score += value; FallSpeedMilliSeconds = Score switch @@ -679,6 +680,7 @@ void TetrominoFall(object? e) if (!collision) TETROMINO.Y = yAfterFall; //Clean Lines + int clearedLines = 0; for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) { string line = PLAYFIELD[lineIndex]; @@ -689,7 +691,7 @@ void TetrominoFall(object? e) if (!notCompleted) { PLAYFIELD[lineIndex] = "│ │"; - AddScoreChangeSpeed(1); + clearedLines++; for (int lineM = lineIndex; lineM >= 1; lineM--) { @@ -706,6 +708,8 @@ void TetrominoFall(object? e) } } + AddScoreChangeSpeed(clearedLines / 2); + //VerifiedCollision if (Collision(Direction.None) && FallTimer is not null) Gameover(); } From 09e9e1fe401fa17e4dc001e2300aa5e754b885fd Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Tue, 31 Oct 2023 17:08:00 -0500 Subject: [PATCH 17/24] refactoring (Timer -> Stopwatch) --- Projects/Tetris/Program.cs | 944 ++++++++++++++++++------------------- Projects/Tetris/README.md | 71 ++- 2 files changed, 509 insertions(+), 506 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index 728f41fe..43510fa4 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -1,62 +1,20 @@ using System; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Text; -using System.Threading; -Console.OutputEncoding = Encoding.UTF8; -Console.CursorVisible = false; -Stopwatch Stopwatch = Stopwatch.StartNew(); - -bool DEBUGCONTROLS = false; +#region Constants -string[] FIELD = new[] +string[] emptyField = new string[42]; +emptyField[0] = "╭──────────────────────────────╮"; +for (int i = 1; i < 41; i++) { - "╭──────────────────────────────╮", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰──────────────────────────────╯" -}; + emptyField[i] = "│ │"; +} +emptyField[^1] = "╰──────────────────────────────╯"; -string[] NEXTTETROMINO = new[] +string[] nextTetrominoBorder = new[] { "╭─────────╮", "│ │", @@ -70,13 +28,15 @@ "╰─────────╯" }; -string[] SCORE = new[]{ +string[] scoreBorder = new[] +{ "╭─────────╮", "│ │", "╰─────────╯" }; -string[] PAUSE = new[]{ +string[] pauseRender = new[] +{ "█████╗ ███╗ ██╗██╗█████╗█████╗", "██╔██║██╔██╗██║██║██╔══╝██╔══╝", "█████║█████║██║██║ ███╗ █████╗", @@ -85,7 +45,7 @@ "╚═╝ ╚═╝╚═╝╚════╝╚════╝╚════╝", }; -string[][] TETROMINOS = new[] +string[][] tetrominos = new[] { new[]{ "╭─╮", @@ -135,143 +95,235 @@ }, }; -string[] PLAYFIELD = (string[])FIELD.Clone(); -const int BORDER = 1; -int FallSpeedMilliSeconds = 1000; -bool CloseGame = false; -int Score = 0; -int PauseCount = 0; -int TextColor = 0; -GameStatus GameStatus = GameStatus.Gameover; - -int INITIALTETROMINOX = (PLAYFIELD[0].Length / 2) - 3; -int INITIALTETROMINOY = 1; -Tetromino TETROMINO = new() -{ - Shape = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)], - X = INITIALTETROMINOX, - Y = INITIALTETROMINOY -}; +const int borderSize = 1; -AutoResetEvent AutoEvent = new(false); -Timer? FallTimer = null; -GameStatus = GameStatus.Playing; - -Console.WriteLine(); -Console.WriteLine(""" - ██████╗█████╗██████╗█████╗ ██╗█████╗ - ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝ - ██║ █████╗ ██║ █████╔╝██║ ███╗ - ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗ - ██║ █████╗ ██║ ██║ ██║██║█████║ - ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝ - - Controls: - WASD or ARROW to move - Q or E to spin left or right - Spacebar to HardDrop - P to paused the game, press enter - key to resume - R to change Text color - - Press escape to close the game at any time. - - Press enter to start tetris... - """); -Console.CursorVisible = false; -StartGame(); -Console.Clear(); +int initialX = (emptyField[0].Length / 2) - 3; +int initialY = 1; + +int consoleWidthMin = 44; +int consoleHeightMin = 43; -FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); +#endregion -while (!CloseGame) +Stopwatch timer = new(); +bool closeRequested = false; +bool gameOver; +int score = 0; +TimeSpan fallSpeed; +string[] field; +Tetromino tetromino; +int consoleWidth = Console.WindowWidth; +int consoleHeight = Console.WindowHeight; +bool consoleTooSmallScreen = false; + +Console.OutputEncoding = Encoding.UTF8; +while (!closeRequested) { - if (CloseGame) + Console.Clear(); + Console.Write(""" + + ██████╗█████╗██████╗█████╗ ██╗█████╗ + ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝ + ██║ █████╗ ██║ █████╔╝██║ ███╗ + ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗ + ██║ █████╗ ██║ ██║ ██║██║█████║ + ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝ + + Controls: + + [A] or [←] move left + [D] or [→] move right + [S] or [↓] fall faster + [Q] spin left + [E] spin right + [Spacebar] drop + [P] pause and unpause + [Escape] close game + [Enter] start game + """); + bool mainMenuScreen = true; + while (!closeRequested && mainMenuScreen) + { + Console.CursorVisible = false; + switch (Console.ReadKey(true).Key) + { + case ConsoleKey.Enter: mainMenuScreen = false; break; + case ConsoleKey.Escape: closeRequested = true; break; + } + } + Initialize(); + Console.Clear(); + DrawFrame(); + while (!closeRequested && !gameOver) + { + // if user changed the size of the console, we need to clear the console + if (consoleWidth != Console.WindowWidth || consoleHeight != Console.WindowHeight) + { + consoleWidth = Console.WindowWidth; + consoleHeight = Console.WindowHeight; + if (!consoleTooSmallScreen) + { + Console.Clear(); + DrawFrame(); + } + else + { + consoleTooSmallScreen = false; + } + } + + // if the console isn't big enough to render the game, pause the game and tell the user + if (consoleWidth < consoleWidthMin || consoleHeight < consoleHeightMin) + { + if (!consoleTooSmallScreen) + { + Console.Clear(); + Console.Write($"Please increase size of console to at least {consoleWidthMin}x{consoleHeightMin}. Current size is {consoleWidth}x{consoleHeight}."); + timer.Stop(); + consoleTooSmallScreen = true; + } + } + else if (consoleTooSmallScreen) + { + consoleTooSmallScreen = false; + Console.Clear(); + DrawFrame(); + } + + HandlePlayerInput(); + if (closeRequested || gameOver) + { + break; + } + if (timer.IsRunning && timer.Elapsed > fallSpeed) + { + TetrominoFall(); + if (closeRequested || gameOver) + { + break; + } + DrawFrame(); + } + } + if (closeRequested) { break; } + Console.Clear(); + Console.Write($""" - PlayerControl(); - if (GameStatus is GameStatus.Playing) - { - DrawFrame(); - SleepAfterRender(); + ██████╗ █████╗ ██ ██╗█████╗ + ██╔════╝ ██╔══██╗███ ███║██╔══╝ + ██║ ███╗███████║██╔██═██║█████╗ + ██║ ██║██╔══██║██║ ██║██╔══╝ + ╚██████╔╝██║ ██║██║ ██║█████╗ + ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝ + ██████╗██╗ ██╗█████╗█████╗ + ██ ██║██║ ██║██╔══╝██╔═██╗ + ██ ██║██║ ██║█████╗█████╔╝ + ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ + ██████║ ╚███╔╝ █████╗██║ ██║ + ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ + + Final Score: {score} + + Press enter to play again + Press escape to close the game + """); + Console.CursorVisible = false; + bool gameOverScreen = true; + while (!closeRequested && gameOverScreen) + { + Console.CursorVisible = false; + switch (Console.ReadKey(true).Key) + { + case ConsoleKey.Enter: gameOverScreen = false; break; + case ConsoleKey.Escape: closeRequested = true; break; + } } } +Console.Clear(); +Console.WriteLine("Tetris was closed."); +Console.CursorVisible = true; -void PlayerControl() +void Initialize() { - while (Console.KeyAvailable && GameStatus is GameStatus.Playing) + gameOver = false; + score = 0; + field = emptyField[..]; + initialX = (field[0].Length / 2) - 3; + initialY = 1; + tetromino = new() + { + Shape = tetrominos[Random.Shared.Next(0, tetrominos.Length)], + Next = tetrominos[Random.Shared.Next(0, tetrominos.Length)], + X = initialX, + Y = initialY + }; + fallSpeed = GetFallSpeed(); + timer.Restart(); +} + +void HandlePlayerInput() +{ + while (Console.KeyAvailable && !closeRequested) { switch (Console.ReadKey(true).Key) { case ConsoleKey.A or ConsoleKey.LeftArrow: - if (Collision(Direction.Left)) break; - TETROMINO.X -= 3; + if (timer.IsRunning && !Collision(Direction.Left)) + { + tetromino.X -= 3; + } + DrawFrame(); break; case ConsoleKey.D or ConsoleKey.RightArrow: - if (Collision(Direction.Right)) break; - TETROMINO.X += 3; + if (timer.IsRunning && !Collision(Direction.Right)) + { + tetromino.X += 3; + } + DrawFrame(); break; case ConsoleKey.S or ConsoleKey.DownArrow: - FallTimer.Change(0, FallSpeedMilliSeconds); + if (timer.IsRunning) + { + TetrominoFall(); + } break; case ConsoleKey.E: - TetrominoSpin(Direction.Right); + if (timer.IsRunning) + { + TetrominoSpin(Direction.Right); + DrawFrame(); + } break; case ConsoleKey.Q: - TetrominoSpin(Direction.Left); + if (timer.IsRunning) + { + TetrominoSpin(Direction.Left); + DrawFrame(); + } break; case ConsoleKey.P: - PauseGame(); - break; - case ConsoleKey.R: - TextColor++; - if (TextColor is 16) TextColor = 1; - Console.ForegroundColor = (ConsoleColor)TextColor; + if (timer.IsRunning) + { + timer.Stop(); + } + else if (!consoleTooSmallScreen) + { + timer.Start(); + } + DrawFrame(); break; case ConsoleKey.Spacebar: - HardDrop(); - break; - - //DEBUG - case ConsoleKey.Tab: - if (!DEBUGCONTROLS) return; - PLAYFIELD = (string[])FIELD.Clone(); - break; - case ConsoleKey.I: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[0]; - break; - case ConsoleKey.J: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[1]; - break; - case ConsoleKey.L: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[2]; - break; - case ConsoleKey.O: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[3]; - break; - case ConsoleKey.C: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[4]; - break; - case ConsoleKey.T: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[5]; - break; - case ConsoleKey.Z: - if (!DEBUGCONTROLS) return; - TETROMINO.Shape = TETROMINOS[6]; - break; - case ConsoleKey.X: - if (!DEBUGCONTROLS) return; - Score += 10; + if (timer.IsRunning) + { + HardDrop(); + } break; + case ConsoleKey.Escape: + closeRequested = true; + return; } } } @@ -279,132 +331,131 @@ void PlayerControl() void DrawFrame() { bool collision = false; - int yScope = TETROMINO.Y; - string[] shapeScope = TETROMINO.Shape; - string[] nextShapeScope = TETROMINO.Next; - char[][] frame = new char[PLAYFIELD.Length][]; + char[][] frame = new char[field.Length][]; - //Field - for (int y = 0; y < PLAYFIELD.Length; y++) + // Field + for (int y = 0; y < field.Length; y++) { - frame[y] = PLAYFIELD[y].ToCharArray(); + frame[y] = field[y].ToCharArray(); } - //Draw Tetromino - for (int y = 0; y < shapeScope.Length && !collision; y++) + // Tetromino + for (int y = 0; y < tetromino.Shape.Length && !collision; y++) { - for (int x = 0; x < shapeScope[y].Length; x++) + for (int x = 0; x < tetromino.Shape[y].Length; x++) { - int tY = yScope + y; - int tX = TETROMINO.X + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino is ' ') continue; - + int tY = tetromino.Y + y; + int tX = tetromino.X + x; + char charToReplace = field[tY][tX]; + char charTetromino = tetromino.Shape[y][x]; + if (charTetromino is ' ') + { + continue; + } if (charToReplace is not ' ') { collision = true; break; } - - if (charTetromino is 'x') charTetromino = '╭'; + if (charTetromino is 'x') + { + charTetromino = '╭'; + } frame[tY][tX] = charTetromino; } } - //Draw Preview - for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) + // Draw Preview + for (int yField = field.Length - tetromino.Shape.Length - borderSize; yField >= 0; yField -= 2) { - if (CollisionBottom(yField, yScope, shapeScope)) continue; - - for (int y = 0; y < shapeScope.Length && !collision; y++) + if (CollisionBottom(yField, tetromino.Y, tetromino.Shape)) + { + continue; + } + for (int y = 0; y < tetromino.Shape.Length && !collision; y++) { - for (int x = 0; x < shapeScope[y].Length; x++) + for (int x = 0; x < tetromino.Shape[y].Length; x++) { int tY = yField + y; - - if (yScope + shapeScope.Length > tY) continue; - - int tX = TETROMINO.X + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino is ' ') continue; - + if (tetromino.Y + tetromino.Shape.Length > tY) + { + continue; + } + int tX = tetromino.X + x; + char charToReplace = field[tY][tX]; + char charTetromino = tetromino.Shape[y][x]; + if (charTetromino is ' ') + { + continue; + } if (charToReplace is not ' ') { collision = true; break; } - frame[tY][tX] = '•'; } } - break; } - //Next Square - for (int y = 0; y < NEXTTETROMINO.Length; y++) + // Next + for (int y = 0; y < nextTetrominoBorder.Length; y++) { - frame[y] = frame[y].Concat(NEXTTETROMINO[y]).ToArray(); + frame[y] = frame[y].Concat(nextTetrominoBorder[y]).ToArray(); } - - //Score Square - for (int y = 0; y < SCORE.Length; y++) - { - int sY = NEXTTETROMINO.Length + y; - frame[sY] = frame[sY].Concat(SCORE[y]).ToArray(); - } - - //Draw Next - for (int y = 0; y < nextShapeScope.Length; y++) + for (int y = 0; y < tetromino.Next.Length; y++) { - for (int x = 0; x < nextShapeScope[y].Length; x++) + for (int x = 0; x < tetromino.Next[y].Length; x++) { - int tY = y + BORDER; - int tX = PLAYFIELD[y].Length + x + BORDER; - char charTetromino = nextShapeScope[y][x]; - if (charTetromino is 'x') charTetromino = '╭'; + int tY = y + borderSize; + int tX = field[y].Length + x + borderSize; + char charTetromino = tetromino.Next[y][x]; + if (charTetromino is 'x') + { + charTetromino = '╭'; + } frame[tY][tX] = charTetromino; } } - //Draw Score - char[] score = Score.ToString().ToCharArray(); - for (int scoreX = score.Length - 1; scoreX >= 0; scoreX--) + // Score + for (int y = 0; y < scoreBorder.Length; y++) + { + int sY = nextTetrominoBorder.Length + y; + frame[sY] = frame[sY].Concat(scoreBorder[y]).ToArray(); + } + char[] scoreRender = score.ToString(CultureInfo.InvariantCulture).ToCharArray(); + for (int scoreX = scoreRender.Length - 1; scoreX >= 0; scoreX--) { - int sY = NEXTTETROMINO.Length + BORDER; - int sX = frame[sY].Length - (score.Length - scoreX) - BORDER; - frame[sY][sX] = score[scoreX]; + int sY = nextTetrominoBorder.Length + borderSize; + int sX = frame[sY].Length - (scoreRender.Length - scoreX) - borderSize; + frame[sY][sX] = scoreRender[scoreX]; } - //Draw Pause - if (GameStatus is GameStatus.Paused) + // Pause + if (!timer.IsRunning) { - for (int y = 0; y < PAUSE.Length; y++) + for (int y = 0; y < pauseRender.Length; y++) { - int fY = (PLAYFIELD.Length / 2) + y - PAUSE.Length; - for (int x = 0; x < PAUSE[y].Length; x++) + int fY = (field.Length / 2) + y - pauseRender.Length; + for (int x = 0; x < pauseRender[y].Length; x++) { - int fX = x + BORDER; + int fX = x + borderSize; - if (x >= PLAYFIELD[fY].Length) break; + if (x >= field[fY].Length) break; - frame[fY][fX] = PAUSE[y][x]; + frame[fY][fX] = pauseRender[y][x]; } } } - //Create Render StringBuilder render = new(); for (int y = 0; y < frame.Length; y++) { render.AppendLine(new string(frame[y])); } - - Console.Clear(); + Console.SetCursorPosition(0, 0); Console.Write(render); Console.CursorVisible = false; } @@ -413,77 +464,78 @@ char[][] DrawLastFrame(int yS) { bool collision = false; int yScope = yS - 2; - int xScope = TETROMINO.X; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); - char[][] frame = new char[PLAYFIELD.Length][]; - - //Field - for (int y = 0; y < PLAYFIELD.Length; y++) + int xScope = tetromino.X; + char[][] frame = new char[field.Length][]; + for (int y = 0; y < field.Length; y++) { - frame[y] = PLAYFIELD[y].ToCharArray(); + frame[y] = field[y].ToCharArray(); } - - //Draw Tetromino - for (int y = 0; y < shapeScope.Length && !collision; y++) + for (int y = 0; y < tetromino.Shape.Length && !collision; y++) { - for (int x = 0; x < shapeScope[y].Length; x++) + for (int x = 0; x < tetromino.Shape[y].Length; x++) { int tY = yScope + y; int tX = xScope + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino is ' ') continue; - + char charToReplace = field[tY][tX]; + char charTetromino = tetromino.Shape[y][x]; + if (charTetromino is ' ') + { + continue; + } if (charToReplace is not ' ') { collision = true; break; } - - if (charTetromino is 'x') charTetromino = '╭'; + if (charTetromino is 'x') + { + charTetromino = '╭'; + } frame[tY][tX] = charTetromino; } } - return frame; } bool Collision(Direction direction) { - int xNew = TETROMINO.X; - int yScope = TETROMINO.Y; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + int xNew = tetromino.X; bool collision = false; - switch (direction) { case Direction.Right: xNew += 3; - if (xNew + shapeScope[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; + if (xNew + tetromino.Shape[0].Length > field[0].Length - borderSize) + { + collision = true; + } break; case Direction.Left: xNew -= 3; - if (xNew < BORDER) collision = true; + if (xNew < borderSize) + { + collision = true; + } break; case Direction.None: break; } - - if (collision) return collision; - - for (int y = 0; y < shapeScope.Length && !collision; y++) + if (collision) { - for (int x = 0; x < shapeScope[y].Length; x++) + return collision; + } + for (int y = 0; y < tetromino.Shape.Length && !collision; y++) + { + for (int x = 0; x < tetromino.Shape[y].Length; x++) { - int tY = yScope + y; + int tY = tetromino.Y + y; int tX = xNew + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino is ' ') continue; - + char charToReplace = field[tY][tX]; + char charTetromino = tetromino.Shape[y][x]; + if (charTetromino is ' ') + { + continue; + } if (charToReplace is not ' ') { collision = true; @@ -491,14 +543,12 @@ bool Collision(Direction direction) } } } - return collision; } bool CollisionBottom(int initY, int yScope, string[] shape) { - int xNew = TETROMINO.X; - + int xNew = tetromino.X; for (int yUpper = initY; yUpper >= yScope; yUpper -= 2) { for (int y = shape.Length - 1; y >= 0; y -= 2) @@ -507,11 +557,12 @@ bool CollisionBottom(int initY, int yScope, string[] shape) { int tY = yUpper + y; int tX = xNew + x; - char charToReplace = PLAYFIELD[tY][tX]; + char charToReplace = field[tY][tX]; char charTetromino = shape[y][x]; - - if (charTetromino is ' ') continue; - + if (charTetromino is ' ') + { + continue; + } if (charToReplace is not ' ') { return true; @@ -519,259 +570,189 @@ bool CollisionBottom(int initY, int yScope, string[] shape) } } } - return false; } -void Gameover() +TimeSpan GetFallSpeed() => + TimeSpan.FromMilliseconds( + score switch + { + > 162 => 100, + > 144 => 200, + > 126 => 300, + > 108 => 400, + > 090 => 500, + > 072 => 600, + > 054 => 700, + > 036 => 800, + > 018 => 900, + _ => 1000, + }); + +void TetrominoFall() { - GameStatus = GameStatus.Gameover; - AutoEvent.Dispose(); - FallTimer.Dispose(); - - SleepAfterRender(); - - Console.Clear(); - Console.Write($""" - - ██████╗ █████╗ ██ ██╗█████╗ - ██╔════╝ ██╔══██╗███ ███║██╔══╝ - ██║ ███╗███████║██╔██═██║█████╗ - ██║ ██║██╔══██║██║ ██║██╔══╝ - ╚██████╔╝██║ ██║██║ ██║█████╗ - ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝ - ██████╗██╗ ██╗█████╗█████╗ - ██ ██║██║ ██║██╔══╝██╔═██╗ - ██ ██║██║ ██║█████╗█████╔╝ - ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ - ██████║ ╚███╔╝ █████╗██║ ██║ - ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ - - Final Score: {Score} - Pause Count: {PauseCount} - - Press enter to play again - Press escape to close the game - """); - Console.CursorVisible = false; - StartGame(); - RestartGame(); -} + int yAfterFall = tetromino.Y; + bool collision = false; -void StartGame(ConsoleKey key = ConsoleKey.Enter) -{ - ConsoleKey input = default; - while (input != key && !CloseGame) + if (tetromino.Y + tetromino.Shape.Length + 2 > field.Length) { - input = Console.ReadKey(true).Key; - if (input is ConsoleKey.Escape) - { - CloseGame = true; - return; - } + yAfterFall = field.Length - tetromino.Shape.Length + 1; } -} - -void RestartGame() -{ - PLAYFIELD = (string[])FIELD.Clone(); - FallSpeedMilliSeconds = 1000; - Score = 0; - TETROMINO = new() - { - Shape = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)], - X = INITIALTETROMINOX, - Y = INITIALTETROMINOY - }; - - AutoEvent = new AutoResetEvent(false); - FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); - GameStatus = GameStatus.Playing; -} - -void PauseGame() -{ - PauseCount++; - FallTimer.Change(Timeout.Infinite, Timeout.Infinite); - GameStatus = GameStatus.Paused; - DrawFrame(); - - ResumeGame(); -} - -void ResumeGame(ConsoleKey key = ConsoleKey.Enter) -{ - ConsoleKey input = default; - while (input != key && !CloseGame) + else { - input = Console.ReadKey(true).Key; - if (input is ConsoleKey.Enter && GameStatus is GameStatus.Paused && FallTimer != null) - { - FallTimer.Change(0, FallSpeedMilliSeconds); - GameStatus = GameStatus.Playing; - return; - } + yAfterFall += 2; } -} - -void AddScoreChangeSpeed(int value) -{ - if (value >= 4) value++; - Score += value; - - FallSpeedMilliSeconds = Score switch - { - > 100 => FallSpeedMilliSeconds = 050, - > 070 => FallSpeedMilliSeconds = 100, - > 060 => FallSpeedMilliSeconds = 200, - > 050 => FallSpeedMilliSeconds = 300, - > 040 => FallSpeedMilliSeconds = 500, - > 030 => FallSpeedMilliSeconds = 700, - > 020 => FallSpeedMilliSeconds = 800, - > 010 => FallSpeedMilliSeconds = 900, - _ => 1000, - }; -} - -void TetrominoFall(object? e) -{ - int yAfterFall = TETROMINO.Y; - bool collision = false; - if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) yAfterFall = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; - else yAfterFall += 2; - - //Y Collision - for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) + // Y Collision + for (int xCollision = 0; xCollision < tetromino.Shape[0].Length;) { - for (int yCollision = TETROMINO.Shape.Length - 1; yCollision >= 0; yCollision -= 2) + for (int yCollision = tetromino.Shape.Length - 1; yCollision >= 0; yCollision -= 2) { - char exist = TETROMINO.Shape[yCollision][xCollision]; - - if (exist is ' ') continue; - - char[] lineYC = PLAYFIELD[yAfterFall + yCollision - 1].ToCharArray(); - - if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; - - if (lineYC[TETROMINO.X + xCollision] is not ' ' or '│') + char exist = tetromino.Shape[yCollision][xCollision]; + if (exist is ' ') + { + continue; + } + char[] lineYC = field[yAfterFall + yCollision - 1].ToCharArray(); + if (tetromino.X + xCollision < 0 || tetromino.X + xCollision > lineYC.Length) + { + continue; + } + if (lineYC[tetromino.X + xCollision] is not ' ' or '│') { char[][] lastFrame = DrawLastFrame(yAfterFall); for (int y = 0; y < lastFrame.Length; y++) { - PLAYFIELD[y] = new string(lastFrame[y]); + field[y] = new string(lastFrame[y]); } - - TETROMINO.X = INITIALTETROMINOX; - TETROMINO.Y = INITIALTETROMINOY; - TETROMINO.Shape = TETROMINO.Next; - TETROMINO.Next = TETROMINOS[Random.Shared.Next(0, TETROMINOS.Length)]; - - xCollision = TETROMINO.Shape[0].Length; + tetromino.X = initialX; + tetromino.Y = initialY; + tetromino.Shape = tetromino.Next; + tetromino.Next = tetrominos[Random.Shared.Next(0, tetrominos.Length)]; + xCollision = tetromino.Shape[0].Length; collision = true; break; } } - xCollision += 3; } - if (!collision) TETROMINO.Y = yAfterFall; + if (!collision) + { + tetromino.Y = yAfterFall; + } - //Clean Lines + // Clean Lines int clearedLines = 0; - for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) + for (int lineIndex = field.Length - 1; lineIndex >= 0; lineIndex--) { - string line = PLAYFIELD[lineIndex]; + string line = field[lineIndex]; bool notCompleted = line.Any(e => e is ' '); - - if (lineIndex is 0 || lineIndex == PLAYFIELD.Length - 1) continue; - + if (lineIndex is 0 || lineIndex == field.Length - 1) + { + continue; + } if (!notCompleted) { - PLAYFIELD[lineIndex] = "│ │"; + field[lineIndex] = "│ │"; clearedLines++; - for (int lineM = lineIndex; lineM >= 1; lineM--) { - if (PLAYFIELD[lineM - 1] is "╭──────────────────────────────╮") + if (field[lineM - 1] is "╭──────────────────────────────╮") { - PLAYFIELD[lineM] = "│ │"; + field[lineM] = "│ │"; continue; } - - PLAYFIELD[lineM] = PLAYFIELD[lineM - 1]; + field[lineM] = field[lineM - 1]; } - lineIndex++; } } - - AddScoreChangeSpeed(clearedLines / 2); - - //VerifiedCollision - if (Collision(Direction.None) && FallTimer is not null) Gameover(); + clearedLines /= 2; + if (clearedLines > 0) + { + int value = clearedLines /= 2 switch + { + 1 => 1, + 2 => 3, + 3 => 6, + 4 => 9, + _ => throw new NotImplementedException(), + }; + score += value; + fallSpeed = GetFallSpeed(); + } + if (Collision(Direction.None)) + { + gameOver = true; + } + else + { + DrawFrame(); + timer.Restart(); + } } void HardDrop() { - int y = TETROMINO.Y; - int x = TETROMINO.X; - var shapeScope = TETROMINO.Shape; - for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) + int y = tetromino.Y; + int x = tetromino.X; + for (int yField = field.Length - tetromino.Shape.Length - borderSize; yField >= 0; yField -= 2) { - if (CollisionBottom(yField, y, shapeScope)) continue; - TETROMINO.Y = yField; + if (CollisionBottom(yField, y, tetromino.Shape)) + { + continue; + } + tetromino.Y = yField; break; } + DrawFrame(); + timer.Restart(); } void TetrominoSpin(Direction spinDirection) { - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - int yScope = TETROMINO.Y; - int xScope = TETROMINO.X; - string[] newShape = new string[shapeScope[0].Length / 3 * 2]; + int yScope = tetromino.Y; + int xScope = tetromino.X; + string[] newShape = new string[tetromino.Shape[0].Length / 3 * 2]; int newY = 0; int rowEven = 0; int rowOdd = 1; - //Turn - for (int y = 0; y < shapeScope.Length;) + // Turn + for (int y = 0; y < tetromino.Shape.Length;) { switch (spinDirection) { case Direction.Right: - SpinRight(newShape, shapeScope, ref newY, rowEven, rowOdd, y); + SpinRight(newShape, tetromino.Shape, ref newY, rowEven, rowOdd, y); break; case Direction.Left: - SpinLeft(newShape, shapeScope, ref newY, rowEven, rowOdd, y); + SpinLeft(newShape, tetromino.Shape, ref newY, rowEven, rowOdd, y); break; } - newY = 0; rowEven += 2; rowOdd += 2; y += 2; } - //Old Pivot + // Old Pivot (int y, int x) offsetOP = (0, 0); - for (int y = 0; y < shapeScope.Length; y += 2) + for (int y = 0; y < tetromino.Shape.Length; y += 2) { - for (int x = 0; x < shapeScope[y].Length; x += 3) + for (int x = 0; x < tetromino.Shape[y].Length; x += 3) { - if (shapeScope[y][x] is 'x') + if (tetromino.Shape[y][x] is 'x') { offsetOP = (y / 2, x / 3); - y = shapeScope.Length; + y = tetromino.Shape.Length; break; } } } - //New Pivot + // New Pivot (int y, int x) offsetNP = (0, 0); for (int y = 0; y < newShape.Length; y += 2) { @@ -789,13 +770,13 @@ void TetrominoSpin(Direction spinDirection) yScope += (offsetOP.y - offsetNP.y) * 2; xScope += (offsetOP.x - offsetNP.x) * 3; - //Tetromino Square(O) special case + // Tetromino Square(O) special case if (newShape.Length / 2 == newShape[0].Length / 3) { - yScope = TETROMINO.Y; - xScope = TETROMINO.X; + yScope = tetromino.Y; + xScope = tetromino.X; } - //Tetromino I special case + // Tetromino I special case else if (newShape.Length is 8 && newShape[0].Length is 3 && offsetNP.y is 2) { newShape[2] = "x─╮"; @@ -803,23 +784,30 @@ void TetrominoSpin(Direction spinDirection) yScope += 2; } - if (xScope < 1 || yScope < 1) return; + if (xScope < 1 || yScope < 1) + { + return; + } - //Verified Collision + // Verified Collision for (int y = 0; y < newShape.Length - 1; y++) { for (int x = 0; x < newShape[y].Length; x++) { - if (newShape[y][x] is ' ') continue; - - char c = PLAYFIELD[yScope + y][xScope + x]; - if (c is not ' ') return; + if (newShape[y][x] is ' ') + { + continue; + } + char c = field[yScope + y][xScope + x]; + if (c is not ' ') + { + return; + } } } - - TETROMINO.Y = yScope; - TETROMINO.X = xScope; - TETROMINO.Shape = newShape; + tetromino.Y = yScope; + tetromino.X = xScope; + tetromino.Shape = newShape; } void SpinLeft(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) @@ -831,7 +819,6 @@ void SpinLeft(string[] newShape, string[] shape, ref int newY, int rowEven, int newShape[newY] += shape[rowEven][x - xS]; newShape[newY + 1] += shape[rowOdd][x - xS]; } - newY += 2; } } @@ -845,27 +832,15 @@ void SpinRight(string[] newShape, string[] shape, ref int newY, int rowEven, int newShape[newY] = ""; newShape[newY + 1] = ""; } - for (int xS = 0; xS <= 2; xS++) { - newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString()); - newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString()); + newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString(CultureInfo.InvariantCulture)); + newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString(CultureInfo.InvariantCulture)); } - newY += 2; } } -void SleepAfterRender() -{ - TimeSpan sleep = TimeSpan.FromSeconds(1d / 60d) - Stopwatch.Elapsed; - if (sleep > TimeSpan.Zero) - { - Thread.Sleep(sleep); - } - Stopwatch.Restart(); -} - class Tetromino { public required string[] Shape { get; set; } @@ -876,14 +851,7 @@ class Tetromino enum Direction { + None, Right, Left, - None } - -enum GameStatus -{ - Gameover, - Playing, - Paused -} \ No newline at end of file diff --git a/Projects/Tetris/README.md b/Projects/Tetris/README.md index 1d91366e..7cac7dd6 100644 --- a/Projects/Tetris/README.md +++ b/Projects/Tetris/README.md @@ -25,28 +25,63 @@ Well, just tetris! -## Input +``` +╭──────────────────────────────╮╭─────────╮ +│ ││ ╭─╮ │ +│ ││ ╰─╯ │ +│ ││╭─╮╭─╮╭─╮│ +│ ││╰─╯╰─╯╰─╯│ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ │╰─────────╯ +│ │╭─────────╮ +│ ││ 12│ +│ │╰─────────╯ +│ ╭─╮ │ +│ ╰─╯ │ +│ ╭─╮ │ +│ ╰─╯ │ +│ ╭─╮ │ +│ ╰─╯ │ +│ ╭─╮ │ +│ ╰─╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ ••• │ +│ ••• │ +│ ╭─╮╭─╮╭─╮••• │ +│ ╰─╯╰─╯╰─╯••• │ +│ ╭─╮╭─╮╭─╮╭─╮••• │ +│ ╰─╯╰─╯╰─╯╰─╯••• │ +│ ╭─╮╭─╮╭─╮╭─╮╭─╮•••╭─╮ ╭─╮│ +│ ╰─╯╰─╯╰─╯╰─╯╰─╯•••╰─╯ ╰─╯│ +╰──────────────────────────────╯ +``` -|Key|Action| -|---|---| -|`↓`, `S`, `←`, `A`, `→`, `D` |Move | -|`Q` |Spin Left | -|`E` |Spin Right | -|`P` |Pause, enter to resume | -|`R` |Change Text Color | +## Input -Debug controls (only if debug mode its active, it may break the game): |Key|Action| |---|---| -|`Spacebar` |Clean Field | -|`X` |Add 10 score points | -|`I` |change active tetromino to I| -|`J` |change active tetromino to J| -|`L` |change active tetromino to L| -|`O` |change active tetromino to O| -|`C` |change active tetromino to S| -|`T` |change active tetromino to T| -|`Z` |change active tetromino to Z| +| `←` or `A` | Move Left | +| `→` or `D` | Move Right | +| `↓` or `S` | Fall Faster | +| `Q` | Spin Left | +| `E` | Spin Right | +| `P` | Pause or Resume | +| `Enter` | Confirm | +| `Escape` | Close Game | ## Downloads From c5ba97dcd35045f2b0a71e76cb7c8e0609e98669 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Tue, 31 Oct 2023 17:18:11 -0500 Subject: [PATCH 18/24] pause while console small fix --- Projects/Tetris/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index 43510fa4..19f50644 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -308,12 +308,13 @@ void HandlePlayerInput() if (timer.IsRunning) { timer.Stop(); + DrawFrame(); } else if (!consoleTooSmallScreen) { timer.Start(); + DrawFrame(); } - DrawFrame(); break; case ConsoleKey.Spacebar: if (timer.IsRunning) From c974ec1aee958eab8f7b24f804839608a1483df5 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Tue, 31 Oct 2023 17:42:19 -0500 Subject: [PATCH 19/24] game over screen and synch to web and clearedLines bug fix --- Projects/Tetris/Program.cs | 6 +- Projects/Website/Games/Tetris/Tetris.cs | 963 +++++++++++++----------- Projects/Website/Pages/Tetris.razor | 6 +- 3 files changed, 524 insertions(+), 451 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index 19f50644..2543ef4c 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -227,8 +227,8 @@ [Spacebar] drop Final Score: {score} - Press enter to play again - Press escape to close the game + [Enter] return to menu + [Escape] close game """); Console.CursorVisible = false; bool gameOverScreen = true; @@ -672,7 +672,7 @@ void TetrominoFall() clearedLines /= 2; if (clearedLines > 0) { - int value = clearedLines /= 2 switch + int value = clearedLines switch { 1 => 1, 2 => 3, diff --git a/Projects/Website/Games/Tetris/Tetris.cs b/Projects/Website/Games/Tetris/Tetris.cs index 00563e63..ce6b007c 100644 --- a/Projects/Website/Games/Tetris/Tetris.cs +++ b/Projects/Website/Games/Tetris/Tetris.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Text; using System.Linq; +using System.Globalization; namespace Website.Games.Tetris; @@ -13,57 +14,17 @@ public class Tetris public async Task Run() { - Console.OutputEncoding = Encoding.UTF8; - Console.CursorVisible = false; - Stopwatch Stopwatch = Stopwatch.StartNew(); + #region Constants - string[] FIELD = new[] + string[] emptyField = new string[42]; + emptyField[0] = "╭──────────────────────────────╮"; + for (int i = 1; i < 41; i++) { - "╭──────────────────────────────╮", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰──────────────────────────────╯" - }; + emptyField[i] = "│ │"; + } + emptyField[^1] = "╰──────────────────────────────╯"; - string[] NEXTTETROMINO = new[] + string[] nextTetrominoBorder = new[] { "╭─────────╮", "│ │", @@ -77,13 +38,15 @@ public async Task Run() "╰─────────╯" }; - string[] SCORE = new[]{ + string[] scoreBorder = new[] + { "╭─────────╮", "│ │", "╰─────────╯" }; - string[] PAUSE = new[]{ + string[] pauseRender = new[] + { "█████╗ ███╗ ██╗██╗█████╗█████╗", "██╔██║██╔██╗██║██║██╔══╝██╔══╝", "█████║█████║██║██║ ███╗ █████╗", @@ -92,12 +55,12 @@ public async Task Run() "╚═╝ ╚═╝╚═╝╚════╝╚════╝╚════╝", }; - string[][] TETROMINOS = new[] + string[][] tetrominos = new[] { new[]{ "╭─╮", "╰─╯", - "╭─╮", + "x─╮", "╰─╯", "╭─╮", "╰─╯", @@ -107,128 +70,272 @@ public async Task Run() new[]{ "╭─╮ ", "╰─╯ ", - "╭─╮╭─╮╭─╮", + "╭─╮x─╮╭─╮", "╰─╯╰─╯╰─╯" }, new[]{ " ╭─╮", " ╰─╯", - "╭─╮╭─╮╭─╮", + "╭─╮x─╮╭─╮", "╰─╯╰─╯╰─╯" }, new[]{ "╭─╮╭─╮", "╰─╯╰─╯", - "╭─╮╭─╮", + "x─╮╭─╮", "╰─╯╰─╯" }, new[]{ " ╭─╮╭─╮", " ╰─╯╰─╯", - "╭─╮╭─╮ ", + "╭─╮x─╮ ", "╰─╯╰─╯ " }, new[]{ " ╭─╮ ", " ╰─╯ ", - "╭─╮╭─╮╭─╮", + "╭─╮x─╮╭─╮", "╰─╯╰─╯╰─╯" }, new[]{ "╭─╮╭─╮ ", "╰─╯╰─╯ ", - " ╭─╮╭─╮", + " x─╮╭─╮", " ╰─╯╰─╯" }, }; - string[] PLAYFIELD = (string[])FIELD.Clone(); - const int BORDER = 1; - int FallSpeedMilliSeconds = 1000; - bool CloseGame = false; - int Score = 0; - int PauseCount = 0; - GameStatus GameStatus = GameStatus.Gameover; + const int borderSize = 1; - Random RamdomGenerator = new(); + int initialX = (emptyField[0].Length / 2) - 3; + int initialY = 1; - int INITIALTETROMINOX = Convert.ToInt16(PLAYFIELD[0].Length / 2) - 3; - int INITIALTETROMINOY = 1; - Tetromino TETROMINO = new() - { - Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - X = INITIALTETROMINOX, - Y = INITIALTETROMINOY - }; + int consoleWidthMin = 44; + int consoleHeightMin = 43; - AutoResetEvent AutoEvent = new AutoResetEvent(false); - Timer? FallTimer = null; - GameStatus = GameStatus.Playing; - - await Console.WriteLine(); - await Console.WriteLine(" ██████╗█████╗██████╗█████╗ ██╗█████╗"); - await Console.WriteLine(" ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝"); - await Console.WriteLine(" ██║ █████╗ ██║ █████╔╝██║ ███╗ "); - await Console.WriteLine(" ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗"); - await Console.WriteLine(" ██║ █████╗ ██║ ██║ ██║██║█████║"); - await Console.WriteLine(" ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝"); - - await Console.WriteLine(); - await Console.WriteLine(" Controls:"); - await Console.WriteLine(" WASD or ARROW to move"); - await Console.WriteLine(" Q or E to spin left or right"); - await Console.WriteLine(" P to paused the game, press enter"); - await Console.WriteLine(" key to resume"); - await Console.WriteLine(); - await Console.Write(" Press enter to start tetris..."); - Console.CursorVisible = false; - await StartGame(); - await Console.Clear(); + #endregion - FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); + Stopwatch timer = new(); + bool closeRequested = false; + bool gameOver; + int score = 0; + TimeSpan fallSpeed; + string[] field; + Tetromino tetromino; + int consoleWidth = Console.WindowWidth; + int consoleHeight = Console.WindowHeight; + bool consoleTooSmallScreen = false; - while (!CloseGame) + Console.OutputEncoding = Encoding.UTF8; + while (!closeRequested) { - if (CloseGame) + await Console.Clear(); + await Console.Write(""" + + ██████╗█████╗██████╗█████╗ ██╗█████╗ + ╚═██╔═╝██╔══╝╚═██╔═╝██╔═██╗██║██╔══╝ + ██║ █████╗ ██║ █████╔╝██║ ███╗ + ██║ ██╔══╝ ██║ ██╔═██╗██║ ██╗ + ██║ █████╗ ██║ ██║ ██║██║█████║ + ╚═╝ ╚════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚════╝ + + Controls: + + [A] or [←] move left + [D] or [→] move right + [S] or [↓] fall faster + [Q] spin left + [E] spin right + [Spacebar] drop + [P] pause and unpause + [Escape] close game + [Enter] start game + """); + bool mainMenuScreen = true; + while (!closeRequested && mainMenuScreen) { - break; + Console.CursorVisible = false; + switch ((await Console.ReadKey(true)).Key) + { + case ConsoleKey.Enter: mainMenuScreen = false; break; + case ConsoleKey.Escape: closeRequested = true; break; + } } + Initialize(); + await Console.Clear(); + await DrawFrame(); + while (!closeRequested && !gameOver) + { + // if user changed the size of the console, we need to clear the console + if (consoleWidth != Console.WindowWidth || consoleHeight != Console.WindowHeight) + { + consoleWidth = Console.WindowWidth; + consoleHeight = Console.WindowHeight; + if (!consoleTooSmallScreen) + { + await Console.Clear(); + await DrawFrame(); + } + else + { + consoleTooSmallScreen = false; + } + } + + // if the console isn't big enough to render the game, pause the game and tell the user + if (consoleWidth < consoleWidthMin || consoleHeight < consoleHeightMin) + { + if (!consoleTooSmallScreen) + { + await Console.Clear(); + await Console.Write($"Please increase size of console to at least {consoleWidthMin}x{consoleHeightMin}. Current size is {consoleWidth}x{consoleHeight}."); + timer.Stop(); + consoleTooSmallScreen = true; + } + } + else if (consoleTooSmallScreen) + { + consoleTooSmallScreen = false; + await Console.Clear(); + await DrawFrame(); + } - await PlayerControl(); - if (GameStatus == GameStatus.Playing) + await HandlePlayerInput(); + if (closeRequested || gameOver) + { + break; + } + if (timer.IsRunning && timer.Elapsed > fallSpeed) + { + await TetrominoFall(); + if (closeRequested || gameOver) + { + break; + } + await DrawFrame(); + } + } + if (closeRequested) { - await DrawFrame(); - await SleepAfterRender(); + break; + } + await Console.Clear(); + await Console.Write($""" + + ██████╗ █████╗ ██ ██╗█████╗ + ██╔════╝ ██╔══██╗███ ███║██╔══╝ + ██║ ███╗███████║██╔██═██║█████╗ + ██║ ██║██╔══██║██║ ██║██╔══╝ + ╚██████╔╝██║ ██║██║ ██║█████╗ + ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝ + ██████╗██╗ ██╗█████╗█████╗ + ██ ██║██║ ██║██╔══╝██╔═██╗ + ██ ██║██║ ██║█████╗█████╔╝ + ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ + ██████║ ╚███╔╝ █████╗██║ ██║ + ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ + + Final Score: {score} + + [Enter] return to menu + [Escape] close game + """); + Console.CursorVisible = false; + bool gameOverScreen = true; + while (!closeRequested && gameOverScreen) + { + Console.CursorVisible = false; + switch ((await Console.ReadKey(true)).Key) + { + case ConsoleKey.Enter: gameOverScreen = false; break; + case ConsoleKey.Escape: closeRequested = true; break; + } } } + await Console.Clear(); + await Console.WriteLine("Tetris was closed."); + Console.CursorVisible = true; + await Console.Refresh(); + + void Initialize() + { + gameOver = false; + score = 0; + field = emptyField[..]; + initialX = (field[0].Length / 2) - 3; + initialY = 1; + tetromino = new() + { + Shape = tetrominos[Random.Shared.Next(0, tetrominos.Length)], + Next = tetrominos[Random.Shared.Next(0, tetrominos.Length)], + X = initialX, + Y = initialY + }; + fallSpeed = GetFallSpeed(); + timer.Restart(); + } - async Task PlayerControl() + async Task HandlePlayerInput() { - while (await Console.KeyAvailable() && GameStatus == GameStatus.Playing) + while ((await Console.KeyAvailable()) && !closeRequested) { switch ((await Console.ReadKey(true)).Key) { case ConsoleKey.A or ConsoleKey.LeftArrow: - if (Collision(Direction.Left)) break; - TETROMINO.X -= 3; + if (timer.IsRunning && !Collision(Direction.Left)) + { + tetromino.X -= 3; + } + await DrawFrame(); break; case ConsoleKey.D or ConsoleKey.RightArrow: - if (Collision(Direction.Right)) break; - TETROMINO.X += 3; + if (timer.IsRunning && !Collision(Direction.Right)) + { + tetromino.X += 3; + } + await DrawFrame(); break; case ConsoleKey.S or ConsoleKey.DownArrow: - FallTimer.Change(0, FallSpeedMilliSeconds); + if (timer.IsRunning) + { + await TetrominoFall(); + } break; case ConsoleKey.E: - TetrominoSpin(Direction.Right); + if (timer.IsRunning) + { + TetrominoSpin(Direction.Right); + await DrawFrame(); + } break; case ConsoleKey.Q: - TetrominoSpin(Direction.Left); + if (timer.IsRunning) + { + TetrominoSpin(Direction.Left); + await DrawFrame(); + } break; case ConsoleKey.P: - PauseGame(); + if (timer.IsRunning) + { + timer.Stop(); + await DrawFrame(); + } + else if (!consoleTooSmallScreen) + { + timer.Start(); + await DrawFrame(); + } break; + case ConsoleKey.Spacebar: + if (timer.IsRunning) + { + await HardDrop(); + } + break; + case ConsoleKey.Escape: + closeRequested = true; + return; } } } @@ -236,130 +343,131 @@ async Task PlayerControl() async Task DrawFrame() { bool collision = false; - int yScope = TETROMINO.Y; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); - char[][] frame = new char[PLAYFIELD.Length][]; + char[][] frame = new char[field.Length][]; - //Field - for (int y = 0; y < PLAYFIELD.Length; y++) + // Field + for (int y = 0; y < field.Length; y++) { - frame[y] = PLAYFIELD[y].ToCharArray(); + frame[y] = field[y].ToCharArray(); } - //Draw Tetromino - for (int y = 0; y < shapeScope.Length && !collision; y++) + // Tetromino + for (int y = 0; y < tetromino.Shape.Length && !collision; y++) { - for (int x = 0; x < shapeScope[y].Length; x++) + for (int x = 0; x < tetromino.Shape[y].Length; x++) { - int tY = yScope + y; - int tX = TETROMINO.X + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') + int tY = tetromino.Y + y; + int tX = tetromino.X + x; + char charToReplace = field[tY][tX]; + char charTetromino = tetromino.Shape[y][x]; + if (charTetromino is ' ') + { + continue; + } + if (charToReplace is not ' ') { collision = true; break; } - + if (charTetromino is 'x') + { + charTetromino = '╭'; + } frame[tY][tX] = charTetromino; } } - //Draw Preview - for (int yField = PLAYFIELD.Length - shapeScope.Length - BORDER; yField >= 0; yField -= 2) + // Draw Preview + for (int yField = field.Length - tetromino.Shape.Length - borderSize; yField >= 0; yField -= 2) { - if (CollisionPreview(yField, yScope, shapeScope)) continue; - - for (int y = 0; y < shapeScope.Length && !collision; y++) + if (CollisionBottom(yField, tetromino.Y, tetromino.Shape)) { - for (int x = 0; x < shapeScope[y].Length; x++) + continue; + } + for (int y = 0; y < tetromino.Shape.Length && !collision; y++) + { + for (int x = 0; x < tetromino.Shape[y].Length; x++) { int tY = yField + y; - - if (yScope + shapeScope.Length > tY) continue; - - int tX = TETROMINO.X + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') + if (tetromino.Y + tetromino.Shape.Length > tY) + { + continue; + } + int tX = tetromino.X + x; + char charToReplace = field[tY][tX]; + char charTetromino = tetromino.Shape[y][x]; + if (charTetromino is ' ') + { + continue; + } + if (charToReplace is not ' ') { collision = true; break; } - frame[tY][tX] = '•'; } } - break; } - //Next Square - for (int y = 0; y < NEXTTETROMINO.Length; y++) - { - frame[y] = frame[y].Concat(NEXTTETROMINO[y]).ToArray(); - } - - //Score Square - for (int y = 0; y < SCORE.Length; y++) + // Next + for (int y = 0; y < nextTetrominoBorder.Length; y++) { - int sY = NEXTTETROMINO.Length + y; - frame[sY] = frame[sY].Concat(SCORE[y]).ToArray(); + frame[y] = frame[y].Concat(nextTetrominoBorder[y]).ToArray(); } - - //Draw Next - for (int y = 0; y < nextShapeScope.Length; y++) + for (int y = 0; y < tetromino.Next.Length; y++) { - for (int x = 0; x < nextShapeScope[y].Length; x++) + for (int x = 0; x < tetromino.Next[y].Length; x++) { - int tY = y + BORDER; - int tX = PLAYFIELD[y].Length + x + BORDER; - char charTetromino = nextShapeScope[y][x]; + int tY = y + borderSize; + int tX = field[y].Length + x + borderSize; + char charTetromino = tetromino.Next[y][x]; + if (charTetromino is 'x') + { + charTetromino = '╭'; + } frame[tY][tX] = charTetromino; } } - //Draw Score - char[] score = Score.ToString().ToCharArray(); - for (int scoreX = score.Length - 1; scoreX >= 0; scoreX--) + // Score + for (int y = 0; y < scoreBorder.Length; y++) + { + int sY = nextTetrominoBorder.Length + y; + frame[sY] = frame[sY].Concat(scoreBorder[y]).ToArray(); + } + char[] scoreRender = score.ToString(CultureInfo.InvariantCulture).ToCharArray(); + for (int scoreX = scoreRender.Length - 1; scoreX >= 0; scoreX--) { - int sY = NEXTTETROMINO.Length + BORDER; - int sX = frame[sY].Length - (score.Length - scoreX) - BORDER; - frame[sY][sX] = score[scoreX]; + int sY = nextTetrominoBorder.Length + borderSize; + int sX = frame[sY].Length - (scoreRender.Length - scoreX) - borderSize; + frame[sY][sX] = scoreRender[scoreX]; } - //Draw Pause - if (GameStatus == GameStatus.Paused) + // Pause + if (!timer.IsRunning) { - for (int y = 0; y < PAUSE.Length; y++) + for (int y = 0; y < pauseRender.Length; y++) { - int fY = (PLAYFIELD.Length / 2) + y - PAUSE.Length; - for (int x = 0; x < PAUSE[y].Length; x++) + int fY = (field.Length / 2) + y - pauseRender.Length; + for (int x = 0; x < pauseRender[y].Length; x++) { - int fX = x + BORDER; + int fX = x + borderSize; - if (x >= PLAYFIELD[fY].Length) break; + if (x >= field[fY].Length) break; - frame[fY][fX] = PAUSE[y][x]; + frame[fY][fX] = pauseRender[y][x]; } } } - //Create Render StringBuilder render = new(); for (int y = 0; y < frame.Length; y++) { render.AppendLine(new string(frame[y])); } - - await Console.Clear(); + await Console.SetCursorPosition(0, 0); await Console.Write(render); Console.CursorVisible = false; } @@ -368,91 +476,91 @@ char[][] DrawLastFrame(int yS) { bool collision = false; int yScope = yS - 2; - int xScope = TETROMINO.X; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - string[] nextShapeScope = (string[])TETROMINO.Next.Clone(); - char[][] frame = new char[PLAYFIELD.Length][]; - - //Field - for (int y = 0; y < PLAYFIELD.Length; y++) + int xScope = tetromino.X; + char[][] frame = new char[field.Length][]; + for (int y = 0; y < field.Length; y++) { - frame[y] = PLAYFIELD[y].ToCharArray(); + frame[y] = field[y].ToCharArray(); } - - //Draw Tetromino - for (int y = 0; y < shapeScope.Length && !collision; y++) + for (int y = 0; y < tetromino.Shape.Length && !collision; y++) { - for (int x = 0; x < shapeScope[y].Length; x++) + for (int x = 0; x < tetromino.Shape[y].Length; x++) { int tY = yScope + y; int tX = xScope + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') + char charToReplace = field[tY][tX]; + char charTetromino = tetromino.Shape[y][x]; + if (charTetromino is ' ') + { + continue; + } + if (charToReplace is not ' ') { collision = true; break; } - + if (charTetromino is 'x') + { + charTetromino = '╭'; + } frame[tY][tX] = charTetromino; } } - return frame; } bool Collision(Direction direction) { - int xNew = TETROMINO.X; - int yScope = TETROMINO.Y; - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); + int xNew = tetromino.X; bool collision = false; - switch (direction) { case Direction.Right: xNew += 3; - if (xNew + shapeScope[0].Length > PLAYFIELD[0].Length - BORDER) collision = true; + if (xNew + tetromino.Shape[0].Length > field[0].Length - borderSize) + { + collision = true; + } break; case Direction.Left: xNew -= 3; - if (xNew < BORDER) collision = true; + if (xNew < borderSize) + { + collision = true; + } break; case Direction.None: break; } - - if (collision) return collision; - - for (int y = 0; y < shapeScope.Length && !collision; y++) + if (collision) + { + return collision; + } + for (int y = 0; y < tetromino.Shape.Length && !collision; y++) { - for (int x = 0; x < shapeScope[y].Length; x++) + for (int x = 0; x < tetromino.Shape[y].Length; x++) { - int tY = yScope + y; + int tY = tetromino.Y + y; int tX = xNew + x; - char charToReplace = PLAYFIELD[tY][tX]; - char charTetromino = shapeScope[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') + char charToReplace = field[tY][tX]; + char charTetromino = tetromino.Shape[y][x]; + if (charTetromino is ' ') + { + continue; + } + if (charToReplace is not ' ') { collision = true; break; } } } - return collision; } - bool CollisionPreview(int initY, int yScope, string[] shape) + bool CollisionBottom(int initY, int yScope, string[] shape) { - int xNew = TETROMINO.X; - + int xNew = tetromino.X; for (int yUpper = initY; yUpper >= yScope; yUpper -= 2) { for (int y = shape.Length - 1; y >= 0; y -= 2) @@ -461,270 +569,257 @@ bool CollisionPreview(int initY, int yScope, string[] shape) { int tY = yUpper + y; int tX = xNew + x; - char charToReplace = PLAYFIELD[tY][tX]; + char charToReplace = field[tY][tX]; char charTetromino = shape[y][x]; - - if (charTetromino == ' ') continue; - - if (charToReplace != ' ') + if (charTetromino is ' ') + { + continue; + } + if (charToReplace is not ' ') { return true; } } } } - return false; } - async void Gameover() - { - GameStatus = GameStatus.Gameover; - AutoEvent.Dispose(); - FallTimer.Dispose(); - - await SleepAfterRender(); - - await Console.Clear(); - await Console.WriteLine(); - await Console.WriteLine(" ██████╗ █████╗ ██ ██╗█████╗"); - await Console.WriteLine(" ██╔════╝ ██╔══██╗███ ███║██╔══╝"); - await Console.WriteLine(" ██║ ███╗███████║██╔██═██║█████╗"); - await Console.WriteLine(" ██║ ██║██╔══██║██║ ██║██╔══╝"); - await Console.WriteLine(" ╚██████╔╝██║ ██║██║ ██║█████╗"); - await Console.WriteLine(" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚════╝"); - await Console.WriteLine(" ██████╗██╗ ██╗█████╗█████╗ "); - await Console.WriteLine(" ██ ██║██║ ██║██╔══╝██╔═██╗ "); - await Console.WriteLine(" ██ ██║██║ ██║█████╗█████╔╝ "); - await Console.WriteLine(" ██ ██║╚██╗██╔╝██╔══╝██╔═██╗ "); - await Console.WriteLine(" ██████║ ╚███╔╝ █████╗██║ ██║ "); - await Console.WriteLine(" ╚═════╝ ╚══╝ ╚════╝╚═╝ ╚═╝ "); - - await Console.WriteLine(); - await Console.WriteLine($" Final Score: {Score}"); - await Console.WriteLine($" Pause Count: {PauseCount}"); - await Console.WriteLine(); - await Console.WriteLine(" Press enter to play again"); - await Console.WriteLine(" Press escape to close the game"); - Console.CursorVisible = false; - await StartGame(); - RestartGame(); - } - - async Task StartGame(ConsoleKey key = ConsoleKey.Enter) - { - ConsoleKey input = default; - while (input != key && !CloseGame) - { - input = (await Console.ReadKey(true)).Key; - if (input is ConsoleKey.Escape) + TimeSpan GetFallSpeed() => + TimeSpan.FromMilliseconds( + score switch { - CloseGame = true; - return; - } - } - } - - void RestartGame() - { - PLAYFIELD = (string[])FIELD.Clone(); - FallSpeedMilliSeconds = 1000; - Score = 0; - TETROMINO = new() - { - Shape = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)], - X = INITIALTETROMINOX, - Y = INITIALTETROMINOY - }; - - AutoEvent = new AutoResetEvent(false); - FallTimer = new Timer(TetrominoFall, AutoEvent, FallSpeedMilliSeconds, FallSpeedMilliSeconds); - GameStatus = GameStatus.Playing; - } - - async void PauseGame() + > 162 => 100, + > 144 => 200, + > 126 => 300, + > 108 => 400, + > 090 => 500, + > 072 => 600, + > 054 => 700, + > 036 => 800, + > 018 => 900, + _ => 1000, + }); + + async Task TetrominoFall() { - PauseCount++; - FallTimer.Change(Timeout.Infinite, Timeout.Infinite); - GameStatus = GameStatus.Paused; - await DrawFrame(); - - await ResumeGame(); - } + int yAfterFall = tetromino.Y; + bool collision = false; - async Task ResumeGame(ConsoleKey key = ConsoleKey.Enter) - { - ConsoleKey input = default; - while (input != key && !CloseGame) + if (tetromino.Y + tetromino.Shape.Length + 2 > field.Length) { - input = (await Console.ReadKey(true)).Key; - if (input is ConsoleKey.Enter && GameStatus == GameStatus.Paused && FallTimer != null) - { - FallTimer.Change(0, FallSpeedMilliSeconds); - GameStatus = GameStatus.Playing; - return; - } + yAfterFall = field.Length - tetromino.Shape.Length + 1; } - } - - void AddScoreChangeSpeed(int value) - { - Score += value; - - if (Score > 100) return; - - switch (Score) + else { - case 10: - FallSpeedMilliSeconds = 900; - break; - case 20: - FallSpeedMilliSeconds = 800; - break; - case 30: - FallSpeedMilliSeconds = 700; - break; - case 40: - FallSpeedMilliSeconds = 500; - break; - case 50: - FallSpeedMilliSeconds = 300; - break; - case 60: - FallSpeedMilliSeconds = 200; - break; - case 70: - FallSpeedMilliSeconds = 100; - break; - case 100: - FallSpeedMilliSeconds = 50; - break; + yAfterFall += 2; } - } - - void TetrominoFall(object? e) - { - int yAfterFall = TETROMINO.Y; - bool collision = false; - if (TETROMINO.Y + TETROMINO.Shape.Length + 2 > PLAYFIELD.Length) yAfterFall = PLAYFIELD.Length - TETROMINO.Shape.Length + 1; - else yAfterFall += 2; - - //Y Collision - for (int xCollision = 0; xCollision < TETROMINO.Shape[0].Length;) + // Y Collision + for (int xCollision = 0; xCollision < tetromino.Shape[0].Length;) { - for (int yCollision = TETROMINO.Shape.Length - 1; yCollision >= 0; yCollision -= 2) + for (int yCollision = tetromino.Shape.Length - 1; yCollision >= 0; yCollision -= 2) { - char exist = TETROMINO.Shape[yCollision][xCollision]; - - if (exist == ' ') continue; - - char[] lineYC = PLAYFIELD[yAfterFall + yCollision - 1].ToCharArray(); - - if (TETROMINO.X + xCollision < 0 || TETROMINO.X + xCollision > lineYC.Length) continue; - - if - ( - lineYC[TETROMINO.X + xCollision] != ' ' && - lineYC[TETROMINO.X + xCollision] != '│' - ) + char exist = tetromino.Shape[yCollision][xCollision]; + if (exist is ' ') + { + continue; + } + char[] lineYC = field[yAfterFall + yCollision - 1].ToCharArray(); + if (tetromino.X + xCollision < 0 || tetromino.X + xCollision > lineYC.Length) + { + continue; + } + if (lineYC[tetromino.X + xCollision] is not ' ' or '│') { char[][] lastFrame = DrawLastFrame(yAfterFall); for (int y = 0; y < lastFrame.Length; y++) { - PLAYFIELD[y] = new string(lastFrame[y]); + field[y] = new string(lastFrame[y]); } - - TETROMINO.X = INITIALTETROMINOX; - TETROMINO.Y = INITIALTETROMINOY; - TETROMINO.Shape = TETROMINO.Next; - TETROMINO.Next = TETROMINOS[RamdomGenerator.Next(0, TETROMINOS.Length)]; - - xCollision = TETROMINO.Shape[0].Length; + tetromino.X = initialX; + tetromino.Y = initialY; + tetromino.Shape = tetromino.Next; + tetromino.Next = tetrominos[Random.Shared.Next(0, tetrominos.Length)]; + xCollision = tetromino.Shape[0].Length; collision = true; break; } } - xCollision += 3; } - if (!collision) TETROMINO.Y = yAfterFall; - - //Clean Lines - for (var lineIndex = PLAYFIELD.Length - 1; lineIndex >= 0; lineIndex--) + if (!collision) { - string line = PLAYFIELD[lineIndex]; - bool notCompleted = line.Any(e => e == ' '); - - if (lineIndex == 0 || lineIndex == PLAYFIELD.Length - 1) continue; + tetromino.Y = yAfterFall; + } + // Clean Lines + int clearedLines = 0; + for (int lineIndex = field.Length - 1; lineIndex >= 0; lineIndex--) + { + string line = field[lineIndex]; + bool notCompleted = line.Any(e => e is ' '); + if (lineIndex is 0 || lineIndex == field.Length - 1) + { + continue; + } if (!notCompleted) { - PLAYFIELD[lineIndex] = "│ │"; - AddScoreChangeSpeed(1); - + field[lineIndex] = "│ │"; + clearedLines++; for (int lineM = lineIndex; lineM >= 1; lineM--) { - if (PLAYFIELD[lineM - 1] == "╭──────────────────────────────╮") + if (field[lineM - 1] is "╭──────────────────────────────╮") { - PLAYFIELD[lineM] = "│ │"; + field[lineM] = "│ │"; continue; } - - PLAYFIELD[lineM] = PLAYFIELD[lineM - 1]; + field[lineM] = field[lineM - 1]; } - lineIndex++; } } + clearedLines /= 2; + if (clearedLines > 0) + { + int value = clearedLines switch + { + 1 => 1, + 2 => 3, + 3 => 6, + 4 => 9, + _ => throw new NotImplementedException(), + }; + score += value; + fallSpeed = GetFallSpeed(); + } + if (Collision(Direction.None)) + { + gameOver = true; + } + else + { + await DrawFrame(); + timer.Restart(); + } + } - //VerifiedCollision - if (Collision(Direction.None) && FallTimer != null) Gameover(); + async Task HardDrop() + { + int y = tetromino.Y; + int x = tetromino.X; + for (int yField = field.Length - tetromino.Shape.Length - borderSize; yField >= 0; yField -= 2) + { + if (CollisionBottom(yField, y, tetromino.Shape)) + { + continue; + } + tetromino.Y = yField; + break; + } + await DrawFrame(); + timer.Restart(); } void TetrominoSpin(Direction spinDirection) { - string[] shapeScope = (string[])TETROMINO.Shape.Clone(); - int yScope = TETROMINO.Y; - string[] newShape = new string[shapeScope[0].Length / 3 * 2]; + int yScope = tetromino.Y; + int xScope = tetromino.X; + string[] newShape = new string[tetromino.Shape[0].Length / 3 * 2]; int newY = 0; int rowEven = 0; int rowOdd = 1; - //Turn - for (int y = 0; y < shapeScope.Length;) + // Turn + for (int y = 0; y < tetromino.Shape.Length;) { switch (spinDirection) { case Direction.Right: - SpinRight(newShape, shapeScope, ref newY, rowEven, rowOdd, y); + SpinRight(newShape, tetromino.Shape, ref newY, rowEven, rowOdd, y); break; case Direction.Left: - SpinLeft(newShape, shapeScope, ref newY, rowEven, rowOdd, y); + SpinLeft(newShape, tetromino.Shape, ref newY, rowEven, rowOdd, y); break; } - newY = 0; rowEven += 2; rowOdd += 2; y += 2; } - //Verified Collision - for (int y = 0; y < newShape.Length - 1; y++) + // Old Pivot + (int y, int x) offsetOP = (0, 0); + for (int y = 0; y < tetromino.Shape.Length; y += 2) { - for (int x = 0; x < newShape[y].Length; x++) + for (int x = 0; x < tetromino.Shape[y].Length; x += 3) { - if (newShape[y][x] == ' ') continue; + if (tetromino.Shape[y][x] is 'x') + { + offsetOP = (y / 2, x / 3); + y = tetromino.Shape.Length; + break; + } + } + } - char c = PLAYFIELD[yScope + y][TETROMINO.X + x]; - if (c != ' ') return; + // New Pivot + (int y, int x) offsetNP = (0, 0); + for (int y = 0; y < newShape.Length; y += 2) + { + for (int x = 0; x < newShape[y].Length; x += 3) + { + if (newShape[y][x] is 'x') + { + offsetNP = (y / 2, x / 3); + y = newShape.Length; + break; + } } } - TETROMINO.Shape = newShape; + yScope += (offsetOP.y - offsetNP.y) * 2; + xScope += (offsetOP.x - offsetNP.x) * 3; + + // Tetromino Square(O) special case + if (newShape.Length / 2 == newShape[0].Length / 3) + { + yScope = tetromino.Y; + xScope = tetromino.X; + } + // Tetromino I special case + else if (newShape.Length is 8 && newShape[0].Length is 3 && offsetNP.y is 2) + { + newShape[2] = "x─╮"; + newShape[4] = "╭─╮"; + yScope += 2; + } + + if (xScope < 1 || yScope < 1) + { + return; + } + + // Verified Collision + for (int y = 0; y < newShape.Length - 1; y++) + { + for (int x = 0; x < newShape[y].Length; x++) + { + if (newShape[y][x] is ' ') + { + continue; + } + char c = field[yScope + y][xScope + x]; + if (c is not ' ') + { + return; + } + } + } + tetromino.Y = yScope; + tetromino.X = xScope; + tetromino.Shape = newShape; } void SpinLeft(string[] newShape, string[] shape, ref int newY, int rowEven, int rowOdd, int y) @@ -736,7 +831,6 @@ void SpinLeft(string[] newShape, string[] shape, ref int newY, int rowEven, int newShape[newY] += shape[rowEven][x - xS]; newShape[newY + 1] += shape[rowOdd][x - xS]; } - newY += 2; } } @@ -745,32 +839,19 @@ void SpinRight(string[] newShape, string[] shape, ref int newY, int rowEven, int { for (int x = 2; x < shape[y].Length; x += 3) { - if (newShape[newY] == null) + if (newShape[newY] is null) { newShape[newY] = ""; newShape[newY + 1] = ""; } - for (int xS = 0; xS <= 2; xS++) { - newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString()); - newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString()); + newShape[newY] = newShape[newY].Insert(0, shape[rowEven][x - xS].ToString(CultureInfo.InvariantCulture)); + newShape[newY + 1] = newShape[newY + 1].Insert(0, shape[rowOdd][x - xS].ToString(CultureInfo.InvariantCulture)); } - newY += 2; } } - - async Task SleepAfterRender() - { - TimeSpan sleep = TimeSpan.FromSeconds(1d / 60d) - Stopwatch.Elapsed; - if (sleep > TimeSpan.Zero) - { - await Console.RefreshAndDelay(sleep); - } - Stopwatch.Restart(); - } - } class Tetromino @@ -783,16 +864,8 @@ class Tetromino enum Direction { + None, Right, Left, - None } - - enum GameStatus - { - Gameover, - Playing, - Paused - } - } diff --git a/Projects/Website/Pages/Tetris.razor b/Projects/Website/Pages/Tetris.razor index 4bc7b4f0..6a0de575 100644 --- a/Projects/Website/Pages/Tetris.razor +++ b/Projects/Website/Pages/Tetris.razor @@ -17,7 +17,6 @@
- @@ -25,6 +24,7 @@ +
@@ -45,8 +45,8 @@ { Game = new(); Console = Game.Console; - Console.WindowWidth = 43; - Console.WindowHeight = 42; + Console.WindowWidth = 45; + Console.WindowHeight = 44; Console.TriggerRefresh = StateHasChanged; } From 29dab220226e73b7ecc5a1c8ca55dccf50c80518 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Tue, 31 Oct 2023 18:04:07 -0500 Subject: [PATCH 20/24] tetris weight = 5 --- .vscode/launch.json | 20 ++++++++++---------- Projects/Website/Shared/NavMenu.razor | 10 +++++----- README.md | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9d48e2c0..f22e1c47 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -462,6 +462,16 @@ "console": "externalTerminal", "stopAtEntry": false, }, + { + "name": "Tetris", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build Tetris", + "program": "${workspaceFolder}/Projects/Tetris/bin/Debug/Tetris.dll", + "cwd": "${workspaceFolder}/Projects/Tetris/bin/Debug", + "console": "externalTerminal", + "stopAtEntry": false, + }, { "name": "Role Playing Game", "type": "coreclr", @@ -492,15 +502,5 @@ "console": "externalTerminal", "stopAtEntry": false, }, - { - "name": "Tetris", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "Build Tetris", - "program": "${workspaceFolder}/Projects/Tetris/bin/Debug/Tetris.dll", - "cwd": "${workspaceFolder}/Projects/Tetris/bin/Debug", - "console": "externalTerminal", - "stopAtEntry": false, - }, ], } diff --git a/Projects/Website/Shared/NavMenu.razor b/Projects/Website/Shared/NavMenu.razor index 4f3b0348..db4f7628 100644 --- a/Projects/Website/Shared/NavMenu.razor +++ b/Projects/Website/Shared/NavMenu.razor @@ -233,6 +233,11 @@ Gravity + - diff --git a/README.md b/README.md index ea5d9322..f001b076 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,10 @@ |[Maze](Projects/Maze)|5|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Maze) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Maze%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[PacMan](Projects/PacMan)|5|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/PacMan) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/PacMan%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[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)| +|[Tetris](Projects/Tetris)|5|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Tetris) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Tetris%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_[Community Contribution](https://github.com/dotnet/dotnet-console-games/pull/89)_| |[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)_| -|[Tetris](Projects/Tetris)|?|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Tetris) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Tetris%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_[Community Contribution](https://github.com/dotnet/dotnet-console-games/pull/89)_| \*_**Weight**: A relative rating for how advanced the source code is._
From 07fc5b0d334d46f6ad809daecb736bec6ae2ddad Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Tue, 31 Oct 2023 18:08:50 -0500 Subject: [PATCH 21/24] task.json order --- .vscode/tasks.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8e3d42b3..f66b39b7 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -640,25 +640,25 @@ "problemMatcher": "$msCompile", }, { - "label": "Build Solution", + "label": "Build Tetris", "command": "dotnet", "type": "process", "args": [ "build", - "${workspaceFolder}", + "${workspaceFolder}/Projects/Tetris/Tetris.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary", ], "problemMatcher": "$msCompile", }, { - "label": "Restore Solution", + "label": "Build Solution", "command": "dotnet", "type": "process", "args": [ - "restore", + "build", "${workspaceFolder}", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary", @@ -666,13 +666,13 @@ "problemMatcher": "$msCompile", }, { - "label": "Build Tetris", + "label": "Restore Solution", "command": "dotnet", "type": "process", "args": [ - "build", - "${workspaceFolder}/Projects/Tetris/Tetris.csproj", + "restore", + "${workspaceFolder}", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary", ], From a0883be3f5fba64c5ade0b3a39b60bfca00e0b93 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Tue, 31 Oct 2023 18:10:21 -0500 Subject: [PATCH 22/24] unpause -> resume --- Projects/Tetris/Program.cs | 2 +- Projects/Website/Games/Tetris/Tetris.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Projects/Tetris/Program.cs b/Projects/Tetris/Program.cs index 2543ef4c..05646a0e 100644 --- a/Projects/Tetris/Program.cs +++ b/Projects/Tetris/Program.cs @@ -137,7 +137,7 @@ [Q] spin left [E] spin right [Spacebar] drop - [P] pause and unpause + [P] pause and resume [Escape] close game [Enter] start game """); diff --git a/Projects/Website/Games/Tetris/Tetris.cs b/Projects/Website/Games/Tetris/Tetris.cs index ce6b007c..1597199d 100644 --- a/Projects/Website/Games/Tetris/Tetris.cs +++ b/Projects/Website/Games/Tetris/Tetris.cs @@ -147,7 +147,7 @@ public async Task Run() [Q] spin left [E] spin right [Spacebar] drop - [P] pause and unpause + [P] pause and resume [Escape] close game [Enter] start game """); From 45b05392bbece18be80911fbb2947369177aca0f Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Tue, 31 Oct 2023 18:17:31 -0500 Subject: [PATCH 23/24] removed /.vscode/ --- Projects/Website/.vscode/launch.json | 11 -------- Projects/Website/.vscode/tasks.json | 41 ---------------------------- 2 files changed, 52 deletions(-) delete mode 100644 Projects/Website/.vscode/launch.json delete mode 100644 Projects/Website/.vscode/tasks.json diff --git a/Projects/Website/.vscode/launch.json b/Projects/Website/.vscode/launch.json deleted file mode 100644 index b0726229..00000000 --- a/Projects/Website/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch and Debug Standalone Blazor WebAssembly App", - "type": "blazorwasm", - "request": "launch", - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/Projects/Website/.vscode/tasks.json b/Projects/Website/.vscode/tasks.json deleted file mode 100644 index 46740de4..00000000 --- a/Projects/Website/.vscode/tasks.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/Website.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary;ForceNoAlign" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/Website.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary;ForceNoAlign" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "--project", - "${workspaceFolder}/Website.csproj" - ], - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file From f65c14cf142009790f1fe9c336e8d63c43f831d1 Mon Sep 17 00:00:00 2001 From: ZacharyPatten Date: Tue, 31 Oct 2023 18:19:07 -0500 Subject: [PATCH 24/24] slnf fix --- dotnet-console-games.slnf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet-console-games.slnf b/dotnet-console-games.slnf index 5b642937..e6060bff 100644 --- a/dotnet-console-games.slnf +++ b/dotnet-console-games.slnf @@ -42,7 +42,7 @@ "Projects\\Snake\\Snake.csproj", "Projects\\Sudoku\\Sudoku.csproj", "Projects\\Tanks\\Tanks.csproj", - "Projects\\Tetris\\Tetris.csproj" + "Projects\\Tetris\\Tetris.csproj", "Projects\\Tic Tac Toe\\Tic Tac Toe.csproj", "Projects\\Tents\\Tents.csproj", "Projects\\Tower Of Hanoi\\Tower Of Hanoi.csproj",