546 lines
16 KiB
Python
546 lines
16 KiB
Python
# 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() |