update
This commit is contained in:
@@ -54,8 +54,12 @@ NIVEAUS = {
|
|||||||
|
|
||||||
def score_berechnen(basis: int, delta: int, floor: int, cap: int) -> int:
|
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
|
"""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.
|
(Quiz +1/+3, Lückentext +1/+3, Erklären −1…+3; falsch −1). Auf [floor, cap] geklemmt.
|
||||||
floor = höchster erreichter Meilenstein (10/20/30); cap = Max des höchsten Formats."""
|
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))
|
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.
|
// 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 MODI = ['quiz_leicht', 'quiz_schwer', 'lueck_leicht', 'lueck_schwer', 'erklaeren']
|
||||||
const score = computed(() => st.value.gute_antworten || 0)
|
const score = computed(() => st.value.gute_antworten || 0)
|
||||||
const schwerWahl = ref(false) // leicht/schwer unter Score 30 (ALT+E)
|
const schwerWahl = ref(false) // leicht/schwer in den Bändern unter 19 (ALT+E)
|
||||||
const freiModus = ref('quiz_leicht') // gewählter Modus ab Score 30 (ALT+E zyklisch)
|
// 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(() => {
|
const aktiverModus = computed(() => {
|
||||||
if (!musterModus.value) return 'erklaeren'
|
if (!musterModus.value) return 'erklaeren'
|
||||||
if (score.value >= GEMEISTERT) return freiModus.value
|
if (score.value > GEMEISTERT) return 'zufall'
|
||||||
if (score.value >= VERSTANDEN) return 'erklaeren'
|
if (score.value > VERSTANDEN) return 'erklaeren'
|
||||||
if (score.value >= ABSOLVIERT) return schwerWahl.value ? 'lueck_schwer' : 'lueck_leicht'
|
if (score.value > ABSOLVIERT) return schwerWahl.value ? 'lueck_schwer' : 'lueck_leicht'
|
||||||
return schwerWahl.value ? 'quiz_schwer' : 'quiz_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'))
|
const schwer = computed(() => aktiverModus.value.endsWith('_schwer'))
|
||||||
// Angezeigte Form: folgt der aktiven Form, friert aber ein, solange eine gezogene Frage
|
// 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).
|
// sichtbar ist (sonst verschwindet das Ergebnis, wenn die Antwort eine Bandgrenze überschreitet).
|
||||||
const anzeigeForm = ref(null)
|
const anzeigeForm = ref(null)
|
||||||
const zeigeForm = computed(() => anzeigeForm.value || aktiveForm.value)
|
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.
|
// Schwierigkeit des gerade gezeigten Widgets (eingefroren) bzw. der aktiven Wahl.
|
||||||
const aktSchwer = computed(() => {
|
const aktSchwer = computed(() => {
|
||||||
if (zeigeForm.value === 'quiz' && quizAktuell.value) return quizAktuell.value.schwer
|
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.
|
// Ende des aktuellen Bands (nächster Meilenstein) und ob der Score am Format-Deckel klebt.
|
||||||
const bandZiel = computed(() => {
|
const bandZiel = computed(() => {
|
||||||
if (score.value >= GEMEISTERT) return VOLL
|
if (score.value > GEMEISTERT) return VOLL
|
||||||
if (score.value >= VERSTANDEN) return GEMEISTERT
|
if (score.value > VERSTANDEN) return GEMEISTERT
|
||||||
if (score.value >= ABSOLVIERT) return VERSTANDEN
|
if (score.value > ABSOLVIERT) return VERSTANDEN
|
||||||
return ABSOLVIERT
|
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 FORM_NAME = { quiz: 'Quiz', lueckentext: 'Lückentext', erklaeren: 'Erklären' }
|
||||||
const NAECHSTES_FORMAT = { 6: 'Guide', 12: 'ProGuide', 18: 'FullGuide' }
|
const NAECHSTES_FORMAT = { 6: 'Guide', 12: 'ProGuide', 18: 'FullGuide' }
|
||||||
|
|
||||||
@@ -230,13 +249,19 @@ function musterNehmen() {
|
|||||||
return musterPool.value.shift()
|
return musterPool.value.shift()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pool-Schlüssel = Form + Schwierigkeit (Quiz leicht/schwer ziehen verschiedene Fragen).
|
// Pool-Schlüssel = Form + Schwierigkeit; im Zufallband ein gemischter Pool ('zufall').
|
||||||
const poolKey = computed(() => `${aktiveForm.value}/${schwer.value}`)
|
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).
|
// Ein Frage-Objekt des aktiven Modus generieren (aus gezogener Saat; Erklären hat Fallback).
|
||||||
function frageEinzeln() {
|
function frageEinzeln() {
|
||||||
const form = aktiveForm.value
|
const modus = naechsterModus()
|
||||||
const istSchwer = schwer.value
|
const form = modus.startsWith('quiz') ? 'quiz' : modus.startsWith('lueck') ? 'lueckentext' : 'erklaeren'
|
||||||
|
const istSchwer = modus.endsWith('_schwer')
|
||||||
const muster = musterNehmen()
|
const muster = musterNehmen()
|
||||||
const basis = {
|
const basis = {
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||||
@@ -305,6 +330,7 @@ async function poolNehmen() {
|
|||||||
function frageObjZeigen(obj) {
|
function frageObjZeigen(obj) {
|
||||||
pruefFehler.value = ''
|
pruefFehler.value = ''
|
||||||
anzeigeForm.value = obj.form // Anzeige an die gezogene Frage binden (Form einfrieren)
|
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') {
|
if (obj.form === 'quiz') {
|
||||||
quizAktuell.value = { frage: obj.frage, optionen: obj.optionen, schwer: obj.schwer, gewaehlt: [], fertig: false, punkte: null, bewertung: null, feedback: '' }
|
quizAktuell.value = { frage: obj.frage, optionen: obj.optionen, schwer: obj.schwer, gewaehlt: [], fertig: false, punkte: null, bewertung: null, feedback: '' }
|
||||||
} else if (obj.form === 'lueckentext') {
|
} else if (obj.form === 'lueckentext') {
|
||||||
@@ -336,6 +362,18 @@ async function naechsteFrageZeigen() {
|
|||||||
}
|
}
|
||||||
const frageAnfordern = 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. ---
|
// --- Quiz: leicht = Single-Select (genau 1) · schwer = Multi-Select. Deterministisch. ---
|
||||||
function quizToggle(i) {
|
function quizToggle(i) {
|
||||||
const q = quizAktuell.value
|
const q = quizAktuell.value
|
||||||
@@ -528,17 +566,14 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener('keydown', onPruefKey)
|
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() {
|
function variantWechseln() {
|
||||||
const vorher = aktiverModus.value
|
if (imZufall.value) return
|
||||||
if (score.value >= GEMEISTERT) {
|
if (aktiveForm.value !== 'quiz' && aktiveForm.value !== 'lueckentext') return
|
||||||
freiModus.value = MODI[(MODI.indexOf(freiModus.value) + 1) % MODI.length]
|
|
||||||
} else if (aktiveForm.value === 'quiz' || aktiveForm.value === 'lueckentext') {
|
|
||||||
schwerWahl.value = !schwerWahl.value
|
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.
|
// Einfrieren + altes Widget verwerfen → frische Frage der neuen Variante anfordern.
|
||||||
anzeigeForm.value = null
|
anzeigeForm.value = null
|
||||||
|
anzeigeBand.value = null
|
||||||
quizAktuell.value = null
|
quizAktuell.value = null
|
||||||
lueckAktuell.value = null
|
lueckAktuell.value = null
|
||||||
}
|
}
|
||||||
@@ -557,15 +592,20 @@ function eingabeFeld() {
|
|||||||
if (zeigeForm.value === 'erklaeren' && pruefPhase.value === 'frage_offen') return pruefInputEl.value
|
if (zeigeForm.value === 'erklaeren' && pruefPhase.value === 'frage_offen') return pruefInputEl.value
|
||||||
return null
|
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() {
|
function primaerAktion() {
|
||||||
if (pruefLoading.value) return
|
if (pruefLoading.value) return
|
||||||
const f = zeigeForm.value
|
const f = zeigeForm.value
|
||||||
if (f === 'quiz') { if (!quizAktuell.value) frageAnfordern(); else if (!quizAktuell.value.fertig) quizAntworten(); 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 naechsteFrageZeigen(); return }
|
if (f === 'lueckentext') { if (!lueckAktuell.value) frageAnfordern(); else if (!lueckAktuell.value.fertig) lueckAntworten(); else weiterAktion(); return }
|
||||||
if (pruefPhase.value === 'idle') frageAnfordern()
|
if (pruefPhase.value === 'idle') frageAnfordern()
|
||||||
else if (pruefPhase.value === 'frage_offen') { if (pruefInput.value.trim()) antwortAbgeben() }
|
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.
|
// 1–4 (nackt): Option im Wahl-Widget toggeln. Alt allein: ins Eingabefeld springen.
|
||||||
@@ -647,15 +687,12 @@ function onPruefKey(e) {
|
|||||||
<!-- Prüfung: gesteuerter Dialog -->
|
<!-- Prüfung: gesteuerter Dialog -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p class="bp-hint">
|
<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-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 }}/{{ score >= GEMEISTERT ? VOLL : bandZiel }} · {{ modusRegel }} · <span class="bp-hint-key">Alt+E: {{ score >= GEMEISTERT ? 'Modus' : 'Schwierigkeit' }}</span></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>
|
</p>
|
||||||
|
|
||||||
<!-- Am Format-Deckel: kein Widget, nur der Hinweis oben. -->
|
<!-- Quiz: Frage + Mehrfachauswahl (Widget bleibt auch am Deckel — Üben ohne Punkte) -->
|
||||||
<template v-if="amDeckel"></template>
|
<template v-if="zeigeForm === 'quiz'">
|
||||||
|
|
||||||
<!-- Quiz: Frage + Mehrfachauswahl -->
|
|
||||||
<template v-else-if="zeigeForm === 'quiz'">
|
|
||||||
<div v-if="!quizAktuell" class="bp-actions">
|
<div v-if="!quizAktuell" class="bp-actions">
|
||||||
<button v-if="pruefLoading" class="bp-action cancel" title="ESC" @click="pruefAbbrechen">Abbrechen</button>
|
<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>
|
<button v-else class="bp-action primary" @click="frageAnfordern">Frage anfordern</button>
|
||||||
@@ -681,7 +718,8 @@ function onPruefKey(e) {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="bp-niveau" :class="quizAktuell.bewertung">{{ punkteLabel(quizAktuell.punkte) }}</span>
|
<span class="bp-niveau" :class="quizAktuell.bewertung">{{ punkteLabel(quizAktuell.punkte) }}</span>
|
||||||
<span class="bp-form-feedback">{{ quizAktuell.feedback }}</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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -725,7 +763,8 @@ function onPruefKey(e) {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="bp-niveau" :class="lueckAktuell.bewertung">{{ punkteLabel(lueckAktuell.punkte) }}</span>
|
<span class="bp-niveau" :class="lueckAktuell.bewertung">{{ punkteLabel(lueckAktuell.punkte) }}</span>
|
||||||
<span class="bp-form-feedback">{{ lueckAktuell.feedback }}</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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<button v-if="hatDiskussion" class="bp-action" title="Alt+3" @click="verlaufBewerten"><span class="bp-kbd">3</span>Verlauf bewerten</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user