From eae889c52bec219a934930b35ceb2d9d6e12fab0 Mon Sep 17 00:00:00 2001 From: George Turkington Date: Mon, 5 Oct 2026 06:12:27 +0000 Subject: [PATCH] Add the Verbs tab, and stop Daily rounds repeating a preposition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The possessive and pronoun levels already build sentences with no preposition in them - "Ich kenne ___ Mann", "Ich helfe ___ Mann" - where the case is decided purely by the verb. Six verbs were doing that silently across seven levels (sehen, kennen, lieben, hoeren as accusative, helfen as dative, geben as both) and nothing in the app had ever taught or tested it. The framing the whole section rests on: there is no accusative list to learn. Accusative is the default for hundreds of transitive verbs, so you learn the dative exceptions - the same shape as the preposition lists the app already teaches. 39 verbs of data, in four groups: - accusative foils, including the ones English makes feel dative (fragen, anrufen, treffen, unterstuetzen, moegen) - 12 dative verbs: helfen, danken, antworten, glauben, folgen, gratulieren, vertrauen, verzeihen, begegnen, zuhoeren, raten, schaden - 4 that flip subject and object against English: gefallen, schmecken, fehlen, gehoeren - 8 two-object verbs, all covered by one rule: person Dativ, thing Akkusativ Six lessons plus Daily Practice: Welcher Fall? / Die Fallen / Gefallen & Co. / Dativ + Akkusativ / Im Satz / Alles gemischt. The tab sits second, right after Prepositions, so case government is learned before the forms that depend on it. Near-synonyms that disagree are modelled as a `trap` link, and a test asserts every trap points at a real verb taking the other case and that the link is symmetric - so fragen/antworten, treffen/begegnen, anrufen/zuhoeren, unterstuetzen/helfen and moegen/gefallen cannot drift apart. The inverted verbs carry an explicit English reordering rule rather than a literal gloss, so "Das Fenster gefaellt der Aerztin" renders as "The doctor likes the window" and never "The window pleases the doctor". Tested. Verb sentences are enumerated into corpus.txt like everything else, keyed on verb x noun, which is what caught "I lend the teacher the bed" and "I recommend the girl the cupboard" before they shipped - two-object verbs now name the things that can actually be handed over, and the English uses the "to the person" order. 167 tests pass. Known gap: a handful of verbs take the genitive (gedenken, beduerfen, beschuldigen). They are formal and rare, so the rule panel mentions them and the app does not drill them. Daily rounds could serve the same preposition twice, even back-to-back Reported as "für" twice in a row in the Prepositions daily. Measured before the fix: 14% of prep-daily rounds had a back-to-back repeat and 51% repeated some preposition. A Daily round mixes several lessons, and each one was handed its own newState(). The anti-repeat memory therefore only spanned one lesson's slice of the round, so level 8 drawing "für" and level 10 drawing "für" could not see each other. drawBag also only avoided the previous item when the bag refilled, not on every draw. - newState(shared) lets several lesson states run off one memory, and the Daily hands the same one to every lesson it mixes. - drawBag now skips anything already used this cycle, and never repeats the previous item, checked on every draw rather than only at a refill. - Kontraktionen drew through a different bag ('contr') and so bypassed the preposition memory entirely, letting "im" follow "in". It now consults and records the shared memory. - Adjective frames were chosen with a plain pick(), so a Daily could repeat a whole carrier sentence. They are dealt from the shared memory too. Rejected draws are put BACK into the bag rather than discarded. The first version burned them, which skewed the Kontraktionen deal 1.5x toward the forms whose preposition is not shared with a sibling form; reinsertion holds the spread at 1.04x. After: every Daily and every preposition-drilling level is at 0% for both back-to-back and within-round repeats. Two cases remain non-zero and are arithmetic rather than bugs - a round is 10 questions, so Wechsel im Kontext (9 prepositions) must repeat one, and the Stark adjective level has exactly one prepositional carrier frame. The adjective tab is held to frame variety rather than preposition variety, since there the preposition is scenery and the ending is the question. Tests pin all of it: no Daily may repeat a preposition, no level may repeat one back-to-back where it has a choice, and no Daily may repeat a carrier sentence back-to-back. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8 --- src/App.jsx | 239 +++++- src/corpus.txt | 1744 ++++++++++++++++++++++++++++++++++++++++++++ src/engine.js | 303 +++++++- src/engine.test.js | 151 +++- 4 files changed, 2419 insertions(+), 18 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 2620972..3beb3a1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import { PRONOUNS, POSSESSIVES, POSS_ENDINGS, combinePoss, LEVELS, wechselExample, createSession, bumpStreak, currentStreak, weekDots, nounForm, ADJ_ENDINGS, ADJ_CLASS_LABEL, ADJ_CLASS_HINT, DER_ENDINGS, + CASE_VERBS, VERB_BY_NAME, VERB_CASE_LABEL, DATIVE_VERBS, } from './engine.js'; // ═══════════════════════════════════════════ @@ -56,6 +57,7 @@ const Access = { const TABS = [ {id:'prep', label:'Prepositions'}, + {id:'verb', label:'Verbs'}, {id:'article', label:'Articles'}, {id:'poss', label:'Possessives'}, {id:'pronoun', label:'Pronouns'}, @@ -345,7 +347,7 @@ export default function App() { const rc = res.filter(Boolean).length; const cur = LEVELS.find(l=>l.id===lv); - const chartSection = scr === 'menu' ? tab : (q?.type === 'wechselContext' ? 'prep' : q?.type === 'chunk' ? 'article' : (q?.type === 'adjective' || q?.type === 'adjClass') ? 'adj' : (q?.type || tab)); + const chartSection = scr === 'menu' ? tab : (q?.type === 'wechselContext' ? 'prep' : q?.type === 'chunk' ? 'article' : (q?.type === 'adjective' || q?.type === 'adjClass') ? 'adj' : (q?.type === 'verbCase' || q?.type === 'verbSentence') ? 'verb' : (q?.type || tab)); const nextInTab = nextLevelInTab(lv); const quickActive = !!cur?.daily || !!st.auto; const quickTick = quickActive && rev && ok; @@ -768,6 +770,76 @@ export default function App() { )} + {/* PRACTICE: which case does this verb take? */} + {scr === 'practice' && q?.type === 'verbCase' && ( +
+
Which case does this verb take?
+
{q.verb.de}
+
{q.verb.en}
+
+ +
+ {!rev ? ( +
+ {q.choices.map((val,i) => ( + + ))} +
+ ) : quickTick ? : ( + setRule('verb')}/> + )} +
+ )} + + {/* PRACTICE: the verb decides the case, in a sentence */} + {scr === 'practice' && q?.type === 'verbSentence' && ( +
+
+ {q.sentence.split('_____').map((part,i,a)=>( + + {part} + {i < a.length-1 && ( + {rev ? q.ans : ' '} + )} + + ))} +
+ + + + + + + +
+ The verb decides + +
+ + {!rev ? ( + + ) : quickTick ? : ( + setRule('verb')}/> + )} +
+ )} + {/* PRACTICE: adjective endings */} {scr === 'practice' && (q?.type === 'adjective' || q?.type === 'adjClass') && (
@@ -904,7 +976,9 @@ export default function App() {
{chart && setChart(false)} onRule={kind => { setChart(false); setRule(kind || true); }}/>} - {rule && (rule === 'adj' ? setRule(false)}/> : setRule(false)}/>)} + {rule && (rule === 'adj' ? setRule(false)}/> + : rule === 'verb' ? setRule(false)}/> + : setRule(false)}/>)} {nounInfo && setNounInfo(null)}/>}