1
0

Add files via upload

This commit is contained in:
RokasPuzonas 2019-12-29 21:29:16 +02:00 committed by GitHub
commit 499517a2bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 625 additions and 0 deletions

21
LICENSE Normal file
View File

@ -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.

48
README.md Normal file
View File

@ -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)

9
board.txt Normal file
View File

@ -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

546
main.py Normal file
View File

@ -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()

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
pyglet==1.3.2