#!/usr/bin/env python3
"""
GUI Tic‑Tac‑Toe (Tkinter)
Features
--------
* Human‑vs‑Human, Human‑vs‑AI (Easy), Human‑vs‑AI (Hard)
* Switch difficulty on the fly
* Reset / New Game button
* Simple, well‑commented code – easy to extend (e.g., add sound, animations)
Author : Lumo (Proton)
"""
import random
import tkinter as tk
from tkinter import messagebox
# ----------------------------------------------------------------------
# Board logic (independent of the UI)
# ----------------------------------------------------------------------
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 check_winner(board):
"""Return 'X', 'O' if there is a winner, otherwise None."""
for a, b, c in WIN_PATTERNS:
if board[a] == board[b] == board[c] != " ":
return board[a]
return None
def is_draw(board):
return " " not in board and check_winner(board) is None
def available_moves(board):
return [i for i, v in enumerate(board) if v == " "]
# --------------------------- AI ---------------------------------------
def ai_random(board, player):
"""Easy AI – pick a random empty square."""
move = random.choice(available_moves(board))
board[move] = player
return move
def minimax(board, depth, is_maximizing, ai_player, human_player):
"""Classic minimax returning (score, move)."""
winner = check_winner(board)
if winner == ai_player:
return 10 - depth, None
if winner == human_player:
return depth - 10, None
if is_draw(board):
return 0, None
if is_maximizing:
best_score = -float("inf")
best_move = None
for m in available_moves(board):
board[m] = ai_player
score, _ = minimax(board, depth + 1, False, ai_player, human_player)
board[m] = " "
if score > best_score:
best_score, best_move = score, m
return best_score, best_move
else:
best_score = float("inf")
best_move = None
for m in available_moves(board):
board[m] = human_player
score, _ = minimax(board, depth + 1, True, ai_player, human_player)
board[m] = " "
if score < best_score:
best_score, best_move = score, m
return best_score, best_move
def ai_minimax(board, player):
"""Hard AI – perfect play."""
_, move = minimax(board, 0, True, player, "O" if player == "X" else "X")
board[move] = player
return move
# ----------------------------------------------------------------------
# Tkinter GUI
# ----------------------------------------------------------------------
class TicTacToeGUI(tk.Tk):
def __init__(self):
super().__init__()
self.title("Tic‑Tac‑Toe")
self.resizable(False, False)
# ---------- Game state ----------
self.board = [" "] * 9
self.current_player = "X" # X always starts
self.game_mode = tk.StringVar(value="HvH") # HvH, HvE, HvHARD
self.difficulty = "easy" # easy / hard (used only for AI modes)
# ---------- UI elements ----------
self.create_widgets()
self.update_status()
# ------------------------------------------------------------------
# UI construction
# ------------------------------------------------------------------
def create_widgets(self):
# Top frame – mode selector
top = tk.Frame(self)
top.pack(pady=5)
tk.Label(top, text="Mode:").pack(side=tk.LEFT)
modes = [("Human vs Human", "HvH"),
("Human vs AI (Easy)", "HvE"),
("Human vs AI (Hard)", "HvHARD")]
for txt, val in modes:
rb = tk.Radiobutton(
top, text=txt, variable=self.game_mode, value=val,
command=self.reset_game)
rb.pack(side=tk.LEFT, padx=4)
# Board – 3×3 grid of buttons
self.buttons = []
board_frame = tk.Frame(self)
board_frame.pack(padx=10, pady=10)
for i in range(9):
btn = tk.Button(
board_frame, text=" ", font=("Helvetica", 32),
width=3, height=1,
command=lambda idx=i: self.on_square_click(idx))
btn.grid(row=i // 3, column=i % 3, padx=2, pady=2)
btn.bind("<Enter>", lambda e, b=btn: b.config(bg="#e0e0e0"))
btn.bind("<Leave>", lambda e, b=btn: b.config(bg="SystemButtonFace"))
self.buttons.append(btn)
# Bottom frame – status, reset, difficulty switch
bottom = tk.Frame(self)
bottom.pack(pady=5)
self.status_lbl = tk.Label(bottom, text="", font=("Helvetica", 12))
self.status_lbl.pack(side=tk.LEFT, padx=10)
self.switch_btn = tk.Button(
bottom, text="Switch Difficulty", command=self.toggle_difficulty)
self.switch_btn.pack(side=tk.RIGHT, padx=5)
reset_btn = tk.Button(bottom, text="New Game", command=self.reset_game)
reset_btn.pack(side=tk.RIGHT, padx=5)
# ------------------------------------------------------------------
# Game flow helpers
# ------------------------------------------------------------------
def reset_game(self):
"""Clear board, set starting player, update UI."""
self.board = [" "] * 9
self.current_player = "X"
for b in self.buttons:
b.config(text=" ", state=tk.NORMAL)
self.update_status()
# If AI goes first (only possible in Human‑vs‑AI modes),
# make the first AI move automatically.
if self.game_mode.get() != "HvH" and self.current_player == "O":
self.after(200, self.ai_move)
def toggle_difficulty(self):
"""Flip between easy and hard AI (only matters for AI modes)."""
self.difficulty = "hard" if self.difficulty == "easy" else "easy"
messagebox.showinfo(
"Difficulty switched",
f"AI difficulty is now **{self.difficulty.upper()}**.",
parent=self)
def update_status(self):
"""Refresh the status line."""
if self.game_mode.get() == "HvH":
txt = f"Turn: Player {self.current_player}"
else:
if self.current_player == "X":
txt = "Your turn (X)"
else:
txt = f"Computer's turn ({self.current_player})"
self.status_lbl.config(text=txt)
# ------------------------------------------------------------------
# Event handlers
# ------------------------------------------------------------------
def on_square_click(self, idx):
"""Human clicked a square."""
if self.board[idx] != " ":
return # ignore already‑filled squares (shouldn't happen)
# Place the mark
self.board[idx] = self.current_player
self.buttons[idx].config(text=self.current_player, state=tk.DISABLED)
# Check for end‑game
winner = check_winner(self.board)
if winner:
self.end_game(f"Player {winner} wins!")
return
if is_draw(self.board):
self.end_game("It's a draw!")
return
# Switch player
self.current_player = "O" if self.current_player == "X" else "X"
self.update_status()
# If it's now the AI's turn, schedule its move
if self.game_mode.get() != "HvH" and self.current_player == "O":
self.after(300, self.ai_move) # slight delay for UX
def ai_move(self):
"""Perform an AI move according to the selected difficulty."""
if self.difficulty == "easy":
move = ai_random(self.board, "O")
else:
move = ai_minimax(self.board, "O")
# Update UI
self.buttons[move].config(text="O", state=tk.DISABLED)
# End‑game check
winner = check_winner(self.board)
if winner:
self.end_game(f"Player {winner} wins!")
return
if is_draw(self.board):
self.end_game("It's a draw!")
return
# Back to human
self.current_player = "X"
self.update_status()
def end_game(self, message):
"""Disable all squares and show result."""
for b in self.buttons:
b.config(state=tk.DISABLED)
self.status_lbl.config(text=message)
messagebox.showinfo("Game Over", message, parent=self)
# ----------------------------------------------------------------------
# Run the application
# ----------------------------------------------------------------------
if __name__ == "__main__":
app = TicTacToeGUI()
app.mainloop()