Back to C# Examples

C# Tic Tac Toe

Build a two-player Tic Tac Toe console game in C# — from a blank project to a fully working game.

Contents

  1. What We're Building
  2. Prerequisites
  3. Create the Project
  4. Namespace & Class Structure
  5. Declare State Variables
  6. Draw the Board — SetField()
  7. Reset the Board — ResetBoard()
  8. Place a Piece — EnterXorO()
  9. Detect a Win — CheckWinner()
  10. Main Game Loop — PlayGame()
  11. Play Again Loop — GameTime()
  12. Run the Program

1. What We're Building

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.

FeatureHow it works
Board displayASCII grid rendered with Console.WriteLine after every move
InputPlayer types 1–9; invalid or already-taken squares are rejected
Win detectionAll 8 possible win lines are checked after each move
Draw detectionTriggered when all 9 squares are filled with no winner
Play againOuter loop asks Y/N after each game and resets the board
The full source is on GitHub: github.com/CrusadorBoz/C-Sharp-Projects

2. Prerequisites

Verify your install by opening a terminal and running:

dotnet --version
// Should print something like:  8.0.100

3. Create the Project

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.

If you prefer Visual Studio: File → New → Project → Console App (.NET), name it TicTacToe.

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

4. Namespace & Class Structure

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
        }
    }
}
Why 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.

5. Declare State Variables

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
    };
}
Why a 2D array? 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 indexCol index
1 (bottom-left)00
2 (bottom-center)01
3 (bottom-right)02
5 (center)11
9 (top-right)22

6. Draw the Board — 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
     |     |
Console.Clear() erases everything on screen before redrawing. This gives the illusion that the board "updates" instead of scrolling down with each move.

7. Reset the Board — 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 = "";
}
We create a brand-new array (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#.

8. Place a Piece — 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);
    }
}

How the index math works

The board numbers 1–9 map to row/column pairs using integer division and modulo:

Input(input-1) / 3 = row(input-1) % 3 = col
100
201
302
410
511
612
720
821
922
Recursion for duplicate squares: Instead of a loop, 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.

9. Detect a Win — 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;
    }
}
There are exactly 8 ways to win Tic Tac Toe: 3 rows + 3 columns + 2 diagonals. We check all 8 every time because adding a new piece can only complete one of these lines.

10. Main Game Loop — 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
        }
    }
}

Key concepts used here

ConceptExampleWhat it does
Modulo for alternatingp % 2 == 0Even turns are Player X, odd turns are Player Y
int.TryParseint.TryParse(selection, out int selectionInput)Converts string to int safely — returns false instead of crashing on letters
Early loop exitp = 11Sets the loop counter past 9, so the condition p < 9 fails and the loop ends
ConsoleColorConsole.ForegroundColor = ConsoleColor.GreenChanges 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

11. Play Again Loop — 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
        }
    }
}
.ToUpper() converts the player's input to uppercase before comparing, so both "y" and "Y" are accepted. This is a simple way to make string comparisons case-insensitive.

12. Run 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.

Complete source file

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();
        }
    }
}
View and download the complete source at github.com/CrusadorBoz/C-Sharp-Projects