320 lines
11 KiB
Python
320 lines
11 KiB
Python
from PyQt5.QtGui import QPainter, QBrush, QPen, QColor, QFont
|
|
from PyQt5.QtWidgets import (QWidget, QLineEdit, QHBoxLayout, QGridLayout, QVBoxLayout,
|
|
QPushButton, QLabel)
|
|
from PyQt5.QtCore import (QAbstractAnimation, QObject, QPointF, Qt, QRectF, QLineF,
|
|
QPropertyAnimation, pyqtProperty, pyqtSignal, QSizeF, QTimer)
|
|
from PyQt5.Qt import QApplication
|
|
import sys
|
|
import random
|
|
|
|
if not __name__ == "__main__":
|
|
sys.path.append("~/PycharmProjects/sudoku")
|
|
|
|
from general import highscore as hs
|
|
|
|
BACKWARD = -1
|
|
FORWARD = 1
|
|
|
|
class HighScoreBoard(QWidget):
|
|
|
|
def __init__(self, width, height):
|
|
super().__init__()
|
|
|
|
self.final_time = "00:10:00"
|
|
self.current_difficulty = hs.DIFFICULTIES[1]
|
|
self.layout = QVBoxLayout(self)
|
|
self.layout.setAlignment(Qt.AlignCenter)
|
|
self.diff_switch = DifficultySwitch()
|
|
self.layout.addLayout(self.diff_switch)
|
|
self.score_grid = ScoreGrid()
|
|
self.layout.addLayout(self.score_grid)
|
|
self.name_input = NameInput()
|
|
self.layout.addWidget(self.name_input)
|
|
|
|
self.setFixedSize(width, height)
|
|
|
|
self.setStyleSheet("""
|
|
background-color: rgb(0, 0, 0);
|
|
color: rgb(255, 255, 255);
|
|
""")
|
|
|
|
self.name_input.setVisible(False)
|
|
|
|
self.diff_switch.difficultySelected.connect(self.change_score_board)
|
|
self.name_input.nameReceived.connect(self.set_score)
|
|
self.score_grid.scoreUpdate.connect(self.diff_switch.go_to_difficulty)
|
|
|
|
def change_score_board(self, difficulty):
|
|
self.score_grid.replace_scores(difficulty)
|
|
|
|
def show_scores(self, toggle):
|
|
if self.isVisible():
|
|
self.score_grid.show_score_info(toggle)
|
|
|
|
def set_score(self, name):
|
|
self.score_grid.set_highscore(self.current_difficulty, name, self.final_time)
|
|
|
|
class DifficultySwitch(QHBoxLayout):
|
|
difficultySelected = pyqtSignal(str)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
circular_text = hs.DIFFICULTIES.copy()
|
|
circular_text.insert(0, hs.DIFFICULTIES[-1])
|
|
circular_text.append(hs.DIFFICULTIES[0])
|
|
self.max_length = max(len(diff) for diff in hs.DIFFICULTIES)
|
|
self.full_text = ''.join(d.center(self.max_length) for d in circular_text)
|
|
left_btn = QPushButton('<')
|
|
self.difficulty_display = QLabel('Normal')
|
|
self.difficulty_display.setAlignment(Qt.AlignCenter)
|
|
right_btn = QPushButton('>')
|
|
|
|
self.addWidget(left_btn)
|
|
self.addWidget(self.difficulty_display)
|
|
self.addWidget(right_btn)
|
|
|
|
self.shift_direction = FORWARD
|
|
self.show_pos = self.max_length
|
|
self.next_pos = self.max_length
|
|
self.timer = QTimer(self)
|
|
self.timer.setInterval(20)
|
|
self.timer.timeout.connect(self.shift_pos)
|
|
left_btn.clicked.connect(lambda: self.shift_difficulty(BACKWARD))
|
|
right_btn.clicked.connect(lambda: self.shift_difficulty(FORWARD))
|
|
|
|
@pyqtProperty(int)
|
|
def show_pos(self):
|
|
"""
|
|
int : The value for the animation
|
|
|
|
When the value is set, the text to be shown is selected from the full text.
|
|
"""
|
|
return self._shown_length
|
|
|
|
@show_pos.setter
|
|
def show_pos(self, value):
|
|
self._shown_length = value
|
|
self.difficulty_display.setText(self.full_text[value:value+self.max_length])
|
|
|
|
def shift_difficulty(self, direction):
|
|
if not self.timer.isActive():
|
|
self.shift_direction = direction
|
|
self.next_pos = self.circular_value(self.next_pos + direction * self.max_length)
|
|
self.timer.start()
|
|
|
|
def go_to_difficulty(self, difficulty):
|
|
pos = (hs.DIFFICULTIES.index(difficulty) + 1) * self.max_length
|
|
self.show_pos = pos
|
|
self.next_pos = pos
|
|
|
|
def shift_pos(self):
|
|
self.show_pos = self.circular_value(self.show_pos + self.shift_direction)
|
|
if self.show_pos == self.next_pos:
|
|
self.timer.stop()
|
|
self.difficultySelected.emit(self.difficulty_display.text().strip(' '))
|
|
|
|
def circular_value(self, value):
|
|
if value == (len(hs.DIFFICULTIES)+1) * self.max_length:
|
|
value = self.max_length
|
|
elif value == 0:
|
|
value = len(hs.DIFFICULTIES) * self.max_length
|
|
return value
|
|
|
|
|
|
class ScoreGrid(QGridLayout):
|
|
scoreUpdate = pyqtSignal(str)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
try:
|
|
self.highscore_list = hs.read_highscore_file("/home/eyt21/PycharmProjects/sudoku/general/highscore.txt")
|
|
except Exception as e:
|
|
print('Cannot open file', e)
|
|
|
|
for i in range(5):
|
|
label = QLabel(str(i+1)+'.')
|
|
self.addWidget(label, i, 0)
|
|
|
|
self.animated_labels = []
|
|
for i, name in enumerate('ABCDE'):
|
|
label1 = AnimatedLabel(name * 7)
|
|
label1.setAlignment(Qt.AlignCenter)
|
|
label2 = AnimatedLabel('0'*5)
|
|
label2.setAlignment(Qt.AlignRight)
|
|
self.addWidget(label1, i, 1)
|
|
self.addWidget(label2, i, 2)
|
|
self.animated_labels.append(label1)
|
|
self.animated_labels.append(label2)
|
|
|
|
self.replace_scores(hs.DIFFICULTIES[0])
|
|
|
|
def show_score_info(self, toggle):
|
|
for label in self.animated_labels:
|
|
label.toggle_anim(toggle)
|
|
|
|
def replace_scores(self, difficulty):
|
|
scores = self.highscore_list[difficulty]
|
|
for i in range(len(scores)):
|
|
self.animated_labels[2*i].replace_text(scores[i]['name'])
|
|
self.animated_labels[2*i+1].replace_text(scores[i]['time'])
|
|
|
|
def set_highscore(self, difficulty, name, time):
|
|
hs.replace_placing(self.highscore_list, difficulty, name, time)
|
|
self.replace_scores(difficulty)
|
|
self.scoreUpdate.emit(difficulty)
|
|
|
|
class NameInput(QWidget):
|
|
nameReceived = pyqtSignal(str)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.layout = QHBoxLayout(self)
|
|
|
|
self.layout.addWidget(QLabel('Name'))
|
|
|
|
self.name_input = QLineEdit(self)
|
|
self.layout.addWidget(self.name_input)
|
|
self.name_input.returnPressed.connect(self.receive_name_input)
|
|
|
|
self.name_input.setStyleSheet("""
|
|
border: 2px solid gray;
|
|
""")
|
|
|
|
def receive_name_input(self):
|
|
print(self.name_input.text().strip(' '))
|
|
name = self.name_input.text().strip(' ')
|
|
if name:
|
|
self.nameReceived.emit(name)
|
|
|
|
|
|
class AnimatedLabel(QLabel):
|
|
"""
|
|
QLabel that a message, which is displayed through animation.
|
|
"""
|
|
|
|
def __init__(self, text, speed=75, delay=20, parent=None):
|
|
"""
|
|
Does some text processing, and set up the animation to display the text
|
|
|
|
Parameters
|
|
----------
|
|
text: str
|
|
Text to be displayed
|
|
speed : int
|
|
The period at which a new character is printed
|
|
The total time is calculated as length of text * speed
|
|
0 means instant display, like a regular QLabel.
|
|
delay : int
|
|
The number of garbage to be printed before printing the actual text
|
|
parent: QWidget
|
|
Pass into QLabel init method
|
|
"""
|
|
super().__init__(text, parent)
|
|
|
|
self.speed = speed
|
|
|
|
self.setWordWrap(True)
|
|
self.setContentsMargins(0, 0, 0, 0)
|
|
# Text processing
|
|
# Make sure the garbage does not exceed the length of actual text
|
|
self.actual_text = text
|
|
self.shown_text = ''
|
|
if delay >= 0:
|
|
self.delay = min(delay, len(self.actual_text))
|
|
else:
|
|
self.delay = len(self.actual_text)
|
|
|
|
# Find out where the new paragraphs are so that it is retained
|
|
self.splitpoints = []
|
|
current_point = 0
|
|
line_splits = self.actual_text.split('\n')
|
|
for line in line_splits:
|
|
current_point += len(line)
|
|
self.splitpoints.append(current_point)
|
|
current_point += 1
|
|
|
|
# Set up the shown text length to be animated
|
|
self.shown_length = 0
|
|
self.anim = QPropertyAnimation(self, b'shown_length')
|
|
self.anim.setDuration(len(self.actual_text) * speed)
|
|
self.anim.setStartValue(0)
|
|
self.anim.setEndValue(len(self.actual_text) + self.delay)
|
|
|
|
#self.setStyleSheet("""
|
|
# color: rgb(0, 255, 0);
|
|
# """)
|
|
self.toggle_anim(True)
|
|
|
|
@pyqtProperty(int)
|
|
def shown_length(self):
|
|
"""
|
|
int : The value for the animation
|
|
|
|
When the value is set, the text to be printed is generated accordingly.
|
|
It determines whether actual text is to be printed, and retains the
|
|
paragraphs when printing garbage.
|
|
"""
|
|
return self._shown_length
|
|
|
|
@shown_length.setter
|
|
def shown_length(self, value):
|
|
self._shown_length = value
|
|
|
|
if value < self.delay:
|
|
# All printed text should be garbage
|
|
garbage = [chr(num) for num in
|
|
[random.choice(range(33, 127)) for _ in range(value)]]
|
|
|
|
# Retain the paragraphs
|
|
for num in self.splitpoints[:-1]:
|
|
if num < value:
|
|
garbage[num] = '\n'
|
|
|
|
self.setText(''.join(garbage))
|
|
else:
|
|
# Printed text contain some actual text
|
|
garbage = [chr(num) for num in
|
|
[random.choice(range(33, 127)) for _ in
|
|
range(min(len(self.actual_text) + self.delay - value, self.delay))]]
|
|
|
|
# Retain the paragraphs, but offset by the number of actual text
|
|
non_garbage = value - self.delay
|
|
for num in self.splitpoints[:-1]:
|
|
if num - non_garbage > 0 and num < value:
|
|
garbage[num - non_garbage] = '\n'
|
|
|
|
self.setText(self.actual_text[:value - self.delay] + ''.join(garbage))
|
|
|
|
def toggle_anim(self, toggling):
|
|
"""
|
|
Toggle the animation to be play forward or backward
|
|
|
|
Parameters
|
|
----------
|
|
toggling: bool
|
|
True for forward, False for backward
|
|
"""
|
|
if toggling:
|
|
self.anim.setDirection(QAbstractAnimation.Forward)
|
|
else:
|
|
self.anim.setDirection(QAbstractAnimation.Backward)
|
|
|
|
self.anim.start()
|
|
|
|
def replace_text(self, new_text):
|
|
self.shown_length = 0
|
|
self.actual_text = new_text
|
|
self.anim.setDuration(len(self.actual_text) * self.speed)
|
|
self.anim.setEndValue(len(self.actual_text) + self.delay)
|
|
self.toggle_anim(True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app = 0
|
|
app = QApplication(sys.argv)
|
|
|
|
ex = HighScoreBoard(500, 500)
|
|
ex.show()
|
|
sys.exit(app.exec_()) |