From 949d6b8959aed3db449db189e42d0f19462623e0 Mon Sep 17 00:00:00 2001 From: henriquefsousa Date: Fri, 29 Aug 2025 20:15:47 -0300 Subject: [PATCH 1/2] feat: implement O(1) set-based validation with row_sets, col_sets, box_sets --- GUI.py | 144 ++++++++++++-------- GUI_original.py | 344 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 433 insertions(+), 55 deletions(-) create mode 100644 GUI_original.py diff --git a/GUI.py b/GUI.py index d39a705..fec5e45 100644 --- a/GUI.py +++ b/GUI.py @@ -4,6 +4,7 @@ pygame.font.init() + class Grid: board = [ [7, 8, 0, 4, 0, 0, 1, 2, 0], @@ -16,34 +17,71 @@ class Grid: [1, 2, 0, 0, 0, 7, 4, 0, 0], [0, 4, 9, 2, 0, 6, 0, 0, 7] ] + def __init__(self, rows, cols, width, height, win): self.rows = rows self.cols = cols self.cubes = [[Cube(self.board[i][j], i, j, width, height) for j in range(cols)] for i in range(rows)] + self.row_sets = [set() for _ in range(9)] + self.box_sets = [set() for _ in range(9)] + self.col_sets = [set() for _ in range(9)] self.width = width self.height = height - self.model = None - self.update_model() self.selected = None self.win = win - - def update_model(self): - self.model = [[self.cubes[i][j].value for j in range(self.cols)] for i in range(self.rows)] + for i in range(9): + for j in range(9): + if self.board[i][j] != 0: + val = self.board[i][j] + box_index = (i // 3) * 3 + (j // 3) + self.row_sets[i].add(val) + self.col_sets[j].add(val) + self.box_sets[box_index].add(val) + + def valid(self, val,pos): + row, col = pos + box_index = (row//3) * 3 + (col//3) + if val in self.col_sets[col] or val in self.row_sets[row] or val in self.box_sets[box_index]: + return False + + return True def place(self, val): row, col = self.selected if self.cubes[row][col].value == 0: - self.cubes[row][col].set(val) - self.update_model() + box_index = (row//3) * 3 + (col//3) - if valid(self.model, val, (row,col)) and self.solve(): - return True - else: - self.cubes[row][col].set(0) - self.cubes[row][col].set_temp(0) - self.update_model() + if val in self.col_sets[col] or val in self.row_sets[row] or val in self.box_sets[box_index]: return False + + self.cubes[row][col].set(val) + self.col_sets[col].add(val) + self.row_sets[row].add(val) + self.box_sets[box_index].add(val) + + return True + + return False + + def remove_number(self): + row, col = self.selected + if self.cubes[row][col].value != 0: + val = self.cubes[row][col].value + box_index = (row//3) * 3 + (col//3) + + if val in self.row_sets[row]: + self.row_sets[row].remove(val) + if val in self.col_sets[col]: + self.col_sets[col].remove(val) + if val in self.box_sets[box_index]: + self.box_sets[box_index].remove(val) + + self.cubes[row][col].set(0) + self.cubes[row][col].set_temp(0) + return True + return False + def sketch(self, val): row, col = self.selected @@ -93,54 +131,70 @@ def click(self, pos): return None def is_finished(self): - for i in range(self.rows): - for j in range(self.cols): - if self.cubes[i][j].value == 0: - return False + for row_set in self.row_sets: + if len(row_set) != 9: + return False return True def solve(self): - find = find_empty(self.model) + temp_model = [[self.cubes[i][j].value for j in range(self.cols)] for i in range(self.rows)] + find = find_empty(temp_model) if not find: return True else: row, col = find for i in range(1, 10): - if valid(self.model, i, (row, col)): - self.model[row][col] = i + if self.valid(i, (row, col)): + box_index = (row//3) * 3 + (col//3) + self.row_sets[row].add(i) + self.col_sets[col].add(i) + self.box_sets[box_index].add(i) + temp_model[row][col] = i if self.solve(): return True - - self.model[row][col] = 0 + + self.row_sets[row].remove(i) + self.col_sets[col].remove(i) + self.box_sets[box_index].remove(i) + temp_model[row][col] = 0 return False def solve_gui(self): - self.update_model() - find = find_empty(self.model) + temp_model = [[self.cubes[i][j].value for j in range(self.cols)] for i in range(self.rows)] + find = find_empty(temp_model) if not find: return True else: row, col = find for i in range(1, 10): - if valid(self.model, i, (row, col)): - self.model[row][col] = i + if self.valid(i, (row, col)): + box_index = (row//3) * 3 + (col//3) + + self.row_sets[row].add(i) + self.col_sets[col].add(i) + self.box_sets[box_index].add(i) + + temp_model[row][col] = i self.cubes[row][col].set(i) self.cubes[row][col].draw_change(self.win, True) - self.update_model() + pygame.display.update() pygame.time.delay(100) if self.solve_gui(): return True - - self.model[row][col] = 0 + + self.row_sets[row].remove(i) + self.col_sets[col].remove(i) + self.box_sets[box_index].remove(i) + temp_model[row][col] = 0 self.cubes[row][col].set(0) - self.update_model() self.cubes[row][col].draw_change(self.win, False) + pygame.display.update() pygame.time.delay(100) @@ -209,29 +263,6 @@ def find_empty(bo): return None -def valid(bo, num, pos): - # Check row - for i in range(len(bo[0])): - if bo[pos[0]][i] == num and pos[1] != i: - return False - - # Check column - for i in range(len(bo)): - if bo[i][pos[1]] == num and pos[0] != i: - return False - - # Check box - box_x = pos[1] // 3 - box_y = pos[0] // 3 - - for i in range(box_y*3, box_y*3 + 3): - for j in range(box_x * 3, box_x*3 + 3): - if bo[i][j] == num and (i,j) != pos: - return False - - return True - - def redraw_window(win, board, time, strikes): win.fill((255,255,255)) # Draw time @@ -306,8 +337,11 @@ def main(): key = 8 if event.key == pygame.K_KP9: key = 9 - if event.key == pygame.K_DELETE: - board.clear() + if event.key == pygame.K_d: + if board.remove_number(): + print("number removed") + else: + board.clear() key = None if event.key == pygame.K_SPACE: diff --git a/GUI_original.py b/GUI_original.py new file mode 100644 index 0000000..d39a705 --- /dev/null +++ b/GUI_original.py @@ -0,0 +1,344 @@ +# GUI.py +import pygame +import time +pygame.font.init() + + +class Grid: + board = [ + [7, 8, 0, 4, 0, 0, 1, 2, 0], + [6, 0, 0, 0, 7, 5, 0, 0, 9], + [0, 0, 0, 6, 0, 1, 0, 7, 8], + [0, 0, 7, 0, 4, 0, 2, 6, 0], + [0, 0, 1, 0, 5, 0, 9, 3, 0], + [9, 0, 4, 0, 6, 0, 0, 0, 5], + [0, 7, 0, 3, 0, 0, 0, 1, 2], + [1, 2, 0, 0, 0, 7, 4, 0, 0], + [0, 4, 9, 2, 0, 6, 0, 0, 7] + ] + + def __init__(self, rows, cols, width, height, win): + self.rows = rows + self.cols = cols + self.cubes = [[Cube(self.board[i][j], i, j, width, height) for j in range(cols)] for i in range(rows)] + self.width = width + self.height = height + self.model = None + self.update_model() + self.selected = None + self.win = win + + def update_model(self): + self.model = [[self.cubes[i][j].value for j in range(self.cols)] for i in range(self.rows)] + + def place(self, val): + row, col = self.selected + if self.cubes[row][col].value == 0: + self.cubes[row][col].set(val) + self.update_model() + + if valid(self.model, val, (row,col)) and self.solve(): + return True + else: + self.cubes[row][col].set(0) + self.cubes[row][col].set_temp(0) + self.update_model() + return False + + def sketch(self, val): + row, col = self.selected + self.cubes[row][col].set_temp(val) + + def draw(self): + # Draw Grid Lines + gap = self.width / 9 + for i in range(self.rows+1): + if i % 3 == 0 and i != 0: + thick = 4 + else: + thick = 1 + pygame.draw.line(self.win, (0,0,0), (0, i*gap), (self.width, i*gap), thick) + pygame.draw.line(self.win, (0, 0, 0), (i * gap, 0), (i * gap, self.height), thick) + + # Draw Cubes + for i in range(self.rows): + for j in range(self.cols): + self.cubes[i][j].draw(self.win) + + def select(self, row, col): + # Reset all other + for i in range(self.rows): + for j in range(self.cols): + self.cubes[i][j].selected = False + + self.cubes[row][col].selected = True + self.selected = (row, col) + + def clear(self): + row, col = self.selected + if self.cubes[row][col].value == 0: + self.cubes[row][col].set_temp(0) + + def click(self, pos): + """ + :param: pos + :return: (row, col) + """ + if pos[0] < self.width and pos[1] < self.height: + gap = self.width / 9 + x = pos[0] // gap + y = pos[1] // gap + return (int(y),int(x)) + else: + return None + + def is_finished(self): + for i in range(self.rows): + for j in range(self.cols): + if self.cubes[i][j].value == 0: + return False + return True + + def solve(self): + find = find_empty(self.model) + if not find: + return True + else: + row, col = find + + for i in range(1, 10): + if valid(self.model, i, (row, col)): + self.model[row][col] = i + + if self.solve(): + return True + + self.model[row][col] = 0 + + return False + + def solve_gui(self): + self.update_model() + find = find_empty(self.model) + if not find: + return True + else: + row, col = find + + for i in range(1, 10): + if valid(self.model, i, (row, col)): + self.model[row][col] = i + self.cubes[row][col].set(i) + self.cubes[row][col].draw_change(self.win, True) + self.update_model() + pygame.display.update() + pygame.time.delay(100) + + if self.solve_gui(): + return True + + self.model[row][col] = 0 + self.cubes[row][col].set(0) + self.update_model() + self.cubes[row][col].draw_change(self.win, False) + pygame.display.update() + pygame.time.delay(100) + + return False + + +class Cube: + rows = 9 + cols = 9 + + def __init__(self, value, row, col, width, height): + self.value = value + self.temp = 0 + self.row = row + self.col = col + self.width = width + self.height = height + self.selected = False + + def draw(self, win): + fnt = pygame.font.SysFont("comicsans", 40) + + gap = self.width / 9 + x = self.col * gap + y = self.row * gap + + if self.temp != 0 and self.value == 0: + text = fnt.render(str(self.temp), 1, (128,128,128)) + win.blit(text, (x+5, y+5)) + elif not(self.value == 0): + text = fnt.render(str(self.value), 1, (0, 0, 0)) + win.blit(text, (x + (gap/2 - text.get_width()/2), y + (gap/2 - text.get_height()/2))) + + if self.selected: + pygame.draw.rect(win, (255,0,0), (x,y, gap ,gap), 3) + + def draw_change(self, win, g=True): + fnt = pygame.font.SysFont("comicsans", 40) + + gap = self.width / 9 + x = self.col * gap + y = self.row * gap + + pygame.draw.rect(win, (255, 255, 255), (x, y, gap, gap), 0) + + text = fnt.render(str(self.value), 1, (0, 0, 0)) + win.blit(text, (x + (gap / 2 - text.get_width() / 2), y + (gap / 2 - text.get_height() / 2))) + if g: + pygame.draw.rect(win, (0, 255, 0), (x, y, gap, gap), 3) + else: + pygame.draw.rect(win, (255, 0, 0), (x, y, gap, gap), 3) + + def set(self, val): + self.value = val + + def set_temp(self, val): + self.temp = val + + +def find_empty(bo): + for i in range(len(bo)): + for j in range(len(bo[0])): + if bo[i][j] == 0: + return (i, j) # row, col + + return None + + +def valid(bo, num, pos): + # Check row + for i in range(len(bo[0])): + if bo[pos[0]][i] == num and pos[1] != i: + return False + + # Check column + for i in range(len(bo)): + if bo[i][pos[1]] == num and pos[0] != i: + return False + + # Check box + box_x = pos[1] // 3 + box_y = pos[0] // 3 + + for i in range(box_y*3, box_y*3 + 3): + for j in range(box_x * 3, box_x*3 + 3): + if bo[i][j] == num and (i,j) != pos: + return False + + return True + + +def redraw_window(win, board, time, strikes): + win.fill((255,255,255)) + # Draw time + fnt = pygame.font.SysFont("comicsans", 40) + text = fnt.render("Time: " + format_time(time), 1, (0,0,0)) + win.blit(text, (540 - 160, 560)) + # Draw Strikes + text = fnt.render("X " * strikes, 1, (255, 0, 0)) + win.blit(text, (20, 560)) + # Draw grid and board + board.draw() + + +def format_time(secs): + sec = secs%60 + minute = secs//60 + hour = minute//60 + + mat = " " + str(minute) + ":" + str(sec) + return mat + + +def main(): + win = pygame.display.set_mode((540,600)) + pygame.display.set_caption("Sudoku") + board = Grid(9, 9, 540, 540, win) + key = None + run = True + start = time.time() + strikes = 0 + while run: + + play_time = round(time.time() - start) + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + run = False + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_1: + key = 1 + if event.key == pygame.K_2: + key = 2 + if event.key == pygame.K_3: + key = 3 + if event.key == pygame.K_4: + key = 4 + if event.key == pygame.K_5: + key = 5 + if event.key == pygame.K_6: + key = 6 + if event.key == pygame.K_7: + key = 7 + if event.key == pygame.K_8: + key = 8 + if event.key == pygame.K_9: + key = 9 + if event.key == pygame.K_KP1: + key = 1 + if event.key == pygame.K_KP2: + key = 2 + if event.key == pygame.K_KP3: + key = 3 + if event.key == pygame.K_KP4: + key = 4 + if event.key == pygame.K_KP5: + key = 5 + if event.key == pygame.K_KP6: + key = 6 + if event.key == pygame.K_KP7: + key = 7 + if event.key == pygame.K_KP8: + key = 8 + if event.key == pygame.K_KP9: + key = 9 + if event.key == pygame.K_DELETE: + board.clear() + key = None + + if event.key == pygame.K_SPACE: + board.solve_gui() + + if event.key == pygame.K_RETURN: + i, j = board.selected + if board.cubes[i][j].temp != 0: + if board.place(board.cubes[i][j].temp): + print("Success") + else: + print("Wrong") + strikes += 1 + key = None + + if board.is_finished(): + print("Game over") + + if event.type == pygame.MOUSEBUTTONDOWN: + pos = pygame.mouse.get_pos() + clicked = board.click(pos) + if clicked: + board.select(clicked[0], clicked[1]) + key = None + + if board.selected and key != None: + board.sketch(key) + + redraw_window(win, board, play_time, strikes) + pygame.display.update() + + +main() +pygame.quit() From 904c059e5d864323bf78c2023f4c03cbf9dca847 Mon Sep 17 00:00:00 2001 From: henriquefsousa Date: Fri, 29 Aug 2025 20:24:28 -0300 Subject: [PATCH 2/2] feat: optimize Sudoku solver with O(1) set-based validation --- GUI_original.py | 344 ------------------------------------------------ 1 file changed, 344 deletions(-) delete mode 100644 GUI_original.py diff --git a/GUI_original.py b/GUI_original.py deleted file mode 100644 index d39a705..0000000 --- a/GUI_original.py +++ /dev/null @@ -1,344 +0,0 @@ -# GUI.py -import pygame -import time -pygame.font.init() - - -class Grid: - board = [ - [7, 8, 0, 4, 0, 0, 1, 2, 0], - [6, 0, 0, 0, 7, 5, 0, 0, 9], - [0, 0, 0, 6, 0, 1, 0, 7, 8], - [0, 0, 7, 0, 4, 0, 2, 6, 0], - [0, 0, 1, 0, 5, 0, 9, 3, 0], - [9, 0, 4, 0, 6, 0, 0, 0, 5], - [0, 7, 0, 3, 0, 0, 0, 1, 2], - [1, 2, 0, 0, 0, 7, 4, 0, 0], - [0, 4, 9, 2, 0, 6, 0, 0, 7] - ] - - def __init__(self, rows, cols, width, height, win): - self.rows = rows - self.cols = cols - self.cubes = [[Cube(self.board[i][j], i, j, width, height) for j in range(cols)] for i in range(rows)] - self.width = width - self.height = height - self.model = None - self.update_model() - self.selected = None - self.win = win - - def update_model(self): - self.model = [[self.cubes[i][j].value for j in range(self.cols)] for i in range(self.rows)] - - def place(self, val): - row, col = self.selected - if self.cubes[row][col].value == 0: - self.cubes[row][col].set(val) - self.update_model() - - if valid(self.model, val, (row,col)) and self.solve(): - return True - else: - self.cubes[row][col].set(0) - self.cubes[row][col].set_temp(0) - self.update_model() - return False - - def sketch(self, val): - row, col = self.selected - self.cubes[row][col].set_temp(val) - - def draw(self): - # Draw Grid Lines - gap = self.width / 9 - for i in range(self.rows+1): - if i % 3 == 0 and i != 0: - thick = 4 - else: - thick = 1 - pygame.draw.line(self.win, (0,0,0), (0, i*gap), (self.width, i*gap), thick) - pygame.draw.line(self.win, (0, 0, 0), (i * gap, 0), (i * gap, self.height), thick) - - # Draw Cubes - for i in range(self.rows): - for j in range(self.cols): - self.cubes[i][j].draw(self.win) - - def select(self, row, col): - # Reset all other - for i in range(self.rows): - for j in range(self.cols): - self.cubes[i][j].selected = False - - self.cubes[row][col].selected = True - self.selected = (row, col) - - def clear(self): - row, col = self.selected - if self.cubes[row][col].value == 0: - self.cubes[row][col].set_temp(0) - - def click(self, pos): - """ - :param: pos - :return: (row, col) - """ - if pos[0] < self.width and pos[1] < self.height: - gap = self.width / 9 - x = pos[0] // gap - y = pos[1] // gap - return (int(y),int(x)) - else: - return None - - def is_finished(self): - for i in range(self.rows): - for j in range(self.cols): - if self.cubes[i][j].value == 0: - return False - return True - - def solve(self): - find = find_empty(self.model) - if not find: - return True - else: - row, col = find - - for i in range(1, 10): - if valid(self.model, i, (row, col)): - self.model[row][col] = i - - if self.solve(): - return True - - self.model[row][col] = 0 - - return False - - def solve_gui(self): - self.update_model() - find = find_empty(self.model) - if not find: - return True - else: - row, col = find - - for i in range(1, 10): - if valid(self.model, i, (row, col)): - self.model[row][col] = i - self.cubes[row][col].set(i) - self.cubes[row][col].draw_change(self.win, True) - self.update_model() - pygame.display.update() - pygame.time.delay(100) - - if self.solve_gui(): - return True - - self.model[row][col] = 0 - self.cubes[row][col].set(0) - self.update_model() - self.cubes[row][col].draw_change(self.win, False) - pygame.display.update() - pygame.time.delay(100) - - return False - - -class Cube: - rows = 9 - cols = 9 - - def __init__(self, value, row, col, width, height): - self.value = value - self.temp = 0 - self.row = row - self.col = col - self.width = width - self.height = height - self.selected = False - - def draw(self, win): - fnt = pygame.font.SysFont("comicsans", 40) - - gap = self.width / 9 - x = self.col * gap - y = self.row * gap - - if self.temp != 0 and self.value == 0: - text = fnt.render(str(self.temp), 1, (128,128,128)) - win.blit(text, (x+5, y+5)) - elif not(self.value == 0): - text = fnt.render(str(self.value), 1, (0, 0, 0)) - win.blit(text, (x + (gap/2 - text.get_width()/2), y + (gap/2 - text.get_height()/2))) - - if self.selected: - pygame.draw.rect(win, (255,0,0), (x,y, gap ,gap), 3) - - def draw_change(self, win, g=True): - fnt = pygame.font.SysFont("comicsans", 40) - - gap = self.width / 9 - x = self.col * gap - y = self.row * gap - - pygame.draw.rect(win, (255, 255, 255), (x, y, gap, gap), 0) - - text = fnt.render(str(self.value), 1, (0, 0, 0)) - win.blit(text, (x + (gap / 2 - text.get_width() / 2), y + (gap / 2 - text.get_height() / 2))) - if g: - pygame.draw.rect(win, (0, 255, 0), (x, y, gap, gap), 3) - else: - pygame.draw.rect(win, (255, 0, 0), (x, y, gap, gap), 3) - - def set(self, val): - self.value = val - - def set_temp(self, val): - self.temp = val - - -def find_empty(bo): - for i in range(len(bo)): - for j in range(len(bo[0])): - if bo[i][j] == 0: - return (i, j) # row, col - - return None - - -def valid(bo, num, pos): - # Check row - for i in range(len(bo[0])): - if bo[pos[0]][i] == num and pos[1] != i: - return False - - # Check column - for i in range(len(bo)): - if bo[i][pos[1]] == num and pos[0] != i: - return False - - # Check box - box_x = pos[1] // 3 - box_y = pos[0] // 3 - - for i in range(box_y*3, box_y*3 + 3): - for j in range(box_x * 3, box_x*3 + 3): - if bo[i][j] == num and (i,j) != pos: - return False - - return True - - -def redraw_window(win, board, time, strikes): - win.fill((255,255,255)) - # Draw time - fnt = pygame.font.SysFont("comicsans", 40) - text = fnt.render("Time: " + format_time(time), 1, (0,0,0)) - win.blit(text, (540 - 160, 560)) - # Draw Strikes - text = fnt.render("X " * strikes, 1, (255, 0, 0)) - win.blit(text, (20, 560)) - # Draw grid and board - board.draw() - - -def format_time(secs): - sec = secs%60 - minute = secs//60 - hour = minute//60 - - mat = " " + str(minute) + ":" + str(sec) - return mat - - -def main(): - win = pygame.display.set_mode((540,600)) - pygame.display.set_caption("Sudoku") - board = Grid(9, 9, 540, 540, win) - key = None - run = True - start = time.time() - strikes = 0 - while run: - - play_time = round(time.time() - start) - - for event in pygame.event.get(): - if event.type == pygame.QUIT: - run = False - if event.type == pygame.KEYDOWN: - if event.key == pygame.K_1: - key = 1 - if event.key == pygame.K_2: - key = 2 - if event.key == pygame.K_3: - key = 3 - if event.key == pygame.K_4: - key = 4 - if event.key == pygame.K_5: - key = 5 - if event.key == pygame.K_6: - key = 6 - if event.key == pygame.K_7: - key = 7 - if event.key == pygame.K_8: - key = 8 - if event.key == pygame.K_9: - key = 9 - if event.key == pygame.K_KP1: - key = 1 - if event.key == pygame.K_KP2: - key = 2 - if event.key == pygame.K_KP3: - key = 3 - if event.key == pygame.K_KP4: - key = 4 - if event.key == pygame.K_KP5: - key = 5 - if event.key == pygame.K_KP6: - key = 6 - if event.key == pygame.K_KP7: - key = 7 - if event.key == pygame.K_KP8: - key = 8 - if event.key == pygame.K_KP9: - key = 9 - if event.key == pygame.K_DELETE: - board.clear() - key = None - - if event.key == pygame.K_SPACE: - board.solve_gui() - - if event.key == pygame.K_RETURN: - i, j = board.selected - if board.cubes[i][j].temp != 0: - if board.place(board.cubes[i][j].temp): - print("Success") - else: - print("Wrong") - strikes += 1 - key = None - - if board.is_finished(): - print("Game over") - - if event.type == pygame.MOUSEBUTTONDOWN: - pos = pygame.mouse.get_pos() - clicked = board.click(pos) - if clicked: - board.select(clicked[0], clicked[1]) - key = None - - if board.selected and key != None: - board.sketch(key) - - redraw_window(win, board, play_time, strikes) - pygame.display.update() - - -main() -pygame.quit()