362 lines
15 KiB
Python
362 lines
15 KiB
Python
"""
|
|
🎮 PERSISTENTES GAMIFICATION-SYSTEM FÜR PYTHON LERNPORTAL 🎮
|
|
============================================================
|
|
Wissenschaftlich fundiert nach der Selbstbestimmungstheorie (Deci & Ryan)
|
|
und dem Octalysis-Framework (Yu-kai Chou).
|
|
|
|
Funktionsweise:
|
|
- Speichert den Spielstand lokal in '.gamification.json'.
|
|
- Schreibt zusätzlich 'assets/gamification_data.js', damit index.html auch
|
|
ohne Webserver (per Doppelklick / file://) ohne CORS-Probleme darauf zugreifen kann!
|
|
- Verwaltet XP, Level-Aufstiege, Streaks und 14 didaktische Trophäen (Badges).
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from datetime import datetime, date
|
|
from pathlib import Path
|
|
from typing import Dict, List, Any, Optional
|
|
|
|
ROOT_DIR = Path(__file__).parent.resolve()
|
|
DATA_JSON_PATH = ROOT_DIR / ".gamification.json"
|
|
DATA_JS_PATH = ROOT_DIR / "assets" / "gamification_data.js"
|
|
|
|
# 10 Entwickler-Level mit XP-Schwellen und professionellen IT-Kompetenzstufen
|
|
LEVEL_STUFEN = [
|
|
{"level": 1, "min_xp": 0, "max_xp": 150, "titel": "Junior IT-Trainee I: Syntax & Basistypen", "rang": "Stufe 1"},
|
|
{"level": 2, "min_xp": 151, "max_xp": 400, "titel": "Junior IT-Trainee II: Kontrollfluss & Logik", "rang": "Stufe 2"},
|
|
{"level": 3, "min_xp": 401, "max_xp": 800, "titel": "IT-Specialist: Funktionen & Datenstrukturen", "rang": "Stufe 3"},
|
|
{"level": 4, "min_xp": 801, "max_xp": 1300, "titel": "Software Developer: OOP-Modellierung", "rang": "Stufe 4"},
|
|
{"level": 5, "min_xp": 1301, "max_xp": 2000, "titel": "DevOps Engineer: Versionskontrolle & Tooling", "rang": "Stufe 5"},
|
|
{"level": 6, "min_xp": 2001, "max_xp": 2900, "titel": "Quality Engineer: Test-Driven Development (TDD)", "rang": "Stufe 6"},
|
|
{"level": 7, "min_xp": 2901, "max_xp": 4000, "titel": "Advanced Engineer: Entwurfsmuster & Polymorphie", "rang": "Stufe 7"},
|
|
{"level": 8, "min_xp": 4001, "max_xp": 5300, "titel": "Systems Engineer: Security & Error Handling", "rang": "Stufe 8"},
|
|
{"level": 9, "min_xp": 5301, "max_xp": 6800, "titel": "Application Engineer: Schnittstellen & UI", "rang": "Stufe 9"},
|
|
{"level": 10, "min_xp": 6801, "max_xp": 99999, "titel": "Certified IT Systems Professional (IHK AP2)", "rang": "Master-Stufe"},
|
|
]
|
|
|
|
# 14 freischaltbare IT-Kompetenz-Zertifikate (abgestimmt auf IHK-Rahmenlehrplan)
|
|
BADGES_KATALOG = {
|
|
"rechenkonegin": {
|
|
"id": "rechenkonegin",
|
|
"icon": "🧮",
|
|
"name": "Zertifikat: Numerische Typen & Arithmetik",
|
|
"desc": "Grundlagen 01 & 02 abgeschlossen (Zahlen, Operatoren & Typen gemeistert).",
|
|
"xp_bonus": 100,
|
|
"kriterium": [
|
|
"lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner",
|
|
"lehrpfad_1_grundlagen/02_variablen_und_datentypen"
|
|
]
|
|
},
|
|
"dialog_profi": {
|
|
"id": "dialog_profi",
|
|
"icon": "💬",
|
|
"name": "Zertifikat: Interaktive I/O & String-Interpolation",
|
|
"desc": "Grundlagen 03 abgeschlossen (Interaktive Ein-/Ausgabe mit f-Strings).",
|
|
"xp_bonus": 75,
|
|
"kriterium": ["lehrpfad_1_grundlagen/03_ein_und_ausgabe"]
|
|
},
|
|
"weichenstellerin": {
|
|
"id": "weichenstellerin",
|
|
"icon": "🚦",
|
|
"name": "Zertifikat: Verzweigungslogik & Boolesche Algebra",
|
|
"desc": "Grundlagen 04 abgeschlossen (Bedingungslogik & Kontrollfluss gemeistert).",
|
|
"xp_bonus": 75,
|
|
"kriterium": ["lehrpfad_1_grundlagen/04_verzweigungen_und_bedingungen"]
|
|
},
|
|
"schleifen_dompteurin": {
|
|
"id": "schleifen_dompteurin",
|
|
"icon": "🎡",
|
|
"name": "Zertifikat: Iterationslogik & Sequenzverarbeitung",
|
|
"desc": "Grundlagen 05 abgeschlossen (While- & For-Schleifen gemeistert).",
|
|
"xp_bonus": 100,
|
|
"kriterium": ["lehrpfad_1_grundlagen/05_schleifen_und_wiederholungen"]
|
|
},
|
|
"funktions_zauberin": {
|
|
"id": "funktions_zauberin",
|
|
"icon": "📐",
|
|
"name": "Zertifikat: Modulare Funktionsarchitektur",
|
|
"desc": "Grundlagen 06 abgeschlossen (Modulare Funktionen & Parameterübergabe).",
|
|
"xp_bonus": 100,
|
|
"kriterium": ["lehrpfad_1_grundlagen/06_funktionen_und_module"]
|
|
},
|
|
"listen_archivarin": {
|
|
"id": "listen_archivarin",
|
|
"icon": "📋",
|
|
"name": "Zertifikat: Datenstrukturen: Listen & Slicing",
|
|
"desc": "Grundlagen 07 & 08 abgeschlossen (Sequenzen, Listen & Slicing analysiert).",
|
|
"xp_bonus": 120,
|
|
"kriterium": [
|
|
"lehrpfad_1_grundlagen/07_listen_und_sequenzen",
|
|
"lehrpfad_1_grundlagen/08_textverarbeitung_und_strings"
|
|
]
|
|
},
|
|
"daten_strategin": {
|
|
"id": "daten_strategin",
|
|
"icon": "🗃️",
|
|
"name": "Zertifikat: Assoziative Arrays: Dicts & Sets",
|
|
"desc": "Grundlagen 09 & 10 abgeschlossen (Dictionaries, Sets & Hash-Lookup O(1)).",
|
|
"xp_bonus": 150,
|
|
"kriterium": [
|
|
"lehrpfad_1_grundlagen/09_dictionaries_und_sets",
|
|
"lehrpfad_1_grundlagen/10_comprehensions_datum_algorithmen"
|
|
]
|
|
},
|
|
"bug_jaegerin": {
|
|
"id": "bug_jaegerin",
|
|
"icon": "🔍",
|
|
"name": "Zertifikat: Systematisches Code-Debugging",
|
|
"desc": "Warm-up abgeschlossen (Fehleranalyse & Traceback-Debugging).",
|
|
"xp_bonus": 100,
|
|
"kriterium": ["lehrpfad_2_oop_einstieg/00_fehlersuche_und_grundlagen"]
|
|
},
|
|
"erste_architektin": {
|
|
"id": "erste_architektin",
|
|
"icon": "🏗️",
|
|
"name": "Zertifikat: OOP-Grundlagen: Klassen & Kapselung",
|
|
"desc": "OOP 01 bis 03 gelöst (Klassendefinition, Konstruktoren & Instanzmethoden).",
|
|
"xp_bonus": 200,
|
|
"kriterium": [
|
|
"lehrpfad_2_oop_einstieg/01_einstieg_klassen",
|
|
"lehrpfad_2_oop_einstieg/02_init_und_self",
|
|
"lehrpfad_2_oop_einstieg/03_methoden_und_verhalten"
|
|
]
|
|
},
|
|
"tamagotchi_mama": {
|
|
"id": "tamagotchi_mama",
|
|
"icon": "📦",
|
|
"name": "Zertifikat: State-Machine & Domänenmodellierung",
|
|
"desc": "OOP Mini-Projekt abgeschlossen (Zustandsverwaltung & Objektinteraktion).",
|
|
"xp_bonus": 250,
|
|
"kriterium": ["lehrpfad_2_oop_einstieg/06_abschlussprojekt_tamagotchi"]
|
|
},
|
|
"zeit_reisende": {
|
|
"id": "zeit_reisende",
|
|
"icon": "🌿",
|
|
"name": "Zertifikat: String-Repräsentation & Dunder-Methoden",
|
|
"desc": "OOP 04 gelöst (Spezielle Methoden __str__, __repr__ implementiert).",
|
|
"xp_bonus": 100,
|
|
"kriterium": ["lehrpfad_2_oop_einstieg/04_str_und_darstellung"]
|
|
},
|
|
"qualitaets_garantin": {
|
|
"id": "qualitaets_garantin",
|
|
"icon": "🧪",
|
|
"name": "Zertifikat: Automatisierte Testsuites & TDD",
|
|
"desc": "OOP 09 abgeschlossen (Eigene professionelle Unit Tests mit Assertions).",
|
|
"xp_bonus": 200,
|
|
"kriterium": ["lehrpfad_3_fortgeschrittenes_oop/09_eigene_unit_tests_schreiben"]
|
|
},
|
|
"oop_grossmeisterin": {
|
|
"id": "oop_grossmeisterin",
|
|
"icon": "🧬",
|
|
"name": "Zertifikat: Fortgeschrittene OOP & Robustes Error-Handling",
|
|
"desc": "OOP 10 bis 12 abgeschlossen (Vererbung, Polymorphie & Exceptions).",
|
|
"xp_bonus": 300,
|
|
"kriterium": [
|
|
"lehrpfad_3_fortgeschrittenes_oop/10_vererbung_und_super",
|
|
"lehrpfad_3_fortgeschrittenes_oop/11_polymorphie_und_interfaces",
|
|
"lehrpfad_3_fortgeschrittenes_oop/12_exceptions_und_fehlerbehandlung"
|
|
]
|
|
},
|
|
"software_architektin": {
|
|
"id": "software_architektin",
|
|
"icon": "🏆",
|
|
"name": "Zertifikat: Master-Projekt Software Engineering",
|
|
"desc": "Master-Projekt abgeschlossen & vollwertige Anwendungsarchitektur gebaut!",
|
|
"xp_bonus": 500,
|
|
"kriterium": ["lehrpfad_3_fortgeschrittenes_oop/16_master_abschlussprojekt"]
|
|
}
|
|
}
|
|
|
|
|
|
class GamificationManager:
|
|
"""Verwaltet den persistenten Gamification-Status."""
|
|
|
|
def __init__(self):
|
|
self.state = self._load_state()
|
|
self.save_state()
|
|
|
|
def _default_state(self) -> Dict[str, Any]:
|
|
return {
|
|
"spieler_name": "IT-Entwickler",
|
|
"xp": 0,
|
|
"level": 1,
|
|
"titel": "Junior IT-Trainee I: Syntax & Basistypen",
|
|
"rang": "Stufe 1",
|
|
"geloeste_kapitel": [],
|
|
"bestandene_tests": 0,
|
|
"freigeschaltete_badges": [],
|
|
"streak_tage": 1,
|
|
"letzter_aktiver_tag": str(date.today()),
|
|
"historie": [],
|
|
"zuletzt_aktualisiert": datetime.now().isoformat()
|
|
}
|
|
|
|
def _load_state(self) -> Dict[str, Any]:
|
|
if DATA_JSON_PATH.exists():
|
|
try:
|
|
with open(DATA_JSON_PATH, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
return {**self._default_state(), **data}
|
|
except Exception:
|
|
pass
|
|
return self._default_state()
|
|
|
|
def save_state(self):
|
|
"""Speichert den Status in JSON und exportiert gleichzeitig gamification_data.js für den Browser."""
|
|
self.state["zuletzt_aktualisiert"] = datetime.now().isoformat()
|
|
|
|
# 1. JSON speichern
|
|
try:
|
|
with open(DATA_JSON_PATH, "w", encoding="utf-8") as f:
|
|
json.dump(self.state, f, indent=2, ensure_ascii=False)
|
|
except Exception as e:
|
|
print(f"Warnung beim Speichern von .gamification.json: {e}")
|
|
|
|
# 2. JS-Export für lokale Browser-Nutzung (CORS-Frei via file://)
|
|
try:
|
|
DATA_JS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
js_content = f"""// Automatisch generierter lokaler Gamification-Spielstand
|
|
window.GAMIFICATION_DATA = {json.dumps(self.state, indent=2, ensure_ascii=False)};
|
|
window.GAMIFICATION_BADGES_KATALOG = {json.dumps(BADGES_KATALOG, indent=2, ensure_ascii=False)};
|
|
window.GAMIFICATION_LEVEL_STUFEN = {json.dumps(LEVEL_STUFEN, indent=2, ensure_ascii=False)};
|
|
"""
|
|
with open(DATA_JS_PATH, "w", encoding="utf-8") as f:
|
|
f.write(js_content)
|
|
except Exception as e:
|
|
print(f"Warnung beim Exportieren von gamification_data.js: {e}")
|
|
|
|
def get_level_info(self, xp: int) -> Dict[str, Any]:
|
|
"""Berechnet Level, Titel und Fortschritt zur nächsten Stufe."""
|
|
for info in LEVEL_STUFEN:
|
|
if info["min_xp"] <= xp <= info["max_xp"]:
|
|
naechste_xp = info["max_xp"] + 1
|
|
xp_in_level = xp - info["min_xp"]
|
|
xp_fuer_level = naechste_xp - info["min_xp"]
|
|
prozent = min(100, int((xp_in_level / xp_fuer_level) * 100)) if xp_fuer_level > 0 else 100
|
|
return {
|
|
"level": info["level"],
|
|
"titel": info["titel"],
|
|
"rang": info["rang"],
|
|
"aktuelle_xp": xp,
|
|
"min_xp": info["min_xp"],
|
|
"max_xp": info["max_xp"],
|
|
"naechste_stufe_xp": naechste_xp,
|
|
"prozent": prozent
|
|
}
|
|
# Fallback höchstes Level
|
|
return {
|
|
"level": 10,
|
|
"titel": "Software-Architektin 🏆",
|
|
"rang": "Großmeisterin",
|
|
"aktuelle_xp": xp,
|
|
"min_xp": 6801,
|
|
"max_xp": 99999,
|
|
"naechste_stufe_xp": 99999,
|
|
"prozent": 100
|
|
}
|
|
|
|
def sync_mit_test_ergebnissen(self, bestandene_kapitel_ordner: List[str], anzahl_bestandene_tests: int) -> Dict[str, Any]:
|
|
"""
|
|
Gleicht die aktuellen Testergebnisse ab, vergibt XP und schaltet Badges frei.
|
|
Gibt ein Event-Dictionary für feierliche Terminal-Meldungen zurück.
|
|
"""
|
|
alte_geloeste = set(self.state.get("geloeste_kapitel", []))
|
|
neue_geloeste = set(bestandene_kapitel_ordner)
|
|
alte_badges = set(self.state.get("freigeschaltete_badges", []))
|
|
altes_level = self.state.get("level", 1)
|
|
|
|
neu_geloest = neue_geloeste - alte_geloeste
|
|
|
|
# Basis-XP Berechnung:
|
|
# 100 XP pro gelöstem Kapitel + 10 XP pro bestandenem Test
|
|
berechnete_xp = (len(neue_geloeste) * 100) + (anzahl_bestandene_tests * 10)
|
|
|
|
# Badges überprüfen und Bonus-XP addieren
|
|
neu_freigeschaltet_badges = []
|
|
for badge_id, badge in BADGES_KATALOG.items():
|
|
kriterien = badge["kriterium"]
|
|
if all(k in neue_geloeste for k in kriterien):
|
|
if badge_id not in alte_badges:
|
|
neu_freigeschaltet_badges.append(badge)
|
|
berechnete_xp += badge["xp_bonus"]
|
|
|
|
# Streak-Berechnung
|
|
heute = str(date.today())
|
|
letzter_tag = self.state.get("letzter_aktiver_tag", heute)
|
|
if letzter_tag != heute:
|
|
try:
|
|
diff = (date.today() - date.fromisoformat(letzter_tag)).days
|
|
if diff == 1:
|
|
self.state["streak_tage"] = self.state.get("streak_tage", 1) + 1
|
|
elif diff > 1:
|
|
self.state["streak_tage"] = 1
|
|
except Exception:
|
|
self.state["streak_tage"] = 1
|
|
self.state["letzter_aktiver_tag"] = heute
|
|
|
|
# Level-Update
|
|
lvl_info = self.get_level_info(berechnete_xp)
|
|
neues_level = lvl_info["level"]
|
|
|
|
# Status aktualisieren
|
|
self.state["xp"] = berechnete_xp
|
|
self.state["level"] = neues_level
|
|
self.state["titel"] = lvl_info["titel"]
|
|
self.state["rang"] = lvl_info["rang"]
|
|
self.state["geloeste_kapitel"] = list(neue_geloeste)
|
|
self.state["bestandene_tests"] = anzahl_bestandene_tests
|
|
self.state["freigeschaltete_badges"] = [
|
|
b_id for b_id, b in BADGES_KATALOG.items()
|
|
if all(k in neue_geloeste for k in b["kriterium"])
|
|
]
|
|
|
|
self.save_state()
|
|
|
|
return {
|
|
"neue_kapitel": list(neu_geloest),
|
|
"neue_badges": neu_freigeschaltet_badges,
|
|
"level_up": neues_level > altes_level,
|
|
"altes_level": altes_level,
|
|
"neues_level": neues_level,
|
|
"lvl_info": lvl_info,
|
|
"gesamt_xp": berechnete_xp
|
|
}
|
|
|
|
|
|
def report_single_chapter_result(kapitel_pfad_relativ: str, tests_erfolgreich: bool, tests_anzahl: int):
|
|
"""
|
|
Wird von einzelnen test_aufgabe.py Skripten aufgerufen, wenn sie direkt ausgeführt werden.
|
|
Aktualisiert den Spielstand und gibt motivierendes Feedback im Terminal aus.
|
|
"""
|
|
gm = GamificationManager()
|
|
aktuelle_geloeste = set(gm.state.get("geloeste_kapitel", []))
|
|
|
|
if tests_erfolgreich and tests_anzahl > 0:
|
|
war_bereits_geloest = kapitel_pfad_relativ in aktuelle_geloeste
|
|
aktuelle_geloeste.add(kapitel_pfad_relativ)
|
|
|
|
# Gesamtbestandene Tests schätzen/updaten
|
|
alte_tests = gm.state.get("bestandene_tests", 0)
|
|
neue_tests = alte_tests if war_bereits_geloest else alte_tests + tests_anzahl
|
|
|
|
events = gm.sync_mit_test_ergebnissen(list(aktuelle_geloeste), neue_tests)
|
|
lvl = events["lvl_info"]
|
|
|
|
print("\n" + "═" * 60)
|
|
print(f"🎉 KAPITEL BESTANDEN! ⭐ {lvl['titel']} (Level {lvl['level']})")
|
|
print(f"💎 Aktuelle XP: {events['gesamt_xp']} XP | Rang: {lvl['rang']}")
|
|
|
|
if events.get("level_up"):
|
|
print(f"🚀 LEVEL UP! Du bist aufgestiegen zu Level {lvl['level']}!")
|
|
|
|
if events.get("neue_badges"):
|
|
for b in events["neue_badges"]:
|
|
print(f"🏆 NEUE TROPHÄE: {b['icon']} {b['name']} (+{b['xp_bonus']} XP Bonus!)")
|
|
|
|
print(f"💡 Zeige deinen Fortschrittspass mit: python3 profil.py")
|
|
print("═" * 60 + "\n")
|
|
|
|
|
|
# Singleton-Instanz
|
|
manager = GamificationManager()
|