update
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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') {
|
||||
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()
|
||||
}
|
||||
|
||||
// 1–4 (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>
|
||||
|
||||
Reference in New Issue
Block a user