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
+
+
+
+
+
+
+
+
+
+
+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|[](https://dotnet.github.io/dotnet-console-games/Sliding%20Puzzle) [](https://github.com/dotnet/dotnet-console-games/actions)|
|[Snake](Projects/Snake)|3|[](https://dotnet.github.io/dotnet-console-games/Snake) [](https://github.com/dotnet/dotnet-console-games/actions)|
|[Word Search](Projects/Word%20Search)|3|[](https://dotnet.github.io/dotnet-console-games/Word%20Search) [](https://github.com/dotnet/dotnet-console-games/actions)|
+|[Pipes](Projects/Pipes)|3|[](https://dotnet.github.io/dotnet-console-games/Pipes) [](https://github.com/dotnet/dotnet-console-games/actions)|
|[Hurdles](Projects/Hurdles)|3|[](https://dotnet.github.io/dotnet-console-games/Hurdles) [](https://github.com/dotnet/dotnet-console-games/actions)|
|[Pong](Projects/Pong)|3|[](https://dotnet.github.io/dotnet-console-games/Pong) [](https://github.com/dotnet/dotnet-console-games/actions)|
|[Flappy Bird](Projects/Flappy%20Bird)|3|[](https://dotnet.github.io/dotnet-console-games/Flappy%20Bird) [](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