Skip to content

Instantly share code, notes, and snippets.

@icorbrey
Created October 3, 2023 19:24
Show Gist options
  • Save icorbrey/8e88469ee78c3e915faf5288764f956d to your computer and use it in GitHub Desktop.
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#.
#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