Enhance Phrasen-Matcher: add Kernstoff/All/Zusatz filter and strict 'falsch ist falsch' rules with no trial-and-error cheating

This commit is contained in:
harald committed 2026-09-28 16:51:31 +02:00
1 parent 64dfd30853
commit eb0c464112
2 files changed
+221 -79

No files matched your search

+220 -78
View File
@@ -720,21 +720,39 @@
<!-- VIEW 3: PHRASEN-MATCHER (Mini-Game) -->
<!-- ========================================== -->
<section id="view-matcher" class="hidden space-y-4 sm:space-y-6">
<div class="bg-white p-4 sm:p-5 rounded-2xl border border-slate-200 shadow-sm flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
<div class="bg-white p-4 sm:p-5 rounded-2xl border border-slate-200 shadow-sm flex flex-col lg:flex-row justify-between items-start lg:items-center gap-3 sm:gap-4">
<div>
<h2 class="text-base sm:text-xl font-extrabold text-slate-900 flex items-center gap-2">
<i class="fa-solid fa-puzzle-piece text-indigo-600"></i>
<span>Phrasen-Matcher (Schnell-Zuordnung)</span>
<span>Phrasen-Matcher (Direkt-Zuordnung)</span>
</h2>
<p class="text-xs sm:text-sm text-slate-500 mt-0.5">Tippe zuerst auf ein Zitat und anschließend auf das passende Stilmittel.</p>
<p class="text-xs sm:text-sm text-slate-500 mt-0.5">
Wähle ein Zitat und tippe auf das passende Stilmittel. <strong>Achtung: Nur 1 Versuch pro Paar – falsch ist falsch!</strong>
</p>
</div>
<div class="flex items-center gap-2.5 w-full sm:w-auto justify-between sm:justify-end">
<div class="px-3.5 py-1.5 bg-emerald-50 text-emerald-800 border border-emerald-200 rounded-xl text-xs sm:text-sm font-bold">
Gelöst: <span id="matchScore">0</span> / 5 Paare
<div class="flex flex-wrap items-center gap-2.5 w-full lg:w-auto justify-between lg:justify-end">
<!-- Selection Filter: Kernstoff (bevorzugt) | Alle | Zusatz -->
<div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold">
<button id="matcherFilterKernBtn" onclick="setMatcherFilter('highlighted')" class="px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition">🌟 Kernstoff (17)</button>
<button id="matcherFilterAllBtn" onclick="setMatcherFilter('all')" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">Alle (23)</button>
<button id="matcherFilterZusatzBtn" onclick="setMatcherFilter('zusatz')" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">🔍 Zusatz (6)</button>
</div>
<!-- Score & Mistakes Badges -->
<div class="flex items-center gap-2 text-xs sm:text-sm font-bold">
<div class="px-3 py-1.5 bg-emerald-50 text-emerald-800 border border-emerald-200 rounded-xl flex items-center gap-1.5">
<i class="fa-solid fa-check text-emerald-600"></i>
<span>Richtig: <span id="matchScore">0</span> / 5</span>
</div>
<div class="px-3 py-1.5 bg-rose-50 text-rose-800 border border-rose-200 rounded-xl flex items-center gap-1.5">
<i class="fa-solid fa-xmark text-rose-600"></i>
<span>Fehler: <span id="matchFailScore">0</span></span>
</div>
<button onclick="initMatcherGame()" title="Neu mischen" class="p-2 sm:px-3 sm:py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl font-bold transition flex items-center gap-1">
<i class="fa-solid fa-arrows-rotate"></i>
</button>
</div>
<button onclick="initMatcherGame()" class="px-3.5 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl text-xs sm:text-sm font-bold transition flex items-center gap-1.5">
<i class="fa-solid fa-arrows-rotate"></i> Neu mischen
</button>
</div>
</div>
@@ -757,13 +775,22 @@
</div>
</div>
<div id="matcherWinBanner" class="hidden bg-emerald-500 text-white p-6 sm:p-8 rounded-3xl text-center space-y-3 shadow-xl max-w-md mx-auto">
<div class="text-4xl"><i class="fa-solid fa-circle-check"></i></div>
<h4 class="text-2xl font-black">Perfekt zugeordnet!</h4>
<p class="text-sm sm:text-base text-emerald-100">Du hast alle 5 Paare fehlerfrei zusammengeführt.</p>
<button onclick="initMatcherGame()" class="px-6 py-3 rounded-2xl bg-white text-emerald-800 font-black text-base shadow-md hover:bg-emerald-50 transition">
Nächste Runde spielen
</button>
<!-- Matcher Result Screen (Fehler & Richtig) -->
<div id="matcherResultBanner" class="hidden text-white p-6 sm:p-8 rounded-3xl text-center space-y-4 shadow-xl max-w-md mx-auto transition-all">
<div id="matcherResultIcon" class="text-4xl"><i class="fa-solid fa-circle-check"></i></div>
<div>
<h4 id="matcherResultTitle" class="text-2xl font-black">Perfekt zugeordnet!</h4>
<p id="matcherResultSubtitle" class="text-sm sm:text-base text-white/90 mt-1">Du hast alle 5 Paare fehlerfrei zusammengeführt.</p>
</div>
<div class="flex flex-col sm:flex-row gap-2.5 justify-center pt-2">
<button onclick="initMatcherGame()" class="px-6 py-3 rounded-2xl bg-white text-slate-900 font-extrabold text-sm sm:text-base shadow hover:bg-slate-100 transition">
Nächste Runde
</button>
<button id="matcherWeakspotsBtn" onclick="switchTab('quiz'); startWeakSpotsQuiz();" class="hidden px-5 py-3 rounded-2xl bg-rose-600 hover:bg-rose-700 text-white font-extrabold text-sm transition flex items-center justify-center gap-1.5">
<i class="fa-solid fa-triangle-exclamation"></i>
<span>Fehler im Quiz üben</span>
</button>
</div>
</div>
</section>
@@ -1877,10 +1904,6 @@
let isQuestionAnswered = false;
let currentQuizMode = 'highlighted';
// Matcher Game State
let matcherSelectedPhrase = null;
let matcherSelectedTerm = null;
let matcherMatchedCount = 0;
let currentMatcherPairs = [];
// Glossary State
@@ -2917,19 +2940,68 @@
repeatBtn.classList.add('hidden');
}
}
// MATCHER GAME
// ==========================================
// PHRASEN-MATCHER GAME ENGINE ("Falsch ist falsch")
// ==========================================
let matcherFilter = 'highlighted'; // 'highlighted' (bevorzugt) | 'all' | 'zusatz'
let matcherSelectedPhrase = null;
let matcherSelectedTerm = null;
let matcherCorrectCount = 0;
let matcherFailedCount = 0;
function setMatcherFilter(filter) {
matcherFilter = filter;
['matcherFilterKernBtn', 'matcherFilterAllBtn', 'matcherFilterZusatzBtn'].forEach(id => {
const el = document.getElementById(id);
if (el) el.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold';
});
const activeMap = {
'highlighted': 'matcherFilterKernBtn',
'all': 'matcherFilterAllBtn',
'zusatz': 'matcherFilterZusatzBtn'
};
const activeEl = document.getElementById(activeMap[filter]);
if (activeEl) {
activeEl.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition';
}
initMatcherGame();
}
function initMatcherGame() {
matcherSelectedPhrase = null;
matcherSelectedTerm = null;
matcherMatchedCount = 0;
matcherCorrectCount = 0;
matcherFailedCount = 0;
document.getElementById('matchScore').innerText = '0';
document.getElementById('matcherWinBanner').classList.add('hidden');
document.getElementById('matchFailScore').innerText = '0';
document.getElementById('matcherResultBanner').classList.add('hidden');
const shuffled = [...STILMITTEL_DATA].sort(() => Math.random() - 0.5).slice(0, 5);
let pool = STILMITTEL_DATA;
if (matcherFilter === 'highlighted') {
pool = STILMITTEL_DATA.filter(x => x.highlighted);
} else if (matcherFilter === 'zusatz') {
pool = STILMITTEL_DATA.filter(x => !x.highlighted);
}
if (pool.length < 5) pool = [...STILMITTEL_DATA];
const shuffled = [...pool].sort(() => Math.random() - 0.5).slice(0, 5);
currentMatcherPairs = shuffled;
const phrases = shuffled.map(x => ({ id: x.id, text: x.examples[0] })).sort(() => Math.random() - 0.5);
const terms = shuffled.map(x => ({ id: x.id, name: x.term })).sort(() => Math.random() - 0.5);
// Pick randomly from all 10 examples for rich variation
const phrases = shuffled.map(x => ({
id: x.id,
term: x.term,
text: x.examples[Math.floor(Math.random() * x.examples.length)],
resolved: false
})).sort(() => Math.random() - 0.5);
const terms = shuffled.map(x => ({
id: x.id,
name: x.term,
resolved: false
})).sort(() => Math.random() - 0.5);
const phrasesContainer = document.getElementById('matcherPhrasesContainer');
const termsContainer = document.getElementById('matcherTermsContainer');
@@ -2938,93 +3010,163 @@
termsContainer.innerHTML = '';
phrases.forEach(p => {
const btn = document.createElement('div');
btn.className = 'match-phrase-card p-4 bg-white rounded-2xl border-2 border-slate-200 text-sm font-serif italic text-slate-800 cursor-pointer hover:border-indigo-400 hover:bg-slate-50 transition shadow-sm select-none';
btn.innerText = `„${p.text}“`;
btn.onclick = () => selectMatcherPhrase(p.id, btn);
phrasesContainer.appendChild(btn);
const card = document.createElement('div');
card.id = `match-phrase-${p.id}`;
card.className = 'match-phrase-card p-4 bg-white rounded-2xl border-2 border-slate-200 text-sm font-serif italic text-slate-800 cursor-pointer hover:border-indigo-400 hover:bg-slate-50 transition shadow-sm select-none flex items-start justify-between gap-2';
const cleanSpeechText = p.text.replace(/'/g, "\\'").replace(/"/g, '&quot;');
card.innerHTML = `
<div class="leading-relaxed">„${p.text}“</div>
<button onclick="speakText('${cleanSpeechText}', event)" title="Vorlesen" class="p-1 rounded-lg text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 transition shrink-0 not-italic">
<i class="fa-solid fa-volume-high text-xs"></i>
</button>
`;
card.onclick = () => selectMatcherPhrase(p.id, card, p.term, p.text);
phrasesContainer.appendChild(card);
});
terms.forEach(t => {
const btn = document.createElement('div');
btn.className = 'match-term-card p-4 bg-white rounded-2xl border-2 border-slate-200 text-sm font-sans font-bold text-slate-800 cursor-pointer hover:border-indigo-400 hover:bg-slate-50 transition shadow-sm select-none flex items-center justify-between';
btn.innerHTML = `<span>${t.name}</span><i class="fa-solid fa-link text-slate-300 text-xs"></i>`;
btn.onclick = () => selectMatcherTerm(t.id, btn);
termsContainer.appendChild(btn);
const card = document.createElement('div');
card.id = `match-term-${t.id}`;
card.className = 'match-term-card p-4 bg-white rounded-2xl border-2 border-slate-200 text-sm font-sans font-bold text-slate-800 cursor-pointer hover:border-indigo-400 hover:bg-slate-50 transition shadow-sm select-none flex items-center justify-between';
card.innerHTML = `<span>${t.name}</span><i class="fa-solid fa-link text-slate-300 text-xs"></i>`;
card.onclick = () => selectMatcherTerm(t.id, card, t.name);
termsContainer.appendChild(card);
});
}
function selectMatcherPhrase(id, el) {
if (el.classList.contains('opacity-40')) return;
function selectMatcherPhrase(id, el, term, text) {
if (el.classList.contains('pointer-events-none')) return;
document.querySelectorAll('.match-phrase-card').forEach(c => {
if (!c.classList.contains('opacity-40')) {
c.classList.remove('border-indigo-600', 'bg-indigo-50');
if (!c.classList.contains('pointer-events-none')) {
c.classList.remove('border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
c.classList.add('border-slate-200');
}
});
matcherSelectedPhrase = { id, el };
matcherSelectedPhrase = { id, el, term, text };
el.classList.remove('border-slate-200');
el.classList.add('border-indigo-600', 'bg-indigo-50');
el.classList.add('border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
playTone('flip');
checkMatcherMatch();
}
function selectMatcherTerm(id, el) {
if (el.classList.contains('opacity-40')) return;
function selectMatcherTerm(id, el, name) {
if (el.classList.contains('pointer-events-none')) return;
document.querySelectorAll('.match-term-card').forEach(c => {
if (!c.classList.contains('opacity-40')) {
c.classList.remove('border-indigo-600', 'bg-indigo-50');
if (!c.classList.contains('pointer-events-none')) {
c.classList.remove('border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
c.classList.add('border-slate-200');
}
});
matcherSelectedTerm = { id, el };
matcherSelectedTerm = { id, el, name };
el.classList.remove('border-slate-200');
el.classList.add('border-indigo-600', 'bg-indigo-50');
el.classList.add('border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
playTone('flip');
checkMatcherMatch();
}
function checkMatcherMatch() {
if (matcherSelectedPhrase && matcherSelectedTerm) {
if (matcherSelectedPhrase.id === matcherSelectedTerm.id) {
playTone('success');
const pEl = matcherSelectedPhrase.el;
const tEl = matcherSelectedTerm.el;
if (!matcherSelectedPhrase || !matcherSelectedTerm) return;
pEl.className = 'p-4 bg-emerald-50 border-2 border-emerald-400 text-sm font-serif italic text-emerald-900 rounded-2xl opacity-40 pointer-events-none transition';
tEl.className = 'p-4 bg-emerald-50 border-2 border-emerald-400 text-sm font-sans font-bold text-emerald-900 rounded-2xl opacity-40 pointer-events-none transition flex items-center justify-between';
tEl.innerHTML += `<i class="fa-solid fa-check text-emerald-600"></i>`;
const pObj = matcherSelectedPhrase;
const tObj = matcherSelectedTerm;
const isMatch = pObj.id === tObj.id;
matcherMatchedCount++;
document.getElementById('matchScore').innerText = matcherMatchedCount;
if (isMatch) {
// Richtig zugeordnet!
playTone('success');
matcherCorrectCount++;
document.getElementById('matchScore').innerText = matcherCorrectCount;
matcherSelectedPhrase = null;
matcherSelectedTerm = null;
// Phrase dauerhaft als RICHTIG sperren
pObj.el.className = 'p-4 bg-emerald-50 border-2 border-emerald-500 text-sm font-serif italic text-emerald-950 rounded-2xl transition shadow-sm pointer-events-none flex items-start justify-between gap-2';
pObj.el.innerHTML = `
<div class="leading-relaxed">„${pObj.text}“</div>
<span class="px-2 py-0.5 rounded-lg bg-emerald-200 text-emerald-950 text-xs font-sans font-bold flex items-center gap-1 shrink-0">
<i class="fa-solid fa-check"></i> Richtig
</span>
`;
if (matcherMatchedCount === 5) {
playTone('win');
document.getElementById('matcherWinBanner').classList.remove('hidden');
}
} else {
playTone('error');
const pEl = matcherSelectedPhrase.el;
const tEl = matcherSelectedTerm.el;
// Begriff dauerhaft als RICHTIG sperren
tObj.el.className = 'p-4 bg-emerald-50 border-2 border-emerald-500 text-sm font-sans font-bold text-emerald-950 rounded-2xl transition shadow-sm pointer-events-none flex items-center justify-between';
tObj.el.innerHTML = `<span>${tObj.name}</span><i class="fa-solid fa-check text-emerald-600 text-base"></i>`;
pEl.classList.add('border-rose-500', 'bg-rose-50');
tEl.classList.add('border-rose-500', 'bg-rose-50');
addXP(20, 'Matcher Paar gelöst');
matcherSelectedPhrase = null;
matcherSelectedTerm = null;
} else {
// "FALSCH IST FALSCH!" - Ein Fehlversuch verbrennt dieses Zitat unwiderruflich!
playTone('error');
matcherFailedCount++;
document.getElementById('matchFailScore').innerText = matcherFailedCount;
setTimeout(() => {
pEl.classList.remove('border-rose-500', 'bg-rose-50', 'border-indigo-600', 'bg-indigo-50');
tEl.classList.remove('border-rose-500', 'bg-rose-50', 'border-indigo-600', 'bg-indigo-50');
pEl.classList.add('border-slate-200');
tEl.classList.add('border-slate-200');
matcherSelectedPhrase = null;
matcherSelectedTerm = null;
}, 350);
// Phrase DAUERHAFT als FALSCH sperren und Lösung aufdecken
pObj.el.className = 'p-4 bg-rose-50 border-2 border-rose-500 text-sm font-serif italic text-rose-950 rounded-2xl transition shadow-sm pointer-events-none space-y-1.5';
pObj.el.innerHTML = `
<div class="leading-relaxed">„${pObj.text}“</div>
<div class="pt-1.5 border-t border-rose-200/80 flex items-center justify-between font-sans text-xs">
<span class="text-rose-700 font-extrabold flex items-center gap-1"><i class="fa-solid fa-xmark"></i> Falsch!</span>
<span class="text-slate-700 font-medium">Lösung: <strong class="text-indigo-950 font-black">${pObj.term}</strong></span>
</div>
`;
// Begriff blinkt kurz rot auf und wird dann wieder neutral freigegeben (für andere Zitate)
tObj.el.classList.add('border-rose-500', 'bg-rose-50');
setTimeout(() => {
tObj.el.classList.remove('border-rose-500', 'bg-rose-50', 'border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
tObj.el.classList.add('border-slate-200');
}, 450);
// Als Schwachstelle speichern für gezieltes Nachlernen
if (!quizMistakes.includes(pObj.id)) {
quizMistakes.push(pObj.id);
localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes));
updateQuizMistakesBadge();
}
matcherSelectedPhrase = null;
matcherSelectedTerm = null;
}
// Prüfen, ob alle 5 Paare versucht wurden (richtig + falsch = 5)
if (matcherCorrectCount + matcherFailedCount === 5) {
setTimeout(finishMatcherRound, 500);
}
}
function finishMatcherRound() {
const banner = document.getElementById('matcherResultBanner');
const title = document.getElementById('matcherResultTitle');
const subtitle = document.getElementById('matcherResultSubtitle');
const icon = document.getElementById('matcherResultIcon');
const weakBtn = document.getElementById('matcherWeakspotsBtn');
banner.classList.remove('hidden', 'bg-emerald-600', 'bg-indigo-600', 'bg-slate-900', 'bg-rose-600');
if (matcherCorrectCount === 5) {
playTone('win');
banner.classList.add('bg-emerald-600');
icon.innerHTML = '<i class="fa-solid fa-crown text-amber-300 text-5xl"></i>';
title.innerText = 'Perfekt zugeordnet! 🏆';
subtitle.innerText = 'Alle 5 Paare auf Anhieb fehlerfrei erkannt. Keine Fehler gemacht!';
addXP(60, 'Matcher fehlerfrei');
weakBtn.classList.add('hidden');
} else if (matcherCorrectCount >= 3) {
playTone(matcherCorrectCount >= 4 ? 'win' : 'flip');
banner.classList.add('bg-indigo-600');
icon.innerHTML = '<i class="fa-solid fa-trophy text-amber-300 text-5xl"></i>';
title.innerText = `Runde beendet: ${matcherCorrectCount} von 5 richtig!`;
subtitle.innerText = `Falsch ist falsch: ${matcherFailedCount} Fehler gemacht. Die verpassten Begriffe wurden in deine Schwachstellen übernommen.`;
weakBtn.classList.remove('hidden');
} else {
playTone('error');
banner.classList.add('bg-slate-900');
icon.innerHTML = '<i class="fa-solid fa-triangle-exclamation text-rose-400 text-5xl"></i>';
title.innerText = `Ergebnis: ${matcherCorrectCount} von 5 Paaren richtig`;
subtitle.innerText = `Falsch ist falsch: ${matcherFailedCount} Fehlversuche. Wiederhole die Begriffe gezielt im Schwachstellen-Trainer oder Glossar.`;
weakBtn.classList.remove('hidden');
}
}
// GLOSSAR & SUCHE
function setGlossaryFilter(filter) {
currentGlossaryFilter = filter;