commit 2f64fdd2098197633ff8b32d0eaf6393424c2ad5 Author: En Yi Date: Fri Jul 6 14:13:33 2018 +0800 First Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acf12e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +venv/ +demos/ +.idea/ diff --git a/gameplay/__pycache__/sudoku_gameplay.cpython-35.pyc b/gameplay/__pycache__/sudoku_gameplay.cpython-35.pyc new file mode 100644 index 0000000..d7997b7 Binary files /dev/null and b/gameplay/__pycache__/sudoku_gameplay.cpython-35.pyc differ diff --git a/gameplay/sudoku_gameplay.py b/gameplay/sudoku_gameplay.py new file mode 100644 index 0000000..33df84f --- /dev/null +++ b/gameplay/sudoku_gameplay.py @@ -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 diff --git a/gameplay/test_board.txt b/gameplay/test_board.txt new file mode 100644 index 0000000..58b36d3 --- /dev/null +++ b/gameplay/test_board.txt @@ -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 \ No newline at end of file diff --git a/general/__pycache__/extras.cpython-35.pyc b/general/__pycache__/extras.cpython-35.pyc new file mode 100644 index 0000000..c2e932d Binary files /dev/null and b/general/__pycache__/extras.cpython-35.pyc differ diff --git a/general/extras.py b/general/extras.py new file mode 100644 index 0000000..c6bddc6 --- /dev/null +++ b/general/extras.py @@ -0,0 +1,2 @@ +def bound_value(lower, val, higher): + return min(max(val, lower), higher) \ No newline at end of file diff --git a/graphic_components/__pycache__/board.cpython-35.pyc b/graphic_components/__pycache__/board.cpython-35.pyc new file mode 100644 index 0000000..530f552 Binary files /dev/null and b/graphic_components/__pycache__/board.cpython-35.pyc differ diff --git a/graphic_components/__pycache__/buttons.cpython-35.pyc b/graphic_components/__pycache__/buttons.cpython-35.pyc new file mode 100644 index 0000000..b0d56e5 Binary files /dev/null and b/graphic_components/__pycache__/buttons.cpython-35.pyc differ diff --git a/graphic_components/board.py b/graphic_components/board.py new file mode 100644 index 0000000..8c20021 --- /dev/null +++ b/graphic_components/board.py @@ -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') diff --git a/graphic_components/buttons.py b/graphic_components/buttons.py new file mode 100644 index 0000000..d37d15e --- /dev/null +++ b/graphic_components/buttons.py @@ -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) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..e165b35 --- /dev/null +++ b/main.py @@ -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_()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..81bbbb5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy==1.14.5 +PyQt5==5.9.2 +sip==4.19.6