Gegen blindes Durchtippen: vierte Antwortmöglichkeit + kurze Denkpause nach falscher Antwort

This commit is contained in:
schmop 2026-05-31 18:51:40 +02:00
parent ad1ba740f8
commit 34bcef5da8
3 changed files with 36 additions and 15 deletions

View File

@ -9,9 +9,13 @@
let wrongValue = $state<number | null>(null); let wrongValue = $state<number | null>(null);
let lockedCorrect = $state<number | null>(null); let lockedCorrect = $state<number | null>(null);
// Kurze Denkpause nach falscher Antwort: Knöpfe sind ~0,9s gesperrt und gedimmt.
// Kein Punktverlust — macht blindes Durchtippen nur langsamer als Nachdenken.
let coolingDown = $state(false);
let cooldownTimer: ReturnType<typeof setTimeout> | null = null;
function handle(value: number) { function handle(value: number) {
if (disabled || lockedCorrect !== null) return; if (disabled || coolingDown || lockedCorrect !== null) return;
if (value === correctAnswer) { if (value === correctAnswer) {
lockedCorrect = value; lockedCorrect = value;
onAnswer(value); onAnswer(value);
@ -19,21 +23,34 @@
} else { } else {
wrongValue = value; wrongValue = value;
onAnswer(value); onAnswer(value);
setTimeout(() => { coolingDown = true;
if (wrongValue === value) wrongValue = null; if (cooldownTimer) clearTimeout(cooldownTimer);
}, 380); cooldownTimer = setTimeout(() => {
coolingDown = false;
wrongValue = null;
}, 900);
} }
} }
// Reset bei Aufgabenwechsel // Reset NUR bei echtem Aufgabenwechsel. Der Effekt feuert über den choices-Prop auch
// bei jeder Falsch-Antwort (die den game-Store ändert) — daher per Referenzvergleich
// absichern, sonst würde die Denkpause sofort wieder aufgehoben.
let prevChoices: number[] | null = null;
$effect(() => { $effect(() => {
void choices; // dependency if (choices !== prevChoices) {
prevChoices = choices;
lockedCorrect = null; lockedCorrect = null;
wrongValue = null; wrongValue = null;
coolingDown = false;
if (cooldownTimer) {
clearTimeout(cooldownTimer);
cooldownTimer = null;
}
}
}); });
</script> </script>
<div class="answers" role="group" aria-label="Antwortmöglichkeiten"> <div class="answers" class:cooling={coolingDown} role="group" aria-label="Antwortmöglichkeiten">
{#each choices as value (value)} {#each choices as value (value)}
<button <button
type="button" type="button"
@ -42,7 +59,7 @@
class:correct={lockedCorrect === value} class:correct={lockedCorrect === value}
onclick={() => handle(value)} onclick={() => handle(value)}
aria-label={`Antwort ${value}`} aria-label={`Antwort ${value}`}
{disabled} disabled={disabled || coolingDown}
> >
{value} {value}
</button> </button>
@ -66,12 +83,16 @@
color: var(--c-text); color: var(--c-text);
border-radius: 22px; border-radius: 22px;
box-shadow: 0 6px 0 #b9c4dc; box-shadow: 0 6px 0 #b9c4dc;
transition: transform 0.08s ease, background 0.15s ease, box-shadow 0.08s ease; transition: transform 0.08s ease, background 0.15s ease, box-shadow 0.08s ease, opacity 0.2s ease;
} }
.answer:active:not(:disabled) { .answer:active:not(:disabled) {
transform: translateY(4px); transform: translateY(4px);
box-shadow: 0 2px 0 #b9c4dc; box-shadow: 0 2px 0 #b9c4dc;
} }
/* Während der Denkpause die übrigen Knöpfe sanft dimmen (der falsche bleibt rot sichtbar). */
.answers.cooling .answer:not(.wrong) {
opacity: 0.45;
}
.answer.wrong { .answer.wrong {
animation: shake 0.36s ease; animation: shake 0.36s ease;
background: #ffd0d0; background: #ffd0d0;

View File

@ -29,10 +29,10 @@ describe('buildTask', () => {
} }
}); });
it('returns 3 choices for every target', () => { it('returns 4 choices for every target', () => {
for (const target of TARGETS) { for (const target of TARGETS) {
const task = buildTask(target, 1); const task = buildTask(target, 1);
expect(task.choices.length).toBe(3); expect(task.choices.length).toBe(4);
} }
}); });
}); });

View File

@ -3,7 +3,7 @@
// Eine Aufgabe für Zielzahl T besteht aus: // Eine Aufgabe für Zielzahl T besteht aus:
// given: vorgegebene Zerlegungszahl in [0..T] (inkl. der trivialen 0/T-Zerlegung) // given: vorgegebene Zerlegungszahl in [0..T] (inkl. der trivialen 0/T-Zerlegung)
// answer: T - given (das Kind muss diese tippen) // answer: T - given (das Kind muss diese tippen)
// choices: 3 große Buttons (richtige Antwort + 2 Distraktoren), gemischt // choices: 4 große Buttons (richtige Antwort + 3 Distraktoren), gemischt
// //
// Aufgabenfolge pro Spieldurchlauf: ein "Beutel" (TaskDeck) zieht alle möglichen // Aufgabenfolge pro Spieldurchlauf: ein "Beutel" (TaskDeck) zieht alle möglichen
// Vorgaben ohne Zurücklegen, mischt nach dem Leeren neu und verhindert direkte // Vorgaben ohne Zurücklegen, mischt nach dem Leeren neu und verhindert direkte
@ -21,7 +21,7 @@ export type Task = {
export const TARGETS: Target[] = [4, 5, 6, 7, 8, 9, 10]; export const TARGETS: Target[] = [4, 5, 6, 7, 8, 9, 10];
const DESIRED_CHOICES = 3; const DESIRED_CHOICES = 4;
function shuffle<T>(arr: T[]): T[] { function shuffle<T>(arr: T[]): T[] {
const a = arr.slice(); const a = arr.slice();