Kontraktionen: ask it backwards, as a two-tap word cloud

"hinter + das = ?" with options am / im / uebers / hinters was no test at
all — only one option could start with "hinter", so you never had to know
which article was fused in. The whole level was answerable on the prefix.

Flipped it. The fused form is given and you take it apart:

- The form is shown large ("im"), and you pick the preposition from a word
  cloud, then the article from a second one. Graded on both parts.
- The preposition step is a real question for the truncated forms — im vs
  am, vom, zum/zur — which are exactly the ones that get mixed up.
- The article step is the point: -s means das, -m means dem, -r means der.
  Distractors are always real articles (das/dem/der/die/den), never whole
  contraction forms, so elimination on shape no longer works.
- Feedback names which half was wrong and states the ending rule, then
  highlights the article in the definite-article table.
- The chosen preposition chip is tappable to change before committing;
  backspace does the same on desktop. On reveal the line resolves to the
  correct pair.

Tests: the old flaw is pinned shut (nothing on offer is ever a contraction
form), both choice sets are checked, and a data test asserts the -s/-m/-r
ending predicts the article for all 16 contractions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
This commit is contained in:
2026-09-17 07:05:58 +00:00
co-authored by Claude Opus 5
parent 047454f23f
commit 1d5402edc7
3 changed files with 160 additions and 22 deletions
+16 -3
View File
@@ -688,10 +688,10 @@ const enNoun = (r, det) => `${det}${det ? ' ' : ''}${r.noun.en}`;
// ═══════════════════════════════════════════
// The answer plus 3 distractors drawn from a pool, shuffled.
function pickChoices(ans, pool) {
function pickChoices(ans, pool, n = 4) {
const rem = [...new Set(pool)].filter(a => a !== ans);
const picked = [];
while (picked.length < 3 && rem.length) picked.push(rem.splice(Math.floor(Math.random()*rem.length), 1)[0]);
while (picked.length < n - 1 && rem.length) picked.push(rem.splice(Math.floor(Math.random()*rem.length), 1)[0]);
return [ans, ...picked].sort(() => Math.random() - 0.5);
}
@@ -876,12 +876,25 @@ export function genPronounQ(level, state = newState()) {
return {type:'pronoun', pronKey, pron, cas, ans, sentence, translation, choices:pronounChoices(pron, ans)};
}
// Every preposition that fuses, and every article that shows up inside a
// contraction plus two that never do — so "which article is in there?" can't be
// answered by elimination.
export const CONTRACTION_PREPS = [...new Set(CONTRACTIONS.map(c => c.prep))];
const CONTRACTION_ARTS = ['das','dem','der','die','den'];
// Asked the other way round: the fused form is GIVEN and you take it apart.
// Asking "hinter + das = ?" was no test at all — only one option could start
// with "hinter". Going backwards, the real question survives: -s is das, -m is
// dem, -r is der, and am/ans, im/ins, zum/zur are the pairs that actually cost
// you marks.
export function genContractionQ(state = newState()) {
const form = drawBag(state, 'contr', CONTRACTIONS.map(c=>c.form), state.last.contr);
state.last.contr = form;
const c = CONTRACTIONS.find(x => x.form === form);
return {type:'contraction', prep:c.prep, art:c.art, ans:c.form, pref:c.pref, cas:c.c,
choices:pickChoices(c.form, CONTRACTIONS.map(x=>x.form))};
answerKey: `${c.prep} ${c.art}`,
prepChoices: pickChoices(c.prep, CONTRACTION_PREPS, 6),
artChoices: pickChoices(c.art, CONTRACTION_ARTS, 4)};
}
// Build the 4 choices for a fused prep+article question: always a BLEND of