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.
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:
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.

.png)