First Commit
commit
2f64fdd209
|
@ -0,0 +1,3 @@
|
|||
venv/
|
||||
demos/
|
||||
.idea/
|
Binary file not shown.
|
@ -0,0 +1,120 @@
|
|||
import numpy as np
|
||||
|
||||
EMPTY = 0
|
||||
VALID = 1
|
||||
INVALID = 2
|
||||
FIXED = 3
|
||||
|
||||
TESTING = True
|
||||
if __name__ == "__main__":
|
||||
test_dir = './test_board.txt'
|
||||
else:
|
||||
|
||||
test_dir = './gameplay/test_board.txt'
|
||||
|
||||
class SudokuSystem:
|
||||
|
||||
def __init__(self):
|
||||
self.number_grid = np.zeros((9, 9), dtype=np.uint8)
|
||||
self.cell_status = np.zeros((9, 9), dtype=np.uint8)
|
||||
self.offending_cells = []
|
||||
for i in range(9):
|
||||
row = []
|
||||
for j in range(9):
|
||||
row.append([])
|
||||
self.offending_cells.append(row)
|
||||
|
||||
if TESTING:
|
||||
self.generate_test_board()
|
||||
|
||||
def clear_grid(self):
|
||||
self.number_grid = 0
|
||||
self.cell_status = EMPTY
|
||||
for i in range(9):
|
||||
for j in range(9):
|
||||
while self.offending_cells[i][j]:
|
||||
self.offending_cells[i][j].pop()
|
||||
|
||||
def replace_cell_number(self, row, col, val):
|
||||
self.number_grid[row, col] = int(val)
|
||||
if not val == 0:
|
||||
self.invalid_cell_check(row, col)
|
||||
else:
|
||||
self.change_cell_status(row, col, EMPTY)
|
||||
|
||||
def get_cell_number(self, row, col):
|
||||
return self.number_grid[row, col]
|
||||
|
||||
def change_cell_status(self, row, col, new_status):
|
||||
if not self.cell_status[row, col] == FIXED:
|
||||
self.cell_status[row, col] = new_status
|
||||
|
||||
def get_cell_status(self, row, col):
|
||||
return self.cell_status[row, col]
|
||||
|
||||
def completion_check(self):
|
||||
if np.all(np.logical_or(self.cell_status == VALID, self.cell_status == FIXED)):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def invalid_cell_check(self, row, col):
|
||||
val_check = self.number_grid[row, col]
|
||||
|
||||
row_check = np.where(self.number_grid[row, :] == val_check)[0]
|
||||
col_check = np.where(self.number_grid[:, col] == val_check)[0]
|
||||
local_grid_row = int(row / 3) * 3
|
||||
local_grid_col = int(col / 3) * 3
|
||||
local_grid_check_row, local_grid_check_col = np.where(
|
||||
self.number_grid[local_grid_row:local_grid_row + 3, local_grid_col:local_grid_col + 3] == val_check)
|
||||
|
||||
if len(row_check) == 1 and len(col_check) == 1 and len(local_grid_check_row) == 1:
|
||||
self.cell_status[row, col] = VALID
|
||||
while self.offending_cells[row][col]:
|
||||
r, c = self.offending_cells[row][col].pop()
|
||||
try:
|
||||
self.offending_cells[r][c].remove((row, col))
|
||||
except ValueError:
|
||||
print('No such cell found')
|
||||
if not self.offending_cells[r][c]:
|
||||
self.change_cell_status(r, c, VALID)
|
||||
print('Completion?', self.completion_check())
|
||||
|
||||
else:
|
||||
self.cell_status[row, col] = INVALID
|
||||
bad_cells = []
|
||||
if not len(row_check) == 1:
|
||||
for c in row_check:
|
||||
if not c == col:
|
||||
bad_cells.append((row, c))
|
||||
self.offending_cells[row][c].append((row, col))
|
||||
self.change_cell_status(row, c, INVALID)
|
||||
if not len(col_check) == 1:
|
||||
for r in col_check:
|
||||
if not r == row:
|
||||
bad_cells.append((r, col))
|
||||
self.offending_cells[r][col].append((row, col))
|
||||
self.change_cell_status(r, col, INVALID)
|
||||
if not len(local_grid_check_row) == 1:
|
||||
for r, c in zip(local_grid_check_row + local_grid_row, local_grid_check_col + local_grid_col):
|
||||
if not (c == col or r == row):
|
||||
bad_cells.append((r, c))
|
||||
self.offending_cells[r][c].append((row, col))
|
||||
self.change_cell_status(r, c, INVALID)
|
||||
|
||||
self.offending_cells[row][col] = bad_cells
|
||||
|
||||
def generate_test_board(self):
|
||||
with open(test_dir, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
values = []
|
||||
for line in lines:
|
||||
values.append([int(val) for val in line.strip('\n').split(',')])
|
||||
|
||||
self.number_grid[:] = values
|
||||
self.cell_status[:] = FIXED
|
||||
row, col = np.where(self.number_grid == 0)
|
||||
|
||||
for r, c in zip(row, col):
|
||||
self.cell_status[r, c] = EMPTY
|
|
@ -0,0 +1,9 @@
|
|||
1,2,3,4,5,6,7,8,9
|
||||
4,5,6,7,8,9,1,2,3
|
||||
7,8,9,1,2,3,4,5,6
|
||||
2,3,4,5,6,7,8,9,1
|
||||
5,6,7,8,9,1,2,3,4
|
||||
8,9,1,2,3,4,5,6,7
|
||||
3,4,5,6,7,8,9,1,2
|
||||
6,7,8,9,1,2,3,4,5
|
||||
9,1,2,3,4,5,6,7,0
|
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
def bound_value(lower, val, higher):
|
||||
return min(max(val, lower), higher)
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,203 @@
|
|||
from PyQt5.QtGui import QPainter, QBrush, QPen, QColor, QFont
|
||||
from PyQt5.Qt import QApplication, QTimer
|
||||
from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView, QGraphicsItem,
|
||||
QGraphicsLineItem, QGraphicsRectItem, QGraphicsObject,
|
||||
QGraphicsItemGroup, QGraphicsPathItem)
|
||||
from PyQt5.QtCore import (QAbstractAnimation, QObject, QPointF, Qt, QRectF, QLineF,
|
||||
QPropertyAnimation, pyqtProperty, pyqtSignal)
|
||||
from graphic_components import buttons
|
||||
from general.extras import bound_value
|
||||
from gameplay import sudoku_gameplay as sdk
|
||||
import numpy as np
|
||||
import sys, math
|
||||
|
||||
|
||||
class BoxBoard(QGraphicsObject):
|
||||
|
||||
# Initialisation
|
||||
def __init__(self, width, height, parent = None):
|
||||
super().__init__(parent)
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.circumference = 2*(width+height)
|
||||
|
||||
# Set up pens for drawing
|
||||
self.default_pen = QPen()
|
||||
self.default_pen.setColor(Qt.white)
|
||||
self.default_pen.setWidth(5)
|
||||
|
||||
# The 4 lines to construct the box
|
||||
self.left = QLineF(0, 0, 0, self.height)
|
||||
self.down = QLineF(0, self.height, self.width, self.height)
|
||||
self.right = QLineF(self.width, 0, self.width, self.height)
|
||||
self.up = QLineF(0, 0, self.width, 0)
|
||||
|
||||
self.line_order = [self.up, self.right, self.down, self.left]
|
||||
|
||||
# Reimplemented boundingRect
|
||||
def boundingRect(self):
|
||||
return QRectF(-5, -5, self.width+10, self.height+10)
|
||||
|
||||
# Reimplemented paint
|
||||
def paint(self, painter, style, widget=None):
|
||||
painter.setPen(self.default_pen)
|
||||
for line in self.line_order:
|
||||
if line.length() > 1:
|
||||
painter.drawLine(line)
|
||||
|
||||
class SudokuGrid(QGraphicsObject):
|
||||
# Prepare the signal
|
||||
buttonClicked = pyqtSignal(float, float)
|
||||
|
||||
def __init__(self, width, height, parent=None):
|
||||
super().__init__(parent)
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
self.default_pen = QPen()
|
||||
self.default_pen.setColor(Qt.white)
|
||||
self.default_pen.setWidth(1)
|
||||
self.thick_pen = QPen()
|
||||
self.thick_pen.setColor(Qt.white)
|
||||
self.thick_unit = 5
|
||||
self.thick_pen.setWidth(self.thick_unit)
|
||||
|
||||
self.horiz_gridlines = []
|
||||
self.vert_gridlines = []
|
||||
|
||||
self.thinlines = []
|
||||
self.thicklines = []
|
||||
|
||||
self.cell_width = self.width / 9
|
||||
self.cell_height = self.height /9
|
||||
|
||||
for i in range(1, 9):
|
||||
delta_h = self.cell_height * i
|
||||
delta_w = self.cell_width * i
|
||||
if i%3 == 0:
|
||||
self.thicklines.append(QLineF(0, delta_h, self.width, delta_h))
|
||||
self.thicklines.append(QLineF(delta_w, 0, delta_w, self.height))
|
||||
else:
|
||||
self.thinlines.append(QLineF(0, delta_h, self.width, delta_h))
|
||||
|
||||
self.thinlines.append(QLineF(delta_w, 0, delta_w, self.height))
|
||||
|
||||
self.sudoku_grid = sdk.SudokuSystem()
|
||||
|
||||
self.mouse_w = 0
|
||||
self.mouse_h = 0
|
||||
self.selection_unit = 8
|
||||
self.selection_pen = QPen()
|
||||
self.selection_pen.setColor(Qt.white)
|
||||
self.selection_pen.setWidth(self.selection_unit)
|
||||
self.selection_box = QRectF(0, 0, self.cell_width, self.cell_height)
|
||||
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setAcceptedMouseButtons(Qt.LeftButton)
|
||||
|
||||
self.selected = False
|
||||
|
||||
self.invalid_pen = QPen()
|
||||
self.invalid_pen.setColor(Qt.lightGray)
|
||||
self.invalid_unit = 8
|
||||
self.invalid_pen.setWidth(self.thick_unit)
|
||||
|
||||
def replace_cell_number(self, val):
|
||||
self.sudoku_grid.replace_cell_number(self.mouse_h, self.mouse_w, val)
|
||||
self.update()
|
||||
|
||||
def _draw_number_cell(self, w, h, painter):
|
||||
val = self.sudoku_grid.get_cell_number(h, w)
|
||||
if val == 0:
|
||||
val = ''
|
||||
else:
|
||||
if self.sudoku_grid.get_cell_status(h, w) == sdk.VALID:
|
||||
painter.setPen(self.default_pen)
|
||||
else:
|
||||
painter.setPen(self.invalid_pen)
|
||||
|
||||
painter.drawText((w+0.5)*self.cell_width-5,
|
||||
(h+0.5)*self.cell_height+5,
|
||||
str(val))
|
||||
|
||||
def boundingRect(self):
|
||||
return QRectF(-5, -5, self.width+10, self.height+10)
|
||||
|
||||
# Reimplemented paint
|
||||
def paint(self, painter, style, widget=None):
|
||||
painter.setPen(self.default_pen)
|
||||
for line in self.thinlines:
|
||||
painter.drawLine(line)
|
||||
|
||||
for i in range(9):
|
||||
for j in range(9):
|
||||
self._draw_number_cell(i, j, painter)
|
||||
|
||||
painter.setPen(self.thick_pen)
|
||||
for line in self.thicklines:
|
||||
painter.drawLine(line)
|
||||
|
||||
painter.setPen(self.selection_pen)
|
||||
painter.drawRect(self.selection_box)
|
||||
|
||||
def hoverMoveEvent(self, event):
|
||||
box_w = bound_value(0, int(event.pos().x()/self.cell_width), 8)
|
||||
box_h = bound_value(0, int(event.pos().y() / self.cell_height), 8)
|
||||
if not self.selected:
|
||||
if box_w != self.mouse_w or box_h != self.mouse_h:
|
||||
self.mouse_w = box_w
|
||||
self.mouse_h = box_h
|
||||
self.selection_box.moveTopLeft(QPointF(box_w*self.cell_width, box_h*self.cell_height))
|
||||
self.update()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
w = (self.mouse_w + 0.5) * self.cell_width - 5
|
||||
h = (self.mouse_h + 0.5) * self.cell_height + 5
|
||||
|
||||
if not self.sudoku_grid.get_cell_status(self.mouse_h, self.mouse_w) == sdk.FIXED:
|
||||
self.buttonClicked.emit(w, h)
|
||||
|
||||
class NumberGrid(QGraphicsItem):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
def paint(self, painter, style, widget=None):
|
||||
pass
|
||||
|
||||
class NumberRing(QGraphicsItem):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.setVisible(False)
|
||||
self.radius = 48
|
||||
self.cell_width = 24
|
||||
self.cell_height = 24
|
||||
|
||||
self.cell_buttons = []
|
||||
for i in range(10):
|
||||
cell_x = self.radius * np.sin(np.deg2rad(360/10*i)) - self.cell_width/2
|
||||
cell_y = - self.radius * np.cos(np.deg2rad(360 / 10 * i)) - self.cell_height/2
|
||||
if i == 0:
|
||||
cell_string = 'X'
|
||||
else:
|
||||
cell_string = str(i)
|
||||
btn = buttons.animBox(cell_x, cell_y, self.cell_width,
|
||||
self.cell_height, cell_string, self)
|
||||
|
||||
self.cell_buttons.append(btn)
|
||||
|
||||
def boundingRect(self):
|
||||
return QRectF(-5, -5, self.cell_width+self.radius*2+10,
|
||||
self.cell_height + self.radius * 2 + 10)
|
||||
|
||||
# Reimplemented paint
|
||||
def paint(self, painter, style, widget=None):
|
||||
pass
|
||||
|
||||
def connect_button_signals(self, func):
|
||||
for btn in self.cell_buttons:
|
||||
btn.buttonClicked.connect(func)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
print('Yes')
|
|
@ -0,0 +1,142 @@
|
|||
from PyQt5.QtGui import QPainter, QBrush, QPen, QColor, QFont
|
||||
from PyQt5.Qt import QApplication, QTimer
|
||||
from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView, QGraphicsItem,
|
||||
QGraphicsLineItem, QGraphicsRectItem, QGraphicsObject,
|
||||
QGraphicsItemGroup, QGraphicsPathItem)
|
||||
from PyQt5.QtCore import (QAbstractAnimation, QObject, QPointF, Qt, QRectF,QLineF,
|
||||
QPropertyAnimation, pyqtProperty, pyqtSignal)
|
||||
import sys, math
|
||||
|
||||
|
||||
class animBox(QGraphicsObject):
|
||||
# Prepare the signal
|
||||
hoverEnter = pyqtSignal()
|
||||
hoverExit = pyqtSignal()
|
||||
buttonClicked = pyqtSignal(str)
|
||||
|
||||
# Initialisation
|
||||
def __init__(self, x, y, width, height, text, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.text = text
|
||||
self.circumference = 2*(width+height)
|
||||
|
||||
# Set up pens for drawing
|
||||
self.default_pen = QPen()
|
||||
self.default_pen.setColor(Qt.white)
|
||||
self.outline_pen = QPen()
|
||||
self.outline_pen.setColor(Qt.white)
|
||||
self.outline_pen.setWidth(5)
|
||||
|
||||
# Whether the mouse hover over the box
|
||||
self.detected = False
|
||||
self.btn_rect = self.boundingRect()
|
||||
# The 4 lines to construct the box
|
||||
self.left = QLineF()
|
||||
self.down = QLineF()
|
||||
self.right = QLineF()
|
||||
self.up = QLineF()
|
||||
|
||||
self.line_order = [self.up, self.right, self.down, self.left]
|
||||
|
||||
self.setAcceptHoverEvents(True)
|
||||
#self.hoverEnter.connect(lambda: self.toggle_anim(True))
|
||||
#self.hoverExit.connect(lambda: self.toggle_anim(False))
|
||||
|
||||
# Length of the box to be drawn
|
||||
self.length = 0
|
||||
# Set up the length to be animated
|
||||
self.anim = QPropertyAnimation(self, b'length')
|
||||
self.anim.setDuration(400) # Animation speed
|
||||
self.anim.setStartValue(0)
|
||||
for t in range(1, 10):
|
||||
self.anim.setKeyValueAt(t / 10, self.logistic_func(t / 10))
|
||||
self.anim.setEndValue(self.circumference)
|
||||
|
||||
# Toggle the animation to be play forward or backward
|
||||
def toggle_anim(self, toggling):
|
||||
if toggling:
|
||||
self.anim.setDirection(QAbstractAnimation.Forward)
|
||||
else:
|
||||
self.anim.setDirection(QAbstractAnimation.Backward)
|
||||
|
||||
self.anim.start()
|
||||
|
||||
# The logistic function that determines the animation motion
|
||||
def logistic_func(self, x):
|
||||
return self.circumference / (1+math.exp(-(x-0.5)*18))
|
||||
|
||||
# Reimplemented boundingRect
|
||||
def boundingRect(self):
|
||||
return QRectF(self.x, self.y, self.width, self.height)
|
||||
|
||||
# Reimplemented paint
|
||||
def paint(self, painter, style, widget=None):
|
||||
painter.setPen(self.outline_pen)
|
||||
for line in self.line_order:
|
||||
if line.length() > 1:
|
||||
painter.drawLine(line)
|
||||
painter.setPen(self.default_pen)
|
||||
painter.fillRect(self.btn_rect, Qt.black)
|
||||
painter.drawRect(self.btn_rect)
|
||||
painter.drawText(self.boundingRect(),self.text)
|
||||
|
||||
# Defining the length to be drawn as a pyqtProperty
|
||||
@pyqtProperty(float)
|
||||
def length(self):
|
||||
return self._length
|
||||
|
||||
# Determine the length of the four lines to be drawn
|
||||
@length.setter
|
||||
def length(self, value):
|
||||
self._length = value
|
||||
remaining_length = value
|
||||
if remaining_length >= 2 * self.width + self.height:
|
||||
length_to_draw = remaining_length - (2 * self.width + self.height)
|
||||
remaining_length -= length_to_draw
|
||||
else:
|
||||
length_to_draw = 0
|
||||
|
||||
self.line_order[3].setLine(self.x, self.y + self.height,
|
||||
self.x, self.y + self.height - length_to_draw)
|
||||
if remaining_length >= self.width + self.height:
|
||||
length_to_draw = remaining_length - (self.width + self.height)
|
||||
remaining_length -= length_to_draw
|
||||
else:
|
||||
length_to_draw = 0
|
||||
self.line_order[2].setLine(self.x + self.width, self.y + self.height,
|
||||
self.x + self.width - length_to_draw,
|
||||
self.y + self.height)
|
||||
|
||||
if remaining_length >= self.width:
|
||||
length_to_draw = remaining_length - self.width
|
||||
remaining_length -= length_to_draw
|
||||
else:
|
||||
length_to_draw = 0
|
||||
|
||||
self.line_order[1].setLine(self.x + self.width, self.y,
|
||||
self.x + self.width, self.y + length_to_draw)
|
||||
self.line_order[0].setLine(self.x, self.y,
|
||||
self.x + remaining_length, self.y)
|
||||
self.update()
|
||||
|
||||
# Reimplemented hoverEvents to detect the mouse and toggle the animation
|
||||
def hoverEnterEvent(self, event):
|
||||
if ~self.detected:
|
||||
self.hoverEnter.emit()
|
||||
self.detected = True
|
||||
self.toggle_anim(True)
|
||||
super().hoverEnterEvent(event)
|
||||
|
||||
def hoverLeaveEvent(self, event):
|
||||
if self.detected:
|
||||
self.hoverExit.emit()
|
||||
self.detected = False
|
||||
self.toggle_anim(False)
|
||||
super().hoverLeaveEvent(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.buttonClicked.emit(self.text)
|
|
@ -0,0 +1,67 @@
|
|||
from PyQt5.QtGui import QPainter, QBrush, QPen, QColor, QFont
|
||||
from PyQt5.Qt import QApplication, QTimer
|
||||
from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView, QGraphicsItem,
|
||||
QGraphicsLineItem, QGraphicsRectItem, QGraphicsObject,
|
||||
QGraphicsItemGroup, QGraphicsPathItem)
|
||||
from PyQt5.QtCore import (QAbstractAnimation, QObject, QPoint, QPointF, Qt, QRectF,QLineF,
|
||||
QPropertyAnimation, pyqtProperty, pyqtSignal)
|
||||
import sys, math
|
||||
|
||||
from graphic_components import buttons, board
|
||||
|
||||
|
||||
class SudokuWindow(QGraphicsView):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Set up the Scene to manage the GraphicItems
|
||||
self.scene = QGraphicsScene(0, 0, 500, 500, self)
|
||||
self.setScene(self.scene)
|
||||
self.setSceneRect(self.scene.sceneRect())
|
||||
|
||||
#self.button1 = buttons.animBox(0, 0, 20, 20, 'a')
|
||||
#self.scene.addItem(self.button1)
|
||||
|
||||
self.gameboard = board.BoxBoard(450, 450)
|
||||
self.menuboard = board.BoxBoard(400, 100)
|
||||
self.gamegrid = board.SudokuGrid(450, 450)
|
||||
self.numring = board.NumberRing()
|
||||
self.scene.addItem(self.gameboard)
|
||||
self.scene.addItem(self.gamegrid)
|
||||
self.scene.addItem(self.numring)
|
||||
self.setBackgroundBrush(QBrush(Qt.black))
|
||||
self.setRenderHint(QPainter.Antialiasing)
|
||||
self.setGeometry(0, 0, 600, 600)
|
||||
|
||||
self.gamegrid.buttonClicked.connect(self.show_number_ring)
|
||||
self.numring.connect_button_signals(self.select_ring_number)
|
||||
self.gameboard
|
||||
|
||||
self.ensureVisible(self.scene.sceneRect(), 50, 50)
|
||||
self.fitInView(self.gameboard.boundingRect(), Qt.KeepAspectRatio)
|
||||
self.show()
|
||||
|
||||
def show_number_ring(self, x=0, y=0):
|
||||
if not self.gamegrid.selected:
|
||||
self.numring.setPos(x, y)
|
||||
self.numring.setVisible(True)
|
||||
self.gamegrid.selected = True
|
||||
else:
|
||||
self.numring.setVisible(False)
|
||||
self.gamegrid.selected = False
|
||||
|
||||
def select_ring_number(self, val):
|
||||
if val == 'X':
|
||||
val = 0
|
||||
self.gamegrid.replace_cell_number(int(val))
|
||||
self.show_number_ring()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = 0
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
ex = SudokuWindow()
|
||||
|
||||
sys.exit(app.exec_())
|
|
@ -0,0 +1,3 @@
|
|||
numpy==1.14.5
|
||||
PyQt5==5.9.2
|
||||
sip==4.19.6
|
Loading…
Reference in New Issue