One-tap grids everywhere, hideable translation/hints, cleaner top and bottom bars
- Possessives, pronouns, and Kontraktionen switch from typed input to the same 4-choice tap grid as articles. Possessive choices all share the question's stem; pronoun choices lead with the pronoun's own case forms. - The translation line is hideable everywhere (shows by default, hidden state persists like the other hints, revealed again on a wrong answer). The possessive and pronoun hint lines get hide toggles too. - Header: Charts moves out; One-tap becomes a pill — plain grey when off, green outlined when on. More margin below the header across the board. - Bottom bar: progress dots centered with 'n/10 · level name' underneath, outlined Charts button (capital C) pinned bottom-right. - Touch devices hide the 1-4/enter/esc keyboard chips; answer buttons keep their full size via min-heights so tap targets stay big.
This commit is contained in:
+34
-9
@@ -550,16 +550,40 @@ const enNoun = (r, det) => `${det}${det ? ' ' : ''}${r.noun.en}`;
|
||||
// QUESTION GENERATORS
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
function articleChoices(ans, artType) {
|
||||
const pool = artType === 'indefinite'
|
||||
? ['ein','eine','einen','einem','eines','einer','keine','keinen','keiner']
|
||||
: ['der','die','das','den','dem','des'];
|
||||
const rem = pool.filter(a => a !== ans);
|
||||
// The answer plus 3 distractors drawn from a pool, shuffled.
|
||||
function pickChoices(ans, pool) {
|
||||
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]);
|
||||
return [ans, ...picked].sort(() => Math.random() - 0.5);
|
||||
}
|
||||
|
||||
function articleChoices(ans, artType) {
|
||||
return pickChoices(ans, artType === 'indefinite'
|
||||
? ['ein','eine','einen','einem','eines','einer','keine','keinen','keiner']
|
||||
: ['der','die','das','den','dem','des']);
|
||||
}
|
||||
|
||||
// All declined forms of one possessive stem (mein, meine, meinen, meinem…) —
|
||||
// every distractor shares the stem, so the choices never leak which ending is right.
|
||||
function possChoices(stem, ans) {
|
||||
const pool = [];
|
||||
for (const g of Object.keys(POSS_ENDINGS)) {
|
||||
for (const c of Object.keys(POSS_ENDINGS[g])) pool.push(combinePoss(stem, POSS_ENDINGS[g][c]));
|
||||
}
|
||||
return pickChoices(ans, pool);
|
||||
}
|
||||
|
||||
// The target pronoun's own case forms first (mich vs mir vs ich is the real
|
||||
// test), topped up with other pronouns' object forms.
|
||||
function pronounChoices(pron, ans) {
|
||||
const own = [...new Set([pron.nom, pron.acc, pron.dat])].filter(f => f !== ans);
|
||||
const others = shuffle([...new Set(PRONOUNS.filter(p => p.key !== pron.key).flatMap(p => [p.acc, p.dat]))])
|
||||
.filter(f => f !== ans && !own.includes(f));
|
||||
const picked = [...shuffle(own), ...others].slice(0, 3);
|
||||
return [ans, ...picked].sort(() => Math.random() - 0.5);
|
||||
}
|
||||
|
||||
export function genArticleQ(level, state = newState()) {
|
||||
const lv = LEVELS.find(l=>l.id===level);
|
||||
const indef = lv.artType === 'indefinite';
|
||||
@@ -657,7 +681,7 @@ export function genPossQ(level, state = newState()) {
|
||||
const sentence = `${r.subjDe} ${r.verbDe}${r.midDe} ${prepKey} _____ ${nounForm(r.noun, cas)}${r.postDe}.`;
|
||||
const translation = `${r.subjEn} ${r.verbEn}${r.midEn} ${r.enPrep} ${possMeaning} ${r.noun.en}${r.postEn}.`;
|
||||
const rule = `${stem}- + ${CL[cas]} + ${r.noun.g} → ${ans}`;
|
||||
return {type:'poss', noun:r.noun, stem, possMeaning, cas, ans, sentence, translation, rule, prepKey, tw};
|
||||
return {type:'poss', noun:r.noun, stem, possMeaning, cas, ans, sentence, translation, rule, prepKey, tw, choices:possChoices(stem, ans)};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,7 +701,7 @@ export function genPossQ(level, state = newState()) {
|
||||
state.last.noun = noun.w;
|
||||
const ans = combinePoss(stem, POSS_ENDINGS[noun.g][cas]);
|
||||
const rule = `${stem}- + ${CL[cas]} + ${noun.g} → ${ans}`;
|
||||
return {type:'poss', noun, stem, possMeaning, cas, ans, sentence, translation, rule, prepKey:null, tw:null};
|
||||
return {type:'poss', noun, stem, possMeaning, cas, ans, sentence, translation, rule, prepKey:null, tw:null, choices:possChoices(stem, ans)};
|
||||
}
|
||||
|
||||
export function genPronounQ(level, state = newState()) {
|
||||
@@ -703,14 +727,15 @@ export function genPronounQ(level, state = newState()) {
|
||||
const enPost = tpl.enPost || '';
|
||||
const sentence = `${cap(subj.nom)} ${sDe} _____${dePost}.`;
|
||||
const translation = `${cap(subj.en)} ${sEn} ${pron.enObj}${enPost}.`;
|
||||
return {type:'pronoun', pronKey, pron, cas, ans, sentence, translation};
|
||||
return {type:'pronoun', pronKey, pron, cas, ans, sentence, translation, choices:pronounChoices(pron, ans)};
|
||||
}
|
||||
|
||||
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};
|
||||
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))};
|
||||
}
|
||||
|
||||
// Build the 4 choices for a fused prep+article question: always a BLEND of
|
||||
|
||||
Reference in New Issue
Block a user