Gegen blindes Durchtippen: vierte Antwortmöglichkeit + kurze Denkpause nach falscher Antwort
This commit is contained in:
parent
ad1ba740f8
commit
34bcef5da8
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user