diff --git a/___init___.py b/___init___.py new file mode 100644 index 0000000..e69de29 diff --git a/clickableGear.py b/clickableGear.py index 94ae706..b15d79b 100644 --- a/clickableGear.py +++ b/clickableGear.py @@ -1,6 +1,9 @@ import sys import math - +from PyQt6.QtWidgets import QGridLayout, QLabel, QPushButton, QSizePolicy +from PyQt6.QtGui import QPixmap, QCursor +from PyQt6 import QtCore +from PyQt6.QtCore import QTimer from PyQt6.QtWidgets import QWidget from PyQt6.QtCore import Qt, QRect, QPointF, pyqtSignal from PyQt6.QtGui import QPainter, QColor, QPen, QFont, QPainterPath @@ -8,8 +11,10 @@ class ClickableWidget(QWidget): clicked = pyqtSignal(int) # signal to emit widget ID when clicked + def __init__(self, parent=None): super().__init__(parent) + self.setWindowTitle("Questions in Topic Name") self.setMinimumSize(700,750) @@ -27,10 +32,11 @@ def __init__(self, parent=None): self.gear_states=["unanswered"]*10 # positions for gears (adjusted for your window size) + positions = [ (74, 103), (75, 164), - (191, 232), + (185, 232), (177, 302), (309, 370), (301, 439), @@ -39,7 +45,7 @@ def __init__(self, parent=None): (574, 645), (624, 705) ] - + for i, (x, y) in enumerate(positions): self.widgets.append({ 'center': QPointF(x, y), @@ -65,7 +71,7 @@ def setQuestions(self,questions): def mark_gear_state(self, gear_id, state): widget = self.widgets[gear_id] - widget["staet"]=state + widget["state"]=state if state == "correct": widget["color"] = QColor("#efbc50") widget["enabled"] = False @@ -233,4 +239,4 @@ def mouseMoveEvent(self, event): needs_update = True if needs_update: - self.update() # Trigger repaint \ No newline at end of file + self.update() # Trigger repaint diff --git a/frame1.py b/frame1.py new file mode 100644 index 0000000..d199f0b --- /dev/null +++ b/frame1.py @@ -0,0 +1,40 @@ +from PyQt6 import QtCore +from PyQt6.QtGui import QPixmap, QCursor +from PyQt6.QtWidgets import QLabel, QGridLayout, QWidget, QPushButton +from helperfunctions import grid, widgets, clear_stretch +from frames.frame2 import frame2 + + +def frame1(): + clear_stretch() + + background = QLabel() + background.setPixmap(QPixmap("logo1.png")) + background.setScaledContents(True) # Makes image fill the whole window + widgets["background"] = [background] + grid.addWidget(background, 0, 0, 5, 5) + + button = QPushButton("PLAY NOW!") + button.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + button.clicked.connect(frame2) + button.setStyleSheet( + ''' + *{ + border: 1px solid '#262124'; + border-radius: 15px; + font-size: 15px; + color: white; + padding: 15px 15px; + background-color: rgba(0, 0, 0, 80); + } + *:hover{ + background-color: rgba(56, 53, 55, 180) + } + ''' + ) + + widgets["button"].append(button) + + grid.addWidget(button, 3, 0, 1, 5, QtCore.Qt.AlignmentFlag.AlignCenter) + + # grid.addWidget(button, 3, 2, 1, 1, QtCore.Qt.AlignmentFlag.AlignCenter) \ No newline at end of file diff --git a/frame2.py b/frame2.py new file mode 100644 index 0000000..9400d1c --- /dev/null +++ b/frame2.py @@ -0,0 +1,192 @@ +from PyQt6 import QtCore +from PyQt6.QtGui import QCursor +from PyQt6.QtWidgets import QGridLayout, QLabel, QPushButton, QWidget, QHBoxLayout +from helperfunctions import widgets, grid, progress, save_progress, clear_widgets +from frames.statsframe import frame_stats +from frames.gearpage import open_topic_window + +def frame2(): + clear_widgets() + + #Back button + from frames.frame1 import frame1 + button = QPushButton("⬅️") + button.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + button.clicked.connect(frame1) + button.setStyleSheet( + ''' + *{ + border-radius: 70px; + font-size: 26px; + color: white; + padding: 0px 0px; + } + ''' + ) + widgets["button"].append(button) + + grid.addWidget(button, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignLeft) + + # Title + title = QLabel("Choose Topic") + title.setStyleSheet(''' + color: white; + font-family: "Arial"; + font-size: 28px; + font-weight: bold; + margin-top: 40px; + ''') + title.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) + widgets["title"] = [title] + grid.addWidget(title, 1, 0, 1, 1) + + + # Score label (auto-updates) + score_label = QLabel(f"Score: {progress['score']}") + score_label.setStyleSheet(''' + color: white; + font-family: "Arial"; + font-size: 18px; + font-weight: bold; + margin-right:20px; + ''') + score_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) + widgets["score"] = [score_label] + #grid.addWidget(score_label, 1, 1, 1, 1) + grid.addWidget(score_label, 2, 1, QtCore.Qt.AlignmentFlag.AlignRight) + + # Topics list + topics = [ + "Fundamentals", + "Control Structures", + "Data Structures", + "Functions & Scope", + "OOP", + "Error & Exception Handling", + "File Handling", + "Advanced Topics" + ] + + unlocked = progress["unlocked_topics"] + + # Topic buttons + for i, topic in enumerate(topics): + button = QPushButton(topic) + button.setFixedHeight(90) + button.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + + if topic in progress["completed_topics"]: + # ✅ Completed topic + button.setEnabled(False) + button.setText("✅ " + topic) + button.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + border-radius: 15px; + font-size: 18px; + text-align: left; + padding-left: 15px; + } + """) + elif i < unlocked: + # 🟢 Unlocked topic + button.setStyleSheet(""" + QPushButton { + background-color: #4fa3d1; + color: white; + border-radius: 15px; + font-size: 18px; + text-align: left; + padding-left: 15px; + } + QPushButton:hover { + background-color: #00C080; + } + """) + button.clicked.connect(lambda _, t=topic: open_topic_window(t)) + else: + # 🔒 Locked topic + button.setEnabled(False) + button.setText("🔒 " + topic) + button.setStyleSheet(""" + QPushButton { + background-color: #2e5a77; + color: #aaaaaa; + border-radius: 15px; + font-size: 18px; + text-align: left; + padding-left: 15px; + } + """) + + grid.addWidget(button, i + 3, 0, 1, 2) + + + # ====RESET FUNCTION=== + def reset_progress(): + # Reset the JSON file + global progress + progress = { + "unlocked_topics": 1, + "score": 0, + "completed_topics": set(), + "topic_scores": {} + } + + save_progress() + + frame2() + + # === RESET BUTTON === + reset_btn = QPushButton("Reset Progress") + reset_btn.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + reset_btn.setFixedHeight(50) + reset_btn.setStyleSheet(""" + QPushButton { + color: white; + border-radius: 8px; + font-size: 18px; + background-color: #c97b37; + } + QPushButton:hover { + background-color: #956b48; + } + """) + reset_btn.clicked.connect(reset_progress) + + #Statistics Button + stats_btn = QPushButton("📊") + stats_btn.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + stats_btn.setFixedSize(40,40) + stats_btn.clicked.connect(frame_stats) + stats_btn.setStyleSheet(""" + QPushButton { + color: white; + border-radius: 15px; + font-size: 40px; + } + + """) + #grid.addWidget(stats_btn, len(topics) + 4, 0, 1, 2) + + + #grid.setRowStretch(len(topics) + 3, 2) + # Add to the grid, below the topics + #grid.addWidget(reset_btn, len(topics) + 3, 0, 1, 2) + + grid.setRowStretch(len(topics) + 2, 2) + + bottom_row = QWidget() + hbox = QHBoxLayout() + hbox.setContentsMargins(10, 10, 10, 10) + #hbox.setSpacing(20) + + hbox.addWidget(stats_btn) + hbox.addWidget(reset_btn) + + bottom_row.setLayout(hbox) + # Place stats button at the top-right + grid.addWidget(stats_btn, 0, 1, 1, 1, QtCore.Qt.AlignmentFlag.AlignRight) + + grid.addWidget(bottom_row, len(topics) + 4, 0, 1, 2) \ No newline at end of file diff --git a/frame3.py b/frame3.py new file mode 100644 index 0000000..6dfc154 --- /dev/null +++ b/frame3.py @@ -0,0 +1,59 @@ +from PyQt6 import QtCore +from PyQt6.QtGui import QCursor, QPixmap +from PyQt6.QtWidgets import QLabel,QPushButton +from helperfunctions import grid, widgets, clear_widgets, progress + +#********************************************* +# FRAME 3 - WIN GAME +#********************************************* +def frame3(): + clear_widgets() + #clear_widgets(widgets, grid) + #congradulations widget + message = QLabel("Your score is:") + message.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) + message.setStyleSheet( + "font-family: 'Shanti'; font-size: 25px; color: 'white'; margin: 100px 0px;" + ) + widgets["message"].append(message) + + #score widget + score = QLabel(str(progress["score"])) + score.setStyleSheet("font-size: 100px; color: #8FC740; margin: 0 75px 0px 75px;") + widgets["score"].append(score) + + #go back to work widget + message2 = QLabel("Congratulations! You passed all the challenges!") + message2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + message2.setStyleSheet( + "font-family: 'Shanti'; font-size: 30px; color: 'white'; margin-top:0px; margin-bottom:75px;" + ) + widgets["message2"].append(message2) + + #button widget + button = QPushButton('TRY AGAIN!') + button.setStyleSheet( + "*{background:'#BC006C'; padding:25px 0px; border: 1px solid '#BC006C'; color: 'white'; font-family: 'Arial'; font-size: 25px; border-radius: 40px; margin: 10px 300px;} *:hover{background:'#ff1b9e';}" + ) + button.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + from frames.frame2 import frame2 + button.clicked.connect(frame2) + + widgets["button"].append(button) + + #logo widget + pixmap = QPixmap('logo_bottom.png') + logo = QLabel() + logo.setPixmap(pixmap) + logo.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + logo.setStyleSheet( + "padding :10px; margin-top:75px; margin-bottom: 20px;" + ) + widgets["logo"].append(logo) + + #place widgets on the grid + grid.addWidget(widgets["message"][-1], 2, 0) + grid.addWidget(widgets["score"][-1], 2, 1) + grid.addWidget(widgets["message2"][-1], 3, 0, 1, 2) + grid.addWidget(widgets["button"][-1], 4, 0, 1, 2) + grid.addWidget(widgets["logo"][-1], 5, 0, 2, 2) \ No newline at end of file diff --git a/frame4.py b/frame4.py new file mode 100644 index 0000000..c6f7752 --- /dev/null +++ b/frame4.py @@ -0,0 +1,67 @@ +from PyQt6 import QtCore +from PyQt6.QtGui import QCursor, QPixmap +from PyQt6.QtWidgets import QLabel,QPushButton +from helperfunctions import widgets,grid,clear_widgets + +#********************************************* +# FRAME 4 - FAIL +#********************************************* +def frame4(): + #clear_widgets(widgets, grid) + global current_topic_score + current_topic_score = int(current_topic_score) # force safe int + + clear_widgets() + #sorry widget + message = QLabel("Sorry, you have failed!\n Your score is: ") + message.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) + message.setStyleSheet( + "font-family: 'Shanti'; font-size: 35px; color: 'white'; margin: 75px 5px; padding:20px;" + ) + widgets["message"].append(message) + + #score widget + score = QLabel(str(current_topic_score)) + #score = QLabel(str(progress["score"])) + + score.setStyleSheet("font-size: 100px; color: white; margin: 0 75px 0px 75px;") + widgets["score"].append(score) + + #button widget + button = QPushButton('TRY AGAIN!') + button.setStyleSheet( + '''*{ + padding: 25px 0px; + background: '#966b47'; + color: 'white'; + font-family: 'Arial'; + font-size: 35px; + border-radius: 40px; + margin: 10px 200px; + } + *:hover{ + background: '#966b47'; + }''' + ) + button.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + from frames.frame2 import frame2 + button.clicked.connect(frame2) + + widgets["button"].append(button) + + #logo widget + pixmap = QPixmap('logo_bottom.png') + logo = QLabel() + logo.setPixmap(pixmap) + logo.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + logo.setStyleSheet( + "padding :10px; margin-top:75px;" + ) + widgets["logo"].append(logo) + + #place widgets on the grid + grid.addWidget(widgets["message"][-1], 1, 0) + grid.addWidget(widgets["score"][-1], 1, 1) + grid.addWidget(widgets["button"][-1], 2, 0, 1, 2) + grid.addWidget(widgets["logo"][-1], 3, 0, 1, 2) + diff --git a/gearpage.py b/gearpage.py new file mode 100644 index 0000000..55d5b8c --- /dev/null +++ b/gearpage.py @@ -0,0 +1,58 @@ +from PyQt6 import QtCore +from PyQt6.QtGui import QCursor +from PyQt6.QtWidgets import QGridLayout, QPushButton +from clickableGear import ClickableWidget +from helperfunctions import clear_widgets, widgets, grid, TOPIC_QUESTIONS +from frames.questionpage import show_question_page + +def open_topic_window(topic_name): + + """Open the ClickableWidget gear interface for the selected topic.""" + global current_gear_widget + global current_topic_score + current_topic_score = 0 + + clear_widgets() + + #Back button + from frames.frame2 import frame2 + button = QPushButton("⬅️") + button.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + button.clicked.connect(frame2) + button.setStyleSheet( + ''' + *{ + border-radius: 70px; + font-size: 26px; + color: white; + padding: 0px 0px; + } + ''' + ) + widgets["button"].append(button) + + grid.addWidget(button, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignLeft) + + + questions_for_topic=TOPIC_QUESTIONS[topic_name] + + # Create the ClickableWidget + clickable_widget = ClickableWidget() + clickable_widget.setTopicName(topic_name) + # Set the topic name + clickable_widget.setQuestions(questions_for_topic) # Match window size + clickable_widget.clicked.connect(lambda gear_id: show_question_page(gear_id,questions_for_topic[gear_id])) + current_gear_widget = clickable_widget + widgets["clickable"].append(clickable_widget) + + clickable_widget.gear_states = ["unanswered"] * 10 + for g in clickable_widget.widgets: + g["state"] = "unanswered" + g["enabled"] = True + clickable_widget.update() + + # Add the clickable widget to the grid (spans entire window) + #grid.addWidget(clickable_widget, 2, 0, 1, 2) + #grid.addWidget(clickable_widget, 2, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignCenter) + # grid.setRowStretch(1, 1) + grid.addWidget(clickable_widget, 1, 0, 1, 2) \ No newline at end of file diff --git a/helperfunctions.py b/helperfunctions.py new file mode 100644 index 0000000..a1d8ff2 --- /dev/null +++ b/helperfunctions.py @@ -0,0 +1,199 @@ +from PyQt6.QtWidgets import QGridLayout +import json, os + +ALL_TOPICS = [ + "Fundamentals", + "Control Structures", + "Data Structures", + "Functions & Scope", + "OOP", + "Error & Exception Handling", + "File Handling", + "Advanced Topics" +] + +#---------------load questions---------------- +with open("qna.json", "r") as f: + TOPIC_QUESTIONS=json.load(f) + + +#global dictionary of dynamically changing widgets +widgets = { + "logo": [], + "button": [], + "score": [], + "question": [], + "answer1": [], + "answer2": [], + "answer3": [], + "answer4": [], + "message": [], + "message2": [], + "clickable":[], + "score_indicator":[], + "background": [] +} + +#initialliza grid layout +grid = QGridLayout() + +current_topic_score = 0 +current_gear_widget = None + +#Progress File +PROGRESS_FILE = "prog.json" +default_progress = { + "unlocked_topics": 1, + "score": 0, + "completed_topics": [] +} + + + +def clear_widgets(exclude_current_gear=False): + #Hide and remove all existing widgets from the layout and clear the widgets dict. + global current_gear_widget + while grid.count(): + item = grid.takeAt(0) + widget = item.widget() + if widget is not None: + if exclude_current_gear and widget ==current_gear_widget: + continue # fully removes widget from layout and window + widget.setParent(None) + + # also clear your tracking dictionary + #for widget_list in widgets.values(): + # widget_list.clear() + # Reset all widget lists cleanly + for key in widgets: + widgets[key] = [] + +def clear_stretch(exclude_current_gear=False): + #Hide and remove all existing widgets from the layout and clear the widgets dict. + global current_gear_widget + while grid.count(): + item = grid.takeAt(0) + widget = item.widget() + if widget is not None: + if exclude_current_gear and widget ==current_gear_widget: + continue # fully removes widget from layout and window + widget.setParent(None) + + # also clear your tracking dictionary + for widget_list in widgets.values(): + widget_list.clear() + + for r in range(100): + grid.setRowStretch(r, 0) + for c in range(100): + grid.setColumnStretch(c, 0) + +#------------ Score and Progress --------------------- +# --- Load from file --- +def load_progress(): + if os.path.exists(PROGRESS_FILE): + try: + with open(PROGRESS_FILE, "r") as f: + data = json.load(f) + data["completed_topics"] = set(data.get("completed_topics", [])) + if "topic_scores" not in data: + data["topic_scores"] = {} + return data + except Exception: + pass + return { + "unlocked_topics": 1, + "score": 0, + "completed_topics": set(), + "topic_scores": {} + } + + +# --- Save to file --- +def save_progress(): + data = { + "unlocked_topics": progress["unlocked_topics"], + "score": progress["score"], + "completed_topics": list(progress["completed_topics"]), + "topic_scores": progress.get("topic_scores", {}) + } + with open(PROGRESS_FILE, "w") as f: + json.dump(data, f, indent=4) + + +def update_score(amount=1): + global progress + + if "score" not in progress: + progress["score"]=0 + progress["score"]+= amount + save_progress() + refresh_score() + +def get_score(): #returns current score + global progress + if "score" in progress: + return progress["score"] + return 0 + +def refresh_score(): # updates the score label + if widgets.get("score"): + # widgets["score"][0] is used as the score label in frame2 + try: + widgets["score"][0].setText(f"Score: {get_score()}") + except Exception: + pass + + +#----------Stats Helpers ------------------ + +def get_highest(topic): + if "topic_scores" not in progress: + return 0 + if topic not in progress["topic_scores"]: + return 0 + if len(progress["topic_scores"][topic]) == 0: + return 0 + return max(progress["topic_scores"][topic]) + + +def get_average(topic): + if "topic_scores" not in progress: + return 0 + if topic not in progress["topic_scores"]: + return 0 + scores = progress["topic_scores"][topic] + if len(scores) == 0: + return 0 + # each quiz is out of 10 + return int((sum(scores) / (len(scores) * 10)) * 100) + + +def get_overall_highest(): + if "topic_scores" not in progress: + return 0 + h = 0 + for topic, scores in progress["topic_scores"].items(): + if len(scores) > 0: + h = max(h, max(scores)) + return h + + +def get_overall_percentage(): + if "topic_scores" not in progress: + return 0 + total_attempts = 0 + earned = 0 + for scores in progress["topic_scores"].values(): + for s in scores: + total_attempts += 10 + earned += s + if total_attempts == 0: + return 0 + return int((earned / total_attempts) * 100) + +#----------------------------Progress ---------------- +progress = load_progress() + +if "topic_scores" not in progress: + progress["topic_scores"] = {} \ No newline at end of file diff --git a/main.py b/main.py index bfbb852..4c94d5f 100644 --- a/main.py +++ b/main.py @@ -1,25 +1,38 @@ -# Only needed for access to command line arguments -import sys - -from PyQt6.QtWidgets import QApplication, QWidget -from pages import frame1, grid -# You need one (and only one) QApplication instance per application. -# Pass in sys.argv to allow command line arguments for your app. -# If you know you won't use command line arguments QApplication([]) works too. -app = QApplication(sys.argv) - -# Create a Qt widget, which will be our window. -window = QWidget() - -window.setWindowTitle("Python Trivia Game") -#window.setFixedWidth(1000) -#window.setFixedHeight(1000) -window.setStyleSheet("background: #397591") - -frame1() - -window.setLayout(grid) -window.show() # IMPORTANT!!!!! Windows are hidden by default. - -# Start the event loop. -app.exec() +import sys +from PyQt6.QtWidgets import QApplication, QWidget, QScrollArea, QVBoxLayout +from frames.frame1 import frame1 +from helperfunctions import grid + +app = QApplication(sys.argv) + +# Get screen information +screen = app.primaryScreen() +screen_rect = screen.availableGeometry() + +# set window size +w = int(screen_rect.width() * 0.8) +h = int(screen_rect.height() * 1) + +# Main window +window = QWidget() +window.setWindowTitle("Python Trivia Game") +window.resize(w, h) +window.setStyleSheet("background: #397591") + +frame1() + +# the grid layout in a scrollable widget +content_widget = QWidget() # holds your layout +content_widget.setLayout(grid) + +scroll = QScrollArea() +scroll.setWidgetResizable(True) +scroll.setWidget(content_widget) + +# scroll area into the main window +main_layout = QVBoxLayout() +main_layout.addWidget(scroll) +window.setLayout(main_layout) + +window.show() +app.exec() diff --git a/prog.json b/prog.json index 69e2db3..c5559bd 100644 --- a/prog.json +++ b/prog.json @@ -1,5 +1,6 @@ { "unlocked_topics": 1, - "score": 0, - "completed_topics": [] + "score": 10, + "completed_topics": [], + "topic_scores": {} } \ No newline at end of file diff --git a/questionpage.py b/questionpage.py new file mode 100644 index 0000000..59c1ccf --- /dev/null +++ b/questionpage.py @@ -0,0 +1,237 @@ +from PyQt6 import QtCore +from PyQt6.QtCore import QTimer +from PyQt6.QtGui import QCursor +from PyQt6.QtWidgets import QPushButton, QLabel, QSizePolicy +from helperfunctions import grid, widgets,clear_widgets, progress, refresh_score, update_score, save_progress, ALL_TOPICS, current_gear_widget, current_topic_score + +def show_question_page(gear_id, question_data, gear_widget): + global current_gear_widget, current_topic_score + + current_gear_widget = gear_widget # always update reference + + clear_widgets(exclude_current_gear=True) + + # Add score label that always reads from progress + score_label = QLabel(f"Score: {progress['score']}") + widgets["score"] = [score_label] + grid.addWidget(score_label, 1, 1, 1, 1) + + # ... add question and answer buttons as before ... + for i, answer in enumerate(question_data["answers"]): + btn = QPushButton(answer) + btn.clicked.connect(lambda _, a=answer: check_answer(a, question_data, gear_id, gear_widget)) + widgets[f"answer{i}"] = [btn] + grid.addWidget(btn, 2 + i//2, i % 2) + + +def check_answer(selected, question_data, gear_id, gear_widget): + global current_topic_score, progress + + correct = question_data["correct"] + + if selected == correct: + current_topic_score += 1 + update_score(1) # increment global score + refresh_score() + gear_widget.disable_gear(gear_id) + else: + gear_widget.mark_gear_state(gear_id, "wrong") + + # when all gears answered + all_done = all(s in ("correct", "wrong") for s in gear_widget.gear_states) + if all_done: + topic_name = gear_widget.topic_name + # save attempt + if topic_name not in progress["topic_scores"]: + progress["topic_scores"][topic_name] = [] + progress["topic_scores"][topic_name].append(current_topic_score) + + # unlock next topic if passed + if current_topic_score > 5: + progress["completed_topics"].add(topic_name) + progress["unlocked_topics"] = len(progress["completed_topics"]) + 1 + save_progress() + refresh_score() + # show next frame + from frames.frame3 import frame3 + frame3() # win frame if all passed or frame2 if continue + else: + # fail frame + save_progress() + from frames.frame4 import frame4 + frame4() + + + + +"""def show_question_page(gear_id, question_data): + global current_gear_widget + current_gear_widget = question_data.get("gear_widget", current_gear_widget) + current_topic_score=0 + + clear_widgets(exclude_current_gear=True) + + # Set equal column stretches for consistent sizing + grid.setColumnStretch(0, 1) + grid.setColumnStretch(1, 1) + + #l_margin = 50 + #r_margin = 50 + + #Back button + from frames.frame2 import frame2 + button = QPushButton("⬅️") + button.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + button.clicked.connect(frame2) + button.setStyleSheet( + ''' + *{ + border-radius: 70px; + font-size: 26px; + color: white; + padding: 0px 0px; + } + ''' + ) + widgets["button"].append(button) + + grid.addWidget(button, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignLeft) + + # Score label (auto-updates) + score_label = QLabel(f"Score: {progress['score']}") + score_label.setStyleSheet(''' + color: white; + font-family: "Arial"; + font-size: 18px; + font-weight: bold; + margin-right:20px; + ''') + score_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) + widgets["score"] = [score_label] + grid.addWidget(score_label, 1, 1, 1, 1) + + + question = QLabel(question_data ["question"]) + #question.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + question.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + question.setWordWrap(True) + question.setStyleSheet("" + font-family: 'shanti'; + font-size: 25px; + color: 'white'; + padding: 75px; + margin-top: 40px; + "") + widgets["question"].append(question) + grid.addWidget(question, 1, 0, 1, 2) + + + + # Answer buttons + for i, answer in enumerate(question_data["answers"]): + btn = QPushButton(answer) + btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + btn.setStyleSheet( + ''' + *{ + min-width: 350px; /* Add this line */ + max-width: 550px; /* Add this line */ + border: 3px solid '#bc9d00'; + color: white; + font-family: 'shanti'; + font-size: 16px; + border-radius: 25px; + padding: 15px 0; + margin-top: 40px; + } + *:hover{ + background: '#bc9d00'; + } + ''' + ) + btn.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + btn.clicked.connect(lambda _, a=answer: check_answer(a, question_data, gear_id)) + widgets[f"answer{i}"] = [btn] + grid.addWidget(btn, 2 + i//2, i % 2) + + +def check_answer(selected, question_data, gear_id): + global current_gear_widget, current_topic_score + gear_widget = current_gear_widget + correct = question_data["correct"] + + # Check answer + if selected == correct: + result_text = "✅ Correct!" + update_score() + refresh_score() + current_topic_score += 1 # ---> count correct answers + gear_widget.disable_gear(gear_id) + else: + result_text = "❌ Wrong!" + gear_widget.mark_gear_state(gear_id, "wrong") + + # Show temporary result + result_label = QLabel(result_text) + result_label.setStyleSheet("color: yellow; font-size: 20px;") + result_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + grid.addWidget(result_label, 6, 0, 1, 2) + + def return_to_gear(): + grid.removeWidget(result_label) + result_label.deleteLater() + + # Remove question widgets + for key in ["question", "answer0", "answer1", "answer2", "answer3", "score"]: + if widgets.get(key): + w = widgets[key].pop() + grid.removeWidget(w) + w.deleteLater() + + # --- Detect if ALL gears are answered --- + all_done = all(s in ("correct", "wrong") for s in gear_widget.gear_states) + + if not all_done: + # Return to gear view (normal) + grid.addWidget(gear_widget, 0, 0, 1, 2) + gear_widget.update() + return + + # --- ALL questions answered — decide WIN OR FAIL --- + topic_name = gear_widget.topic_name + + progress["topic_scores"].setdefault(topic_name,[]) + progress["topic_scores"][topic_name].append(current_topic_score) + + # Ensure topic_scores exists + if topic_name not in progress["topic_scores"]: + progress["topic_scores"][topic_name] = [] + + # Save this attempt + progress["topic_scores"][topic_name].append(current_topic_score) + + if current_topic_score > 5: + progress["completed_topics"].add(topic_name) + progress["unlocked_topics"] = len(progress["completed_topics"]) + 1 + save_progress() + + # 🎉 Check if ALL topics completed + if progress["completed_topics"] == set(ALL_TOPICS): + from frames.frame3 import frame3 + frame3() # <-- WIN GAME SCREEN + else: + from frames.frame2 import frame2 + frame2() # <-- Continue normally + + + else: + # ❗ FAIL CASE — SHOW FAIL FRAME + progress["score"] -= current_topic_score + save_progress() + #refresh_score() + from frames.frame4 import frame4 + frame4() + + + + QTimer.singleShot(1000, return_to_gear)""" diff --git a/statsframe.py b/statsframe.py new file mode 100644 index 0000000..8d1e8a7 --- /dev/null +++ b/statsframe.py @@ -0,0 +1,150 @@ +from PyQt6 import QtCore +from PyQt6.QtGui import QCursor +from PyQt6.QtWidgets import QLabel, QPushButton +from helperfunctions import grid, widgets, clear_widgets, get_average, get_highest, get_overall_highest, get_overall_percentage + +#------------------ Statistics Frame ----------------- + +def frame_stats(): + clear_widgets() + + # Title + title = QLabel("Statistics") + title.setStyleSheet(''' + color: white; + font-family: "Arial"; + font-size: 28px; + font-weight: bold; + margin-top: 20px; + ''') + title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + widgets["title"] = [title] + grid.addWidget(title, 1, 0, 1, 2) + + #Back button + from frames.frame2 import frame2 + button = QPushButton("⬅️") + button.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) + button.clicked.connect(frame2) + button.setStyleSheet( + ''' + *{ + border-radius: 70px; + font-size: 26px; + color: white; + padding: 0px 0px; + } + ''' + ) + widgets["button"].append(button) + + grid.addWidget(button, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignLeft) + + + topics = [ + "Fundamentals", + "Control Structures", + "Data Structures", + "Functions & Scope", + "OOP", + "Error & Exception Handling", + "File Handling", + "Advanced Topics" + ] + + row = 2 + + # ✅ Overall + overall_label = QLabel("Overall") + overall_label.setStyleSheet(""" + font-size: 20px; + color: orange; + margin-left: 50px; + margin-top: 50px; + """) + grid.addWidget(overall_label, row, 0) + + overall_score = QLabel(f"Highest Score: {get_overall_highest()}") + overall_score.setStyleSheet(""" + color: white; + font-size: 15px; + margin-left: 60px; + """) + + overall_percent = QLabel(f"Win Percent: {get_overall_percentage()}%") + overall_percent.setStyleSheet(""" + color: white; + font-size: 15px; + margin-left: 60px; + margin-bottom: 30px; + """) + + + grid.addWidget(overall_score, row+1, 0, 1, 1) + grid.addWidget(overall_percent, row+2, 0, 1, 1) + + row += 3 + + left_topics = topics[:4] # first 4 + right_topics = topics[-4:] # last 4 + # Start rows for both columns + left_row = row + right_row = row + +# LEFT COLUMN (first 4 topics) + for topic in left_topics: + label = QLabel(topic) + label.setStyleSheet("font-size: 15px; color: orange; margin-left: 80px") + grid.addWidget(label, left_row, 0) + + high = get_highest(topic) + avg = get_average(topic) + + high_label = QLabel(f"Highest Score: {high}") + high_label.setStyleSheet(""" + color: white; + font-size: 13px; + margin-left: 100px; + """) + avg_label = QLabel(f"Win Percent: {avg}%") + avg_label.setStyleSheet(""" + color: white; + font-size: 13px; + margin-left: 100px; + margin-bottom: 15px; + """) + grid.addWidget(high_label, left_row+1, 0) + grid.addWidget(avg_label, left_row+2, 0) + left_row += 3 # move down 3 rows for next topic + + +# RIGHT COLUMN (last 4 topics) + for topic in right_topics: + label = QLabel(topic) + label.setStyleSheet("font-size: 15px; color: orange; margin-left: 80px") + grid.addWidget(label, right_row, 1) + + high = get_highest(topic) + avg = get_average(topic) + + high_label = QLabel(f"Highest Score: {high}") + high_label.setStyleSheet(""" + color: white; + font-size: 13px; + margin-left: 100px; + """) + avg_label = QLabel(f"Win Percent: {avg}%") + avg_label.setStyleSheet(""" + color: white; + font-size: 13px; + margin-left: 100px; + margin-bottom: 15px; + """) + grid.addWidget(high_label, right_row+1, 1) + grid.addWidget(avg_label, right_row+2, 1) + + right_row += 3 # move down 3 rows for next topic + + + for i in range(20): + grid.setRowStretch(i, 1) \ No newline at end of file