This commit is contained in:
team3
2026-06-24 09:25:14 +02:00
parent 3b1e84e17d
commit d389584b58
10 changed files with 251 additions and 212 deletions

View File

@@ -21,12 +21,40 @@ from textkit import _norm_titel
log = logging.getLogger("creator.lernen")
# Meilensteine je Baustein (Score 030). Form ergibt sich aus dem Score-Band:
# 06 Quiz, 712 Lückentext, 1318 Erklären, 1930 frei.
NOETIG = 6 # MiniGuide-Meilenstein → „absolviert" (Quiz-Band durch)
MASTERY = 12 # Guide-Meilenstein → „verstanden" (Lückentext-Band durch)
MEISTERN = 18 # ProGuide-Meilenstein → „gemeistert" (Erklären-Band durch, Thema fertig)
MAX_VOLL = 30 # FullGuide-Maximum (freie Formwahl ab 18)
# Lernstufen je Baustein — relativ zum cap (Floor in % des Maximal-Scores):
# grün=Anfänger 20% · blau=Fortgeschritten 40% · lila=Experte 60% · gold=Meister 100%.
# Prüfungsform ist immer zufällig (5 Formen); der cap skaliert mit der Stoffmenge.
STUFEN = (("anfaenger", 0.2), ("fortgeschritten", 0.4), ("experte", 0.6), ("meister", 1.0))
def cap_fuer(n_subs: int) -> int:
"""Maximal-Score eines Bausteins: 2 Punkte je relevantem Subbaustein, gedeckelt [10, 50]."""
return max(10, min(50, 2 * n_subs))
def _schwelle(p: float, cap: int) -> int:
return round(p * cap)
def stufe_aus_score(score: int, cap: int) -> str | None:
"""Höchste erreichte Stufe (None unter 20 %)."""
erreicht = None
for key, p in STUFEN:
if score >= _schwelle(p, cap):
erreicht = key
return erreicht
def progressiver_malus(basis: int, cap: int) -> int:
"""Fehler-Strafe nach Fortschritt: 020 %→0 · 2140 %1 · 4160 %2 · 61100 %3."""
pct = (basis / cap) if cap else 0.0
if pct <= 0.2:
return 0
if pct <= 0.4:
return -1
if pct <= 0.6:
return -2
return -3
CHAT_TIMEOUT = 240
PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfungs-Schritt
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger
@@ -54,30 +82,22 @@ NIVEAUS = {
def score_berechnen(basis: int, delta: int, floor: int, cap: int) -> int:
"""Neuer Score · driftfrei aus der Basis. `delta` liefert die Route je Prüfungsform
(Quiz +1/+3, Lückentext +1/+3, Erklären 1…+3; falsch 1). Auf [floor, cap] geklemmt.
floor = höchster erreichter Meilenstein (6/12/18); cap = Max des höchsten Formats.
Am Format-Deckel (basis ≥ cap) eingefroren: „Max" — weder + noch , bis das nächste
Format freischaltet. Bei cap 30 (FullGuide) bleibt 30 endgültig."""
(Quiz +1/+3, Lückentext +1/+3, Erklären 1…+3; falsch = progressiver_malus). Auf
[floor, cap] geklemmt. floor = höchste erreichte Stufenschwelle (kein Rückfall);
cap = 2× relevante Subbausteine [10, 50]. Am Deckel (basis ≥ cap) eingefroren („Max")."""
if basis >= cap:
return basis
return max(floor, min(cap, basis + delta))
def _zufall_malus(basis: int, delta: int) -> int:
"""Im Zufallband (Score ≥ MEISTERN=18, FullGuide) kostet jeder Fehler 3 statt 1 —
die 30 erreicht man nur mit Sicherheit. Richtige Antworten bleiben unverändert."""
return -3 if (basis >= MEISTERN and delta < 0) else delta
def floor_aus_meilensteinen(absolviert: bool, verstanden: bool, gemeistert: bool) -> int:
"""Untergrenze des Scores: ein erreichter Meilenstein hält (kein Rückfall ins Band davor)."""
if gemeistert:
return MEISTERN
if verstanden:
return MASTERY
if absolviert:
return NOETIG
return 0
def floor_aus_score(basis: int, cap: int) -> int:
"""Untergrenze: höchste erreichte Stufenschwelle — kein Rückfall unter die Stufe."""
floor = 0
for _, p in STUFEN:
s = _schwelle(p, cap)
if basis >= s:
floor = s
return floor
def _transcript(messages: list[dict]) -> str: