diff --git a/.github/workflows/Pipes Build.yml b/.github/workflows/Pipes Build.yml new file mode 100644 index 00000000..430913ca --- /dev/null +++ b/.github/workflows/Pipes Build.yml @@ -0,0 +1,20 @@ +name: Pipes Build +on: + push: + paths: + - 'Projects/Pipes/**' + - '!**.md' + pull_request: + paths: + - 'Projects/Pipes/**' + - '!**.md' + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.0.x + - run: dotnet build "Projects\Pipes\Pipes.csproj" --configuration Release diff --git a/.vscode/launch.json b/.vscode/launch.json index 77b7a819..2794bb43 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -282,6 +282,16 @@ "console": "externalTerminal", "stopAtEntry": false, }, + { + "name": "Pipes", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build Pipes", + "program": "${workspaceFolder}/Projects/Pipes/bin/Debug/Pipes.dll", + "cwd": "${workspaceFolder}/Projects/Pipes/bin/Debug", + "console": "externalTerminal", + "stopAtEntry": false, + }, { "name": "Pong", "type": "coreclr", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0250913c..d284d1f9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -171,6 +171,19 @@ ], "problemMatcher": "$msCompile", }, + { + "label": "Build Pipes", + "command": "dotnet", + "type": "process", + "args": + [ + "build", + "${workspaceFolder}/Projects/Pipes/Pipes.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + ], + "problemMatcher": "$msCompile", + }, { "label": "Build Pong", "command": "dotnet", diff --git a/Projects/Pipes/Pipes.csproj b/Projects/Pipes/Pipes.csproj new file mode 100644 index 00000000..a222ba7d --- /dev/null +++ b/Projects/Pipes/Pipes.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + disable + enable + + + diff --git a/Projects/Pipes/Program.cs b/Projects/Pipes/Program.cs new file mode 100644 index 00000000..2fd73cb9 --- /dev/null +++ b/Projects/Pipes/Program.cs @@ -0,0 +1,385 @@ +using System; +using System.Text; +using System.Threading; + +#region Level Arrays + Pipe[,] level1 = + { + { new Pipe(Pipe.Line, 0, 0), new Pipe(Pipe.Corner, 0, 1), new Pipe(Pipe.Corner, 0, 2) }, + { new Pipe(Pipe.Line, 1, 0), new Pipe(Pipe.Line, 1, 1), new Pipe(Pipe.Line, 1, 2) }, + { new Pipe(Pipe.Corner, 2, 0), new Pipe(Pipe.Corner, 2, 1), new Pipe(Pipe.Line, 2, 2) }, +}; + + Pipe[,] level2 = + { + { new Pipe(Pipe.Junction, 0, 0), new Pipe(Pipe.Line, 0, 1), new Pipe(Pipe.Junction, 0, 2), new Pipe(Pipe.Corner, 0, 3) }, + { new Pipe(Pipe.Line, 1, 0), new Pipe(Pipe.Line, 1, 1), new Pipe(Pipe.Corner, 1, 2), new Pipe(Pipe.Corner, 1, 3) }, + { new Pipe(Pipe.Corner, 2, 0), new Pipe(Pipe.Cross, 2, 1), new Pipe(Pipe.Corner, 2, 2), new Pipe(Pipe.Line, 2, 3) }, + { new Pipe(Pipe.Line, 3, 0), new Pipe(Pipe.Junction, 3, 1), new Pipe(Pipe.End, 3, 2), new Pipe(Pipe.Junction, 3, 3) }, +}; + + Pipe[,] level3 = + { + { new Pipe(Pipe.Corner, 0, 0), new Pipe(Pipe.Edge, 0, 1), new Pipe(Pipe.End, 0, 2) }, + { new Pipe(Pipe.Edge, 1, 0), new Pipe(Pipe.Corner, 1, 1), new Pipe(Pipe.Line, 1, 2) }, + { new Pipe(Pipe.Corner, 2, 0), new Pipe(Pipe.Line, 2, 1), new Pipe(Pipe.Corner, 2, 2) }, +}; + + Pipe[,] level4 = + { + { new Pipe(Pipe.Edge, 0, 0), new Pipe(Pipe.Line, 0, 1), new Pipe(Pipe.Junction, 0, 2), new Pipe(Pipe.Corner, 0, 3) }, + { new Pipe(Pipe.Line, 1, 0), new Pipe(Pipe.Junction, 1, 1), new Pipe(Pipe.Cross, 1, 2), new Pipe(Pipe.Corner, 1, 3) }, + { new Pipe(Pipe.Edge, 2, 0), new Pipe(Pipe.Cross, 2, 1), new Pipe(Pipe.Corner, 2, 2), new Pipe(Pipe.Edge, 2, 3) }, + { new Pipe(Pipe.Line, 3, 0), new Pipe(Pipe.Junction, 3, 1), new Pipe(Pipe.End, 3, 2), new Pipe(Pipe.Junction, 3, 3) }, +}; + + Pipe[,] level5 = + { + { new Pipe(Pipe.Corner, 0, 0), new Pipe(Pipe.Junction, 0, 1), new Pipe(Pipe.Line, 0, 2), new Pipe(Pipe.Corner, 0, 3), new Pipe(Pipe.End, 0, 4) }, + { new Pipe(Pipe.Corner, 1, 0), new Pipe(Pipe.Cross, 1, 1), new Pipe(Pipe.Corner, 1, 2), new Pipe(Pipe.End, 1, 3), new Pipe(Pipe.Edge, 1, 4) }, + { new Pipe(Pipe.Line, 2, 0), new Pipe(Pipe.Line, 2, 1), new Pipe(Pipe.Corner, 2, 2), new Pipe(Pipe.Cross, 2, 3), new Pipe(Pipe.Line, 2, 4) }, + { new Pipe(Pipe.Junction, 3, 0), new Pipe(Pipe.Line, 3, 1), new Pipe(Pipe.Dual, 3, 2), new Pipe(Pipe.Corner, 3, 3), new Pipe(Pipe.Junction, 3, 4) }, + { new Pipe(Pipe.End, 4, 0), new Pipe(Pipe.Junction, 4, 1), new Pipe(Pipe.Corner, 4, 2), new Pipe(Pipe.Line, 4, 3), new Pipe(Pipe.Edge, 4, 4) }, +}; + + Pipe[][,] levels = + { + level1, + level2, + level3, + level4, + level5 +}; + #endregion + +int level = 1; +int pipeCount; +int boardSize; +char[,] board; +Pipe[,] pipes; +bool levelComplete; +bool gameOver = false; +(int row, int col) sink; +(int row, int col) cursor; +(int row, int col) drain; +ConsoleColor defaultcolor = Console.ForegroundColor; + +try +{ + Console.CursorVisible = false; + Console.WriteLine(); + Console.WriteLine(" Pipes"); + Console.WriteLine(); + Console.WriteLine(" Orient the pipes to allow the water"); + Console.WriteLine(" to flow from the sink to the drain!"); + Console.WriteLine(); + Console.WriteLine(" Controls"); + Console.WriteLine(" - arrow keys: change selected pipe"); + Console.WriteLine(" - spacebar: rotate selected pipe"); + Console.WriteLine(" - S: skip level"); + Console.WriteLine(" - R: randomize board after level 5"); + Console.WriteLine(" - escape: exit game"); + Console.WriteLine(); + Console.WriteLine(" Recommended Window Size: 120 width x 30 height"); + Console.WriteLine(); + Console.WriteLine(" Press any key to begin..."); + Console.ReadKey(true); + + LoadLevel(level1); + DrawTitle("Level 1"); + + while (!gameOver) + { + if (Console.KeyAvailable) + { + switch (Console.ReadKey(true).Key) + { + case ConsoleKey.UpArrow or ConsoleKey.K: cursor.row--; break; + case ConsoleKey.DownArrow or ConsoleKey.J: cursor.row++; break; + case ConsoleKey.LeftArrow or ConsoleKey.H: cursor.col--; break; + case ConsoleKey.RightArrow or ConsoleKey.L: cursor.col++; break; + case ConsoleKey.R: if (level is 5) RandomizeBoard(); break; + case ConsoleKey.Spacebar: + pipes[cursor.row, cursor.col].Rotate(); + WriteToBoard(pipes[cursor.row, cursor.col]); + break; + case ConsoleKey.S: levelComplete = true; continue; + case ConsoleKey.Escape: gameOver = true; continue; + } + + cursor.row = Math.Clamp(cursor.row, 0, pipeCount - 1); + cursor.col = Math.Clamp(cursor.col, 0, pipeCount - 1); + + DrawBoard(); + DrawCursor(); + CalculateAndDrawWater(); + } + + if (levelComplete) + { + if (level is 5) + { + Console.SetCursorPosition(0, 0); + Console.Write("Gameover! Press [R] to scramble board, scrambled boards may not be solvable. Good Luck!"); + } + else + { + Console.SetCursorPosition(0, 0); + Console.Write($"Level complete moving to level {level + 1}"); + Thread.Sleep(TimeSpan.FromSeconds(3)); + + LoadLevel(levels[level]); + DrawTitle($"Level {++level}"); + } + } + } + + Console.SetCursorPosition(0, boardSize + pipeCount); + + void DrawTitle(string title) + { + Console.SetCursorPosition(0, 0); + Console.Write(title); + } + + void LoadLevel(Pipe[,] level) + { + pipes = level; + pipeCount = pipes.GetLength(0); + boardSize = Pipe.TILE_SIZE * pipeCount; + board = new char[boardSize, boardSize]; + cursor = (0, 0); + sink = (2, 2); + drain = (sink.row + boardSize + 1, boardSize - 1); + + Console.Clear(); + Console.SetCursorPosition(0, 1); + Console.WriteLine(BuildBorder()); + + for (int r = 0; r < pipeCount; r++) + { + for (int c = 0; c < pipeCount; c++) + { + WriteToBoard(pipes[r, c]); + } + } + + DrawBoard(); + DrawCursor(); + CalculateAndDrawWater(); + } + + string BuildBorder() + { + StringBuilder sb = new(); + int total = pipeCount * Pipe.TILE_SIZE; + + string empty = new(' ', total); + string horizontal = new('─', total - Pipe.TILE_SIZE); + + sb.AppendLine(" ┌o┐"); + sb.AppendLine($"┌┘ └{horizontal}┐"); + + for (int i = 0; i < total; i++) + { + sb.AppendLine($"│{empty}│"); + } + + sb.AppendLine($"└{horizontal}┐ ┌┘"); + sb.AppendLine($"{empty}\b\b└o┘"); + + return sb.ToString(); + } + + void WriteToBoard(Pipe pipe) + { + char[,] mask = pipe.Mask; + (int row, int col) pos = pipe.GetBoardPosition(); + for (int r = 0; r < Pipe.TILE_SIZE; r++) + { + for (int c = 0; c < Pipe.TILE_SIZE; c++) + { + board[pos.row + r, pos.col + c] = mask[r, c]; + } + } + } + + void RandomizeBoard() + { + Random rng = new(); + string[] pipeShapes = + { + Pipe.Line, + Pipe.Corner, + Pipe.Cross, + Pipe.Dual, + Pipe.Junction, + Pipe.Edge, + Pipe.End + }; + + Console.SetCursorPosition(drain.col, drain.row); + Console.Write(' '); + for (int r = 0; r < pipeCount; r++) + { + for (int c = 0; c < pipeCount; c++) + { + string shape = pipeShapes[rng.Next(pipeShapes.Length)]; + pipes[r, c] = new Pipe(shape, r, c); + WriteToBoard(pipes[r, c]); + } + } + } + + void DrawBoard() + { + StringBuilder sb = new(); + int rows = board.GetLength(0); + int cols = board.GetLength(1); + + for (int r = 0; r < rows; r++) + { + sb.Append('│'); + for (int c = 0; c < cols; c++) + { + sb.Append(board[r, c] is not '\0' ? board[r, c] : ' '); + } + sb.AppendLine(); + } + + Console.SetCursorPosition(0, 3); + Console.Write(sb.ToString()); + } + + void DrawCursor() + { + Pipe currentPipe = pipes[cursor.row, cursor.col]; + string shape = currentPipe.PipeString; + (int row, int col) = currentPipe.GetBoardPosition(); + + Console.ForegroundColor = ConsoleColor.DarkGreen; + + for (int i = 0; i < Pipe.TILE_SIZE; i++) + { + Console.SetCursorPosition(col + 1, row + i + 3); + Console.Write(shape[(i * 3)..((i * 3) + 3)]); + } + + Console.ForegroundColor = defaultcolor; + } + + void CalculateAndDrawWater() + { + Console.ForegroundColor = ConsoleColor.Blue; + Console.SetCursorPosition(sink.col, sink.row); + Console.Write('▒'); + + levelComplete = false; + int rows = board.GetLength(0); + int cols = board.GetLength(1); + bool[,] visited = new bool[rows, cols]; + + FloodFill(0, 1); + + if (levelComplete) + { + Console.SetCursorPosition(drain.col, drain.row); + Console.Write('▒'); + } + + Console.ForegroundColor = defaultcolor; + + void FloodFill(int r, int c) + { + if (r < 0 || r >= rows || c < 0 || c >= cols) return; + if (visited[r, c]) return; + if (board[r, c] != ' ') return; + if (r == drain.row - 4 && c == drain.col - 1) levelComplete = true; + + visited[r, c] = true; + Console.SetCursorPosition(c + 1, r + 3); + Console.Write('▒'); + + FloodFill(r + 1, c); + FloodFill(r - 1, c); + FloodFill(r, c + 1); + FloodFill(r, c - 1); + } + } +} +finally +{ + Console.CursorVisible = true; + Console.ResetColor(); + Console.ForegroundColor = defaultcolor; + Console.Clear(); +} +public class Pipe +{ + public const int TILE_SIZE = 3; + + public const string Line = "█ ██ ██ █"; + public const string Corner = "████ █ █"; + public const string Dual = "█ █ █"; + public const string Junction = "███ █ █"; + public const string Cross = "█ █ █ █"; + public const string Edge = "█ ██ ███"; + public const string End = "███ ████"; + + public int Row, Col; + public string PipeString; + + public char[,] Mask => StringToMask(PipeString); + + public Pipe(string pipeString, int row, int col) + { + PipeString = pipeString; + Row = row; + Col = col; + } + + public void Rotate() + { + char[,] mask = Mask; + int n = mask.GetLength(0); + char[,] rotated = new char[n, n]; + + for (int i = 0; i < n; i++) + { + for (int j = 0; j < n; j++) + { + rotated[j, n - 1 - i] = mask[i, j]; + } + } + + PipeString = MaskToString(rotated); + } + + private static char[,] StringToMask(string s) => new char[,] + { + { s[0], s[1], s[2] }, + { s[3], s[4], s[5] }, + { s[6], s[7], s[8] } + }; + + + private static string MaskToString(char[,] mask) + { + StringBuilder sb = new(); + + for (int r = 0; r < mask.GetLength(0); r++) + { + for (int c = 0; c < mask.GetLength(1); c++) + { + sb.Append(mask[r, c]); + } + } + + return sb.ToString(); + } + + public (int row, int col) GetBoardPosition() + { + return (Row * TILE_SIZE, Col * TILE_SIZE); + } +} diff --git a/Projects/Pipes/README.md b/Projects/Pipes/README.md new file mode 100644 index 00000000..3d9212ff --- /dev/null +++ b/Projects/Pipes/README.md @@ -0,0 +1,43 @@ +

+ Pipes +

+ +

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

+ +The goal is to rotate all pipes until the water can flow from the sink to the drain! + +``` + Sink + v + ┌o┐ +┌┘▒└──────┐ +│█▒███████│ +│█▒██ █│ +│█▒██ ██ █│ +│█▒██ ██ █│ +│█▒██ ██ █│ +│█▒██ ██ █│ +│████ ██ █│ +│█ ██ █│ +│█ █████ █│ +└──────┐ ┌┘ + └o┘ + ^ + Drain +``` + +## Input + +- `arrow keys`: change selected pipe +- `spacebar`: rotate selected pipe +- `S`: skip level +- `R`: randomize board +- `escape`: exit game + +## Downloads \ No newline at end of file diff --git a/Projects/Website/Games/Pipes/Pipes.cs b/Projects/Website/Games/Pipes/Pipes.cs new file mode 100644 index 00000000..3983a3b4 --- /dev/null +++ b/Projects/Website/Games/Pipes/Pipes.cs @@ -0,0 +1,407 @@ +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Website.Games.Pipes; + +public class Pipes +{ + + public readonly BlazorConsole Console = new(); + + public async Task Run() + { + + #region Level Arrays + Pipe[,] level1 = + { + { new Pipe(Pipe.Line, 0, 0), new Pipe(Pipe.Corner, 0, 1), new Pipe(Pipe.Corner, 0, 2) }, + { new Pipe(Pipe.Line, 1, 0), new Pipe(Pipe.Line, 1, 1), new Pipe(Pipe.Line, 1, 2) }, + { new Pipe(Pipe.Corner, 2, 0), new Pipe(Pipe.Corner, 2, 1), new Pipe(Pipe.Line, 2, 2) }, +}; + + Pipe[,] level2 = + { + { new Pipe(Pipe.Junction, 0, 0), new Pipe(Pipe.Line, 0, 1), new Pipe(Pipe.Junction, 0, 2), new Pipe(Pipe.Corner, 0, 3) }, + { new Pipe(Pipe.Line, 1, 0), new Pipe(Pipe.Line, 1, 1), new Pipe(Pipe.Corner, 1, 2), new Pipe(Pipe.Corner, 1, 3) }, + { new Pipe(Pipe.Corner, 2, 0), new Pipe(Pipe.Cross, 2, 1), new Pipe(Pipe.Corner, 2, 2), new Pipe(Pipe.Line, 2, 3) }, + { new Pipe(Pipe.Line, 3, 0), new Pipe(Pipe.Junction, 3, 1), new Pipe(Pipe.End, 3, 2), new Pipe(Pipe.Junction, 3, 3) }, +}; + + Pipe[,] level3 = + { + { new Pipe(Pipe.Corner, 0, 0), new Pipe(Pipe.Edge, 0, 1), new Pipe(Pipe.End, 0, 2) }, + { new Pipe(Pipe.Edge, 1, 0), new Pipe(Pipe.Corner, 1, 1), new Pipe(Pipe.Line, 1, 2) }, + { new Pipe(Pipe.Corner, 2, 0), new Pipe(Pipe.Line, 2, 1), new Pipe(Pipe.Corner, 2, 2) }, +}; + + Pipe[,] level4 = + { + { new Pipe(Pipe.Edge, 0, 0), new Pipe(Pipe.Line, 0, 1), new Pipe(Pipe.Junction, 0, 2), new Pipe(Pipe.Corner, 0, 3) }, + { new Pipe(Pipe.Line, 1, 0), new Pipe(Pipe.Junction, 1, 1), new Pipe(Pipe.Cross, 1, 2), new Pipe(Pipe.Corner, 1, 3) }, + { new Pipe(Pipe.Edge, 2, 0), new Pipe(Pipe.Cross, 2, 1), new Pipe(Pipe.Corner, 2, 2), new Pipe(Pipe.Edge, 2, 3) }, + { new Pipe(Pipe.Line, 3, 0), new Pipe(Pipe.Junction, 3, 1), new Pipe(Pipe.End, 3, 2), new Pipe(Pipe.Junction, 3, 3) }, +}; + + Pipe[,] level5 = + { + { new Pipe(Pipe.Corner, 0, 0), new Pipe(Pipe.Junction, 0, 1), new Pipe(Pipe.Line, 0, 2), new Pipe(Pipe.Corner, 0, 3), new Pipe(Pipe.End, 0, 4) }, + { new Pipe(Pipe.Corner, 1, 0), new Pipe(Pipe.Cross, 1, 1), new Pipe(Pipe.Corner, 1, 2), new Pipe(Pipe.End, 1, 3), new Pipe(Pipe.Edge, 1, 4) }, + { new Pipe(Pipe.Line, 2, 0), new Pipe(Pipe.Line, 2, 1), new Pipe(Pipe.Corner, 2, 2), new Pipe(Pipe.Cross, 2, 3), new Pipe(Pipe.Line, 2, 4) }, + { new Pipe(Pipe.Junction, 3, 0), new Pipe(Pipe.Line, 3, 1), new Pipe(Pipe.Dual, 3, 2), new Pipe(Pipe.Corner, 3, 3), new Pipe(Pipe.Junction, 3, 4) }, + { new Pipe(Pipe.End, 4, 0), new Pipe(Pipe.Junction, 4, 1), new Pipe(Pipe.Corner, 4, 2), new Pipe(Pipe.Line, 4, 3), new Pipe(Pipe.Edge, 4, 4) }, +}; + + Pipe[][,] levels = + { + level1, + level2, + level3, + level4, + level5 +}; + #endregion + + int level = 1; + int pipeCount = 0; + int boardSize = 0; + char[,] board = new char[1, 1]; + Pipe[,] pipes = level1; + bool levelComplete = false; + bool gameOver = false; + (int row, int col) sink = (0,0); + (int row, int col) cursor = (0,0); + (int row, int col) drain = (0,0); + ConsoleColor defaultcolor = Console.ForegroundColor; + + try + { + Console.CursorVisible = false; + await Console.WriteLine(); + await Console.WriteLine(" Pipes"); + await Console.WriteLine(); + await Console.WriteLine(" Orient the pipes to allow the water"); + await Console.WriteLine(" to flow from the sink to the drain!"); + await Console.WriteLine(); + await Console.WriteLine(" Controls"); + await Console.WriteLine(" - arrow keys: change selected pipe"); + await Console.WriteLine(" - spacebar: rotate selected pipe"); + await Console.WriteLine(" - S: skip level"); + await Console.WriteLine(" - R: randomize board after level 5"); + await Console.WriteLine(" - escape: exit game"); + await Console.WriteLine(); + await Console.WriteLine(" Recommended Window Size: 120 width x 30 height"); + await Console.WriteLine(); + await Console.WriteLine(" Press any key to begin..."); + await Console.ReadKey(true); + + await LoadLevel(level1); + await DrawTitle("Level 1"); + + while (!gameOver) + { + if (await Console.KeyAvailable()) + { + switch ((await Console.ReadKey(true)).Key) + { + case ConsoleKey.UpArrow or ConsoleKey.K: cursor.row--; break; + case ConsoleKey.DownArrow or ConsoleKey.J: cursor.row++; break; + case ConsoleKey.LeftArrow or ConsoleKey.H: cursor.col--; break; + case ConsoleKey.RightArrow or ConsoleKey.L: cursor.col++; break; + case ConsoleKey.R: if (level is 5) await RandomizeBoard(); break; + case ConsoleKey.Spacebar: + pipes[cursor.row, cursor.col].Rotate(); + WriteToBoard(pipes[cursor.row, cursor.col]); + break; + case ConsoleKey.S: levelComplete = true; continue; + case ConsoleKey.Escape: gameOver = true; continue; + } + + cursor.row = Math.Clamp(cursor.row, 0, pipeCount - 1); + cursor.col = Math.Clamp(cursor.col, 0, pipeCount - 1); + + await DrawBoard(); + await DrawCursor(); + await CalculateAndDrawWater(); + } + + if (levelComplete) + { + if (level is 5) + { + await Console.SetCursorPosition(0, 0); + await Console.Write("Gameover! Press [R] to scramble board, scrambled boards may not be solvable. Good Luck!"); + } + else + { + await Console.SetCursorPosition(0, 0); + await Console.Write($"Level complete moving to level {level + 1}"); + await Console.RefreshAndDelay(TimeSpan.FromSeconds(3)); + + await LoadLevel(levels[level]); + await DrawTitle($"Level {++level}"); + } + } + } + + await Console.SetCursorPosition(0, boardSize + pipeCount); + + async Task DrawTitle(string title) + { + await Console.SetCursorPosition(0, 0); + await Console.Write(title); + return false; + } + + async Task LoadLevel(Pipe[,] level) + { + pipes = level; + pipeCount = pipes.GetLength(0); + boardSize = Pipe.TILE_SIZE * pipeCount; + board = new char[boardSize, boardSize]; + cursor = (0, 0); + sink = (2, 2); + drain = (sink.row + boardSize + 1, boardSize - 1); + + await Console.Clear(); + await Console.SetCursorPosition(0, 1); + await Console.WriteLine(BuildBorder()); + + for (int r = 0; r < pipeCount; r++) + { + for (int c = 0; c < pipeCount; c++) + { + WriteToBoard(pipes[r, c]); + } + } + + await DrawBoard(); + await DrawCursor(); + await CalculateAndDrawWater(); + return false; + } + + string BuildBorder() + { + StringBuilder sb = new(); + int total = pipeCount * Pipe.TILE_SIZE; + + string empty = new(' ', total); + string horizontal = new('─', total - Pipe.TILE_SIZE); + + sb.AppendLine(" ┌o┐"); + sb.AppendLine($"┌┘ └{horizontal}┐"); + + for (int i = 0; i < total; i++) + { + sb.AppendLine($"│{empty}│"); + } + + sb.AppendLine($"└{horizontal}┐ ┌┘"); + sb.AppendLine($"{empty}\b\b└o┘"); + + return sb.ToString(); + } + + void WriteToBoard(Pipe pipe) + { + char[,] mask = pipe.Mask; + (int row, int col) pos = pipe.GetBoardPosition(); + for (int r = 0; r < Pipe.TILE_SIZE; r++) + { + for (int c = 0; c < Pipe.TILE_SIZE; c++) + { + board[pos.row + r, pos.col + c] = mask[r, c]; + } + } + } + + async Task RandomizeBoard() + { + Random rng = new(); + string[] pipeShapes = + { + Pipe.Line, + Pipe.Corner, + Pipe.Cross, + Pipe.Dual, + Pipe.Junction, + Pipe.Edge, + Pipe.End + }; + + await Console.SetCursorPosition(drain.col, drain.row); + await Console.Write(' '); + for (int r = 0; r < pipeCount; r++) + { + for (int c = 0; c < pipeCount; c++) + { + string shape = pipeShapes[rng.Next(pipeShapes.Length)]; + pipes[r, c] = new Pipe(shape, r, c); + WriteToBoard(pipes[r, c]); + } + } + return false; + } + + async Task DrawBoard() + { + StringBuilder sb = new(); + int rows = board.GetLength(0); + int cols = board.GetLength(1); + + for (int r = 0; r < rows; r++) + { + sb.Append('│'); + for (int c = 0; c < cols; c++) + { + sb.Append(board[r, c] is not '\0' ? board[r, c] : ' '); + } + sb.AppendLine(); + } + + await Console.SetCursorPosition(0, 3); + await Console.Write(sb.ToString()); + return false; + } + + async Task DrawCursor() + { + Pipe currentPipe = pipes[cursor.row, cursor.col]; + string shape = currentPipe.PipeString; + (int row, int col) = currentPipe.GetBoardPosition(); + + Console.ForegroundColor = ConsoleColor.DarkGreen; + + for (int i = 0; i < Pipe.TILE_SIZE; i++) + { + await Console.SetCursorPosition(col + 1, row + i + 3); + await Console.Write(shape[(i * 3)..((i * 3) + 3)]); + } + + Console.ForegroundColor = defaultcolor; + return false; + } + + async Task CalculateAndDrawWater() + { + Console.ForegroundColor = ConsoleColor.Blue; + await Console.SetCursorPosition(sink.col, sink.row); + await Console.Write('▒'); + + levelComplete = false; + int rows = board.GetLength(0); + int cols = board.GetLength(1); + bool[,] visited = new bool[rows, cols]; + + await FloodFill(0, 1); + + if (levelComplete) + { + await Console.SetCursorPosition(drain.col, drain.row); + await Console.Write('▒'); + } + + Console.ForegroundColor = defaultcolor; + return false; + + async Task FloodFill(int r, int c) + { + if (r < 0 || r >= rows || c < 0 || c >= cols) return false; + if (visited[r, c]) return false; + if (board[r, c] != ' ') return false; + if (r == drain.row - 4 && c == drain.col - 1) levelComplete = true; + + visited[r, c] = true; + await Console.SetCursorPosition(c + 1, r + 3); + await Console.Write('▒'); + + await FloodFill(r + 1, c); + await FloodFill(r - 1, c); + await FloodFill(r, c + 1); + await FloodFill(r, c - 1); + return false; + } + } + } + finally + { + Console.CursorVisible = true; + Console.ResetColor(); + Console.ForegroundColor = defaultcolor; + await Console.Clear(); + } + } + + public class Pipe + { + public const int TILE_SIZE = 3; + + public const string Line = "█ ██ ██ █"; + public const string Corner = "████ █ █"; + public const string Dual = "█ █ █"; + public const string Junction = "███ █ █"; + public const string Cross = "█ █ █ █"; + public const string Edge = "█ ██ ███"; + public const string End = "███ ████"; + + public int Row, Col; + public string PipeString; + + public char[,] Mask => StringToMask(PipeString); + + public Pipe(string pipeString, int row, int col) + { + PipeString = pipeString; + Row = row; + Col = col; + } + + public void Rotate() + { + char[,] mask = Mask; + int n = mask.GetLength(0); + char[,] rotated = new char[n, n]; + + for (int i = 0; i < n; i++) + { + for (int j = 0; j < n; j++) + { + rotated[j, n - 1 - i] = mask[i, j]; + } + } + + PipeString = MaskToString(rotated); + } + + private static char[,] StringToMask(string s) => new char[,] + { + { s[0], s[1], s[2] }, + { s[3], s[4], s[5] }, + { s[6], s[7], s[8] } + }; + + + private static string MaskToString(char[,] mask) + { + StringBuilder sb = new(); + + for (int r = 0; r < mask.GetLength(0); r++) + { + for (int c = 0; c < mask.GetLength(1); c++) + { + sb.Append(mask[r, c]); + } + } + + return sb.ToString(); + } + + public (int row, int col) GetBoardPosition() + { + return (Row * TILE_SIZE, Col * TILE_SIZE); + } + } + +} \ No newline at end of file diff --git a/README.md b/README.md index c6d2637e..6a0d6ba6 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ |[Sliding Puzzle](Projects/Sliding%20Puzzle)|2|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Sliding%20Puzzle) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Sliding%20Puzzle%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[Snake](Projects/Snake)|3|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Snake) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Snake%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[Word Search](Projects/Word%20Search)|3|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Word%20Search) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Word%20Search%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| +|[Pipes](Projects/Pipes)|3|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Pipes) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Pipes%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[Hurdles](Projects/Hurdles)|3|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Hurdles) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Hurdles%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[Pong](Projects/Pong)|3|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Pong) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Pong%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| |[Flappy Bird](Projects/Flappy%20Bird)|3|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Flappy%20Bird) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Flappy%20Bird%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)| diff --git a/dotnet-console-games.sln b/dotnet-console-games.sln index 19180f80..0ef2be83 100644 --- a/dotnet-console-games.sln +++ b/dotnet-console-games.sln @@ -113,6 +113,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reversi", "Projects\Reversi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "First Person Shooter", "Projects\First Person Shooter\First Person Shooter.csproj", "{5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pipes", "Projects\Pipes\Pipes.csproj", "{9633A198-F964-FC48-6399-55FB10AE16AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -339,6 +341,10 @@ Global {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Release|Any CPU.Build.0 = Release|Any CPU + {9633A198-F964-FC48-6399-55FB10AE16AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9633A198-F964-FC48-6399-55FB10AE16AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9633A198-F964-FC48-6399-55FB10AE16AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9633A198-F964-FC48-6399-55FB10AE16AE}.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 6c8623c5..d55dee38 100644 --- a/dotnet-console-games.slnf +++ b/dotnet-console-games.slnf @@ -1,61 +1,63 @@ { - "solution": { - "path": "dotnet-console-games.sln", - "projects": [ - "Projects\\2048\\2048.csproj", - "Projects\\Battleship\\Battleship.csproj", - "Projects\\Beep Pad\\Beep Pad.csproj", - "Projects\\Bound\\Bound.csproj", - "Projects\\Blackjack\\Blackjack.csproj", - "Projects\\Checkers\\Checkers.csproj", - "Projects\\Clicker\\Clicker.csproj", - "Projects\\Connect 4\\Connect 4.csproj", - "Projects\\Console Monsters\\Console Monsters.csproj", - "Projects\\Darts\\Darts.csproj", - "Projects\\Dice Game\\Dice Game.csproj", - "Projects\\Draw\\Draw.csproj", - "Projects\\Drive\\Drive.csproj", - "Projects\\Duck Hunt\\Duck Hunt.csproj", - "Projects\\Fighter\\Fighter.csproj", - "Projects\\First Person Shooter\\First Person Shooter.csproj", - "Projects\\Flappy Bird\\Flappy Bird.csproj", - "Projects\\Flash Cards\\Flash Cards.csproj", - "Projects\\Guess A Number\\Guess A Number.csproj", - "Projects\\Gravity\\Gravity.csproj", - "Projects\\Hangman\\Hangman.csproj", - "Projects\\Helicopter\\Helicopter.csproj", - "Projects\\Hurdles\\Hurdles.csproj", - "Projects\\Lights Out\\Lights Out.csproj", - "Projects\\Mancala\\Mancala.csproj", - "Projects\\Maze\\Maze.csproj", - "Projects\\Memory\\Memory.csproj", - "Projects\\Minesweeper\\Minesweeper.csproj", - "Projects\\Oligopoly\\Oligopoly.csproj", - "Projects\\PacMan\\PacMan.csproj", - "Projects\\Pong\\Pong.csproj", - "Projects\\Quick Draw\\Quick Draw.csproj", - "Projects\\Reversi\\Reversi.csproj", - "Projects\\Rock Paper Scissors\\Rock Paper Scissors.csproj", - "Projects\\Role Playing Game\\Role Playing Game.csproj", - "Projects\\Roll And Move\\Roll And Move.csproj", - "Projects\\Rythm\\Rythm.csproj", - "Projects\\Shmup\\Shmup.csproj", - "Projects\\Simon\\Simon.csproj", - "Projects\\Sliding Puzzle\\Sliding Puzzle.csproj", - "Projects\\Snake\\Snake.csproj", - "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", - "Projects\\Tug Of War\\Tug Of War.csproj", - "Projects\\Type\\Type.csproj", - "Projects\\Whack A Mole\\Whack A Mole.csproj", - "Projects\\Word Search\\Word Search.csproj", - "Projects\\Wordle\\Wordle.csproj", - "Projects\\Wumpus World\\Wumpus World.csproj", - "Projects\\Yahtzee\\Yahtzee.csproj", - ] - } + "solution": { + "path": "dotnet-console-games.sln", + "projects": [ + "Projects\\2048\\2048.csproj", + "Projects\\Battleship\\Battleship.csproj", + "Projects\\Beep Pad\\Beep Pad.csproj", + "Projects\\Blackjack\\Blackjack.csproj", + "Projects\\Bound\\Bound.csproj", + "Projects\\Checkers\\Checkers.csproj", + "Projects\\Clicker\\Clicker.csproj", + "Projects\\Connect 4\\Connect 4.csproj", + "Projects\\Console Monsters\\Console Monsters.csproj", + "Projects\\Darts\\Darts.csproj", + "Projects\\Dice Game\\Dice Game.csproj", + "Projects\\Draw\\Draw.csproj", + "Projects\\Drive\\Drive.csproj", + "Projects\\Duck Hunt\\Duck Hunt.csproj", + "Projects\\Fighter\\Fighter.csproj", + "Projects\\First Person Shooter\\First Person Shooter.csproj", + "Projects\\Flappy Bird\\Flappy Bird.csproj", + "Projects\\Flash Cards\\Flash Cards.csproj", + "Projects\\Gravity\\Gravity.csproj", + "Projects\\Guess A Number\\Guess A Number.csproj", + "Projects\\Hangman\\Hangman.csproj", + "Projects\\Helicopter\\Helicopter.csproj", + "Projects\\Hurdles\\Hurdles.csproj", + "Projects\\Lights Out\\Lights Out.csproj", + "Projects\\Mancala\\Mancala.csproj", + "Projects\\Maze\\Maze.csproj", + "Projects\\Memory\\Memory.csproj", + "Projects\\Minesweeper\\Minesweeper.csproj", + "Projects\\Oligopoly\\Oligopoly.csproj", + "Projects\\PacMan\\PacMan.csproj", + "Projects\\Pipes\\Pipes.csproj", + "Projects\\Pong\\Pong.csproj", + "Projects\\Quick Draw\\Quick Draw.csproj", + "Projects\\Reversi\\Reversi.csproj", + "Projects\\Rock Paper Scissors\\Rock Paper Scissors.csproj", + "Projects\\Role Playing Game\\Role Playing Game.csproj", + "Projects\\Roll And Move\\Roll And Move.csproj", + "Projects\\Rythm\\Rythm.csproj", + "Projects\\Shmup\\Shmup.csproj", + "Projects\\Simon\\Simon.csproj", + "Projects\\Sliding Puzzle\\Sliding Puzzle.csproj", + "Projects\\Snake\\Snake.csproj", + "Projects\\Sudoku\\Sudoku.csproj", + "Projects\\Tanks\\Tanks.csproj", + "Projects\\Tents\\Tents.csproj", + "Projects\\Tetris\\Tetris.csproj", + "Projects\\Tic Tac Toe\\Tic Tac Toe.csproj", + "Projects\\Tower Of Hanoi\\Tower Of Hanoi.csproj", + "Projects\\Tug Of War\\Tug Of War.csproj", + "Projects\\Type\\Type.csproj", + "Projects\\Website\\Website.csproj", + "Projects\\Whack A Mole\\Whack A Mole.csproj", + "Projects\\Word Search\\Word Search.csproj", + "Projects\\Wordle\\Wordle.csproj", + "Projects\\Wumpus World\\Wumpus World.csproj", + "Projects\\Yahtzee\\Yahtzee.csproj" + ] + } } \ No newline at end of file