This commit is contained in:
team3
2026-06-21 16:22:13 +02:00
parent 76ae2161e1
commit 89a853981d
2 changed files with 82 additions and 38 deletions

View File

@@ -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))

View File

@@ -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 06, Lückentext 712, Erklären 1318,
// Zufall 1930 (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 1930) 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') {
if (imZufall.value) return
if (aktiveForm.value !== 'quiz' && aktiveForm.value !== 'lueckentext') return
schwerWahl.value = !schwerWahl.value
}
if (aktiverModus.value === vorher) return // nichts geändert (z.B. Erklären-Band)
// 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()
}
// 14 (nackt): Option im Wahl-Widget toggeln. Alt allein: ins Eingabefeld springen.
@@ -647,15 +687,12 @@ function onPruefKey(e) {
<!-- Prüfung: gesteuerter Dialog -->
<div v-else>
<p class="bp-hint">
<template v-if="amDeckel">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} Max für dieses Format. Erstelle den {{ NAECHSTES_FORMAT[cap] }}, um weiterzukommen.</template>
<template v-else>{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ score >= GEMEISTERT ? VOLL : bandZiel }} · {{ modusRegel }} · <span class="bp-hint-key">Alt+E: {{ score >= GEMEISTERT ? 'Modus' : 'Schwierigkeit' }}</span></template>
<template v-if="amDeckel">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} — <strong>Max</strong>.<template v-if="cap < VOLL"> Erstelle den {{ NAECHSTES_FORMAT[cap] }} für mehr.</template></template>
<template v-else>{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ bandZiel }} · {{ modusRegel }}<span v-if="imZufall" class="bp-hint-key"> · zufällig</span><span v-else-if="zeigeForm !== 'erklaeren'" class="bp-hint-key"> · Alt+E: Schwierigkeit</span></template>
</p>
<!-- Am Format-Deckel: kein Widget, nur der Hinweis oben. -->
<template v-if="amDeckel"></template>
<!-- Quiz: Frage + Mehrfachauswahl -->
<template v-else-if="zeigeForm === 'quiz'">
<!-- Quiz: Frage + Mehrfachauswahl (Widget bleibt auch am Deckel Üben ohne Punkte) -->
<template v-if="zeigeForm === 'quiz'">
<div v-if="!quizAktuell" class="bp-actions">
<button v-if="pruefLoading" class="bp-action cancel" title="ESC" @click="pruefAbbrechen">Abbrechen</button>
<button v-else class="bp-action primary" @click="frageAnfordern">Frage anfordern</button>
@@ -681,7 +718,8 @@ function onPruefKey(e) {
<template v-else>
<span class="bp-niveau" :class="quizAktuell.bewertung">{{ punkteLabel(quizAktuell.punkte) }}</span>
<span class="bp-form-feedback">{{ quizAktuell.feedback }}</span>
<button class="bp-action primary" @click="naechsteFrageZeigen">Nächste</button>
<button v-if="abschnittWechsel" class="bp-action primary" @click="zumNaechstenAbschnitt">Zum nächsten Abschnitt</button>
<button v-else class="bp-action primary" @click="naechsteFrageZeigen">Nächste</button>
</template>
</div>
</div>
@@ -725,7 +763,8 @@ function onPruefKey(e) {
<template v-else>
<span class="bp-niveau" :class="lueckAktuell.bewertung">{{ punkteLabel(lueckAktuell.punkte) }}</span>
<span class="bp-form-feedback">{{ lueckAktuell.feedback }}</span>
<button class="bp-action primary" @click="naechsteFrageZeigen">Nächste</button>
<button v-if="abschnittWechsel" class="bp-action primary" @click="zumNaechstenAbschnitt">Zum nächsten Abschnitt</button>
<button v-else class="bp-action primary" @click="naechsteFrageZeigen">Nächste</button>
</template>
</div>
</div>
@@ -772,7 +811,8 @@ function onPruefKey(e) {
<button v-if="hatDiskussion" class="bp-action" title="Alt+3" @click="verlaufBewerten"><span class="bp-kbd">3</span>Verlauf bewerten</button>
</template>
<template v-else>
<button class="bp-action primary" title="Alt+2" @click="naechsteFrageZeigen"><span class="bp-kbd">2</span>Nächste Frage</button>
<button v-if="abschnittWechsel" class="bp-action primary" title="Alt+2" @click="zumNaechstenAbschnitt"><span class="bp-kbd">2</span>Zum nächsten Abschnitt</button>
<button v-else class="bp-action primary" title="Alt+2" @click="naechsteFrageZeigen"><span class="bp-kbd">2</span>Nächste Frage</button>
</template>
</div>
</template>