Build a fully playable, interactive Tic Tac Toe GUI game in Java from scratch - complete with click handling, win detection, draw detection, and a restart button. No libraries, just pure Java Swing.
Open your terminal (Command Prompt on Windows, Terminal on Mac/Linux) and type:
java -version
If you see an output like java version "17.0.x" or any version above 8, you are ready. If you see an error saying Java is not recognized, follow the next step.
java -version again. You should now see a version number.java -version still doesn't work after installation, you may need to set the JAVA_HOME environment variable. Search "How to set JAVA_HOME on Windows" - it's a quick 2-minute fix.
TicTacToeGame.TicTacToe.java. This will hold all our code - the entire game fits in one file.We will use the terminal to compile and run the game. Here's what the two commands do:
# Step 1: Navigate into your project folder
cd TicTacToeGame
# Step 2: Compile the Java file (creates TicTacToe.class)
javac TicTacToe.java
# Step 3: Run the compiled program (opens the game window)
java TicTacToe
This is the most important step. Before you write even one line of code, you should understand what tools we are using and how the game logic works. Read this section carefully - it will make the code feel obvious rather than confusing.
Java Swing is a built-in Java library for building Graphical User Interfaces (GUIs) - that means windows, buttons, text labels, input fields, and all the visual things you click on in a desktop application. You do not need to install anything; it ships with the Java JDK.
Think of Swing as a toolkit of ready-made building blocks. You pick the blocks you need (a window, buttons, a grid), place them how you want, and Java handles the drawing and interaction for you.
We only need 4 core Swing components to build this entire game:
The main application window. Everything lives inside JFrame. Think of it as the empty picture frame - you put everything else inside it.
A clickable button. Each cell of our 3×3 Tic Tac Toe grid is one JButton. When a player clicks a button, it shows X or O.
A text display. We use one JLabel at the top to show messages like "Player X's Turn" or "Player O Wins!"
A layout manager that automatically arranges components in rows and columns - perfect for our 3×3 game board.
When a player clicks a button, Java needs to know what to do. That's where ActionListener comes in.
An ActionListener is like a guard standing next to every button. When the button is clicked, the guard immediately says "hey, something happened!" and runs a specific piece of code (the actionPerformed method). In our game, that code will: check if the cell is empty, mark it X or O, check for a win, and switch turns.
We represent the 3×3 grid as a 1D array of 9 JButtons. The positions map to the grid like this:
Using a 1D array keeps the code simple. We just loop through indices 0 to 8 to set up or reset all 9 cells.
In Tic Tac Toe there are 8 possible ways to win: 3 rows, 3 columns, and 2 diagonals. We store these as a list of index groups and check them after every move:
After every click, we loop through all 8 combinations and check if all 3 positions in any combination have the same player's mark (X or O). If yes - that player wins!
| Component / Method | What It Does |
|---|---|
| JFrame frame | Creates the main game window. We set its size, title, and close behavior here. |
| JButton[] buttons | Array of 9 buttons representing the 3×3 game grid. Each button is one cell. |
| JLabel statusLabel | Shows the current game state - whose turn it is, the winner, or a draw message. |
| JButton restartBtn | A separate button outside the grid. Resets all 9 cells and restarts the game. |
| boolean xTurn | A flag that tracks whose turn it currently is. true = Player X, false = Player O. |
| boolean gameOver | Becomes true when the game ends (win or draw). Prevents further clicks. |
| actionPerformed() | Runs every time a grid button is clicked. Marks the cell, checks for a win or draw, and switches turns. |
| checkWinner() | Loops through all 8 winning combinations and checks if any player has won. |
| resetGame() | Clears all button text, resets the turn to X, and sets gameOver back to false. |
Copy the complete code below into your TicTacToe.java file. Every section is clearly commented so you know what each block does as you read.
// ============================================================
// Tic Tac Toe Game using Java Swing
// NKDevSpace Tutorial
// ============================================================
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class TicTacToe implements ActionListener {
// -- WINDOW & PANELS ──────────────────────────────────────
JFrame frame;
JPanel titlePanel; // Top area: status label
JPanel boardPanel; // Middle area: 3x3 button grid
JPanel bottomPanel; // Bottom area: restart button
// -- COMPONENTS ───────────────────────────────────────────
JLabel statusLabel; // Shows game status text
JButton[] buttons; // The 9 grid cells
JButton restartButton; // Reset/restart button
// -- GAME STATE ────────────────────────────────────────────
boolean xTurn = true; // true = X's turn, false = O's turn
boolean gameOver = false; // Stops clicks after game ends
// -- WIN COMBINATIONS ─────────────────────────────────────
// All 8 ways to win: rows, columns, diagonals
int[][] winCombos = {
{0, 1, 2}, // Top row
{3, 4, 5}, // Middle row
{6, 7, 8}, // Bottom row
{0, 3, 6}, // Left column
{1, 4, 7}, // Middle column
{2, 5, 8}, // Right column
{0, 4, 8}, // Diagonal top-left to bottom-right
{2, 4, 6} // Diagonal top-right to bottom-left
};
// ============================================================
// CONSTRUCTOR - Called when the program starts
// Sets up the entire window and game board
// ============================================================
public TicTacToe() {
// ----- 1. Create the main window -----
frame = new JFrame("Tic Tac Toe | NKDevSpace");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(500, 550);
frame.setLocationRelativeTo(null); // Center on screen
frame.setLayout(new BorderLayout());
frame.setResizable(false);
// ----- 2. Status label at the top -----
statusLabel = new JLabel("Player X's Turn");
statusLabel.setFont(new Font("Poppins", Font.BOLD, 20));
statusLabel.setForeground(Color.WHITE);
statusLabel.setHorizontalAlignment(JLabel.CENTER);
statusLabel.setOpaque(true);
statusLabel.setBackground(new Color(30, 37, 53));
statusLabel.setPreferredSize(new Dimension(500, 60));
titlePanel = new JPanel();
titlePanel.setBackground(new Color(30, 37, 53));
titlePanel.add(statusLabel);
// ----- 3. Create the 3x3 button grid -----
boardPanel = new JPanel();
boardPanel.setLayout(new GridLayout(3, 3, 6, 6)); // 3 rows, 3 cols, 6px gaps
boardPanel.setBackground(new Color(42, 51, 72));
boardPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
buttons = new JButton[9];
for (int i = 0; i < 9; i++) {
buttons[i] = new JButton("");
buttons[i].setFont(new Font("Arial", Font.BOLD, 60));
buttons[i].setBackground(new Color(22, 27, 39));
buttons[i].setForeground(Color.WHITE);
buttons[i].setFocusPainted(false);
buttons[i].setBorderPainted(false);
buttons[i].setCursor(new Cursor(Cursor.HAND_CURSOR));
buttons[i].addActionListener(this); // Listen for clicks
boardPanel.add(buttons[i]);
}
// ----- 4. Restart button at the bottom -----
restartButton = new JButton("Restart Game");
restartButton.setFont(new Font("Poppins", Font.BOLD, 15));
restartButton.setBackground(new Color(37, 99, 235));
restartButton.setForeground(Color.WHITE);
restartButton.setFocusPainted(false);
restartButton.setBorderPainted(false);
restartButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
restartButton.setPreferredSize(new Dimension(180, 40));
restartButton.addActionListener(e -> resetGame());
bottomPanel = new JPanel();
bottomPanel.setBackground(new Color(30, 37, 53));
bottomPanel.setPadding(); // Note: see below - we use EmptyBorder
bottomPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 14, 10));
bottomPanel.add(restartButton);
// ----- 5. Add all panels to the frame -----
frame.add(titlePanel, BorderLayout.NORTH);
frame.add(boardPanel, BorderLayout.CENTER);
frame.add(bottomPanel, BorderLayout.SOUTH);
frame.setVisible(true); // Make the window appear
}
// ============================================================
// ACTION PERFORMED - Runs every time a grid button is clicked
// ============================================================
@Override
public void actionPerformed(ActionEvent e) {
// Ignore click if game is already over
if (gameOver) return;
// Find out which button was clicked
JButton clickedButton = (JButton) e.getSource();
// Ignore click if the cell already has X or O
if (!clickedButton.getText().equals("")) return;
// Mark the cell with the current player's symbol
if (xTurn) {
clickedButton.setText("X");
clickedButton.setForeground(new Color(79, 142, 247)); // Blue for X
statusLabel.setText("Player O's Turn");
} else {
clickedButton.setText("O");
clickedButton.setForeground(new Color(34, 211, 165)); // Green for O
statusLabel.setText("Player X's Turn");
}
// Switch turn
xTurn = !xTurn;
// Check if there's a winner
checkWinner();
}
// ============================================================
// CHECK WINNER - Checks all 8 win combinations after each move
// ============================================================
public void checkWinner() {
for (int[] combo : winCombos) {
String a = buttons[combo[0]].getText();
String b = buttons[combo[1]].getText();
String c = buttons[combo[2]].getText();
// If all 3 positions are the same non-empty symbol → someone won!
if (!a.equals("") && a.equals(b) && b.equals(c)) {
// Highlight the winning cells
buttons[combo[0]].setBackground(new Color(37, 99, 235));
buttons[combo[1]].setBackground(new Color(37, 99, 235));
buttons[combo[2]].setBackground(new Color(37, 99, 235));
statusLabel.setText("🎉 Player " + a + " Wins!");
statusLabel.setForeground(new Color(250, 204, 21)); // Yellow for winner
gameOver = true;
return;
}
}
// Check for a draw - all 9 cells are filled and no winner
boolean allFilled = true;
for (JButton btn : buttons) {
if (btn.getText().equals("")) {
allFilled = false;
break;
}
}
if (allFilled) {
statusLabel.setText("It's a Draw! Well Played.");
statusLabel.setForeground(new Color(245, 158, 11)); // Orange for draw
gameOver = true;
}
}
// ============================================================
// RESET GAME - Clears the board and starts a fresh game
// ============================================================
public void resetGame() {
for (JButton btn : buttons) {
btn.setText("");
btn.setBackground(new Color(22, 27, 39)); // Reset to dark background
}
xTurn = true;
gameOver = false;
statusLabel.setText("Player X's Turn");
statusLabel.setForeground(Color.WHITE);
}
// ============================================================
// MAIN METHOD - Entry point of the program
// ============================================================
public static void main(String[] args) {
// Run on the Event Dispatch Thread (best practice for Swing)
SwingUtilities.invokeLater(() -> new TicTacToe());
}
}
bottomPanel.setPadding(), delete that line - it is handled by setBorder(BorderFactory.createEmptyBorder(...)) on the line right after. The comment was for readability only.
Let's go through every major concept in this program clearly. This section will make sure you understand why each line was written - not just what it does.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;* means "import everything from this package" - a shortcut so we don't have to import each class one by one.
public class TicTacToe implements ActionListenerimplements ActionListener is a contract. It tells Java: "This class will handle button click events." By implementing ActionListener, we must provide an actionPerformed() method - that method is called automatically whenever any button that has registered this as its listener is clicked.
actionPerformed() method is your response.
boolean xTurn = true;
boolean gameOver = false;true (X goes first). After every move, we flip it with xTurn = !xTurn. The ! operator means "NOT" - so true becomes false, and false becomes true. Simple and elegant.
true (after a win or draw), the very first line of actionPerformed() checks this and immediately returns - ignoring all further clicks. This prevents players from clicking occupied or post-game cells.
frame = new JFrame("Tic Tac Toe | NKDevSpace");
frame.setSize(500, 550);
frame.setLocationRelativeTo(null);
frame.setLayout(new BorderLayout());null tells Java to center the window on the screen. Without this, the window appears in the top-left corner.
boardPanel.setLayout(new GridLayout(3, 3, 6, 6));
buttons = new JButton[9];
for (int i = 0; i < 9; i++) { ... }for loop, we create each button, style it, and - most importantly - call buttons[i].addActionListener(this). The keyword this means "the current TicTacToe object". So we're telling each button: "When you are clicked, notify this game object."
public void actionPerformed(ActionEvent e) {
if (gameOver) return;
JButton clickedButton = (JButton) e.getSource();
if (!clickedButton.getText().equals("")) return;
...
}return exits the method immediately.
ActionEvent e object contains information about what was clicked. getSource() returns the component that triggered the event. We cast it to JButton to access button-specific methods.
checkWinner().
for (int[] combo : winCombos) {
String a = buttons[combo[0]].getText();
String b = buttons[combo[1]].getText();
String c = buttons[combo[2]].getText();
if (!a.equals("") && a.equals(b) && b.equals(c)) { ... }
}for-each loop goes through each of the 8 combinations. For each one, we read the text of the 3 buttons at those positions.
!a.equals("") (the cell is not empty) AND a.equals(b) AND b.equals(c) - all three cells have the same symbol. If both conditions are true, the current combo is a winning line.
gameOver = true to stop further play.
boolean allFilled = true;
for (JButton btn : buttons) {
if (btn.getText().equals("")) { allFilled = false; break; }
}
if (allFilled) { statusLabel.setText("It's a Draw!"); }allFilled = true). We then loop through every button - if we find even one with empty text, we set allFilled = false and break out of the loop early.
allFilled is still true at the end (meaning all 9 cells have a mark and no winner was found), we declare a draw. The break keyword is an optimization - no need to keep checking once we've found an empty cell.
public void resetGame() {
for (JButton btn : buttons) {
btn.setText("");
btn.setBackground(new Color(22, 27, 39));
}
xTurn = true; gameOver = false;
statusLabel.setText("Player X's Turn");
}xTurn to true (X always starts) and gameOver to false so clicks work again.
e -> resetGame()) as a shortcut instead of writing a full ActionListener - a modern Java feature that keeps the code clean.
SwingUtilities.invokeLater(() -> new TicTacToe());SwingUtilities.invokeLater() schedules our window creation to run on the EDT. It's the recommended best practice for starting any Swing application - even for small projects. Think of it as "politely asking the GUI thread to start our game when it's ready."
cd TicTacToeGame
javac TicTacToe.java
TicTacToe.class appear in the folder. No output means success!
java TicTacToe
A 500×550 pixel window will appear in the center of your screen with:
error: class TicTacToe is public, should be in a file named TicTacToe.javaTicTacToe.java (capital T, capital T, no spaces). Java is case-sensitive about file names.'javac' is not recognized as an internal or external commanderror: cannot find symbol - setPadding()bottomPanel.setPadding(); line. Padding is handled by setBorder(BorderFactory.createEmptyBorder(10,10,14,10)) on the next line.You've built a fully working Tic Tac Toe game. Here are some meaningful improvements to push your skills further: