SudokuGame/graphic_components/sudoku_graphics.py

367 lines
12 KiB
Python

"""
This module contains the components that make up the Sudoku Board
"""
import numpy as np
from PyQt5.QtCore import (QAbstractAnimation, QPointF, Qt, QRectF, QLineF,
QPropertyAnimation, pyqtProperty, pyqtSignal)
from PyQt5.QtGui import QPen, QFont
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsObject
from gameplay import sudoku_gameplay as sdk
from general.extras import bound_value
from . import buttons
from . import menu_graphics as menu_grap
class BaseSudokuItem(QGraphicsObject):
def __init__(self, parent):
super().__init__(parent=parent)
self.setParent(parent)
self.parent = parent
self.default_pen = QPen()
self.default_pen.setColor(Qt.white)
self.default_pen.setWidth(1)
self.default_font = QFont("Helvetica", pointSize=14)
self.freeze = False
class NumberPainter(BaseSudokuItem):
# TODO: Use different font to differentiate the status of a cell
def __init__(self, parent, grid):
super().__init__(parent=parent)
self.sudoku_grid = grid
self.invalid_pen = QPen()
self.invalid_pen.setColor(Qt.lightGray)
self.invalid_font = QFont("Helvetica", pointSize=12, italic=True)
self.fixed_pen = QPen()
self.fixed_pen.setColor(Qt.white)
self.fixed_font = QFont("Helvetica", pointSize=14, weight=QFont.Bold)
self.scribble_font = QFont("Helvetica", pointSize=5)
def paint(self, painter, style, widget=None):
for i in range(9):
for j in range(9):
self._draw_number_cell(i, j, painter)
def boundingRect(self):
return QRectF(-5, -5, self.parent.width+10, self.parent.height+10)
def _draw_number_cell(self, w, h, painter):
val = self.sudoku_grid.get_cell_number(h, w)
if val == 0:
val = ''
else:
status = self.sudoku_grid.get_cell_status(h, w)
if status == sdk.VALID:
painter.setPen(self.default_pen)
painter.setFont(self.default_font)
elif status == sdk.FIXED:
painter.setPen(self.fixed_pen)
painter.setFont(self.fixed_font)
else:
painter.setPen(self.invalid_pen)
painter.setFont(self.invalid_font)
painter.drawText(QRectF(w*self.parent.cell_width, h*self.parent.cell_height,
self.parent.cell_width, self.parent.cell_height), Qt.AlignCenter, str(val))
painter.setPen(self.default_pen)
painter.setFont(self.scribble_font)
radius = 15
for scrib in self.sudoku_grid.scribbles[h, w]:
num = int(scrib)
num_x = radius * np.sin(np.deg2rad(360/10*num)) + w * self.parent.cell_width
num_y = - radius * np.cos(np.deg2rad(360 / 10 * num)) + h * self.parent.cell_height
painter.drawText(QRectF(num_x, num_y, self.parent.cell_width, self.parent.cell_height),
Qt.AlignCenter, scrib)
class SudokuGrid(BaseSudokuItem):
# TODO: Add functions to animated the grid lines
buttonClicked = pyqtSignal(float, float)
finishDrawing = pyqtSignal()
puzzleFinished = pyqtSignal()
def __init__(self, width, height, parent=None):
super().__init__(parent)
self.width = width
self.height = height
self.thick_pen = QPen()
self.thick_pen.setColor(Qt.white)
self.thick_unit = 5
self.thick_pen.setWidth(self.thick_unit)
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.grid_painter = NumberPainter(self, self.sudoku_grid)
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.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.set_disabled(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(500) # Animation speed
self.anim.setStartValue(0)
for t in range(1, 10):
self.anim.setKeyValueAt(t / 10, self.width * t/10)
self.anim.setEndValue(self.width)
self.drawn = False
self.anim.finished.connect(self.finish_drawing)
def set_disabled(self, state):
if state:
self.setAcceptedMouseButtons(Qt.NoButton)
else:
self.setAcceptedMouseButtons(Qt.LeftButton)
self.setAcceptHoverEvents(not state)
def finish_drawing(self):
if self.length == self.width:
self.drawn = True
self.finishDrawing.emit()
# 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()
def generate_new_grid(self, difficulty):
self.sudoku_grid.generate_random_board(difficulty)
#self.sudoku_grid.generate_test_board(difficulty) # Uncomment for testing
self.update()
def replace_cell_number(self, val):
self.sudoku_grid.replace_cell_number(self.mouse_h, self.mouse_w, val)
self.grid_painter.update()
if self.sudoku_grid.completion_check():
self.puzzleFinished.emit()
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)
painter.setPen(self.thick_pen)
for line in self.thicklines:
painter.drawLine(line)
if self.drawn:
painter.setPen(self.selection_pen)
painter.drawRect(self.selection_box)
def hoverMoveEvent(self, event):
if self.drawn:
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 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):
if self.drawn:
w = (self.mouse_w + 0.5) * self.cell_width
h = (self.mouse_h + 0.5) * self.cell_height
if not self.sudoku_grid.get_cell_status(self.mouse_h, self.mouse_w) == sdk.FIXED:
self.buttonClicked.emit(w, h)
else:
self.buttonClicked.emit(0, 0)
def focusInEvent(self, event):
self.set_disabled(False)
def focusOutEvent(self, event):
self.set_disabled(True)
# 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
for lines in self.thinlines:
if lines.x1() == 0:
lines.setP2(QPointF(value, lines.y2()))
else:
lines.setP2(QPointF(lines.x2(), value))
for lines in self.thicklines:
if lines.x1() == 0:
lines.setP2(QPointF(value, lines.y2()))
else:
lines.setP2(QPointF(lines.x2(), value))
self.update()
class NumberRing(BaseSudokuItem):
# TODO: Add functions to animated the ring appearing
# TODO: Adjust the positioning of each element
# TODO: Make it transparent when mouse is out of range
loseFocus = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setVisible(False)
self.cell_width = 24
self.cell_height = 24
self.cell_buttons = []
for i in range(10):
if i == 0:
cell_string = 'X'
else:
cell_string = str(i)
btn = buttons.AnimBox(0, 0, self.cell_width,
self.cell_height, cell_string, parent=self)
self.cell_buttons.append(btn)
self.radius = 54
# Set up the radius to be animated
self.anim = QPropertyAnimation(self, b'radius')
self.anim.setDuration(100) # Animation speed
self.anim.setStartValue(0)
for t in range(1, 10):
self.anim.setKeyValueAt(t / 10, self.radius * t / 10)
self.anim.setEndValue(self.radius)
self.anim.finished.connect(self.finish_animation)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.freeze_buttons(True)
def finish_animation(self):
if self.radius == 0:
self.setVisible(False)
self.freeze_buttons(True)
self.loseFocus.emit()
else:
self.freeze_buttons(False)
# Toggle the animation to be play forward or backward
def toggle_anim(self, toggling):
self.freeze_buttons(True)
if toggling:
self.anim.setDirection(QAbstractAnimation.Forward)
else:
self.anim.setDirection(QAbstractAnimation.Backward)
self.anim.start()
def boundingRect(self):
return QRectF(-5-self.radius-self.cell_width/2, -5-self.radius-self.cell_height/2,
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)
btn.buttonClicked.connect(self.close_menu)
print('Buttons Connected')
def freeze_buttons(self, freeze):
for btn in self.cell_buttons:
btn.set_freeze(freeze)
def focusOutEvent(self, event):
if not any(btn.isUnderMouse() for btn in self.cell_buttons):
self.close_menu()
else:
self.setFocus()
def close_menu(self):
self.toggle_anim(False)
# Defining the length to be drawn as a pyqtProperty
@pyqtProperty(float)
def radius(self):
return self._radius
# Determine the length of the four lines to be drawn
@radius.setter
def radius(self, value):
self._radius = value
for i, btn in enumerate(self.cell_buttons):
cell_x = value * np.sin(np.deg2rad(360/10*i)) - self.cell_width/2
cell_y = - value * np.cos(np.deg2rad(360 / 10 * i)) - self.cell_height/2
btn.setX(cell_x)
btn.setY(cell_y)
self.update()
class PlayMenu(BaseSudokuItem):
buttonClicked = pyqtSignal(str)
def __init__(self, parent):
super().__init__(parent=parent)
self.rect = self.parent.boundingRect()
self.diff_select = menu_grap.DifficultyMenu(self.rect.width()/2, self.rect.height()/8, self)
self.diff_select.setX(self.rect.width()/4)
self.diff_select.setY((self.rect.height() - self.diff_select.height)/2)
self.diff_select.menuClicked.connect(self.difficulty_selected)
def paint(self, painter, style, widget=None):
pass
def boundingRect(self):
return self.diff_select.boundingRect()
def difficulty_selected(self, string):
self.setVisible(False)
self.buttonClicked.emit(string)