Build a two-player Tic Tac Toe console game in C# — from a blank project to a fully working game.
A two-player Tic Tac Toe game that runs in the Windows/Linux console. Player X and Player Y alternate turns, choosing squares numbered 1–9. The game detects wins and draws and asks if you want to play again.
| Feature | How it works |
|---|---|
| Board display | ASCII grid rendered with Console.WriteLine after every move |
| Input | Player types 1–9; invalid or already-taken squares are rejected |
| Win detection | All 8 possible win lines are checked after each move |
| Draw detection | Triggered when all 9 squares are filled with no winner |
| Play again | Outer loop asks Y/N after each game and resets the board |
Verify your install by opening a terminal and running:
dotnet --version
// Should print something like: 8.0.100
Open a terminal, navigate to where you keep your projects, and run:
dotnet new console -n TicTacToe
cd TicTacToe
This creates a new console application with a single Program.cs file. Delete its default contents — we will write everything from scratch.
Your project folder will look like this:
TicTacToe/
├── TicTacToe.csproj // project config — don't edit manually
└── TicTacToe.cs // rename Program.cs to this, or keep Program.cs
Every C# program lives inside a namespace and at least one class. The Main method is the entry point — it runs first when the program starts.
Open TicTacToe.cs and type this skeleton:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TicTacToe
{
class Program
{
// We will add variables and methods here in the next steps
static void Main(string[] args)
{
GameTime(); // Kick off the game
}
}
}
static? We never create an instance of Program — all methods are called directly on the class. Marking them static means they belong to the class itself, not to an object.
Add these static fields at the top of the class, before any methods. They hold the game state and are accessible from all methods without passing parameters.
class Program
{
// Which player's turn it is ("X" or "Y")
static string curPlayer = "";
// Controls the outer play-again loop
static bool play = true;
// Set to true when a player wins
static bool Winner = false;
// Set to true when all 9 squares are filled with no winner
static bool Draw = false;
// The 3x3 board — stored as a 2D char array
// Numbers 1-9 tell players which square to pick
static char[,] currentBoard =
{
{ '1', '2', '3' }, // bottom row
{ '4', '5', '6' }, // middle row
{ '7', '8', '9' } // top row
};
}
char[,] declares a two-dimensional array. We access cells with [row, col] — e.g., currentBoard[0, 0] is the bottom-left square (square 1). Rows and columns are zero-indexed.
| Square (user input) | Row index | Col index |
|---|---|---|
| 1 (bottom-left) | 0 | 0 |
| 2 (bottom-center) | 0 | 1 |
| 3 (bottom-right) | 0 | 2 |
| 5 (center) | 1 | 1 |
| 9 (top-right) | 2 | 2 |
SetField()SetField() clears the console and redraws the current board state using Console.WriteLine format strings. The {0}, {1}, {2} placeholders are filled in with values from currentBoard.
public static void SetField()
{
Console.Clear();
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2} ", currentBoard[2, 0], currentBoard[2, 1], currentBoard[2, 2]);
Console.WriteLine("_____|_____|_____");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2} ", currentBoard[1, 0], currentBoard[1, 1], currentBoard[1, 2]);
Console.WriteLine("_____|_____|_____");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2} ", currentBoard[0, 0], currentBoard[0, 1], currentBoard[0, 2]);
Console.WriteLine(" | | ");
Console.WriteLine("");
}
The board is printed top-to-bottom visually, so row 2 (top) is printed first and row 0 (bottom) is printed last. Here is what it looks like in the console:
| |
7 | 8 | 9
_____|_____|_____
| |
4 | 5 | 6
_____|_____|_____
| |
1 | 2 | 3
| |
ResetBoard()ResetBoard() is called at the start of each new game. It replaces the board with a fresh set of number characters and resets the Winner and Draw flags.
public static void ResetBoard()
{
char[,] newBoard =
{
{ '1', '2', '3' },
{ '4', '5', '6' },
{ '7', '8', '9' }
};
currentBoard = newBoard; // Replace the old board
Winner = false;
Draw = false;
curPlayer = "";
}
newBoard) and assign it to currentBoard. Assigning the reference — rather than copying cell-by-cell — is the cleanest way to reset a 2D array in C#.EnterXorO()This method converts the player's 1–9 input into a row/column index, checks whether that square is free, places the player's symbol, then calls CheckWinner and redraws the board.
public static void EnterXorO(char playerSign, int input)
{
// Convert 1-based input to 0-based row and column
int x = (input - 1) / 3; // row: 1-3 → 0, 4-6 → 1, 7-9 → 2
int y = (input - 1) % 3; // col: 1,4,7 → 0, 2,5,8 → 1, 3,6,9 → 2
// Check the square is not already taken
if (currentBoard[x, y] != 'X' && currentBoard[x, y] != 'x'
&& currentBoard[x, y] != 'Y' && currentBoard[x, y] != 'y')
{
currentBoard[x, y] = playerSign; // Place the X or Y
CheckWinner(playerSign); // Did this move win?
SetField(); // Redraw the board
}
else
{
// Square already taken — ask again (recursive call)
Console.WriteLine("Player {0}, that option is already chosen, please make another selection.", playerSign);
int selection = Convert.ToInt32(Console.ReadLine());
EnterXorO(playerSign, selection);
}
}
The board numbers 1–9 map to row/column pairs using integer division and modulo:
| Input | (input-1) / 3 = row | (input-1) % 3 = col |
|---|---|---|
| 1 | 0 | 0 |
| 2 | 0 | 1 |
| 3 | 0 | 2 |
| 4 | 1 | 0 |
| 5 | 1 | 1 |
| 6 | 1 | 2 |
| 7 | 2 | 0 |
| 8 | 2 | 1 |
| 9 | 2 | 2 |
EnterXorO calls itself when a square is taken. This is a simple and readable approach for a small game — the recursion depth is at most 8.CheckWinner()After every move, we check all 8 possible winning lines: 3 rows, 3 columns, and 2 diagonals. If all three squares in any line match the current player's symbol, Winner is set to true.
public static void CheckWinner(char playerSign)
{
if (
// Row 0 (bottom row)
((currentBoard[0,0] == playerSign) && (currentBoard[0,1] == playerSign) && (currentBoard[0,2] == playerSign))
// Row 1 (middle row)
|| ((currentBoard[1,0] == playerSign) && (currentBoard[1,1] == playerSign) && (currentBoard[1,2] == playerSign))
// Row 2 (top row)
|| ((currentBoard[2,0] == playerSign) && (currentBoard[2,1] == playerSign) && (currentBoard[2,2] == playerSign))
// Column 0 (left column)
|| ((currentBoard[0,0] == playerSign) && (currentBoard[1,0] == playerSign) && (currentBoard[2,0] == playerSign))
// Column 1 (center column)
|| ((currentBoard[0,1] == playerSign) && (currentBoard[1,1] == playerSign) && (currentBoard[2,1] == playerSign))
// Column 2 (right column)
|| ((currentBoard[0,2] == playerSign) && (currentBoard[1,2] == playerSign) && (currentBoard[2,2] == playerSign))
// Diagonal: bottom-left to top-right
|| ((currentBoard[0,0] == playerSign) && (currentBoard[1,1] == playerSign) && (currentBoard[2,2] == playerSign))
// Diagonal: bottom-right to top-left
|| ((currentBoard[0,2] == playerSign) && (currentBoard[1,1] == playerSign) && (currentBoard[2,0] == playerSign))
)
{
Winner = true;
}
}
PlayGame()PlayGame() manages a single game from first move to last. A for loop runs up to 9 times (one per square). It alternates players using the modulo operator, validates input, and exits early when there is a winner.
public static void PlayGame()
{
int p = 0;
for (p = 0; p < 9; p++)
{
// Draw is only possible on or after the 9th move (index 8)
if (p >= 8) { Draw = true; }
// Alternate players: even turns → X, odd turns → Y
if (p % 2 == 0) { curPlayer = "X"; }
else { curPlayer = "Y"; }
SetField();
// Keep asking until the player enters a valid 1-9 number
bool selectionInt = false;
while (selectionInt == false)
{
Console.WriteLine("Player {0}, Make your selection on the board", curPlayer);
var selection = Console.ReadLine();
// int.TryParse safely converts the string — no crash on bad input
if (int.TryParse(selection, out int selectionInput))
{
if (selectionInput > 0 && selectionInput < 10)
{
selectionInt = true;
EnterXorO(Convert.ToChar(curPlayer), selectionInput);
}
}
}
// Show draw message (only when all 9 squares filled with no winner)
if (Draw == true && Winner == false)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("DRAW!, hit enter to continue");
Console.ReadKey(true);
}
// Show winner message and exit the loop
if (Winner == true)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Player {0} is the WINNER!, hit enter to continue", curPlayer);
Console.ReadKey(true);
p = 11; // Force the for loop to end on the next iteration check
}
}
}
| Concept | Example | What it does |
|---|---|---|
| Modulo for alternating | p % 2 == 0 | Even turns are Player X, odd turns are Player Y |
int.TryParse | int.TryParse(selection, out int selectionInput) | Converts string to int safely — returns false instead of crashing on letters |
| Early loop exit | p = 11 | Sets the loop counter past 9, so the condition p < 9 fails and the loop ends |
ConsoleColor | Console.ForegroundColor = ConsoleColor.Green | Changes the text color for win/draw messages |
Console.ReadKey(true) | Console.ReadKey(true) | Waits for any key press; true hides the key character from the screen |
GameTime()GameTime() wraps everything in an outer loop. It asks the player if they want to play, calls ResetBoard() and PlayGame(), and exits when the player types N.
public static void GameTime()
{
string playAgain = " ";
while (play == true)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Would you like to play Tic Tac Toe? (type Y or N)");
playAgain = Console.ReadLine().ToString().ToUpper();
if (playAgain == "Y")
{
ResetBoard();
PlayGame();
}
else if (playAgain == "N")
{
play = false; // Ends the while loop and exits the program
}
}
}
In your terminal, from inside the TicTacToe folder:
dotnet run
Or in Visual Studio, press F5 (with debugging) or Ctrl+F5 (without).
You should see:
Would you like to play Tic Tac Toe? (type Y or N)
Type Y and press Enter to start a game. Players X and Y take turns entering numbers 1–9.
Your finished TicTacToe.cs should look like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TicTacToe
{
class Program
{
static string curPlayer = "";
static bool play = true;
static bool Winner = false;
static bool Draw = false;
static char[,] currentBoard =
{
{ '1', '2', '3' },
{ '4', '5', '6' },
{ '7', '8', '9' }
};
public static void ResetBoard() { /* ... see Step 7 */ }
public static void SetField() { /* ... see Step 6 */ }
public static void EnterXorO(char playerSign, int input) { /* ... see Step 8 */ }
public static void CheckWinner(char playerSign) { /* ... see Step 9 */ }
public static void PlayGame() { /* ... see Step 10 */ }
public static void GameTime() { /* ... see Step 11 */ }
static void Main(string[] args)
{
GameTime();
}
}
}