import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import random
import time
import json
import [Link]
import [Link]
import threading
# --- CONFIGURATION ---
# PASTE YOUR GEMINI API KEY HERE
API_KEY = ""
MODEL_NAME = "gemini-2.5-flash-preview-09-2025"
API_URL =
f"[Link]
ent"
# Colors
CAGE_COLORS = [
'#ffb3ba', '#ffdfba', '#ffffba', '#baffc9', '#bae1ff', '#e6baff',
'#f0e68c', '#e0ffff', '#ffe4e1', '#f0fff0', '#e6e6fa', '#fff0f5',
'#f5f5dc', '#dcdcdc', '#98fb98', '#ffc0cb', '#87cefa'
]
COLOR_BG = "#f8fafc"
COLOR_GRID = "#334155"
COLOR_DASH = "#475569"
COLOR_SELECT = "#94a3b8"
COLOR_ERROR = "#fecaca"
COLOR_TEXT = "#1e293b"
COLOR_NOTE = "#64748b"
class SudokuLogic:
def __init__(self):
[Link] = []
[Link] = []
[Link] = [] # List of {sum, cells: [indices], color}
[Link] = {} # Map index -> set of numbers
def generate_game(self, difficulty="Medium"):
# 1. Generate Solution
[Link] = [0] * 81
self.fill_grid([Link])
# 2. Generate Cages
[Link] = self.generate_cages([Link])
# 3. Create Puzzle (Remove numbers)
[Link] = [0] * 81
[Link] = {i: set() for i in range(81)}
givens_count = {
"Easy": 38,
"Medium": 26,
"Hard": 14,
"Expert": 0
}.get(difficulty, 26)
indices = list(range(81))
[Link](indices)
for i in range(givens_count):
idx = indices[i]
[Link][idx] = [Link][idx]
def fill_grid(self, grid):
empty = -1
for i in range(81):
if grid[i] == 0:
empty = i
break
if empty == -1: return True
row, col = divmod(empty, 9)
nums = list(range(1, 10))
[Link](nums)
for num in nums:
if self.is_valid(grid, row, col, num):
grid[empty] = num
if self.fill_grid(grid):
return True
grid[empty] = 0
return False
def is_valid(self, grid, row, col, num):
# Row check
for c in range(9):
if grid[row * 9 + c] == num: return False
# Col check
for r in range(9):
if grid[r * 9 + col] == num: return False
# Box check
start_r, start_c = (row // 3) * 3, (col // 3) * 3
for r in range(3):
for c in range(3):
if grid[(start_r + r) * 9 + (start_c + c)] == num: return False
return True
def generate_cages(self, grid):
cages = []
visited = [False] * 81
color_idx = 0
while False in visited:
start = [Link](False)
# Determine size (biased towards 2 and 3)
r = [Link]()
if r < 0.1: size = 1
elif r < 0.4: size = 2
elif r < 0.7: size = 3
elif r < 0.9: size = 4
else: size = 5
current_cells = [start]
visited[start] = True
# Grow cage
for _ in range(size - 1):
candidates = []
for cell_idx in current_cells:
cr, cc = divmod(cell_idx, 9)
neighbors = [(cr-1, cc), (cr+1, cc), (cr, cc-1), (cr, cc+1)]
for nr, nc in neighbors:
if 0 <= nr < 9 and 0 <= nc < 9:
n_idx = nr * 9 + nc
if not visited[n_idx] and n_idx not in current_cells:
[Link](n_idx)
if candidates:
next_cell = [Link](candidates)
current_cells.append(next_cell)
visited[next_cell] = True
else:
break
cage_sum = sum(grid[i] for i in current_cells)
[Link]({
"sum": cage_sum,
"cells": current_cells,
"color": CAGE_COLORS[color_idx % len(CAGE_COLORS)],
"id": len(cages)
})
color_idx += 1
return cages
class KillerSudokuApp([Link]):
def __init__(self):
super().__init__()
[Link]("Python Killer Sudoku + Gemini AI")
[Link]("600x850")
[Link](bg=COLOR_BG)
[Link] = SudokuLogic()
self.selected_cell = -1
self.is_note_mode = False
[Link] = 0
self.timer_seconds = 0
self.timer_running = False
[Link] = []
# UI Setup
self.setup_ui()
self.start_new_game()
def setup_ui(self):
# --- Top Controls ---
top_frame = [Link](self, bg=COLOR_BG)
top_frame.pack(pady=10, fill='x', padx=20)
[Link](top_frame, text="Difficulty:", bg=COLOR_BG).pack(side='left')
self.diff_var = [Link](value="Medium")
diff_menu = [Link](top_frame, self.diff_var, "Medium", "Easy",
"Medium", "Hard", "Expert")
diff_menu.pack(side='left', padx=5)
[Link](top_frame, text="New Game",
command=self.start_new_game).pack(side='left', padx=10)
self.stats_label = [Link](top_frame, text="Time: 00:00 | Mistakes: 0/3",
bg=COLOR_BG, font=("Arial", 10, "bold"))
self.stats_label.pack(side='right')
# --- Canvas ---
self.canvas_size = 500
self.cell_size = self.canvas_size / 9
[Link] = [Link](self, width=self.canvas_size,
height=self.canvas_size, bg="white", highlightthickness=0)
[Link](pady=10)
[Link]("<Button-1>", self.on_canvas_click)
# --- Numpad & Tools ---
controls_frame = [Link](self, bg=COLOR_BG)
controls_frame.pack(pady=10)
# Row 1: Undo, 1-3, Erase
btn_grid = [Link](controls_frame, bg=COLOR_BG)
btn_grid.pack()
self.create_btn(btn_grid, "Undo", [Link], 0, 0, width=6)
for i in range(1, 4): self.create_btn(btn_grid, str(i), lambda n=i:
self.input_number(n), 0, i)
self.create_btn(btn_grid, "Erase", lambda: self.input_number(0), 0, 4,
width=6)
# Row 2: Note, 4-6, Hint
self.note_btn = self.create_btn(btn_grid, "Note: Off", self.toggle_note, 1,
0, width=8)
for i in range(4, 7): self.create_btn(btn_grid, str(i), lambda n=i:
self.input_number(n), 1, i)
self.create_btn(btn_grid, "✨Hint", self.ask_gemini_hint, 1, 4, width=6,
bg="#e0e7ff")
# Row 3: Space, 7-9, Cage
[Link](btn_grid, bg=COLOR_BG, width=6).grid(row=2, column=0)
for i in range(7, 10): self.create_btn(btn_grid, str(i), lambda n=i:
self.input_number(n), 2, i)
self.create_btn(btn_grid, "✨Cage", self.ask_gemini_cage, 2, 4, width=6,
bg="#e0e7ff")
# Key bindings
[Link]("<Key>", self.on_key_press)
def create_btn(self, parent, text, cmd, r, c, width=4, bg="white"):
btn = [Link](parent, text=text, command=cmd, width=width, height=2,
bg=bg, relief="groove", borderwidth=1)
[Link](row=r, column=c, padx=3, pady=3)
return btn
def start_new_game(self):
[Link].generate_game(self.diff_var.get())
self.selected_cell = -1
[Link] = 0
self.timer_seconds = 0
[Link] = []
self.update_stats()
self.draw_board()
if not self.timer_running:
self.timer_running = True
self.update_timer()
def update_timer(self):
if self.timer_running:
self.timer_seconds += 1
self.update_stats()
[Link](1000, self.update_timer)
def update_stats(self):
mins, secs = divmod(self.timer_seconds, 60)
self.stats_label.config(text=f"Time: {mins:02}:{secs:02} | Mistakes:
{[Link]}/3")
# --- Drawing ---
def draw_board(self):
[Link]("all")
# 1. Draw Cell Backgrounds (Cages)
cell_to_cage = {}
for cage in [Link]:
for cell_idx in cage['cells']:
cell_to_cage[cell_idx] = cage
r, c = divmod(cell_idx, 9)
x0, y0 = c * self.cell_size, r * self.cell_size
x1, y1 = x0 + self.cell_size, y0 + self.cell_size
[Link].create_rectangle(x0, y0, x1, y1, fill=cage['color'],
outline="")
# 2. Draw Dashed Cage Borders
for r in range(9):
for c in range(9):
idx = r * 9 + c
cage_id = cell_to_cage[idx]['id']
x, y = c * self.cell_size, r * self.cell_size
# Check Right
if c < 8 and cell_to_cage[r * 9 + c + 1]['id'] != cage_id:
[Link].create_line(x + self.cell_size, y, x +
self.cell_size, y + self.cell_size,
fill=COLOR_DASH, width=2, dash=(4, 4))
# Check Bottom
if r < 8 and cell_to_cage[(r + 1) * 9 + c]['id'] != cage_id:
[Link].create_line(x, y + self.cell_size, x +
self.cell_size, y + self.cell_size,
fill=COLOR_DASH, width=2, dash=(4, 4))
# 3. Draw Grid Lines
for i in range(10):
width = 3 if i % 3 == 0 else 1
# Vertical
[Link].create_line(i * self.cell_size, 0, i * self.cell_size,
self.canvas_size, fill=COLOR_GRID, width=width)
# Horizontal
[Link].create_line(0, i * self.cell_size, self.canvas_size, i *
self.cell_size, fill=COLOR_GRID, width=width)
# 4. Draw Numbers and Notes
for i in range(81):
r, c = divmod(i, 9)
val = [Link][i]
x, y = c * self.cell_size, r * self.cell_size
center_x, center_y = x + self.cell_size/2, y + self.cell_size/2
# Selection Highlight
if i == self.selected_cell:
[Link].create_rectangle(x+2, y+2, x+self.cell_size-2,
y+self.cell_size-2,
outline=COLOR_SELECT, width=3)
# Draw Cage Sum (if top-left of cage)
cage = cell_to_cage[i]
if i == min(cage['cells']):
[Link].create_text(x + 4, y + 4, text=str(cage['sum']),
anchor="nw",
font=("Arial", 8, "bold"), fill="#000")
if val != 0:
color = COLOR_TEXT
if val != [Link][i]:
color = "red" # Simply show red for wrong
[Link].create_text(center_x, center_y, text=str(val),
font=("Arial", 20), fill=color)
else:
# Draw Notes
notes = [Link][i]
for n in notes:
nx = x + (10 + ((n-1)%3)*15)
ny = y + (10 + ((n-1)//3)*15)
[Link].create_text(nx, ny, text=str(n), font=("Arial", 8),
fill=COLOR_NOTE)
# --- Interaction ---
def on_canvas_click(self, event):
c = int(event.x // self.cell_size)
r = int(event.y // self.cell_size)
if 0 <= c < 9 and 0 <= r < 9:
self.selected_cell = r * 9 + c
self.draw_board()
def on_key_press(self, event):
if self.selected_cell == -1: return
char = [Link]
if char in "123456789":
self.input_number(int(char))
elif [Link] in ["BackSpace", "Delete"]:
self.input_number(0)
elif [Link]() == 'n':
self.toggle_note()
# Arrow keys
r, c = divmod(self.selected_cell, 9)
if [Link] == "Up": r = max(0, r-1)
elif [Link] == "Down": r = min(8, r+1)
elif [Link] == "Left": c = max(0, c-1)
elif [Link] == "Right": c = min(8, c+1)
self.selected_cell = r * 9 + c
self.draw_board()
def toggle_note(self):
self.is_note_mode = not self.is_note_mode
txt = "Note: On" if self.is_note_mode else "Note: Off"
bg = COLOR_TEXT if self.is_note_mode else "white"
fg = "white" if self.is_note_mode else "black"
self.note_btn.config(text=txt, bg=bg, fg=fg)
def input_number(self, num):
if self.selected_cell == -1: return
if self.is_note_mode:
if num == 0:
[Link][self.selected_cell].clear()
else:
s = [Link][self.selected_cell]
if num in s: [Link](num)
else: [Link](num)
else:
prev = [Link][self.selected_cell]
if prev == num: return
# Basic history
[Link]((self.selected_cell, prev,
set([Link][self.selected_cell])))
[Link][self.selected_cell] = num
# Check correctness immediately
if num != 0 and num != [Link][self.selected_cell]:
[Link] += 1
self.update_stats()
if [Link] >= 3:
[Link]("Game Over", "Too many mistakes!")
self.timer_running = False
# Check win
if 0 not in [Link]:
if [Link] == [Link]:
self.timer_running = False
[Link]("Victory", f"Solved in
{self.stats_label.cget('text')}")
self.draw_board()
def undo(self):
if not [Link]: return
idx, prev_val, prev_notes = [Link]()
[Link][idx] = prev_val
[Link][idx] = prev_notes
self.draw_board()
# --- GEMINI API ---
def check_api_key(self):
global API_KEY
if not API_KEY:
key = [Link]("API Key Required", "Please enter your
Gemini API Key:\n(See code to hardcode it)")
if key:
API_KEY = key
return True
return False
return True
def call_gemini(self, prompt, callback):
if not self.check_api_key(): return
def run_thread():
try:
data = {
"contents": [{"parts": [{"text": prompt}]}]
}
req = [Link](
f"{API_URL}?key={API_KEY}",
data=[Link](data).encode('utf-8'),
headers={'Content-Type': 'application/json'}
)
with [Link](req) as response:
res_json = [Link](response)
text = res_json['candidates'][0]['content']['parts'][0]['text']
[Link](0, lambda: callback(text))
except Exception as e:
[Link](0, lambda: [Link]("API Error", str(e)))
[Link](target=run_thread, daemon=True).start()
def ask_gemini_hint(self):
if self.selected_cell == -1:
[Link]("Hint", "Select a cell first!")
return
cage = next((c for c in [Link] if self.selected_cell in
c['cells']), None)
filled_vals = [[Link][i] for i in cage['cells']]
prompt = f"""
I am playing Killer Sudoku.
I am stuck on cell index {self.selected_cell}.
This cell belongs to a Cage with Sum: {cage['sum']}.
Current values in this cage (0 is empty): {filled_vals}.
The correct value for this cell is:
{[Link][self.selected_cell]}.
Give me a logical hint for this cell. Explain the logic based on the Cage
Sum math.
Keep it short (max 2 sentences). Don't just give the answer.
"""
self.call_gemini(prompt, lambda res: [Link]("✨ Smart Hint",
res))
def ask_gemini_cage(self):
if self.selected_cell == -1:
[Link]("Cage Help", "Select a cell first!")
return
cage = next((c for c in [Link] if self.selected_cell in
c['cells']), None)
prompt = f"""
I am playing Killer Sudoku.
I selected a Cage with Sum {cage['sum']} that has {len(cage['cells'])}
cells.
List ALL valid mathematical combinations of {len(cage['cells'])} unique
digits (1-9) that sum to {cage['sum']}.
Format as a clean list (e.g. "1 + 4").
"""
self.call_gemini(prompt, lambda res: [Link]("✨ Cage Math",
res))
if __name__ == "__main__":
app = KillerSudokuApp()
[Link]()