Build a Tic-Tac-Toe GUI in Python

Build a Tic-Tac-Toe GUI in Python

Introduction

Learning Python is fun when you build games. One of the most timeless beginner-friendly games is Tic-Tac-Toe (also known as Xs and Os). While many tutorials stop at the console version, in this guide we’ll take it one step further and create a modern graphical interface using Python’s built-in tkinter library.

Why Tkinter for GUI?

Tkinter comes pre-installed with Python, so:
  • No external libraries required.
  • Easy to learn with a simple API.
  • Works on Windows, macOS, and Linux.
For quick desktop projects like Tic Tac Toe, it’s the perfect choice.

Features We’ll Build

Our version will not be a plain grid—it will have:

✅ 2-player mode (same computer)
✅ Scoreboard for X, O, and draws
✅ “New Game” and “Reset Scores” buttons
✅ Keyboard shortcuts (press 1–9 to mark squares)
✅ Winning line highlights

Step 1: Setting up the board

We’ll use a 3×3 grid of buttons to represent the board. Each button can be clicked or triggered with a key.>
self.buttons = []
for i in range(9):
    btn = tk.Button(
        grid,
        text="",
        width=4, height=2,
        font=("Segoe UI", 28, "bold"),
        command=lambda idx=i: self._on_click(idx)
    )
    r, c = divmod(i, 3)
    btn.grid(row=r, column=c, padx=6, pady=6, sticky="nsew")
    self.buttons.append(btn)
  • divmod(i, 3) helps map index 0–8 into row & column.
  • Each button is linked to _on_click() for handling moves.

Step 2: Handling player moves

We maintain a list of 9 values (" ", "X", or "O") to represent the board. After each move:
  • Place the mark (X or O)
  • Check if there’s a winner
  • Switch turns if not over
def _place_mark(self, idx: int):
    self.board[idx] = self.current
    btn = self.buttons[idx]
    btn.config(text=self.current, fg="blue" if self.current == "X" else "red")

Step 3: Detecting winners

Winning means having 3 same marks in a row, column, or diagonal.
WIN_PATTERNS = (
    (0,1,2), (3,4,5), (6,7,8),  # rows
    (0,3,6), (1,4,7), (2,5,8),  # cols
    (0,4,8), (2,4,6)            # diagonals
)

def _has_winner(self):
    for a, b, c in WIN_PATTERNS:
        if self.board[a] != " " and self.board[a] == self.board[b] == self.board[c]:
            return self.board[a], (a, b, c)
    return None, None
If a winner is found, we highlight the winning tiles in green.

Step 4: Adding a scoreboard

To make the game engaging, we track scores across multiple rounds:
self.scores = {"X": 0, "O": 0, "Draws": 0}
Each time someone wins or it’s a draw, we update the labels accordingly.

Step 5: Full Code

Here’s the complete runnable script:
import tkinter as tk
from tkinter import messagebox

WIN_PATTERNS = (
    (0, 1, 2),  # rows
    (3, 4, 5),
    (6, 7, 8),
    (0, 3, 6),  # cols
    (1, 4, 7),
    (2, 5, 8),
    (0, 4, 8),  # diagonals
    (2, 4, 6),
)

class TicTacToeGUI(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Tic-Tac-Toe (2 Players) — SkillDedication.in")
        self.resizable(False, False)

        # --- Model ---
        self.board = [" "] * 9
        self.current = "X"
        self.scores = {"X": 0, "O": 0, "Draws": 0}
        self.game_over = False

        # --- Styles ---
        self.colors = {
            "bg": "#0f172a",          # slate-900
            "panel": "#111827",       # gray-900
            "tile_idle": "#1f2937",   # gray-800
            "tile_hover": "#374151",  # gray-700
            "tile_x": "#22d3ee",      # cyan-400
            "tile_o": "#f472b6",      # pink-400
            "tile_win": "#16a34a",    # green-600
            "text": "#e5e7eb",        # gray-200
            "muted": "#9ca3af",       # gray-400
            "accent": "#38bdf8",      # sky-400
        }
        self.configure(bg=self.colors["bg"])

        # --- View ---
        self._build_header()
        self._build_grid()
        self._build_footer()

        # --- Controller: key bindings 1-9 map to board ---
        self.bind_all("", self._on_keypress)

        self._update_status()

    # ---------- UI Builders ----------
    def _build_header(self):
        wrap = tk.Frame(self, bg=self.colors["panel"], padx=14, pady=10)
        wrap.grid(row=0, column=0, sticky="ew")

        self.title_lbl = tk.Label(
            wrap,
            text="Tic-Tac-Toe — 2 Players",
            font=("Segoe UI", 16, "bold"),
            fg=self.colors["text"],
            bg=self.colors["panel"],
        )
        self.title_lbl.pack(side="left")

        self.status_lbl = tk.Label(
            wrap,
            text="",
            font=("Segoe UI", 12),
            fg=self.colors["muted"],
            bg=self.colors["panel"],
        )
        self.status_lbl.pack(side="right")

    def _build_grid(self):
        grid = tk.Frame(self, bg=self.colors["bg"], padx=12, pady=12)
        grid.grid(row=1, column=0)

        self.buttons = []
        for i in range(9):
            btn = tk.Button(
                grid,
                text="",
                width=4,
                height=2,
                font=("Segoe UI", 28, "bold"),
                relief="flat",
                bd=0,
                bg=self.colors["tile_idle"],
                fg=self.colors["text"],
                activebackground=self.colors["tile_hover"],
                activeforeground=self.colors["text"],
                command=lambda idx=i: self._on_click(idx),
            )
            r, c = divmod(i, 3)
            btn.grid(row=r, column=c, padx=6, pady=6, sticky="nsew")

            # Hover effect only if tile is empty and game not over
            btn.bind("", lambda e, idx=i: self._hover(idx, True))
            btn.bind("", lambda e, idx=i: self._hover(idx, False))
            self.buttons.append(btn)

        # Make grid cells expand evenly
        for r in range(3):
            grid.grid_rowconfigure(r, weight=1)
        for c in range(3):
            grid.grid_columnconfigure(c, weight=1)

    def _build_footer(self):
        footer = tk.Frame(self, bg=self.colors["panel"], padx=14, pady=10)
        footer.grid(row=2, column=0, sticky="ew")

        # Scoreboard
        self.score_x = tk.Label(
            footer, text="X: 0", font=("Segoe UI", 12, "bold"),
            fg=self.colors["tile_x"], bg=self.colors["panel"]
        )
        self.score_o = tk.Label(
            footer, text="O: 0", font=("Segoe UI", 12, "bold"),
            fg=self.colors["tile_o"], bg=self.colors["panel"]
        )
        self.score_d = tk.Label(
            footer, text="Draws: 0", font=("Segoe UI", 12),
            fg=self.colors["muted"], bg=self.colors["panel"]
        )
        self.score_x.pack(side="left", padx=(0, 12))
        self.score_o.pack(side="left", padx=(0, 12))
        self.score_d.pack(side="left")

        # Spacer
        tk.Label(footer, text="", bg=self.colors["panel"]).pack(side="left", expand=True)

        # Controls
        btn_new = tk.Button(
            footer, text="New Game", font=("Segoe UI", 11, "bold"),
            command=self.new_game, bg=self.colors["accent"], fg="#0b1020",
            activebackground=self.colors["accent"], relief="flat", padx=10, pady=4
        )
        btn_reset = tk.Button(
            footer, text="Reset Scores", font=("Segoe UI", 11),
            command=self.reset_scores, bg=self.colors["tile_idle"], fg=self.colors["text"],
            activebackground=self.colors["tile_hover"], relief="flat", padx=10, pady=4
        )
        btn_new.pack(side="right", padx=(6, 0))
        btn_reset.pack(side="right")

    # ---------- Game Logic ----------
    def _on_click(self, idx: int):
        if self.game_over or self.board[idx] != " ":
            return
        self._place_mark(idx)
        self._post_move()

    def _on_keypress(self, event):
        if not event.char or not event.char.isdigit():
            return
        num = int(event.char)
        if 1 <= num <= 9:
            idx = num - 1
            self._on_click(idx)

    def _place_mark(self, idx: int):
        self.board[idx] = self.current
        btn = self.buttons[idx]
        btn.config(text=self.current, fg=self.colors["tile_x"] if self.current == "X" else self.colors["tile_o"])
        btn.config(bg=self.colors["tile_idle"])  # reset hover bg if any

    def _post_move(self):
        winner, line = self._has_winner()
        if winner:
            self._highlight_win(line)
            self._finish_game(f"{winner} wins! 🎉")
            self.scores[winner] += 1
            self._update_scores()
            return

        if self._is_full():
            self._finish_game("It's a draw! 🤝")
            self.scores["Draws"] += 1
            self._update_scores()
            return

        self.current = "O" if self.current == "X" else "X"
        self._update_status()

    def _has_winner(self):
        for a, b, c in WIN_PATTERNS:
            if self.board[a] != " " and self.board[a] == self.board[b] == self.board[c]:
                return self.board[a], (a, b, c)
        return None, None

    def _is_full(self):
        return all(cell != " " for cell in self.board)

    def _highlight_win(self, triple):
        for i in triple:
            self.buttons[i].config(bg=self.colors["tile_win"], fg="#e9fdf0")

    def _finish_game(self, msg: str):
        self.game_over = True
        self._update_status(msg)
        # Disable all empty tiles to prevent extra clicks
        for i, btn in enumerate(self.buttons):
            if self.board[i] == " ":
                btn.config(state="disabled")

    def _update_status(self, forced: str = None):
        if forced is not None:
            self.status_lbl.config(text=forced)
        else:
            self.status_lbl.config(text=f"Turn: {self.current}")

    def _update_scores(self):
        self.score_x.config(text=f"X: {self.scores['X']}")
        self.score_o.config(text=f"O: {self.scores['O']}")
        self.score_d.config(text=f"Draws: {self.scores['Draws']}")

    def _hover(self, idx: int, entering: bool):
        if self.game_over or self.board[idx] != " ":
            return
        btn = self.buttons[idx]
        btn.config(bg=self.colors["tile_hover"] if entering else self.colors["tile_idle"])

    # ---------- Public Controls ----------
    def new_game(self):
        self.board = [" "] * 9
        self.current = "X"
        self.game_over = False
        for btn in self.buttons:
            btn.config(text="", state="normal", bg=self.colors["tile_idle"], fg=self.colors["text"])
        self._update_status()

    def reset_scores(self):
        if messagebox.askyesno("Reset Scores", "Reset X, O and Draws to zero?"):
            self.scores = {"X": 0, "O": 0, "Draws": 0}
            self._update_scores()
            self.new_game()


if __name__ == "__main__":
    app = TicTacToeGUI()
    app.mainloop()

Output Preview

  • A dark-themed Tic Tac Toe grid
  • Scoreboard below with X, O, and draws count
  • Status label showing “Turn: X” or “O wins! 🎉”

Possible Extensions

  • Want to take this further? Try these:
  • Add player name input fields
  • Add sounds using winsound (Windows only)
  • Add theme toggle (light/dark)
  • Add AI opponent with a simple random or minimax strategy

OUTPUT RESULT:

Tic - Tac - Toe in Python screenshot

Conclusion

That’s it! 🎉 You’ve successfully built a Tic Tac Toe game with a GUI in Python. Unlike the plain console version, this one feels like a real desktop application—interactive, user-friendly, and fun to play.

Keep experimenting—maybe even add an AI bot opponent in your next upgrade. Remember, the best way to learn programming is to build projects consistently.
Previous Post Next Post

Contact Form