commit 499517a2bfda04ff572936f261f86a809b0f7853 Author: RokasPuzonas <59289995+RokasPuzonas@users.noreply.github.com> Date: Sun Dec 29 21:29:16 2019 +0200 Add files via upload diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..323a574 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Rokas Puzonas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e838d6 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +Sudoku solver +============= + +This is a python application made using [pyglet](https://pypi.org/project/pyglet/) for solving sudoku boards using the [backtracking algorithm](https://en.wikipedia.org/wiki/Backtracking). + +Installation +============ + +First of all download the repository and place it anywhere on your machine. As I metioned before the application uses Python so it will need to be installed from [here](https://www.python.org/). It also requires that you have the [pyglet](https://pypi.org/project/pyglet/) package installed on your machine, you can do so with [pip](https://pypi.org/) using the command: +```bash +pip install pyglet +``` +Or if you already downloaded the repository, then you can run this command while you are in the root directory. +```bash +pip install -r requirements.txt +``` + +Usage +============ + +The application can by launched with the following command: +```bash +python main.py +``` +You can change the starting board by editing `board.txt` in the root directory. Each line must have 9 whitespace seperated values ranging from 0-9, 0 meaning that the cell is empty. There must be 9 lines for in total of 81 values. If the number of values wasen't 81 the board will be blank. Here is an example of how it should look like: +``` +0 0 5 0 0 1 2 7 4 +2 0 0 0 0 5 0 0 0 +4 0 0 0 0 9 0 6 1 +0 0 2 1 0 0 0 0 0 +0 7 0 0 0 0 0 8 0 +0 0 0 0 0 2 3 0 0 +7 2 0 9 0 0 0 0 8 +0 0 0 3 0 0 0 0 6 +6 8 9 4 0 0 1 0 0 +``` + +Screenshots +============ + +![Default board](https://i.imgur.com/2ZdysDk.png "Default board") +![Check mistakes](https://i.imgur.com/0Dyb7oA.gif "Check mistakes") +![Solve](https://i.imgur.com/np2Soky.gif "Solve") +![Stepped solve](https://i.imgur.com/smZvhyn.gif "Stepped solve") + +License +============ +[MIT](LICENSE) \ No newline at end of file diff --git a/board.txt b/board.txt new file mode 100644 index 0000000..be60b9e --- /dev/null +++ b/board.txt @@ -0,0 +1,9 @@ +0 0 5 0 0 1 2 7 4 +2 0 0 0 0 5 0 0 0 +4 0 0 0 0 9 0 6 1 +0 0 2 1 0 0 0 0 0 +0 7 0 0 0 0 0 8 0 +0 0 0 0 0 2 3 0 0 +7 2 0 9 0 0 0 0 8 +0 0 0 3 0 0 0 0 6 +6 8 9 4 0 0 1 0 0 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..bc535b8 --- /dev/null +++ b/main.py @@ -0,0 +1,546 @@ +# Was made using Python 3.6.2 +import sys +import os +import random +try: + import pyglet + from pyglet.gl import * + from pyglet.window import key + from pyglet.window import mouse +except ImportError as e: + print("ERROR: Make sure the packages in requirements.txt are installed.") + print("You can do so with \"pip install -r requirements.txt\"") + sys.exit(1) + +MOVE = { + (0, -1): [(key.UP , 0), (key.W, 0), (key.NUM_8, 0)], + (0, 1): [(key.DOWN , 0), (key.S, 0), (key.NUM_2, 0)], + (-1, 0): [(key.LEFT , 0), (key.A, 0), (key.NUM_4, 0)], + (1 , 0): [(key.RIGHT, 0), (key.D, 0), (key.NUM_6, 0)], +} + +class Graphics: + line_width = 1 + + def vertex2(x, y): + glVertex2i(int(x), int(y)) + + def rectangle(rect_type, x, y, w, h): + if w < 0: + x+=w + w*=-1 + if h < 0: + y+=h + h*=-1 + if rect_type == "fill": + glBegin(GL_QUADS) + Graphics.vertex2(x, y) + Graphics.vertex2(x, y+h) + Graphics.vertex2(x+w, y+h) + Graphics.vertex2(x+w, y) + glEnd() + elif rect_type == "line": + # Could be optimized by having intermediary variables, for common calculations + glBegin(GL_POLYGON) + Graphics.vertex2(x, y) + Graphics.vertex2(x, y+h) + Graphics.vertex2(x+Graphics.line_width, y+h-Graphics.line_width) + Graphics.vertex2(x+Graphics.line_width, y+Graphics.line_width) + Graphics.vertex2(x+w-Graphics.line_width, y+Graphics.line_width) + Graphics.vertex2(x+w, y) + glEnd() + glBegin(GL_POLYGON) + Graphics.vertex2(x+w, y+h) + Graphics.vertex2(x+w, y) + Graphics.vertex2(x+w-Graphics.line_width, y+Graphics.line_width) + Graphics.vertex2(x+w-Graphics.line_width, y+h-Graphics.line_width) + Graphics.vertex2(x+Graphics.line_width, y+h-Graphics.line_width) + Graphics.vertex2(x, y+h) + glEnd() + + def line(x1, y1, x2, y2): + glBegin(GL_LINES) + glVertex2i(int(x1), int(y1)) + glVertex2i(int(x2), int(y2)) + glEnd() + + def setLineWidth(width): + glLineWidth(width) + Graphics.line_width = width + + def getLineWidth(): + return Graphics.line_width + + def setColor(color): + if len(color) == 4: + glEnable( GL_BLEND ); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glColor4ub(int(color[0]), int(color[1]), int(color[2]), int(color[3])) + else: + glColor4ub(int(color[0]), int(color[1]), int(color[2]), 255) + + def setClearColor(color): + glClearColor(color[0]/255, color[1]/255, color[2]/255 ,1) + +class Button: + base_color = (19, 20, 23) + hover_color = (29, 30, 23) + pressed_color = (9, 10, 13) + text_color = (248, 248, 242, 255) + font_name = "Arial" + + def __init__(self, x, y, width, height, text=""): + self.hover = False + self.pressed = False + self.onClick = None + self.x = x + self.y = y + self.width = width + self.height = height + self.label = pyglet.text.Label( + text, + font_name=Button.font_name, font_size=self.height*0.5, + x = self.x + self.width/2, + y = self.y - self.height/2, + anchor_x='center', anchor_y='center', + color=Button.text_color + ) + + def draw(self): + if self.pressed: + Graphics.setColor(self.pressed_color) + elif self.hover: + Graphics.setColor(self.hover_color) + else: + Graphics.setColor(self.base_color) + Graphics.rectangle("fill", self.x, self.y, self.width, -self.height) + self.label.draw() + + def onMouseMotion(self, x, y, dx, dy): + self.hover = self.x <= x < self.x+self.width and self.y-self.height<= y < self.y + + def onMousePress(self, x, y, button, modifiers): + self.pressed = self.hover and button & mouse.LEFT + + def onMouseRelease(self, x, y, button, modifiers): + if self.pressed: + if self.onClick: + self.onClick() + self.pressed = False + +class AboutPopup: + shade_color = (19, 20, 23, 128) + background_color = (39, 40, 34) + border_color = (19, 20, 23) + border_width = 8 + font_name = "Arial" + font_color = (248, 248, 242, 255) + font_size = 10 + + def addLabel(self, text, x, y): + self.elements.append(pyglet.text.Label(text, x=x, y=y, font_name=self.font_name,font_size=self.font_size,color=self.font_color)) + + def __init__(self, parent_width, parent_height): + self.elements = [] + self.visible = False + self.parent_width = parent_width + self.parent_height = parent_height + self.width = 200 + self.height = 150 + self.x = (parent_width - self.width)/2 + self.y = (parent_height + self.height)/2 + + back_button_size = (50, 25) + padding = 5 + self.back_button = Button( + self.x+self.width-back_button_size[0]-padding-self.border_width, + self.y-self.height+back_button_size[1]+padding+self.border_width, + back_button_size[0], back_button_size[1], "Back" + ) + self.back_button.onClick = lambda: self.toggle() + + self.addLabel("Made by Rokas Puzonas ©", self.x+self.border_width+padding, self.y-self.font_size-self.border_width-padding) + self.addLabel("MIT License", self.x+self.border_width+padding, self.y-self.border_width-2*(padding+self.font_size)) + + def toggle(self): + self.visible = not self.visible + + def draw(self): + if not self.visible: return + Graphics.setColor(self.shade_color) + Graphics.rectangle("fill", 0, 0, self.parent_width, self.parent_height) + + Graphics.setColor(self.background_color) + Graphics.rectangle("fill", self.x, self.y, self.width, -self.height) + Graphics.setColor(self.border_color) + Graphics.setLineWidth(self.border_width) + Graphics.rectangle("line", self.x, self.y, self.width, -self.height) + + self.back_button.draw() + + for element in self.elements: + element.draw() + + def onKeyPress(self, symbol, modifiers): + if not self.visible: return + if symbol == key.ESCAPE: + self.visible = False + return True + + def onMouseMotion(self, x, y, dx, dy): + if not self.visible: return + self.back_button.onMouseMotion(x, y, dx, dy) + return True + + def onMousePress(self, x, y, button, modifiers): + if not self.visible: return + self.back_button.onMousePress(x, y, button, modifiers) + return True + + def onMouseRelease(self, x, y, button, modifiers): + if not self.visible: return + self.back_button.onMouseRelease(x, y, button, modifiers) + return True + +class SudokuSolver: + @staticmethod + def findEmpty(board): + for i in range(81): + if board[i] == 0: + return i + return None + + @staticmethod + def solve(board): + if len(board) != 81: return False + empty_pos = SudokuSolver.findEmpty(board) + if empty_pos == None: return True + + for i in range(1, 10): + if SudokuSolver.isValid(board,empty_pos, i): + board[empty_pos] = i + if SudokuSolver.solve(board): return True + board[empty_pos] = 0 + + return False + + @staticmethod + def isValid(board, pos, value=None): + # Get value that is already is board + if value == None: + value = board[pos] + + # Zero is always not valid + if value == 0: return False + + # Check row + row_start = pos//9*9 + for i in range(row_start, row_start+9): + if i != pos and board[i] == value: + return False + + # Check column + for i in range(pos%9, 81, 9): + if i != pos and board[i] == value: + return False + + # Check square + square_start = pos//3%3*3 + pos//27*27 + for x in range(3): + for y in range(square_start, square_start+27, 9): + i = y+x + if i != pos and board[i] == value: + return False + + return True + + @staticmethod + def printBoard(board): + for i in range(0, 81, 9): + if i % 27 == 0 and i != 0: + print("- - - - - - - - - - - -") + print(f" {board[i]} {board[i+1]} {board[i+2]} | {board[i+3]} {board[i+4]} {board[i+5]} | {board[i+6]} {board[i+7]} {board[i+8]}") + +class SudokuBoard: + grid_color = (19, 20, 23) + border_color = (9, 10, 13) + selected_color = (230, 219, 116) + cell_size = 50 + grid_width = 6 + font_name = "Arial" + default_color = (248, 248, 242, 255) + user_color = (117, 113, 94, 255) + mistake_color = (249, 38, 114, 255) + stepped_solve_speed = 60 + + def __init__(self, x=0, y=0, board=None): + self.stepped_cells = None + self.current_cell = None + self.x = x + self.y = y + self.selected_cell = (-1, -1) + self.labels = [] + self.resetBoard(board) + + def resetBoard(self, board=[]): + if len(board) != 81: + board = list(0 for _ in range(81)) + self.start_board = board + self.solution_board = self.start_board.copy() + SudokuSolver.solve(self.solution_board) + self.clear() + + def draw(self): + board_size = self.size() + + Graphics.setLineWidth(self.grid_width) + Graphics.setColor(self.grid_color) + for i in (1, 2, 4, 5, 7, 8): + offset = i*self.cell_size+Graphics.line_width*(i+0.5) + Graphics.line(self.x+offset , self.y+Graphics.line_width, self.x+offset , self.y+board_size) + Graphics.line(self.x+Graphics.line_width, self.y+offset , self.x+board_size, self.y+offset ) + + Graphics.setColor(self.border_color) + for i in (3, 6): + offset = i*self.cell_size+Graphics.line_width*(i+0.5) + Graphics.line(self.x+offset , self.y+Graphics.line_width, self.x+offset , self.y+board_size) + Graphics.line(self.x+Graphics.line_width, self.y+offset , self.x+board_size, self.y+offset ) + + Graphics.setColor(self.border_color) + Graphics.rectangle("line", self.x, self.y, board_size, board_size) + + for label in self.labels: + if label.text != "0": + label.draw() + + if not self.isBusy(): + Graphics.setColor(self.selected_color) + self.highlightCell(self.selected_cell) + + @staticmethod + def size(): + return int(SudokuBoard.cell_size*9 + SudokuBoard.grid_width*10) + + def getCellAt(self, x, y): + board_size = self.size() + left_side = self.x+self.grid_width + bottom_side = self.y+self.grid_width + + if not (left_side <= x < self.x + board_size-self.grid_width and bottom_side <= y < self.y + board_size-self.grid_width): + return (-1, -1) + + cell_span = (self.cell_size+self.grid_width) + return (int((x-left_side+self.grid_width/2)/cell_span), int(8-(y-bottom_side+self.grid_width/2)//cell_span)) + + def highlightCell(self, cell): + if cell[0] < 0 or cell[1] < 0: return + highlight_size = self.cell_size+self.grid_width + x = self.x+self.grid_width/2 + cell[0]*highlight_size + y = self.y+self.grid_width/2 + (8-cell[1])*highlight_size + Graphics.setLineWidth(self.grid_width/2) + Graphics.rectangle("line", x, y, highlight_size, highlight_size) + + def onMousePress(self, x, y, button, modifiers): + if self.isBusy(): return + cell = self.getCellAt(x, y) + if cell == self.selected_cell: + self.selected_cell = (-1, -1) + else: + self.selected_cell = cell + + def onKeyPress(self, symbol, modifiers): + if self.isBusy(): return + if key._0 <= symbol <= key._9: + self.setCell(self.selected_cell[0], self.selected_cell[1], symbol-key._0) + return + elif modifiers & key.MOD_NUMLOCK and (key.NUM_0 <= symbol <= key.NUM_9): + self.setCell(self.selected_cell[0], self.selected_cell[1], symbol-key.NUM_0) + return + elif symbol == key.BACKSPACE: + self.setCell(self.selected_cell[0], self.selected_cell[1], 0) + return + + + for axis in MOVE: + keybinds = MOVE[axis] + for keybind in keybinds: + if symbol == keybind[0] and modifiers & keybind[1] == keybind[1]: + x, y = self.selected_cell + if self.selected_cell == (-1, -1): + x, y = 4, 4 + x, y = x+axis[0], y+axis[1] + if 0 <= x < 9 and 0 <= y < 9: + self.selected_cell = (x, y) + return + + def updateLabels(self): + for i in range(81): + self.updateLabel(i) + + def getLabel(self, index): + if index >= len(self.labels): + step = self.grid_width + self.cell_size + self.labels.append(pyglet.text.Label( + str(self.start_board[index]), + font_name=self.font_name, font_size=self.cell_size*0.6, + x = self.x + (index%9+0.5) * step, + y = self.y + (8-index//9+0.5) * step, + anchor_x='center', anchor_y='center', + color=self.default_color + )) + return self.labels[index] + + def updateLabel(self, index): + label = self.getLabel(index) + + if self.start_board[index] != 0: + label.text = str(self.start_board[index]) + label.color = SudokuBoard.default_color + else: + label.text = str(self.user_board[index]) + label.color = SudokuBoard.user_color + label.italic = False + + def setCell(self, x, y, value): + if self.isBusy(): return + if not (0 <= x < 9 and 0 <= y < 9): return False + index = y*9+x + if self.start_board[index] != 0: return False + self.user_board[index] = value + self.updateLabel(index) + return True + + def checkMistakes(self): + if self.isBusy(): return + for i in range(81): + if self.user_board[i] == 0 or self.user_board[i] == self.solution_board[i]: continue + label = self.getLabel(i) + label.color = SudokuBoard.mistake_color + label.italic = True + + def clear(self): + if self.isBusy(): return + self.user_board = [0 for _ in range(81)] + self.updateLabels() + + def solve(self): + if self.isBusy(): return + self.user_board = self.solution_board.copy() + self.updateLabels() + + def steppedSolve(self): + if self.isBusy(): + pyglet.clock.unschedule(self.step) + self.stepped_cells = None + self.current_cell = None + else: + pyglet.clock.schedule_interval(self.step, 1/SudokuBoard.stepped_solve_speed) + self.stepped_cells = [] + self.user_board = self.start_board.copy() + self.updateLabels() + + def step(self, dt): + if self.current_cell == None: + self.current_cell = SudokuSolver.findEmpty(self.user_board) + + if self.current_cell == None: + self.steppedSolve() + return + + while True: + self.user_board[self.current_cell] = (self.user_board[self.current_cell]+1) % 10 + if SudokuSolver.isValid(self.user_board, self.current_cell): + self.updateLabel(self.current_cell) + self.stepped_cells.append(self.current_cell) + self.current_cell = SudokuSolver.findEmpty(self.user_board) + break + + if self.user_board[self.current_cell] == 0: + self.updateLabel(self.current_cell) + self.current_cell = self.stepped_cells[-1] + self.stepped_cells.pop() + break + + def isBusy(self): + return self.stepped_cells != None + +class SudokuApp(pyglet.window.Window): + background_color = (39, 40, 34) + + def __init__(self, board=[]): + button_height = 16 + padding = 16 + + self.elements = [] + self.board = SudokuBoard(padding, padding, board) + board_size = SudokuBoard.size() + super().__init__(board_size+padding*2, board_size+padding*3+button_height, "Sudoku Solver") + self.about_popup = AboutPopup(self.width, self.height) + buttons = ( + ("Check mistakes", lambda: self.board.checkMistakes() ), + ("Solve" , lambda: self.board.solve() ), + ("Stepped solve" , lambda: self.board.steppedSolve() ), + ("Clear" , lambda: self.board.clear() ), + ("About" , lambda: self.about_popup.toggle() ), + ) + + button_width = (SudokuBoard.size()-padding*(len(buttons)-1))/len(buttons) + button_y = self.height-padding + + for i in range(len(buttons)): + button_x = padding*(i+1)+button_width*i + button = self.addButton(button_x, button_y, button_width, button_height, buttons[i][0]) + button.onClick = buttons[i][1] + + def addButton(self, *args, **kwargs): + button = Button(*args, **kwargs) + self.elements.append(button) + return button + + def on_draw(self): + Graphics.setClearColor(self.background_color) + self.clear() + self.board.draw() + self.emitToElements("draw") + self.about_popup.draw() + + def on_key_press(self, symbol, modifiers): + if self.about_popup.onKeyPress(symbol, modifiers): return + self.board.onKeyPress(symbol, modifiers) + self.emitToElements("onKeyPress", symbol, modifiers) + + def on_mouse_press(self, x, y, button, modifiers): + if self.about_popup.onMousePress(x, y, button, modifiers): return + self.board.onMousePress(x, y, button, modifiers) + self.emitToElements("onMousePress", x, y, button, modifiers) + + def on_mouse_release(self, x, y, button, modifiers): + if self.about_popup.onMouseRelease(x, y, button, modifiers): return + self.emitToElements("onMouseRelease", x, y, button, modifiers) + + def on_mouse_motion(self, x, y, dx, dy): + if self.about_popup.onMouseMotion(x, y, dx, dy): return + self.emitToElements("onMouseMotion", x, y, dx, dy) + + def emitToElements(self, event_name, *args, **kwargs): + for element in self.elements: + event_method = getattr(element, event_name, None) + if callable(event_method): + event_method(*args, **kwargs) + +def boardFromFile(filename): + if not os.path.isfile(filename): return None + board = [] + file = open(filename, "r") + for line in file: + for value in line.split(): + try: + board.append(int(value)) + except ValueError: + pass + file.close() + return board + +if __name__ == "__main__": + SudokuApp(boardFromFile("board.txt")) + pyglet.app.run() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9bd3116 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyglet==1.3.2 \ No newline at end of file