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 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) {
if (disabled || lockedCorrect !== null) return;
if (disabled || coolingDown || lockedCorrect !== null) return;
if (value === correctAnswer) {
lockedCorrect = value;
onAnswer(value);
@ -19,21 +23,34 @@
} else {
wrongValue = value;
onAnswer(value);
setTimeout(() => {
if (wrongValue === value) wrongValue = null;
}, 380);
coolingDown = true;
if (cooldownTimer) clearTimeout(cooldownTimer);
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(() => {
void choices; // dependency
if (choices !== prevChoices) {
prevChoices = choices;
lockedCorrect = null;
wrongValue = null;
coolingDown = false;
if (cooldownTimer) {
clearTimeout(cooldownTimer);
cooldownTimer = null;
}
}
});
</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)}
<button
type="button"
@ -42,7 +59,7 @@
class:correct={lockedCorrect === value}
onclick={() => handle(value)}
aria-label={`Antwort ${value}`}
{disabled}
disabled={disabled || coolingDown}
>
{value}
</button>
@ -66,12 +83,16 @@
color: var(--c-text);
border-radius: 22px;
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) {
transform: translateY(4px);
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 {
animation: shake 0.36s ease;
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) {
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:
// given: vorgegebene Zerlegungszahl in [0..T] (inkl. der trivialen 0/T-Zerlegung)
// 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
// 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];
const DESIRED_CHOICES = 3;
const DESIRED_CHOICES = 4;
function shuffle<T>(arr: T[]): T[] {
const a = arr.slice();