SudokuGame/graphic_components/scoreboard.py

386 lines
13 KiB
Python

"""This module contains the components that makes up the score board. It is constructed using QWigdets
and then embedded into the QGraphicsScene using QGraphicsProxyWidget because it's easier."""
import sys
import os
from PySide2.QtCore import (QAbstractAnimation, Qt, QPropertyAnimation, Property, Signal, QTimer)
from PySide2.QtWidgets import (QWidget, QLineEdit, QHBoxLayout, QGridLayout, QVBoxLayout, QPushButton, QLabel, QApplication)
from general import highscore as hs
from .textbox import AnimatedLabel
if not __name__ == "__main__":
current_dir = os.getcwd()
sys.path.append(current_dir)
hs_file = current_dir + "/general/highscore.txt"
else:
# For testing, maybe wrong
hs_file = "../general/highscore.txt"
if not os.path.exists(hs_file):
print('Missing High Score file. Generating one. ')
hs.generate_highscore_file(hs_file)
BACKWARD = 1
FORWARD = -1
class HighScoreBoard(QWidget):
highScoreSet = Signal()
def __init__(self, width, height):
"""Initialise the widget with the specified width and height.
Parameters
----------
width: float
width of the widget
height: float
height of the widget
"""
super().__init__()
self.final_time = "00:10:00"
self.current_difficulty = hs.DIFFICULTIES[1]
self.layout = QVBoxLayout(self)
self.layout.setAlignment(Qt.AlignCenter)
self.layout.addWidget(QLabel('Score Board', self, alignment=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.name_input.setVisible(False)
self.setFixedSize(width, height)
self.setStyleSheet("""background-color: rgb(0, 0, 0);
color: rgb(255, 255, 255);
""")
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):
"""Change to he score board to the corresponding difficulty
Parameters
----------
difficulty: str
The difficulty for the score board to change to
"""
self.score_grid.replace_scores(difficulty)
def show_scores(self, toggle):
"""Shows the score board in the current difficulty, if the widget is visible
Parameters
----------
toggle: bool
True to show the board, False otherwise
"""
if self.isVisible():
self.score_grid.show_score_info(toggle)
def set_score(self, name):
"""Set the high score with an input name, to the current difficulty and time.
Emits a signal once high score is set.
Parameters
----------
name: str
Name for the high score
"""
self.score_grid.set_highscore(self.current_difficulty, name, self.final_time)
self.name_input.setVisible(False)
self.highScoreSet.emit()
def check_ranking(self, difficulty, time):
"""First, it updates the current difficulty and time. Check if the current time ranks in the Top 5.
If so, display the score board in the correct difficulty.
Parameters
----------
difficulty: str
Current difficulty of the puzzle
time: str
The time taken to solve the puzzle
Returns
-------
bool: True if it ranks Top 5, False otherwise
"""
self.current_difficulty = difficulty
self.final_time = time
rank = self.score_grid.get_rank(difficulty, time)
if rank >= 0:
self.diff_switch.go_to_difficulty(difficulty)
self.score_grid.replace_scores(difficulty)
self.name_input.setVisible(True)
self.name_input.rank_label.setText(str(rank+1))
self.name_input.time_display.setText(time)
return True
return False
class DifficultySwitch(QHBoxLayout):
"""The layout that contains the switches between the difficulties and displays them.
Attributes
----------
difficultySelected: Signal(str)
Emitted when a difficulty is selected. Emits the selected difficulty.
"""
difficultySelected = Signal(str)
def __init__(self):
"""Create the full text to cycle through. Then, create the label and the buttons.
The text is set up such that the last element on the list is additionally inserted in the front
and the first on the list is additionally appended at the back, like this:
[4 0 1 2 3 4 0]
When the cycle reaches the end, it jumps to the second on the new list, and when the cycle reaches the front,
it jumps to the second last, giving the illusion of a circular selection.
Note that the text created is reversed to account for animation.
"""
super().__init__()
# Make a copy of the difficulty list, insert texts to create the circular text buffer, spaced equally
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[::-1])
left_btn = QPushButton('<')
left_btn.setFixedSize(20, 20)
self.difficulty_display = QLabel('Normal')
self.difficulty_display.setAlignment(Qt.AlignCenter)
right_btn = QPushButton('>')
right_btn.setFixedSize(20, 20)
self.addWidget(left_btn)
self.addWidget(self.difficulty_display)
self.addWidget(right_btn)
self.layout().setStretch(1, 2)
self.shift_direction = FORWARD
self.show_pos = self.max_length * len(hs.DIFFICULTIES)
self.next_pos = self.max_length * len(hs.DIFFICULTIES)
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))
@Property(int)
def show_pos(self):
"""
int : The position of the string to be shown
When the value is set, the text from the full text is selected and displayed
"""
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):
"""Connected to the buttons. Change the direction, update the next position
and activate the timer for cycling
"""
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):
"""Directly go the selected difficulty, with no cycling.
Parameters
----------
difficulty: str
The difficulty to tune to
"""
pos = (hs.DIFFICULTIES[::-1].index(difficulty) + 1) * self.max_length
self.show_pos = pos
self.next_pos = pos
def shift_pos(self):
"""Continuously increase the current string position until the destination position is reached, in which case
the timer is stopped and difficultySelected signal is emitted.
"""
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, pos):
"""Ensure the value showing the string jumps to the correct position
Parameters
----------
pos: int
Position in the text
Returns
-------
int: The adjusted position
"""
if pos == (len(hs.DIFFICULTIES)+1) * self.max_length:
pos = self.max_length
elif pos == 0:
pos = len(hs.DIFFICULTIES) * self.max_length
return pos
class ScoreGrid(QGridLayout):
"""The layout that displays the score data.
Attributes
----------
scoreUpdate: Signal
Emitted when the score board is updated.
"""
scoreUpdate = Signal(str)
def __init__(self):
"""Read the high score file, create the animated labels to contain the data.
"""
super().__init__()
try:
self.highscore_list = hs.read_highscore_file(hs_file)
except Exception as e:
raise Exception('Cannot open file', e)
for i in range(5):
label = QLabel(str(i+1)+'.')
self.addWidget(label, i, 0)
# The labels are created with placeholder text.
self.animated_labels = []
for i, name in enumerate('ABCDE'):
label1 = AnimatedLabel(name)
label1.setAlignment(Qt.AlignCenter)
label2 = AnimatedLabel('--:--')
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):
"""Animate the label to show/hide the data
Parameters
----------
toggle: bool
True to show data, False otherwise
"""
for label in self.animated_labels:
label.toggle_anim(toggle)
def replace_scores(self, difficulty):
"""Replace the current scores with data from the selected difficulty
Parameters
----------
difficulty: str
The difficulty to show
"""
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):
"""Set the high score with the given data
Parameters
----------
difficulty: str
The difficulty which the data is set to
name: str
Name to be set
time: str
Time to be set
"""
hs.replace_placing(self.highscore_list, difficulty, name, time)
hs.write_highscore_file(hs_file, self.highscore_list)
self.replace_scores(difficulty)
self.scoreUpdate.emit(difficulty)
def get_rank(self, difficulty, time):
"""Check and get the ranking of a given time and difficulty
Parameters
----------
difficulty: str
The difficulty to check for
time: str
The time to be compared with
Returns
-------
int: The rank in the Top 5. -1 if it is out of Top 5
"""
return hs.check_ranking(self.highscore_list, difficulty, time)
class NameInput(QWidget):
"""The widget to input a name for high score. It should be hidden until needed.
Attributes
----------
nameReceived: Signal(str)
Emitted once a non-whitespace name is received. Emits the name
"""
nameReceived = Signal(str)
def __init__(self):
"""Creates the widget: a label to show the rank and time, and QLineEdit for name input
"""
super().__init__()
self.layout = QHBoxLayout(self)
self.rank_label = QLabel('-')
self.layout.addWidget(self.rank_label)
self.name_input = QLineEdit(self)
self.name_input.setMaxLength(13)
self.layout.addWidget(self.name_input)
self.time_display = QLabel('-:-:-')
self.layout.addWidget(self.time_display)
self.name_input.returnPressed.connect(self.receive_name_input)
self.name_input.setStyleSheet("""
border-top: 1px solid white;
""")
def receive_name_input(self):
"""Strip the name off whitespaces, and emit it if there is any character
"""
name = self.name_input.text().strip(' ')
if name:
self.nameReceived.emit(name)
if __name__ == '__main__':
app = 0
app = QApplication(sys.argv)
ex = HighScoreBoard(500, 500)
ex.show()
sys.exit(app.exec_())