# tk_5x5_Sudoku_Slider.py
from tkinter import *
from PIL import Image, ImageDraw, ImageFont, ImageTk
from random import randint
import itertools
BACKGROUND = 'orange'
TSIZE = 90
MARGIN = 7
ROWS = 5
COLUMNS = 5
board = {}
colors = ['#ff8888', '#8888ff', '#88ff88', '#ffff66']
ARC_COLORS = list(itertools.permutations(colors, 4))
instructions = '''
# tk_5x5_Sudoku_Slider.py
To win this game, the player must satisfy all the following conditions:
1. Valid 5×5 Sudoku Grid
- Each number from 1 to 5 must appear exactly once in each row and each column.
2. Matching Edge Colors
- The top half-circle of a tile must match the bottom half-circle of the tile above it.
- The left half-circle must match the right half-circle of the tile to its left.
'''
def create_tile_image(n, arc_colors_4, idx):
img_size = TSIZE + 2 * MARGIN
img = Image.new('RGBA', (img_size, img_size))
draw = ImageDraw.Draw(img)
draw.rectangle([MARGIN, MARGIN, img_size - MARGIN, img_size - MARGIN], outline=None, fill='white')
r = TSIZE - 50
cx, cy = img_size // 2, img_size // 2
draw.pieslice([cx - r//2, MARGIN - r//2, cx + r//2, MARGIN + r//2], start=0, end=180, fill=arc_colors_4[0])
draw.pieslice([cx - r//2, img_size - MARGIN - r//2, cx + r//2, img_size - MARGIN + r//2], start=180, end=360, fill=arc_colors_4[1])
draw.pieslice([MARGIN - r//2, cy - r//2, MARGIN + r//2, cy + r//2], start=270, end=90, fill=arc_colors_4[2])
draw.pieslice([img_size - MARGIN - r//2, cy - r//2, img_size - MARGIN + r//2, cy + r//2], start=90, end=270, fill=arc_colors_4[3])
font = ImageFont.truetype("arial.ttf", int(TSIZE * 0.7))
text_size = draw.textsize(str(n), font=font)
text_pos = (cx - text_size[0] // 2, cy - text_size[1] // 2 - 6)
draw.text(text_pos, str(n), font=font, fill='black')
#draw.text(text_pos, str(idx), font=font, fill='gray') # testing purpose
draw.rectangle([MARGIN, MARGIN, img_size - MARGIN, img_size - MARGIN], outline='black', fill=None)
return ImageTk.PhotoImage(img)
def generate_latin_square(n=5):
import random
base = list(range(1, n + 1))
square = []
for i in range(n):
row = base[i:] + base[:i]
square.append(row)
for _ in range(10):
i, j = random.sample(range(n), 2)
square[i], square[j] = square[j], square[i]
for row in square:
row[i], row[j] = row[j], row[i]
return square
def permute(array):
n = len(array)
signature = +1
for i in range(n - 2):
j = randint(0, n - 1)
if j != i:
signature = -signature
array[i], array[j] = array[j], array[i]
if signature < 0:
array[-2], array[-1] = array[-1], array[-2]
def create_tile(idx, arc_colors_4, n):
image = create_tile_image(n, arc_colors_4, idx)
tile_id = canvas.create_image(0, 0, image=image, anchor='nw')
return {'id': idx, 'canvas_id': tile_id, 'image': image, 'row': 0, 'column': 0}
def move_tile(tile, dx, dy):
canvas.move(tile['canvas_id'], dx, dy)
def moveto_tile(tile, row, column):
S = MARGIN + TSIZE
x = MARGIN + column * S
y = MARGIN + row * S
current_x = MARGIN + tile['column'] * S
current_y = MARGIN + tile['row'] * S
dx = x - current_x
dy = y - current_y
move_tile(tile, dx, dy)
tile['row'] = row
tile['column'] = column
def create_timer(parent):
label = Label(parent)
label.elapsed = 0
label.config(text='Time: 0s')
label.running = True
def update():
if label.running:
label.elapsed += 1
label.config(text=f'Time: {label.elapsed}s')
label.after_id = label.after(1000, update)
label.after_id = label.after(1000, update)
return label
def stop_timer():
timer = board['timer']
timer.running = False
if hasattr(timer, 'after_id'):
timer.after_cancel(timer.after_id)
def reset_timer():
timer = board['timer']
timer.running = False
if hasattr(timer, 'after_id'):
timer.after_cancel(timer.after_id)
timer.elapsed = 0
timer.config(text='Time: 0s')
timer.running = True
def update():
if timer.running:
timer.elapsed += 1
timer.config(text=f'Time: {timer.elapsed}s')
timer.after_id = timer.after(1000, update)
timer.after_id = timer.after(1000, update)
def assign_matching_combos(rows, columns):
grid = [[None for _ in range(columns)] for _ in range(rows)]
used = set()
def is_valid(combo, r, c):
if r > 0 and grid[r - 1][c] and grid[r - 1][c][1] != combo[0]:
return False
if c > 0 and grid[r][c - 1] and grid[r][c - 1][3] != combo[2]:
return False
return True
def backtrack(r, c):
if r == rows:
return True
next_r, next_c = (r, c + 1) if c + 1 < columns else (r + 1, 0)
if r == rows - 1 and c == columns - 1:
return backtrack(next_r, next_c)
for combo in ARC_COLORS:
if combo in used:
continue
if is_valid(combo, r, c):
grid[r][c] = combo
used.add(combo)
if backtrack(next_r, next_c):
return True
grid[r][c] = None
used.remove(combo)
return False
backtrack(0, 0)
return grid
def create_board(timer, rows, columns):
combo_grid = assign_matching_combos(rows, columns)
latin_square = generate_latin_square(rows)
tiles = []
for r in range(rows):
for c in range(columns):
if r == rows - 1 and c == columns - 1:
continue
arc_colors_4 = combo_grid[r][c]
number = latin_square[r][c]
tile = create_tile(len(tiles) + 1, arc_colors_4, number)
moveto_tile(tile, r, c)
tiles.append(tile)
board['tiles'] = tiles
board['rows'] = rows
board['columns'] = columns
board['row'] = rows - 1
board['col'] = columns - 1
board['timer'] = timer
board['locked'] = False
canvas.bind('', lambda e: clickhandler(e))
canvas.pack()
shuffle_board()
def shuffle_board():
canvas.delete('end')
board['locked'] = False
positions = [(r, c) for r in range(ROWS) for c in range(COLUMNS)]
positions.remove((ROWS - 1, COLUMNS - 1))
permute(positions)
for tile, (r, c) in zip(board['tiles'], positions):
moveto_tile(tile, r, c)
board['row'] = ROWS - 1
board['col'] = COLUMNS - 1
reset_timer()
def update_timer():
timer = board['timer']
if timer.running:
timer.elapsed += 1
timer.config(text=f'Time: {timer.elapsed}s')
timer.after_id = timer.after(1000, lambda: update_timer())
def clickhandler(event):
if board['locked']:
return
S = MARGIN + TSIZE
row = max(0, min((event.y - MARGIN // 2) // S, board['rows'] - 1))
column = max(0, min((event.x - MARGIN // 2) // S, board['columns'] - 1))
if row == board['row'] and column != board['col']:
move_row(row, column)
elif column == board['col'] and row != board['row']:
move_column(row, column)
check_win()
def move_row(r, c):
delta = 1 if c > board['col'] else -1
for col in range(board['col'], c, delta):
move_tile_at(r, col + delta, 0, -delta)
board['col'] = c
def move_column(r, c):
delta = 1 if r > board['row'] else -1
for row in range(board['row'], r, delta):
move_tile_at(row + delta, c, -delta, 0)
board['row'] = r
def move_tile_at(r, c, tr, tc):
for tile in board['tiles']:
if tile['row'] == r and tile['column'] == c:
moveto_tile(tile, r + tr, c + tc)
break
def check_win():
for tile in board['tiles']:
expected_id = tile['row'] * board['columns'] + tile['column'] + 1
if tile['id'] != expected_id:
return
stop_timer()
board['locked'] = True
S = MARGIN + TSIZE
canvas.create_rectangle(0, 40, S * ROWS + MARGIN, 140, fill='gray', stipple='gray75', outline='', tag='end')
canvas.create_text(canvas.winfo_width() // 2, TSIZE - 5, text="Puzzle Complete!", font='Helvetica 40', fill='white', tag='end')
def change_bg(color):
canvas['background'] = color
def show_about():
top = Toplevel(root)
top.title("Program Info")
Label(top, text=instructions, padx=20, pady=20).pack()
Button(top, text="OK", command=top.destroy).pack()
root = Tk()
root.geometry('+0+0')
root.title("# tk_5x5_Sudoku_Slider.py")
S = MARGIN + TSIZE
canvas = Canvas(root, background=BACKGROUND, height=S * ROWS + MARGIN - 2, width=S * COLUMNS + MARGIN - 2)
menubar = Frame(root, relief=RAISED, bd=2)
menubutton1 = Menubutton(menubar, text='Options')
menubutton1.pack(side=LEFT)
menubutton2 = Menubutton(menubar, text='Colors')
menubutton2.pack(side=LEFT)
timer = create_timer(menubar)
timer.pack(side=RIGHT, padx=MARGIN)
menubar.pack(side=TOP, fill=X)
create_board(timer, ROWS, COLUMNS)
menu1 = Menu(menubutton1)
menu1.add_command(label='About this program...', command=show_about)
menu1.add_command(label='New game', command=lambda: shuffle_board())
menu1.add_command(label='Quit', command=root.quit)
menubutton1['menu'] = menu1
menu2 = Menu(menubutton2)
menu2.add_command(label='Burlywood', command=lambda: change_bg('burlywood'))
menu2.add_command(label='Forest Green', command=lambda: change_bg('Forest Green'))
menu2.add_command(label='Orange', command=lambda: change_bg('orange'))
menubutton2['menu'] = menu2
root.mainloop()