import tkinter as tk
from tkinter import messagebox
import random
import math
# -------------------------------
# Game Constants and Global Variables
# -------------------------------
ROW_COUNT = 6
COLUMN_COUNT = 7
PLAYER = 1 # Human coin (red)
AI = 2 # Computer coin (blue)
EMPTY = 0
CELL_SIZE = 100 # Size of each board cell (in pixels)
MARGIN = 8 # Margin inside each cell
# Game state globals
board = None
game_over = False
turn = None # 0 = human, 1 = computer
difficulty = None # "easy", "normal", "hard"
animating = False # Flag to prevent moves during animation
# -------------------------------
# Game Logic Functions
# -------------------------------
def create_board():
"""Create and return an empty board."""
return [[EMPTY for _ in range(COLUMN_COUNT)] for _ in range(ROW_COUNT)]
def is_valid_location(board, col):
"""Return True if the top cell in the given column is empty."""
return board[ROW_COUNT - 1][col] == EMPTY
def get_valid_locations(board):
"""Return a list of column indices that are not full."""
return [col for col in range(COLUMN_COUNT) if is_valid_location(board, col)]
def get_next_open_row(board, col):
"""Return the lowest available row index in the given column."""
for r in range(ROW_COUNT):
if board[r][col] == EMPTY:
return r
def drop_piece(board, row, col, piece):
"""Place a piece (PLAYER or AI) into the board at the given row and column."""
board[row][col] = piece
def winning_move(board, piece):
"""Return True if the given piece has four connected in a row."""
# Horizontal check
for r in range(ROW_COUNT):
for c in range(COLUMN_COUNT - 3):
if board[r][c] == piece and board[r][c+1] == piece and \
board[r][c+2] == piece and board[r][c+3] == piece:
return True
# Vertical check
for c in range(COLUMN_COUNT):
for r in range(ROW_COUNT - 3):
if board[r][c] == piece and board[r+1][c] == piece and \
board[r+2][c] == piece and board[r+3][c] == piece:
return True
# Positive diagonal check
for r in range(ROW_COUNT - 3):
for c in range(COLUMN_COUNT - 3):
if board[r][c] == piece and board[r+1][c+1] == piece and \
board[r+2][c+2] == piece and board[r+3][c+3] == piece:
return True
# Negative diagonal check
for r in range(3, ROW_COUNT):
for c in range(COLUMN_COUNT - 3):
if board[r][c] == piece and board[r-1][c+1] == piece and \
board[r-2][c+2] == piece and board[r-3][c+3] == piece:
return True
return False
def is_terminal_node(board):
"""Return True if the game is over (win or draw)."""
return winning_move(board, PLAYER) or winning_move(board, AI) or len(get_valid_locations(board)) == 0
def evaluate_window(window, piece):
"""Score a list of 4 cells (a window) for the given piece."""
score = 0
opp_piece = PLAYER if piece == AI else AI
if window.count(piece) == 4:
score += 100
elif window.count(piece) == 3 and window.count(EMPTY) == 1:
score += 5
elif window.count(piece) == 2 and window.count(EMPTY) == 2:
score += 2
if window.count(opp_piece) == 3 and window.count(EMPTY) == 1:
score -= 4
return score
def score_position(board, piece):
"""Evaluate the board and return a score from the perspective of the given piece."""
score = 0
# Score center column
center_col = COLUMN_COUNT // 2
center_array = [board[r][center_col] for r in range(ROW_COUNT)]
score += center_array.count(piece) * 3
# Score horizontal windows
for r in range(ROW_COUNT):
row_array = board[r]
for c in range(COLUMN_COUNT - 3):
window = row_array[c:c+4]
score += evaluate_window(window, piece)
# Score vertical windows
for c in range(COLUMN_COUNT):
col_array = [board[r][c] for r in range(ROW_COUNT)]
for r in range(ROW_COUNT - 3):
window = col_array[r:r+4]
score += evaluate_window(window, piece)
# Score positive sloped diagonals
for r in range(ROW_COUNT - 3):
for c in range(COLUMN_COUNT - 3):
window = [board[r+i][c+i] for i in range(4)]
score += evaluate_window(window, piece)
# Score negative sloped diagonals
for r in range(3, ROW_COUNT):
for c in range(COLUMN_COUNT - 3):
window = [board[r-i][c+i] for i in range(4)]
score += evaluate_window(window, piece)
return score
def minimax(board, depth, alpha, beta, maximizingPlayer):
"""
Use minimax with alpha-beta pruning to evaluate the best move.
Returns a tuple (best_column, score).
"""
valid_locations = get_valid_locations(board)
terminal = is_terminal_node(board)
if depth == 0 or terminal:
if terminal:
if winning_move(board, AI):
return (None, 100000000000000)
elif winning_move(board, PLAYER):
return (None, -100000000000000)
else:
return (None, 0)
else:
return (None, score_position(board, AI))
if maximizingPlayer:
value = -math.inf
best_col = random.choice(valid_locations)
for col in valid_locations:
row = get_next_open_row(board, col)
board_copy = [r.copy() for r in board]
drop_piece(board_copy, row, col, AI)
new_score = minimax(board_copy, depth - 1, alpha, beta, False)[1]
if new_score > value:
value = new_score
best_col = col
alpha = max(alpha, value)
if alpha >= beta:
break
return best_col, value
else:
value = math.inf
best_col = random.choice(valid_locations)
for col in valid_locations:
row = get_next_open_row(board, col)
board_copy = [r.copy() for r in board]
drop_piece(board_copy, row, col, PLAYER)
new_score = minimax(board_copy, depth - 1, alpha, beta, True)[1]
if new_score < value:
value = new_score
best_col = col
beta = min(beta, value)
if alpha >= beta:
break
return best_col, value
# -------------------------------
# GUI and Animation Functions
# -------------------------------
def draw_board():
"""Redraw the board on the canvas from the current board state."""
canvas.delete("all")
# Draw board background (a royal blue rectangle)
canvas.create_rectangle(0, 0, COLUMN_COUNT * CELL_SIZE, ROW_COUNT * CELL_SIZE,
fill="royalblue", outline="")
# Draw each cell as a circular "hole"
for r in range(ROW_COUNT):
for c in range(COLUMN_COUNT):
display_row = ROW_COUNT - 1 - r # because row 0 is at bottom
x1 = c * CELL_SIZE + MARGIN
y1 = display_row * CELL_SIZE + MARGIN
x2 = (c + 1) * CELL_SIZE - MARGIN
y2 = (display_row + 1) * CELL_SIZE - MARGIN
if board[r][c] == PLAYER:
color = "red"
elif board[r][c] == AI:
color = "blue"
else:
color = "white"
canvas.create_oval(x1, y1, x2, y2, fill=color, outline="black")
def animate_drop(col, target_row, piece, callback):
"""
Animate a coin drop into the specified column and target row.
When the animation finishes, the callback is invoked.
"""
global animating
animating = True
coin_diameter = CELL_SIZE - 2 * MARGIN
x1 = col * CELL_SIZE + MARGIN
x2 = (col + 1) * CELL_SIZE - MARGIN
final_y = (ROW_COUNT - 1 - target_row) * CELL_SIZE + MARGIN
current_y = -coin_diameter
color = "red" if piece == PLAYER else "blue"
coin = canvas.create_oval(x1, current_y, x2, current_y + coin_diameter,
fill=color, outline="black")
step = 20
delay = 20
def fall():
nonlocal current_y
global animating # Ensure we update the global variable
if current_y < final_y:
current_y = min(current_y + step, final_y)
canvas.coords(coin, x1, current_y, x2, current_y + coin_diameter)
canvas.after(delay, fall)
else:
canvas.delete(coin)
animating = False
callback()
fall()
def perform_move(col, piece, move_callback):
"""
Animate the coin drop for the move.
When the coin finishes dropping, update the board state, redraw,
and then invoke move_callback().
"""
row = get_next_open_row(board, col)
def finish_move():
drop_piece(board, row, col, piece)
draw_board()
move_callback()
animate_drop(col, row, piece, finish_move)
# -------------------------------
# Event Handlers and Game Flow
# -------------------------------
def player_click(event):
"""Handle player's click on the canvas (to drop a coin)."""
global turn, game_over
if game_over or animating or turn != 0:
return
col = event.x // CELL_SIZE
if col < 0 or col >= COLUMN_COUNT or not is_valid_location(board, col):
return
def after_player_move():
if winning_move(board, PLAYER):
messagebox.showinfo("Game Over", "Congratulations, you win!")
end_game()
return
if len(get_valid_locations(board)) == 0:
messagebox.showinfo("Game Over", "Draw!")
end_game()
return
turn_switch()
root.after(300, computer_move)
perform_move(col, PLAYER, after_player_move)
def computer_move():
"""Compute and perform the computer's move."""
global turn, game_over
if game_over or animating or turn != 1:
return
valid_locations = get_valid_locations(board)
if not valid_locations:
messagebox.showinfo("Game Over", "Draw!")
end_game()
return
if difficulty == "easy":
col = random.choice(valid_locations)
elif difficulty == "normal":
col, _ = minimax(board, 3, -math.inf, math.inf, True)
if col is None:
col = random.choice(valid_locations)
elif difficulty == "hard":
col, _ = minimax(board, 5, -math.inf, math.inf, True)
if col is None:
col = random.choice(valid_locations)
else:
col = random.choice(valid_locations)
def after_computer_move():
if winning_move(board, AI):
messagebox.showinfo("Game Over", "Computer wins!")
end_game()
return
if len(get_valid_locations(board)) == 0:
messagebox.showinfo("Game Over", "Draw!")
end_game()
return
turn_switch()
perform_move(col, AI, after_computer_move)
def turn_switch():
"""Switch turn between player (0) and computer (1)."""
global turn
turn = 0 if turn == 1 else 1
def end_game():
"""Mark the game as over."""
global game_over
game_over = True
# -------------------------------
# Control Buttons: Restart and Menu
# -------------------------------
def restart_game():
"""Restart the game with the same difficulty."""
global board, game_over, turn, animating
if animating:
return
board = create_board()
game_over = False
turn = random.choice([0, 1])
draw_board()
if turn == 1:
root.after(500, computer_move)
def go_to_menu():
"""Return to the main menu (difficulty selection)."""
global board, game_over, animating
if animating:
return
game_frame.pack_forget()
menu_frame.pack(pady=20)
# -------------------------------
# Difficulty Menu and Game Setup
# -------------------------------
def set_difficulty(diff):
"""Set the difficulty level and start a new game."""
global difficulty, board, game_over, turn
difficulty = diff
menu_frame.pack_forget()
game_frame.pack()
board = create_board()
game_over = False
turn = random.choice([0, 1])
draw_board()
if turn == 1:
root.after(500, computer_move)
# -------------------------------
# Setting Up the Tkinter GUI
# -------------------------------
root = tk.Tk()
root.title("Connect 4")
root.resizable(False, False)
# --- Menu Frame (Difficulty Selection) ---
menu_frame = tk.Frame(root)
menu_frame.pack(pady=20)
menu_label = tk.Label(menu_frame, text="Select Difficulty", font=("Helvetica", 20, "bold"))
menu_label.pack(pady=10)
button_frame = tk.Frame(menu_frame)
button_frame.pack()
easy_button = tk.Button(button_frame, text="Easy", width=10, font=("Helvetica", 14),
command=lambda: set_difficulty("easy"))
easy_button.grid(row=0, column=0, padx=5, pady=5)
normal_button = tk.Button(button_frame, text="Normal", width=10, font=("Helvetica", 14),
command=lambda: set_difficulty("normal"))
normal_button.grid(row=0, column=1, padx=5, pady=5)
hard_button = tk.Button(button_frame, text="Hard", width=10, font=("Helvetica", 14),
command=lambda: set_difficulty("hard"))
hard_button.grid(row=0, column=2, padx=5, pady=5)
# --- Game Frame (Board + Control Buttons) ---
game_frame = tk.Frame(root)
canvas_width = COLUMN_COUNT * CELL_SIZE
canvas_height = ROW_COUNT * CELL_SIZE
canvas = tk.Canvas(game_frame, width=canvas_width, height=canvas_height, bg="white")
canvas.pack()
canvas.bind("<Button-1>", player_click)
control_frame = tk.Frame(game_frame)
control_frame.pack(pady=10)
restart_button = tk.Button(control_frame, text="Restart", width=12, font=("Helvetica", 12),
command=restart_game)
restart_button.pack(side=tk.LEFT, padx=10)
menu_button = tk.Button(control_frame, text="Menu", width=12, font=("Helvetica", 12),
command=go_to_menu)
menu_button.pack(side=tk.LEFT, padx=10)
root.mainloop()