diff --git a/src/corpus.txt b/src/corpus.txt index 37e7c77..d55ca61 100644 --- a/src/corpus.txt +++ b/src/corpus.txt @@ -3214,6 +3214,24 @@ INDEF|außer|dative|kommen|Mädchen Alle kommen außer einem Mädch INDEF|außer|dative|kommen|Mann Alle kommen außer einem Mann. Everyone comes except a man. INDEF|außerhalb|genitive|arbeiten|Stadt Er arbeitet außerhalb einer Stadt. He works outside a city. INDEF|außerhalb|genitive|wohnen|Stadt Er wohnt außerhalb einer Stadt. He lives outside a city. +INDEF|bei|dative|sein|Arzt Er ist bei einem Arzt. He is at a doctor's. +INDEF|bei|dative|sein|Ärztin Er ist bei einer Ärztin. He is at a doctor's. +INDEF|bei|dative|sein|Freund Er ist bei einem Freund. He is at a friend's. +INDEF|bei|dative|sein|Freundin Er ist bei einer Freundin. He is at a friend's. +INDEF|bei|dative|sein|Lehrer Er ist bei einem Lehrer. He is at a teacher's. +INDEF|bei|dative|sein|Lehrerin Er ist bei einer Lehrerin. He is at a teacher's. +INDEF|bei|dative|warten|Arzt Er wartet bei einem Arzt. He waits at a doctor's. +INDEF|bei|dative|warten|Ärztin Er wartet bei einer Ärztin. He waits at a doctor's. +INDEF|bei|dative|warten|Freund Er wartet bei einem Freund. He waits at a friend's. +INDEF|bei|dative|warten|Freundin Er wartet bei einer Freundin. He waits at a friend's. +INDEF|bei|dative|warten|Lehrer Er wartet bei einem Lehrer. He waits at a teacher's. +INDEF|bei|dative|warten|Lehrerin Er wartet bei einer Lehrerin. He waits at a teacher's. +INDEF|bei|dative|wohnen|Arzt Er wohnt bei einem Arzt. He lives at a doctor's. +INDEF|bei|dative|wohnen|Ärztin Er wohnt bei einer Ärztin. He lives at a doctor's. +INDEF|bei|dative|wohnen|Freund Er wohnt bei einem Freund. He lives at a friend's. +INDEF|bei|dative|wohnen|Freundin Er wohnt bei einer Freundin. He lives at a friend's. +INDEF|bei|dative|wohnen|Lehrer Er wohnt bei einem Lehrer. He lives at a teacher's. +INDEF|bei|dative|wohnen|Lehrerin Er wohnt bei einer Lehrerin. He lives at a teacher's. INDEF|durch|accusative|fahren|Garten Er fährt durch einen Garten. He drives through a garden. INDEF|durch|accusative|fahren|Park Er fährt durch einen Park. He drives through a park. INDEF|durch|accusative|fahren|Stadt Er fährt durch eine Stadt. He drives through a city. @@ -3517,6 +3535,8 @@ INDEF|mit|dative|sprechen|Lehrer Er spricht mit einem Lehrer. INDEF|mit|dative|sprechen|Lehrerin Er spricht mit einer Lehrerin. He talks with a teacher. INDEF|mit|dative|sprechen|Mädchen Er spricht mit einem Mädchen. He talks with a girl. INDEF|mit|dative|sprechen|Mann Er spricht mit einem Mann. He talks with a man. +INDEF|nach|dative|kommen|Nacht Er kommt nach einer Nacht. He comes after a night. +INDEF|nach|dative|kommen|Woche Er kommt nach einer Woche. He comes after a week. INDEF|neben|accusative|legen|Bett Er legt das Buch neben ein Bett. He puts the book next to a bed. INDEF|neben|accusative|legen|Sofa Er legt das Buch neben ein Sofa. He puts the book next to a sofa. INDEF|neben|accusative|legen|Stuhl Er legt das Buch neben einen Stuhl. He puts the book next to a chair. @@ -3610,6 +3630,7 @@ INDEF|ohne|accusative|reisen|Lehrer Er reist ohne einen Lehrer. INDEF|ohne|accusative|reisen|Lehrerin Er reist ohne eine Lehrerin. He travels without a teacher. INDEF|ohne|accusative|reisen|Mädchen Er reist ohne ein Mädchen. He travels without a girl. INDEF|ohne|accusative|reisen|Mann Er reist ohne einen Mann. He travels without a man. +INDEF|seit|dative|wohnen|Woche Er wohnt hier seit einer Woche. He has lived here for a week. INDEF|statt|genitive|nehmen|Auto Er nimmt den Bus statt eines Autos. He takes the bus instead of a car. INDEF|statt|genitive|nehmen|Zug Er nimmt den Bus statt eines Zuges. He takes the bus instead of a train. INDEF|über|accusative|fahren|Brücke Er fährt über eine Brücke. He drives over a bridge. @@ -3707,7 +3728,13 @@ INDEF|unter|dative|warten|Baum Er wartet unter einem Baum. INDEF|unter|dative|warten|Brücke Er wartet unter einer Brücke. He waits under a bridge. INDEF|unter|dative|warten|Dach Er wartet unter einem Dach. He waits under a roof. INDEF|unter|dative|wohnen|Dach Er wohnt unter einem Dach. He lives under a roof. +INDEF|von|dative|kommen|Arzt Er kommt von einem Arzt. He comes from a doctor's. +INDEF|von|dative|kommen|Ärztin Er kommt von einer Ärztin. He comes from a doctor's. INDEF|von|dative|kommen|Büro Er kommt von einem Büro. He comes from an office. +INDEF|von|dative|kommen|Freund Er kommt von einem Freund. He comes from a friend's. +INDEF|von|dative|kommen|Freundin Er kommt von einer Freundin. He comes from a friend's. +INDEF|von|dative|kommen|Lehrer Er kommt von einem Lehrer. He comes from a teacher's. +INDEF|von|dative|kommen|Lehrerin Er kommt von einer Lehrerin. He comes from a teacher's. INDEF|von|dative|kommen|Schule Er kommt von einer Schule. He comes from a school. INDEF|vor|accusative|fahren|Bahnhof Er fährt vor einen Bahnhof. He drives in front of a station. INDEF|vor|accusative|fahren|Haus Er fährt vor ein Haus. He drives in front of a house. @@ -3775,14 +3802,26 @@ INDEF|während|genitive|schlafen|Konzert Er schläft während eines Konze INDEF|wegen|genitive|bleiben|Hund Er bleibt wegen eines Hundes. He stays because of a dog. INDEF|wegen|genitive|bleiben|Katze Er bleibt wegen einer Katze. He stays because of a cat. INDEF|wegen|genitive|bleiben|Kind Er bleibt wegen eines Kindes. He stays because of a child. +INDEF|zu|dative|fahren|Arzt Er fährt zu einem Arzt. He drives to a doctor's. +INDEF|zu|dative|fahren|Ärztin Er fährt zu einer Ärztin. He drives to a doctor's. INDEF|zu|dative|fahren|Büro Er fährt zu einem Büro. He drives to an office. +INDEF|zu|dative|fahren|Freund Er fährt zu einem Freund. He drives to a friend's. +INDEF|zu|dative|fahren|Freundin Er fährt zu einer Freundin. He drives to a friend's. INDEF|zu|dative|fahren|Hotel Er fährt zu einem Hotel. He drives to a hotel. +INDEF|zu|dative|fahren|Lehrer Er fährt zu einem Lehrer. He drives to a teacher's. +INDEF|zu|dative|fahren|Lehrerin Er fährt zu einer Lehrerin. He drives to a teacher's. INDEF|zu|dative|fahren|Museum Er fährt zu einem Museum. He drives to a museum. INDEF|zu|dative|fahren|Park Er fährt zu einem Park. He drives to a park. INDEF|zu|dative|fahren|Restaurant Er fährt zu einem Restaurant. He drives to a restaurant. INDEF|zu|dative|fahren|Schule Er fährt zu einer Schule. He drives to a school. +INDEF|zu|dative|gehen|Arzt Er geht zu einem Arzt. He walks to a doctor's. +INDEF|zu|dative|gehen|Ärztin Er geht zu einer Ärztin. He walks to a doctor's. INDEF|zu|dative|gehen|Büro Er geht zu einem Büro. He walks to an office. +INDEF|zu|dative|gehen|Freund Er geht zu einem Freund. He walks to a friend's. +INDEF|zu|dative|gehen|Freundin Er geht zu einer Freundin. He walks to a friend's. INDEF|zu|dative|gehen|Hotel Er geht zu einem Hotel. He walks to a hotel. +INDEF|zu|dative|gehen|Lehrer Er geht zu einem Lehrer. He walks to a teacher's. +INDEF|zu|dative|gehen|Lehrerin Er geht zu einer Lehrerin. He walks to a teacher's. INDEF|zu|dative|gehen|Museum Er geht zu einem Museum. He walks to a museum. INDEF|zu|dative|gehen|Park Er geht zu einem Park. He walks to a park. INDEF|zu|dative|gehen|Restaurant Er geht zu einem Restaurant. He walks to a restaurant. diff --git a/src/engine.js b/src/engine.js index b4a672d..37cf5f6 100644 --- a/src/engine.js +++ b/src/engine.js @@ -71,6 +71,10 @@ const SENTENCE_ACC_PREPS = ['durch','für','gegen','ohne','um']; // sentence can never show it honestly — it lives in the Kontraktionen levels // and the mixed rounds, which teach the fused form properly. const SENTENCE_DAT_PREPS = PREPS_BY_CASE.dative.filter(p => p !== 'zu'); +// "trotz eines Wetters" is not German — trotz only governs uncountables here, +// so the ein-Wörter lesson leaves it out rather than listing a prep it can +// never ask about. +const INDEF_GEN_PREPS = PREPS_BY_CASE.genitive.filter(p => p !== 'trotz'); export const WECHSEL_EX = { an: {noun:'Tür', g:'feminine', en:'door', mot:'to', loc:'at'}, @@ -476,7 +480,7 @@ export const LEVELS = [ {id:4.5, tab:'article', name:'Kontraktionen', desc:'Fused forms in context: ins, am, zum…', cases:['accusative','dative'], type:'chunk', artType:'definite'}, {id:5, tab:'article', name:'Genitiv', desc:'trotz, während, wegen, statt...', cases:['genitive'], preps:PREPS_BY_CASE.genitive, type:'article', artType:'definite'}, {id:6, tab:'article', name:'Alles gemischt', desc:'All prepositions, definite articles', cases:['accusative','dative','genitive'], preps:[...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.genitive,...PREPS_BY_CASE.wechsel], type:'article', artType:'definite', mixed:true}, - {id:7, tab:'article', name:'ein-Wörter', desc:'Everything with ein/eine/einem...', cases:['accusative','dative','genitive'], preps:[...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.genitive,...PREPS_BY_CASE.wechsel], type:'article', artType:'indefinite', mixed:true}, + {id:7, tab:'article', name:'ein-Wörter', desc:'Everything with ein/eine/einem...', cases:['accusative','dative','genitive'], preps:[...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...INDEF_GEN_PREPS,...PREPS_BY_CASE.wechsel], type:'article', artType:'indefinite', mixed:true}, // Daily Practice pseudo-levels (one per tab): a one-tap drill that mixes // questions from every lesson in the tab. Rendered pinned at the top of each // tab by App.jsx (not inline in the level list) and locked until every @@ -583,9 +587,12 @@ export const PREP_FRAMES = { {verbs:['spielen'], nouns:['Hund','Katze','Kind']}, {verbs:['fahren'], nouns:['Auto','Zug']}, ], - nach: [{verbs:['kommen'], nouns:['Schule','Arbeit','Nacht','Woche'], enPrep:'after', enDet:'', poss:false}], + nach: [{verbs:['kommen'], nouns:['Schule','Arbeit','Nacht','Woche'], enPrep:'after', enDet:'', poss:false, + indefNouns:['Woche','Nacht']}], seit: [{verbs:['wohnen'], mid:{de:'hier',en:'here'}, nouns:['Nacht','Woche'], enPrep:'since', enDet:'that', - enVerb:{Er:'has lived',Sie:'has lived',Wir:'have lived',Ich:'have lived'}, poss:false}], + enVerb:{Er:'has lived',Sie:'has lived',Wir:'have lived',Ich:'have lived'}, poss:false, + // "seit einer Woche" is "FOR a week" in English, not "since a week". + indefNouns:['Woche'], enPrepIndef:'for'}], von: [ {verbs:['kommen'], nouns:VISITABLE, enPossS:true}, {verbs:['kommen'], nouns:['Arbeit','Schule','Büro']}, @@ -859,12 +866,19 @@ export function usableFrames(prepKey, tw, { genderFilter, possOnly, indefOnly } .filter(f => f.nouns.length); } if (indefOnly) { - // ein-word questions: drop frames whose English determiner is a baked-in - // definite idiom (nach der Schule → "after school", seit → "since that…"), - // and drop noIndef nouns — the remaining pairings read naturally with a/an. + // ein-word questions. A frame whose English determiner is a baked-in + // definite idiom ("nach der Schule" → after school) can still work with an + // ein-word if it says which nouns to use instead: "nach einer Woche" is + // fine even though "während einer Woche" is not, so the allowance is + // per-frame rather than per-noun. frames = frames - .filter(f => f.enDet === undefined && !f.enPossS) - .map(f => ({...f, nouns: f.nouns.filter(n => !NOUN_BY_NAME[n].noIndef)})) + .filter(f => f.indefNouns || f.enDet === undefined) + .map(f => { + const g = {...f, nouns: f.indefNouns || f.nouns.filter(n => !NOUN_BY_NAME[n].noIndef)}; + if (f.enPrepIndef) g.enPrep = f.enPrepIndef; + delete g.enDet; // the caller supplies a/an + return g; + }) .filter(f => f.nouns.length); } if (genderFilter) { diff --git a/src/engine.test.js b/src/engine.test.js index 8d174aa..3baf43e 100644 --- a/src/engine.test.js +++ b/src/engine.test.js @@ -12,6 +12,8 @@ import { EIN_WORD_DETS, ADJ_ALL_ENDINGS, } from './engine.js'; +const lvl7Has = (qs, p) => qs.some(q => q.prepKey === p); + const runSession = (levelId, n) => { const s = createSession(levelId); return Array.from({ length: n }, () => s.next()); @@ -115,15 +117,55 @@ describe('article questions', () => { expect([...seen].sort()).toEqual([...lv.preps].sort()); }); - it('ein-Wörter never pairs an ein-word with a mass/definite-only noun', () => { - for (const q of runSession(7, 400)) { - expect(q.noun.noIndef, `„ein ${q.noun.w}" should never be generated`).toBeFalsy(); + // The guard that would have caught ein-Wörter silently losing bei/nach/seit: + // a level that lists a preposition must actually be able to ask about it. + // If a pairing is impossible (zu always fuses, trotz only takes uncountables) + // the honest fix is to drop it from the level's list, not to leave a prep + // advertised that never appears. + it.each(LEVELS.filter(l => l.preps).map(l => [l.id]))( + 'level %s generates every preposition it advertises', (id) => { + const lv = LEVELS.find(l => l.id === id); + const seen = new Set(runSession(id, lv.preps.length * 40).map(q => q.prepKey)); + const missing = lv.preps.filter(p => !seen.has(p)); + expect(missing, `level ${id} lists ${missing.join(', ')} but can never generate them`).toEqual([]); + }); + + it('ein-Wörter reaches every case, including the dative', () => { + const qs = runSession(7, 600); + for (const c of ['accusative', 'dative', 'genitive']) { + expect(qs.some(q => q.cas === c), `ein-Wörter never generated a ${c}`).toBe(true); + } + // and the dative must come from real dative prepositions, not only from + // Wechsel preps that happened to land on dative + const datPreps = PREPS_BY_CASE.dative.filter(p => lvl7Has(qs, p)); + expect(datPreps.length, `only ${datPreps.join(',')} covered`).toBeGreaterThanOrEqual(8); + }); + + it('ein-Wörter never pairs an ein-word with an uncountable noun', () => { + // noIndef means "not with ein unless a frame explicitly re-admits it": + // "eine Woche" is good German, "ein Wetter" is not. These can never be + // re-admitted by any frame. + const UNCOUNTABLE = ['Wasser','Wetter','Regen','Musik','Arbeit','Geld','Zeit', + 'Milch','Brot','Bier','Wein','Kaffee','Tee']; + for (const q of runSession(7, 600)) { + expect(UNCOUNTABLE, `„${q.ans} ${q.noun.w}" should never be generated`).not.toContain(q.noun.w); } }); - it('ein-Wörter never uses definite-idiom frames (nach, seit) or all-mass preps (trotz)', () => { - for (const q of runSession(7, 400)) { - expect(['nach', 'seit', 'trotz']).not.toContain(q.prepKey); + it('a frame may re-admit a countable noun that the global flag blocks', () => { + const qs = runSession(7, 1200); + // "nach einer Woche" and "seit einer Woche" are textbook German … + expect(qs.some(q => q.prepKey === 'nach' && q.noun.w === 'Woche')).toBe(true); + expect(qs.some(q => q.prepKey === 'seit' && q.noun.w === 'Woche')).toBe(true); + // … while "während einer Woche", the original bug, stays impossible. + expect(qs.some(q => q.prepKey === 'während' && ['Woche','Nacht'].includes(q.noun.w))).toBe(false); + }); + + it('ein-Wörter drops only the preps that genuinely cannot take an ein-word', () => { + const lv = LEVELS.find(l => l.id === 7); + expect(lv.preps, 'trotz only governs uncountables').not.toContain('trotz'); + for (const p of ['bei', 'nach', 'seit', 'von', 'zu', 'mit', 'aus']) { + expect(lv.preps, `${p} works fine with an ein-word`).toContain(p); } });