Skip to main content
This tutorial was originally written when I was first learning Java. I’ve kept the original code below as a time capsule of my learning journey. If you want to see how I would write this today, with proper input validation and error handling, check out the retrospective at the bottom.
This tutorial walks you through building a fully playable command-line Tic Tac Toe game in Java. It uses the most basic solution to make it accessible to all skill levels. What you’ll learn:
  • Arrays and 2D array manipulation
  • Boolean logic and conditionals
  • Functions and method design
  • Loop structures and game flow
  • User input validation
  • Game state management
DifficultyBeginner
LanguageJava
Read time11 min
Originally publishedMedium, May 6, 2021

Prerequisites

To check your Java version, open a terminal and run java -version.

Building the game

1

Build the board

The first step is to create the board. We’ll use a 2D character array filled with dashes, vertical bars, and spaces to create the visual layout.
TicTacToe.java
public class TicTacToe {
    public static void main(String[] args) {
        
        char[][] gameBoard = {
            {'_','|','_','|','_'},
            {'_','|','_','|','_'},
            {' ','|',' ','|',' '}
        };
        
        printBoard(gameBoard);
    }
    
    public static void printBoard(char[][] gameBoard) {
        for (char[] row : gameBoard) {
            for (char c : row) {
                System.out.print(c);
            }
            System.out.println();
        }
    }
}
The game positions map to array indices as follows:
PositionArray index
Top row (1, 2, 3)[0][0], [0][2], [0][4]
Middle row (4, 5, 6)[1][0], [1][2], [1][4]
Bottom row (7, 8, 9)[2][0], [2][2], [2][4]
The vertical bars exist for visual formatting only. They occupy even-indexed columns ([*][1] and [*][3]) but are never used in game logic.
2

Place pieces

Next, add a method to update the board when a player makes a move.
  • Player is represented by the character X and the number 1
  • Computer is represented by the character O and the number 2
TicTacToe.java
public static void updateBoard(int position, int player, char[][] gameBoard) {
    
    char character;
    
    if (player == 1) {
        character = 'X';
    } else {
        character = 'O';
    }
    
    switch (position) {
        case 1:
            gameBoard[0][0] = character;
            printBoard(gameBoard);
            break;
        case 2:
            gameBoard[0][2] = character;
            printBoard(gameBoard);
            break;
        case 3:
            gameBoard[0][4] = character;
            printBoard(gameBoard);
            break;
        case 4:
            gameBoard[1][0] = character;
            printBoard(gameBoard);
            break;
        case 5:
            gameBoard[1][2] = character;
            printBoard(gameBoard);
            break;
        case 6:
            gameBoard[1][4] = character;
            printBoard(gameBoard);
            break;
        case 7:
            gameBoard[2][0] = character;
            printBoard(gameBoard);
            break;
        case 8:
            gameBoard[2][2] = character;
            printBoard(gameBoard);
            break;
        case 9:
            gameBoard[2][4] = character;
            printBoard(gameBoard);
            break;
        default:
            break;
    }
}
Test it by calling updateBoard directly from main:
updateBoard(5, 1, gameBoard); // Player X in center
updateBoard(1, 2, gameBoard); // Computer O in top-left
updateBoard(7, 1, gameBoard); // Player X in bottom-left
3

Get player input

Add a Scanner to read input from the keyboard, then call it from a playerMove method.
TicTacToe.java
import java.util.Scanner;

public class TicTacToe {
    
    // Static Scanner for reuse across methods
    static Scanner input = new Scanner(System.in);
    
    public static void main(String[] args) {
        char[][] gameBoard = {
            {'_','|','_','|','_'},
            {'_','|','_','|','_'},
            {' ','|',' ','|',' '}
        };
        
        playerMove(gameBoard);
    }
    
    public static void playerMove(char[][] gameBoard) {
        System.out.println("Please make a move. (1-9)");
        int move = input.nextInt();
        updateBoard(move, 1, gameBoard);
    }
}
We create a static Scanner because player input is needed across multiple methods. One shared instance prevents memory issues and simplifies the code structure.
4

Validate moves

Before placing a piece, check that the chosen position is within range and not already occupied.Valid moves must:
  • Be between 1 and 9
  • Target a position containing _ or ' ' (empty squares only)
TicTacToe.java
public static void playerMove(char[][] gameBoard) {
    boolean validMove = false;
    
    while (!validMove) {
        System.out.println("Please make a move. (1-9)");
        int move = input.nextInt();
        validMove = isValidMove(move, gameBoard);
        if (validMove) {
            updateBoard(move, 1, gameBoard);
        }
    }
}

public static boolean isValidMove(int move, char[][] gameBoard) {
    switch (move) {
        case 1:
            return gameBoard[0][0] == '_';
        case 2:
            return gameBoard[0][2] == '_';
        case 3:
            return gameBoard[0][4] == '_';
        case 4:
            return gameBoard[1][0] == '_';
        case 5:
            return gameBoard[1][2] == '_';
        case 6:
            return gameBoard[1][4] == '_';
        case 7:
            return gameBoard[2][0] == ' ';
        case 8:
            return gameBoard[2][2] == ' ';
        case 9:
            return gameBoard[2][4] == ' ';
        default:
            return false;
    }
}
The bottom row checks for ' ' (space) rather than '_' because the bottom row uses spaces as its empty-cell character.
5

Simulate the computer

The computer picks a random position between 1 and 9, re-rolling until it finds an empty square.
TicTacToe.java
import java.util.Random;

public static void computerMove(char[][] gameBoard) {
    Random rand = new Random();
    boolean validMove = false;
    
    while (!validMove) {
        int move = rand.nextInt(9) + 1; // Random number 1-9
        validMove = isValidMove(move, gameBoard);
        if (validMove) {
            System.out.println("Computer chose position: " + move);
            updateBoard(move, 2, gameBoard);
        }
    }
}
rand.nextInt(9) returns a number from 0 to 8. Adding 1 shifts the range to 1–9, which maps to our board positions.
6

Detect a winner

There are 8 possible winning combinations: 3 horizontal, 3 vertical, and 2 diagonal. Check all of them after every move.
TicTacToe.java
public static boolean isGameOver(char[][] gameBoard) {
    
    // Check horizontal wins
    if (gameBoard[0][0] == gameBoard[0][2] && 
        gameBoard[0][2] == gameBoard[0][4] && 
        gameBoard[0][0] != '_') {
        printWinner(gameBoard[0][0]);
        return true;
    }
    
    if (gameBoard[1][0] == gameBoard[1][2] && 
        gameBoard[1][2] == gameBoard[1][4] && 
        gameBoard[1][0] != '_') {
        printWinner(gameBoard[1][0]);
        return true;
    }
    
    if (gameBoard[2][0] == gameBoard[2][2] && 
        gameBoard[2][2] == gameBoard[2][4] && 
        gameBoard[2][0] != ' ') {
        printWinner(gameBoard[2][0]);
        return true;
    }
    
    // Check vertical wins
    if (gameBoard[0][0] == gameBoard[1][0] && 
        gameBoard[1][0] == gameBoard[2][0] && 
        gameBoard[0][0] != '_' && gameBoard[0][0] != ' ') {
        printWinner(gameBoard[0][0]);
        return true;
    }
    
    // ... (3 more vertical/diagonal checks)
    
    // Check for tie (board full)
    if (isBoardFull(gameBoard)) {
        System.out.println("It's a tie!");
        return true;
    }
    
    return false;
}

public static void printWinner(char winner) {
    if (winner == 'X') {
        System.out.println("Player wins!");
    } else {
        System.out.println("Computer wins!");
    }
}

public static boolean isBoardFull(char[][] gameBoard) {
    for (int i = 0; i < gameBoard.length; i++) {
        for (int j = 0; j < gameBoard[i].length; j++) {
            if (gameBoard[i][j] == '_' || gameBoard[i][j] == ' ') {
                return false;
            }
        }
    }
    return true;
}
7

Create the game loop

Bring everything together in main. The outer loop handles play-again logic; the inner loop alternates turns until the game ends.
TicTacToe.java
public static void main(String[] args) {
    boolean playAgain = true;
    
    while (playAgain) {
        char[][] gameBoard = {
            {'_','|','_','|','_'},
            {'_','|','_','|','_'},
            {' ','|',' ','|',' '}
        };
        
        System.out.println("Welcome to Tic Tac Toe!");
        printBoard(gameBoard);
        
        boolean gameOver = false;
        
        while (!gameOver) {
            // Player's turn
            playerMove(gameBoard);
            gameOver = isGameOver(gameBoard);
            if (gameOver) break;
            
            // Computer's turn
            computerMove(gameBoard);
            gameOver = isGameOver(gameBoard);
        }
        
        System.out.println("Would you like to play again? (y/n)");
        char response = input.next().charAt(0);
        playAgain = (response == 'y' || response == 'Y');
    }
    
    System.out.println("Thanks for playing!");
    input.close();
}

Retrospective

This tutorial reached 75,000+ developers. That’s the success. What follows is how I’d write the same code today. Good documentation meets people where they are. A tutorial for beginners should prioritize clarity over cleverness. The original code isn’t wrong — it’s calibrated to its audience. These are three things I’d do differently now that I have more experience.
The original code mixes game data (X and O) with visual formatting (pipes | and underscores _) in the same array. This makes the win-detection logic harder to read and reason about.Today, I’d keep the data model clean and handle visuals separately:
// Clean data model
private static char[][] board = new char[3][3];

// Visuals handled separately
private static void printBoard() {
    System.out.println("-------");
    for (int i = 0; i < 3; i++) {
        System.out.print("|");
        for (int j = 0; j < 3; j++) {
            char c = (board[i][j] == 0) ? ' ' : board[i][j];
            System.out.print(c + "|");
        }
        System.out.println("\n-------");
    }
}
The original input.nextInt() crashes with an uncaught exception if a user types a letter. Today, I’d wrap all input in a loop that handles NumberFormatException and supports basic help commands:
private static int getPlayerInput() {
    while (true) {
        String line = input.nextLine().trim().toLowerCase();
        
        if (line.equals("help")) {
            System.out.println("Commands: 1-9 (move), score, help");
            continue;
        }
        if (line.equals("score")) {
            System.out.println("Player: " + playerScore + " | Computer: " + computerScore);
            continue;
        }
        
        try {
            int move = Integer.parseInt(line);
            if (move >= 1 && move <= 9) return move;
            System.out.println("Please enter a number between 1-9.");
        } catch (NumberFormatException e) {
            System.out.println("Invalid input. Enter 1-9 or 'help'.");
        }
    }
}
The switch statements work, but each position maps to a row and column through a simple formula. You can replace 18 nearly-identical cases with two lines of math:
// (position - 1) / 3 = row
// (position - 1) % 3 = column

private static boolean isValidMove(int position) {
    int row = (position - 1) / 3;
    int col = (position - 1) % 3;
    return board[row][col] == ' ';
}

private static void updateBoard(int position, char symbol) {
    int row = (position - 1) / 3;
    int col = (position - 1) % 3;
    board[row][col] = symbol;
}
A beginner can read case 5: gameBoard[1][2] and immediately understand it. The compact version requires working backwards from the math. Both are correct. The original was the right choice for a beginner audience.

Original article

Read the full tutorial as originally published on Medium.

On Good Tutorials

Why this tutorial was discussed in a broader study on tutorial quality.

Build docs developers (and LLMs) love