diff --git a/backend/lernen.py b/backend/lernen.py index cd5d6dd..862f68a 100644 --- a/backend/lernen.py +++ b/backend/lernen.py @@ -54,8 +54,12 @@ 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/−2, Lückentext +1/−1, Erklären −1…+3). Auf [floor, cap] geklemmt. - floor = höchster erreichter Meilenstein (10/20/30); cap = Max des höchsten Formats.""" + (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.""" + if basis >= cap: + return basis return max(floor, min(cap, basis + delta)) diff --git a/frontend/src/components/BausteinPanel.vue b/frontend/src/components/BausteinPanel.vue index 828f8e3..5283078 100644 --- a/frontend/src/components/BausteinPanel.vue +++ b/frontend/src/components/BausteinPanel.vue @@ -32,21 +32,39 @@ const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolv // Ohne Frage-Muster (alter Baustein) gibt es nur Erklären — Quiz/Lückentext brauchen die Saat. const MODI = ['quiz_leicht', 'quiz_schwer', 'lueck_leicht', 'lueck_schwer', 'erklaeren'] const score = computed(() => st.value.gute_antworten || 0) -const schwerWahl = ref(false) // leicht/schwer unter Score 30 (ALT+E) -const freiModus = ref('quiz_leicht') // gewählter Modus ab Score 30 (ALT+E zyklisch) +const schwerWahl = ref(false) // leicht/schwer in den Bändern unter 19 (ALT+E) +// Form wechselt NACH dem Meilenstein (>): Quiz 0–6, Lückentext 7–12, Erklären 13–18, +// Zufall 19–30 (jede Frage ein zufälliger Modus). 'zufall' = Platzhalter; das gezogene +// Objekt trägt die echte Form/Schwierigkeit. const aktiverModus = computed(() => { if (!musterModus.value) return 'erklaeren' - if (score.value >= GEMEISTERT) return freiModus.value - if (score.value >= VERSTANDEN) return 'erklaeren' - if (score.value >= ABSOLVIERT) return schwerWahl.value ? 'lueck_schwer' : 'lueck_leicht' + if (score.value > GEMEISTERT) return 'zufall' + if (score.value > VERSTANDEN) return 'erklaeren' + if (score.value > ABSOLVIERT) return schwerWahl.value ? 'lueck_schwer' : 'lueck_leicht' return schwerWahl.value ? 'quiz_schwer' : 'quiz_leicht' }) -const aktiveForm = computed(() => aktiverModus.value.startsWith('quiz') ? 'quiz' : aktiverModus.value.startsWith('lueck') ? 'lueckentext' : 'erklaeren') +const imZufall = computed(() => aktiverModus.value === 'zufall') +// Reines Band aus dem aktuellen Score (ohne Schwierigkeit) — für den Abschnittswechsel. +const aktuellesBand = computed(() => { + if (score.value > GEMEISTERT) return 'zufall' + if (score.value > VERSTANDEN) return 'erklaeren' + if (score.value > ABSOLVIERT) return 'lueckentext' + return 'quiz' +}) +const aktiveForm = computed(() => { + const m = aktiverModus.value + if (m === 'zufall') return 'quiz' // Platzhalter fürs idle-Widget; Objekt überschreibt + return m.startsWith('quiz') ? 'quiz' : m.startsWith('lueck') ? 'lueckentext' : 'erklaeren' +}) const schwer = computed(() => aktiverModus.value.endsWith('_schwer')) // Angezeigte Form: folgt der aktiven Form, friert aber ein, solange eine gezogene Frage // sichtbar ist (sonst verschwindet das Ergebnis, wenn die Antwort eine Bandgrenze überschreitet). const anzeigeForm = ref(null) const zeigeForm = computed(() => anzeigeForm.value || aktiveForm.value) +// Band, in dem die gezeigte Frage gezogen wurde. Weicht es vom aktuellen Band ab, hat die +// letzte Antwort einen Meilenstein überschritten → Button „Zum nächsten Abschnitt". +const anzeigeBand = ref(null) +const abschnittWechsel = computed(() => anzeigeBand.value && anzeigeBand.value !== aktuellesBand.value) // Schwierigkeit des gerade gezeigten Widgets (eingefroren) bzw. der aktiven Wahl. const aktSchwer = computed(() => { if (zeigeForm.value === 'quiz' && quizAktuell.value) return quizAktuell.value.schwer @@ -60,12 +78,13 @@ const modusRegel = computed(() => { }) // Ende des aktuellen Bands (nächster Meilenstein) und ob der Score am Format-Deckel klebt. const bandZiel = computed(() => { - if (score.value >= GEMEISTERT) return VOLL - if (score.value >= VERSTANDEN) return GEMEISTERT - if (score.value >= ABSOLVIERT) return VERSTANDEN + if (score.value > GEMEISTERT) return VOLL + if (score.value > VERSTANDEN) return GEMEISTERT + if (score.value > ABSOLVIERT) return VERSTANDEN return ABSOLVIERT }) -const amDeckel = computed(() => score.value >= props.cap && props.cap < VOLL) +// Am Format-Max: Score eingefroren („Max"). Gilt auch bei cap 30 (FullGuide, endgültig). +const amDeckel = computed(() => score.value >= props.cap) const FORM_NAME = { quiz: 'Quiz', lueckentext: 'Lückentext', erklaeren: 'Erklären' } const NAECHSTES_FORMAT = { 6: 'Guide', 12: 'ProGuide', 18: 'FullGuide' } @@ -230,13 +249,19 @@ function musterNehmen() { return musterPool.value.shift() } -// Pool-Schlüssel = Form + Schwierigkeit (Quiz leicht/schwer ziehen verschiedene Fragen). -const poolKey = computed(() => `${aktiveForm.value}/${schwer.value}`) +// Pool-Schlüssel = Form + Schwierigkeit; im Zufallband ein gemischter Pool ('zufall'). +const poolKey = computed(() => imZufall.value ? 'zufall' : `${aktiveForm.value}/${schwer.value}`) + +// Modus der nächsten Frage: im Zufallband (Score 19–30) zufällig aus allen 5 Modi. +function naechsterModus() { + return imZufall.value ? MODI[Math.floor(Math.random() * MODI.length)] : aktiverModus.value +} // Ein Frage-Objekt des aktiven Modus generieren (aus gezogener Saat; Erklären hat Fallback). function frageEinzeln() { - const form = aktiveForm.value - const istSchwer = schwer.value + const modus = naechsterModus() + const form = modus.startsWith('quiz') ? 'quiz' : modus.startsWith('lueck') ? 'lueckentext' : 'erklaeren' + const istSchwer = modus.endsWith('_schwer') const muster = musterNehmen() const basis = { topic: props.topic, baustein: props.baustein, section: props.section, @@ -305,6 +330,7 @@ async function poolNehmen() { function frageObjZeigen(obj) { pruefFehler.value = '' anzeigeForm.value = obj.form // Anzeige an die gezogene Frage binden (Form einfrieren) + anzeigeBand.value = aktuellesBand.value // Band zum Zieh-Zeitpunkt (vor der Antwort) if (obj.form === 'quiz') { quizAktuell.value = { frage: obj.frage, optionen: obj.optionen, schwer: obj.schwer, gewaehlt: [], fertig: false, punkte: null, bewertung: null, feedback: '' } } else if (obj.form === 'lueckentext') { @@ -336,6 +362,18 @@ async function naechsteFrageZeigen() { } const frageAnfordern = naechsteFrageZeigen +// Meilenstein überschritten: Einfrieren lösen (Ergebnis war sichtbar), ohne neue Frage zu +// ziehen → Leerlauf des neuen Bands („Frage anfordern"). +function zumNaechstenAbschnitt() { + anzeigeForm.value = null + anzeigeBand.value = null + quizAktuell.value = null + lueckAktuell.value = null + pruefMessages.value = [] // alten Erklären-Verlauf verwerfen (neuer Abschnitt) + pruefPhase.value = 'idle' + pruefFehler.value = '' +} + // --- Quiz: leicht = Single-Select (genau 1) · schwer = Multi-Select. Deterministisch. --- function quizToggle(i) { const q = quizAktuell.value @@ -528,17 +566,14 @@ onUnmounted(() => { window.removeEventListener('keydown', onPruefKey) }) -// ALT+E: unter 30 leicht↔schwer der Form (Quiz/Lückentext); ab 30 zyklisch durch alle 5 Modi. +// ALT+E: leicht↔schwer der Form (Quiz/Lückentext). Im Erklären- und Zufallband ohne Wirkung. function variantWechseln() { - const vorher = aktiverModus.value - if (score.value >= GEMEISTERT) { - freiModus.value = MODI[(MODI.indexOf(freiModus.value) + 1) % MODI.length] - } else if (aktiveForm.value === 'quiz' || aktiveForm.value === 'lueckentext') { - schwerWahl.value = !schwerWahl.value - } - if (aktiverModus.value === vorher) return // nichts geändert (z.B. Erklären-Band) + if (imZufall.value) return + if (aktiveForm.value !== 'quiz' && aktiveForm.value !== 'lueckentext') return + schwerWahl.value = !schwerWahl.value // Einfrieren + altes Widget verwerfen → frische Frage der neuen Variante anfordern. anzeigeForm.value = null + anzeigeBand.value = null quizAktuell.value = null lueckAktuell.value = null } @@ -557,15 +592,20 @@ function eingabeFeld() { if (zeigeForm.value === 'erklaeren' && pruefPhase.value === 'frage_offen') return pruefInputEl.value return null } -// Primäre Aktion je Form: keine Frage → anfordern · offen → antworten · fertig → nächste. +// Nach einer Antwort: bei Meilenstein-Überschreitung in den neuen Abschnitt, sonst nächste Frage. +function weiterAktion() { + if (abschnittWechsel.value) zumNaechstenAbschnitt() + else naechsteFrageZeigen() +} +// Primäre Aktion je Form: keine Frage → anfordern · offen → antworten · fertig → weiter. function primaerAktion() { if (pruefLoading.value) return const f = zeigeForm.value - if (f === 'quiz') { if (!quizAktuell.value) frageAnfordern(); else if (!quizAktuell.value.fertig) quizAntworten(); else naechsteFrageZeigen(); return } - if (f === 'lueckentext') { if (!lueckAktuell.value) frageAnfordern(); else if (!lueckAktuell.value.fertig) lueckAntworten(); else naechsteFrageZeigen(); return } + if (f === 'quiz') { if (!quizAktuell.value) frageAnfordern(); else if (!quizAktuell.value.fertig) quizAntworten(); else weiterAktion(); return } + if (f === 'lueckentext') { if (!lueckAktuell.value) frageAnfordern(); else if (!lueckAktuell.value.fertig) lueckAntworten(); else weiterAktion(); return } if (pruefPhase.value === 'idle') frageAnfordern() else if (pruefPhase.value === 'frage_offen') { if (pruefInput.value.trim()) antwortAbgeben() } - else naechsteFrageZeigen() + else weiterAktion() } // 1–4 (nackt): Option im Wahl-Widget toggeln. Alt allein: ins Eingabefeld springen. @@ -647,15 +687,12 @@ function onPruefKey(e) {
- {{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} — Max für dieses Format. Erstelle den {{ NAECHSTES_FORMAT[cap] }}, um weiterzukommen. - {{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ score >= GEMEISTERT ? VOLL : bandZiel }} · {{ modusRegel }} · Alt+E: {{ score >= GEMEISTERT ? 'Modus' : 'Schwierigkeit' }} + {{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} — Max. Erstelle den {{ NAECHSTES_FORMAT[cap] }} für mehr. + {{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ bandZiel }} · {{ modusRegel }} · zufällig · Alt+E: Schwierigkeit
- - - - - + +