Created
October 3, 2023 19:24
-
-
Save icorbrey/8e88469ee78c3e915faf5288764f956d to your computer and use it in GitHub Desktop.
A playable game of Tic Tac Toe in the terminal. Written in C#.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma warning disable CS8524 | |
string[] TEMPLATES = { | |
"┏━━━┱───┬───┐\n┃ A ┃ B │ C │\n┡━━━╃───┼───┤\n│ D │ E │ F │\n├───┼───┼───┤\n│ G │ H │ I │\n└───┴───┴───┘", // (1, 1) selectedTile | |
"┌───┲━━━┱───┐\n│ A ┃ B ┃ C │\n├───╄━━━╃───┤\n│ D │ E │ F │\n├───┼───┼───┤\n│ G │ H │ I │\n└───┴───┴───┘", // (2, 1) selectedTile | |
"┌───┬───┲━━━┓\n│ A │ B ┃ C ┃\n├───┼───╄━━━┩\n│ D │ E │ F │\n├───┼───┼───┤\n│ G │ H │ I │\n└───┴───┴───┘", // (3, 1) selectedTile | |
"┌───┬───┬───┐\n│ A │ B │ C │\n┢━━━╅───┼───┤\n┃ D ┃ E │ F │\n┡━━━╃───┼───┤\n│ G │ H │ I │\n└───┴───┴───┘", // (1, 2) selectedTile | |
"┌───┬───┬───┐\n│ A │ B │ C │\n├───╆━━━╅───┤\n│ D ┃ E ┃ F │\n├───╄━━━╃───┤\n│ G │ H │ I │\n└───┴───┴───┘", // (2, 2) selectedTile | |
"┌───┬───┬───┐\n│ A │ B │ C │\n├───┼───╆━━━┪\n│ D │ E ┃ F ┃\n├───┼───╄━━━┩\n│ G │ H │ I │\n└───┴───┴───┘", // (3, 2) selectedTile | |
"┌───┬───┬───┐\n│ A │ B │ C │\n├───┼───┼───┤\n│ D │ E │ F │\n┢━━━╅───┼───┤\n┃ G ┃ H │ I │\n┗━━━┹───┴───┘", // (1, 3) selectedTile | |
"┌───┬───┬───┐\n│ A │ B │ C │\n├───┼───┼───┤\n│ D │ E │ F │\n├───╆━━━╅───┤\n│ G ┃ H ┃ I │\n└───┺━━━┹───┘", // (2, 3) selectedTile | |
"┌───┬───┬───┐\n│ A │ B │ C │\n├───┼───┼───┤\n│ D │ E │ F │\n├───┼───╆━━━┪\n│ G │ H ┃ I ┃\n└───┴───┺━━━┛", // (3, 3) selectedTile | |
}; | |
int[][] SCENARIOS = { | |
new[] { 0, 1, 2 }, // Horizontal, row 1 | |
new[] { 3, 4, 5 }, // Horizontal, row 2 | |
new[] { 6, 7, 8 }, // Horizontal, row 3 | |
new[] { 0, 3, 6 }, // Vertical, column 1 | |
new[] { 1, 4, 7 }, // Vertical, column 2 | |
new[] { 2, 5, 8 }, // Vertical, column 3 | |
new[] { 0, 4, 8 }, // Diagonal down | |
new[] { 2, 4, 6 }, // Diagonal up | |
}; | |
Console.CursorVisible = false; | |
Console.WriteLine(); | |
var random = new Random(); | |
for (bool isProgramRunning = true; isProgramRunning;) | |
{ | |
var players = getPlayers(isSinglePlayer()); | |
var board = getNewBoard(); | |
var playerNumber = 0; | |
var selectedTile = 4; | |
for (bool isGameRunning = true; isGameRunning;) | |
{ | |
var (token, playerType) = players[playerNumber]; | |
renderBoard(TEMPLATES, board, selectedTile); | |
switch (getGameResult(SCENARIOS, board)) | |
{ | |
case GameResult.VictoryX: | |
Console.WriteLine(red("Player X won!")); | |
isGameRunning = false; | |
break; | |
case GameResult.VictoryO: | |
Console.WriteLine(blue("Player O won!")); | |
isGameRunning = false; | |
break; | |
case GameResult.Cat: | |
Console.WriteLine("The cat won."); | |
isGameRunning = false; | |
break; | |
case GameResult.Continue: | |
switch (playerType) | |
{ | |
case PlayerType.Human: | |
switch (getAction()) | |
{ | |
case GameAction.Exit: | |
isProgramRunning = false; | |
isGameRunning = false; | |
break; | |
case GameAction.MoveUp: moveUp(ref selectedTile); break; | |
case GameAction.MoveLeft: moveLeft(ref selectedTile); break; | |
case GameAction.MoveDown: moveDown(ref selectedTile); break; | |
case GameAction.MoveRight: moveRight(ref selectedTile); break; | |
case GameAction.Place: | |
if (isValidPlacement(board, selectedTile)) | |
{ | |
placeToken(ref board, selectedTile, token); | |
switchPlayers(ref playerNumber); | |
} | |
break; | |
} | |
break; | |
case PlayerType.Computer: | |
var index = getNextMove(SCENARIOS, board, random); | |
selectedTile = index; | |
placeToken(ref board, index, token); | |
switchPlayers(ref playerNumber); | |
break; | |
} | |
clearLines(7); | |
break; | |
} | |
} | |
if (isProgramRunning) | |
{ | |
isProgramRunning = isPlayingAgain(); | |
if (isProgramRunning) | |
clearLines(11); | |
} | |
} | |
Console.CursorVisible = true; | |
static List<(Token, PlayerType)> getPlayers(bool isSinglePlayer) => new() | |
{ | |
(Token.X, PlayerType.Human), | |
(Token.O, isSinglePlayer | |
? PlayerType.Computer | |
: PlayerType.Human), | |
}; | |
static bool isSinglePlayer() | |
{ | |
bool isSinglePlayer = true; | |
while (true) | |
{ | |
Console.Write($"\rHow many players? {inverse("1", isSinglePlayer)} or {inverse("2", !isSinglePlayer)}"); | |
switch (Console.ReadKey(true).Key) | |
{ | |
case ConsoleKey.Tab: | |
case ConsoleKey.LeftArrow: | |
case ConsoleKey.RightArrow: | |
isSinglePlayer = !isSinglePlayer; | |
break; | |
case ConsoleKey.Enter: | |
Console.Write("\n\n"); | |
return isSinglePlayer; | |
} | |
} | |
} | |
static Token[] getNewBoard() => | |
Enumerable.Repeat(Token.None, 9).ToArray(); | |
static void renderBoard(string[] templates, Token[] board, int selectedTile) => | |
Console.WriteLine(templates[selectedTile] | |
.Replace("A", getTokenChar(board[0])) | |
.Replace("B", getTokenChar(board[1])) | |
.Replace("C", getTokenChar(board[2])) | |
.Replace("D", getTokenChar(board[3])) | |
.Replace("E", getTokenChar(board[4])) | |
.Replace("F", getTokenChar(board[5])) | |
.Replace("G", getTokenChar(board[6])) | |
.Replace("H", getTokenChar(board[7])) | |
.Replace("I", getTokenChar(board[8]))); | |
static GameResult getGameResult(int[][] scenarios, Token[] board) | |
{ | |
foreach (var scenario in scenarios) | |
{ | |
var a = board[scenario[0]]; | |
var b = board[scenario[1]]; | |
var c = board[scenario[2]]; | |
// If the first token is real and all the tokens match, someone won. | |
if (a != Token.None && a == b && b == c) | |
{ | |
return a switch | |
{ | |
Token.X => GameResult.VictoryX, | |
Token.O => GameResult.VictoryO, | |
_ => throw new Exception(), | |
}; | |
} | |
} | |
// If no empty spots are left on the board and no scenario matched, the cat won. | |
return !board.Any(t => t == Token.None) switch | |
{ | |
true => GameResult.Cat, | |
false => GameResult.Continue, | |
}; | |
} | |
static GameAction getAction() | |
{ | |
while (true) | |
{ | |
switch (Console.ReadKey(true).Key) | |
{ | |
case ConsoleKey.Escape: return GameAction.Exit; | |
case ConsoleKey.Enter: return GameAction.Place; | |
case ConsoleKey.UpArrow: return GameAction.MoveUp; | |
case ConsoleKey.LeftArrow: return GameAction.MoveLeft; | |
case ConsoleKey.DownArrow: return GameAction.MoveDown; | |
case ConsoleKey.RightArrow: return GameAction.MoveRight; | |
}; | |
} | |
} | |
static void moveUp(ref int selectedTile) | |
{ | |
if (2 < selectedTile) | |
{ | |
selectedTile -= 3; | |
} | |
} | |
static void moveLeft(ref int selectedTile) | |
{ | |
if (selectedTile % 3 != 0) | |
{ | |
selectedTile -= 1; | |
} | |
} | |
static void moveDown(ref int selectedTile) | |
{ | |
if (selectedTile < 6) | |
{ | |
selectedTile += 3; | |
} | |
} | |
static void moveRight(ref int selectedTile) | |
{ | |
if (selectedTile % 3 != 2) | |
{ | |
selectedTile += 1; | |
} | |
} | |
static bool isValidPlacement(Token[] board, int selectedTile) => | |
board[selectedTile] == Token.None; | |
static void placeToken(ref Token[] board, int selectedTile, Token token) => | |
board[selectedTile] = token; | |
static void switchPlayers(ref int playerNumber) => | |
playerNumber = playerNumber switch | |
{ | |
0 => 1, | |
_ => 0, | |
}; | |
static int getNextMove(int[][] scenarios, Token[] board, Random random) | |
{ | |
var scores = Enumerable.Repeat(0, 9).ToArray(); | |
foreach (var scenario in scenarios) | |
{ | |
var scenarioScores = getScenarioScores(board, scenario); | |
for (var i = 0; i < 3; i++) | |
scores[scenario[i]] += scenarioScores[i]; | |
} | |
var sum = scores.Sum(); | |
var n = random.Next(0, sum); | |
for (var i = 0; i < scores.Length; i++) | |
{ | |
if (n < scores[i]) | |
return i; | |
n -= scores[i]; | |
} | |
throw new Exception(); | |
} | |
static void clearLines(int lines) | |
{ | |
int targetLine = Console.CursorTop - lines; | |
Console.SetCursorPosition(0, targetLine); | |
for (var i = 0; i < lines; i++) | |
Console.WriteLine(new string(' ', Console.BufferWidth)); | |
Console.SetCursorPosition(0, targetLine); | |
} | |
static bool isPlayingAgain() | |
{ | |
Console.Write("Play again? [Y/n] "); | |
while (true) | |
{ | |
switch (Console.ReadKey(true).Key) | |
{ | |
case ConsoleKey.Y: | |
case ConsoleKey.Enter: | |
Console.WriteLine("Y"); | |
return true; | |
case ConsoleKey.N: | |
case ConsoleKey.Escape: | |
Console.WriteLine("N"); | |
return false; | |
} | |
} | |
} | |
static string getTokenChar(Token token) => token switch | |
{ | |
Token.None => " ", | |
Token.X => red("X"), | |
Token.O => blue("O"), | |
}; | |
static List<int> getScenarioScores(Token[] board, int[] scenario) | |
{ | |
var tokens = scenario.Select(i => board[i]); | |
var xCount = tokens.Where(t => t == Token.X).Count(); | |
var oCount = tokens.Where(t => t == Token.O).Count(); | |
var score = oCount switch | |
{ | |
0 => xCount switch | |
{ | |
0 => 1, | |
1 => 10, | |
_ => 100, | |
}, | |
1 => xCount switch | |
{ | |
0 => 30, | |
_ => 0, | |
}, | |
_ => xCount switch | |
{ | |
0 => 1000, | |
_ => 0, | |
} | |
}; | |
return tokens | |
.Select(t => t switch | |
{ | |
Token.None => score, | |
_ => 0, | |
}) | |
.ToList(); | |
} | |
static string red(string s) => $"\x1b[91m{s}\x1b[39m"; | |
static string blue(string s) => $"\x1b[94m{s}\x1b[39m"; | |
static string inverse(string s, bool isInverse) => isInverse ? $"\x1b[7m{s}\x1b[27m" : s; | |
enum Player { One, Two }; | |
enum PlayerType { Human, Computer } | |
enum Token { None, X, O }; | |
enum GameAction | |
{ | |
MoveUp, | |
MoveLeft, | |
MoveDown, | |
MoveRight, | |
Place, | |
Exit, | |
} | |
enum GameResult | |
{ | |
Continue, | |
VictoryX, | |
VictoryO, | |
Cat, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment