First Commit

master
En Yi 2018-07-06 14:13:33 +08:00
commit 2f64fdd209
12 changed files with 549 additions and 0 deletions

3
.gitignore vendored 100644
View File

@ -0,0 +1,3 @@
venv/
demos/
.idea/

View File

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

View File

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

View File

@ -0,0 +1,2 @@
def bound_value(lower, val, higher):
return min(max(val, lower), higher)

View File

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

View File

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

67
main.py 100644
View File

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

3
requirements.txt 100644
View File

@ -0,0 +1,3 @@
numpy==1.14.5
PyQt5==5.9.2
sip==4.19.6