Wechsel im Kontext: blank the article, make the verb decide, add the rule

The level showed the article already in the sentence ("unter dem Stuhl"),
so it was answerable by recognising "dem" — it never tested wohin/wo at
all. And the frame pool was tiny: one cat, one lamp, eight nouns.

- The article is now blanked. The only evidence is the verb and whether it
  has a direct object, which is the actual skill. The solved sentence is
  revealed on answer, contracted where German really contracts (ins Regal,
  am Bahnhof), with a note naming the fusion.
- Frames rebuilt around what decides the case: 27 new nouns, 18 new verbs,
  and every frame one-way so no verb+noun pairing licenses both readings.
- Motion verbs now appear on the DATIVE side on purpose — schwimmen im See,
  tanzen in der Kueche, spielen auf der Strasse — because "is it moving?" is
  the wrong test and that is the main thing learners get wrong.
- The put/be pairs are drilled properly: legen/liegen, stellen/stehen,
  setzen/sitzen, plus haengen and stecken, which keep the same present tense
  either way so only the direct object tells you.
- ankommen added as the lexical exception: Dativ despite obvious arrival.
- New "Wohin oder wo?" info panel: the rule, the before-and-after test, the
  movement trap, the verb-pair table and the exception. Reachable from the
  practice screen, from the answer feedback, and from Charts.
- Feedback now explains the specific sentence rather than restating theory.

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 06:55:26 +00:00
co-authored by Claude Opus 5
parent f39db3cb86
commit 047454f23f
3 changed files with 418 additions and 67 deletions
+170 -34
View File
@@ -183,6 +183,7 @@ export default function App() {
const [ok, setOk] = useState(false);
const [res, setRes] = useState([]);
const [chart, setChart] = useState(false);
const [rule, setRule] = useState(false);
const [learnStep, setLearnStep] = useState(0);
const [st, setSt] = useState({sc:{},tc:0,ta:0,streak:0,ld:null});
const [access, setAccess] = useState(null);
@@ -283,6 +284,7 @@ export default function App() {
useEffect(() => {
const h = e => {
if (rule) { if (e.key === 'Escape') setRule(false); return; }
if (chart) { if (e.key === 'Escape') setChart(false); return; }
if (scr === 'summary' && e.key === 'Enter') { e.preventDefault(); summaryRef.current?.(); return; }
if (scr !== 'practice') return;
@@ -640,30 +642,36 @@ export default function App() {
{/* PRACTICE: wechsel in context */}
{scr === 'practice' && q?.type === 'wechselContext' && (
<div style={{textAlign:'center', paddingTop:18}}>
<div style={{...S.sentence, animation:shake?'shake 0.3s':'none'}}>{q.sentence}</div>
{(!hintHidden('translation') || rev) ? (
<>
<div style={S.translation}>
{q.translation}
{!rev && <button style={{...S.hintToggle, marginLeft:8}} onClick={()=>toggleHint('translation')}>hide</button>}
</div>
<div style={S.keyInfo}>
<span style={{fontWeight:700}}>{q.prepKey}</span>
<span style={S.keyInfoMute}> ({PREPS[q.prepKey].en})</span>
</div>
</>
) : (
<>
<div style={S.translation}>
<button style={S.hintToggle} onClick={()=>toggleHint('translation')}>show translation</button>
</div>
<div style={S.keyInfo}>
<span style={{fontWeight:700}}>{q.prepKey}</span>
</div>
</>
)}
<div style={S.secondaryContext}>Wechselpräposition · motion or location?</div>
<div style={{textAlign:'center', paddingTop:10}}>
<div style={{...S.sentence, animation:shake?'shake 0.3s':'none'}}>
{q.sentence.split('_____').map((part,i,a)=>(
<span key={i}>
{part}
{i < a.length-1 && (
<span style={{...S.blank,
borderBottomColor: rev?(ok?GREEN:RED):'#bbb',
color: rev?(ok?GREEN:RED):'#1a1a1a',
}}>
{rev ? q.article : ' '}
</span>
)}
</span>
))}
</div>
<Hideable hidden={hintHidden('translation') && !rev} onToggle={()=>toggleHint('translation')} label="translation" style={S.translation}>
{q.translation}
</Hideable>
<Hideable hidden={hintHidden('prepHint')} onToggle={()=>toggleHint('prepHint')} label="preposition" style={S.keyInfo}>
<span style={{fontWeight:700}}>{q.prepKey}</span>
<span style={S.keyInfoMute}> ({PREPS[q.prepKey].en})</span>
</Hideable>
<div style={S.secondaryContext}>
Wechselpräposition · wohin or wo?
<button style={S.ruleBtn} onClick={()=>setRule(true)}>ⓘ Rule</button>
</div>
{!rev ? (
<div style={{display:'flex',gap:10,justifyContent:'center',maxWidth:340,margin:'8px auto 0'}}>
@@ -678,7 +686,7 @@ export default function App() {
) : quickTick ? (
<QuickTick/>
) : (
<WechselContextFeedback q={q} inp={inp} ok={ok}/>
<WechselContextFeedback q={q} inp={inp} ok={ok} onRule={()=>setRule(true)}/>
)}
</div>
)}
@@ -772,7 +780,8 @@ export default function App() {
{scr !== 'practice' && <FeatureRequestFooter />}
</div>
{chart && <ChartModal section={chartSection} q={q} onClose={()=>setChart(false)}/>}
{chart && <ChartModal section={chartSection} q={q} onClose={()=>setChart(false)} onRule={()=>{ setChart(false); setRule(true); }}/>}
{rule && <WechselRuleModal onClose={()=>setRule(false)}/>}
<style>{`
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}
@@ -1063,17 +1072,135 @@ function PrepFeedback({ q, inp, ok }) {
);
}
function WechselContextFeedback({ q, inp, ok }) {
function WechselContextFeedback({ q, inp, ok, onRule }) {
return (
<div style={{...S.feedback, animation:'fadeUp 0.2s ease-out', maxWidth:420}}>
<div style={{fontSize:15, fontWeight:600, marginBottom:6, color:ok?GREEN:'#1a1a1a'}}>
<div style={{fontSize:15, fontWeight:600, marginBottom:8, color:ok?GREEN:'#1a1a1a'}}>
{ok ? <>✓ <span style={{color:CASE_C[q.ans]}}>{CL[q.ans]}</span></> : <>✗ <span style={{textDecoration:'line-through',color:'#999',fontWeight:400}}>{CL[inp]||inp}</span> <span style={{color:'#999',fontWeight:400}}>→</span> <span style={{color:CASE_C[q.ans]}}>{CL[q.ans]}</span></>}
</div>
<div style={{fontSize:13, color:'#666', marginBottom:6, lineHeight:1.5}}>
„{q.verb}" {q.isMotion ? 'implies motion (wohin?)' : 'implies location (wo?)'} → <span style={{color:CASE_C[q.ans], fontWeight:600}}>{CL[q.ans]}</span>
<div style={{fontSize:15, fontWeight:600, color:'#1a1a1a', marginBottom:8, lineHeight:1.45}}>
{q.answerSentence}
</div>
<div style={{fontSize:12, color:'#999', marginTop:4, lineHeight:1.6}}>
Wechselpräpositionen take <span style={{color:CASE_C.accusative}}>Akkusativ</span> when something moves toward a destination — wohin? (gehen, fahren, legen, stellen) — and <span style={{color:CASE_C.dative}}>Dativ</span> when it stays put — wo? (sein, stehen, sitzen, liegen).
{q.contraction && (
<div style={{fontSize:12, color:'#999', marginBottom:8}}>
{q.prepKey} + {q.article} fuses to <strong style={{color:'#666'}}>{q.contraction}</strong> in real German.
</div>
)}
<div style={{fontSize:13, color:'#666', marginBottom:6, lineHeight:1.5}}>
„{q.verb === 'ankommen' ? 'ankommen' : q.verb}" {q.isMotion
? 'puts something somewhere new (wohin?)'
: q.verb === 'ankommen'
? 'keeps the Dativ even though you arrive — the an- prefix already carries the arrival'
: 'just says where it all happens (wo?)'} → <span style={{color:CASE_C[q.ans], fontWeight:600}}>{CL[q.ans]}</span>
</div>
<div style={{fontSize:12, color:'#999', lineHeight:1.6}}>
Before this happened, was it already {PREPS[q.prepKey].en.split('/')[0]} the {q.nounEn}?
{' '}{q.isMotion
? <>No — it got there because of the action. That is <span style={{color:CASE_C.accusative}}>Akkusativ</span>.</>
: <>Yes — it was already there and stays. That is <span style={{color:CASE_C.dative}}>Dativ</span>.</>}
</div>
{onRule && (
<button style={{...S.ruleBtn, marginLeft:0, marginTop:10}} onClick={onRule}>ⓘ The whole rule</button>
)}
</div>
);
}
// The wohin/wo rule, the trap, the verb pairs, and the one real exception.
function WechselRuleModal({ onClose }) {
const PAIRS = [
['legen', 'lay it down', 'liegen', 'be lying'],
['stellen', 'stand it up', 'stehen', 'be standing'],
['setzen', 'sit it down', 'sitzen', 'be sitting'],
['hängen', 'hang it up (hängte)', 'hängen', 'be hanging (hing)'],
['stecken', 'stick it in', 'stecken', 'be stuck in'],
];
return (
<div style={S.overlay} onClick={onClose}>
<div style={S.modal} onClick={e=>e.stopPropagation()}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
<div style={{fontSize:17,fontWeight:700}}>Wohin oder wo?</div>
<button onClick={onClose} style={{background:'none',border:'none',fontSize:18,color:'#999',cursor:'pointer'}}>✕</button>
</div>
<div style={S.ruleLead}>
Yes — there is a rule, and it is not “is something moving?”. Ask instead:
does the phrase say where the thing <strong>ends up</strong>, or where the
whole action <strong>happens</strong>?
</div>
<div style={{...S.ruleCard, borderLeft:`3px solid ${CASE_C.accusative}`}}>
<div style={{fontWeight:700, color:CASE_C.accusative, fontSize:13, marginBottom:4}}>Akkusativ — wohin?</div>
<div style={S.ruleBody}>The action puts something into a new place. It wasn’t there before; it is now.</div>
<div style={S.ruleEx}>Er legt das Buch auf <strong>den</strong> Tisch. — the book arrives on the table.</div>
</div>
<div style={{...S.ruleCard, borderLeft:`3px solid ${CASE_C.dative}`}}>
<div style={{fontWeight:700, color:CASE_C.dative, fontSize:13, marginBottom:4}}>Dativ — wo?</div>
<div style={S.ruleBody}>The phrase just locates the scene. It was already there and stays there.</div>
<div style={S.ruleEx}>Das Buch liegt auf <strong>dem</strong> Tisch. — it was already on the table.</div>
</div>
<div style={S.ruleH}>The test that actually works</div>
<div style={S.ruleBody}>
<strong>“Before this happened, was it already there?”</strong><br/>
Already there → <span style={{color:CASE_C.dative,fontWeight:600}}>Dativ</span>.
Got there because of the action → <span style={{color:CASE_C.accusative,fontWeight:600}}>Akkusativ</span>.
</div>
<div style={S.ruleH}>The trap: movement is not the test</div>
<div style={S.ruleBody}>
Plenty of motion verbs take the Dativ, because the phrase is the arena the
movement happens in, not a destination:
</div>
<div style={S.ruleEx}>
Er schwimmt in <strong>dem</strong> See (im See) — swimming around <em>in</em> the lake.<br/>
Er springt in <strong>den</strong> See — he was out, now he’s in.
</div>
<div style={S.ruleBody}>Same story with tanzen, spielen, laufen, rennen.</div>
<div style={S.ruleH}>The verb pairs — learn these cold</div>
<div style={{fontSize:11, color:'#999', marginBottom:8, lineHeight:1.5}}>
The Akkusativ ones are transitive: they always have a direct object being moved.
If something is being put somewhere, the preposition takes Akkusativ too.
</div>
<table style={S.fullChart}>
<thead><tr>
<th style={{...S.fcTh, textAlign:'left', color:CASE_C.accusative}}>puts it there · Akk</th>
<th style={{...S.fcTh, textAlign:'left', color:CASE_C.dative}}>is already there · Dat</th>
</tr></thead>
<tbody>
{PAIRS.map(([a, aEn, d, dEn]) => (
<tr key={a + d}>
<td style={{...S.fcTd, textAlign:'left'}}>
<strong>{a}</strong> <span style={{color:'#aaa', fontSize:11}}>{aEn}</span>
</td>
<td style={{...S.fcTd, textAlign:'left'}}>
<strong>{d}</strong> <span style={{color:'#aaa', fontSize:11}}>{dEn}</span>
</td>
</tr>
))}
</tbody>
</table>
<div style={{fontSize:11, color:'#999', marginTop:6, lineHeight:1.5}}>
hängen and stecken are the mean ones — same present tense either way, so the
only clue is whether there is a direct object.
</div>
<div style={S.ruleH}>The exception worth knowing</div>
<div style={S.ruleBody}>
<strong>ankommen</strong> takes the <span style={{color:CASE_C.dative,fontWeight:600}}>Dativ</span>,
even though arriving is the most obvious movement there is:
</div>
<div style={S.ruleEx}>Wir kommen an <strong>dem</strong> Bahnhof an (am Bahnhof).</div>
<div style={S.ruleBody}>
The <em>an-</em> prefix already carries the arrival, so the phrase only says where
you ended up being. <em>befestigen an</em> + Dativ works the same way.
</div>
<button style={{...S.nextBtn, width:'100%', marginTop:16}} onClick={onClose}>
Got it{!IS_TOUCH && <span style={{opacity:0.5,fontSize:12,marginLeft:4}}>esc</span>}
</button>
</div>
</div>
);
@@ -1180,7 +1307,7 @@ function PronounFeedback({ q, inp, ok }) {
// CHART MODAL
// ═══════════════════════════════════════════
function ChartModal({ section, q, onClose }) {
function ChartModal({ section, q, onClose, onRule }) {
const genders = ['masculine','feminine','neuter','plural'];
const cases = ['nominative','accusative','dative','genitive'];
@@ -1251,7 +1378,10 @@ function ChartModal({ section, q, onClose }) {
<div><strong style={{color:CASE_C.wechsel}}>Wechsel:</strong> {PREPS_BY_CASE.wechsel.join(', ')}</div>
</div>
<div style={{fontSize:12, color:'#999', marginTop:14, lineHeight:1.6}}>
<strong>Wechsel</strong>: accusative for motion (wohin?), dative for location (wo?).
<strong>Wechsel</strong>: Akkusativ when the phrase is where something ends up
(wohin?), Dativ when it is where the action happens (wo?) — movement alone does
not decide it.
{onRule && <button style={{...S.ruleBtn, marginLeft:0, marginTop:8, display:'block'}} onClick={onRule}>ⓘ Wohin oder wo?</button>}
</div>
</div>
)}
@@ -1431,6 +1561,12 @@ const S = {
choiceBtn: { flex:'1 1 auto', minWidth:90, padding:'14px 16px', borderRadius:10, border:'1.5px solid', background:'#fff', color:'#1a1a1a', cursor:'pointer', fontSize:14, fontFamily:'inherit', textAlign:'center', transition:'all 0.12s',
...(IS_TOUCH ? {minHeight:66, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center'} : {}) },
ruleBtn: { marginLeft:8, background:'none', border:'1px solid #ddd', borderRadius:999, padding:'3px 10px', color:'#888', fontSize:11, fontWeight:600, cursor:'pointer', fontFamily:'inherit' },
ruleLead: { fontSize:13, color:'#444', lineHeight:1.6, marginBottom:14 },
ruleCard: { background:'#fafafa', borderRadius:8, padding:'12px 14px', marginBottom:10 },
ruleH: { fontSize:12, fontWeight:800, textTransform:'uppercase', letterSpacing:0.6, color:'#bbb', margin:'18px 0 6px' },
ruleBody: { fontSize:13, color:'#555', lineHeight:1.6 },
ruleEx: { fontSize:13, color:'#1a1a1a', lineHeight:1.7, background:'#f5f5f5', borderRadius:6, padding:'8px 10px', margin:'8px 0' },
feedback: { textAlign:'left', background:'#fff', borderRadius:10, padding:'14px 18px', margin:'0 auto', maxWidth:400, border:'1px solid #eee' },
stripTbl: { width:'100%', borderCollapse:'collapse', marginTop:10 },
+178 -32
View File
@@ -134,6 +134,33 @@ export const NOUNS = [
{w:'Wetter',g:'neuter',en:'weather',gen:'Wetters',noIndef:true},
{w:'Dach',g:'neuter',en:'roof',gen:'Daches'},
{w:'Konzert',g:'neuter',en:'concert',gen:'Konzerts'},
{w:'See',g:'masculine',en:'lake',gen:'Sees'},
{w:'Fluss',g:'masculine',en:'river',gen:'Flusses'},
{w:'Berg',g:'masculine',en:'mountain',gen:'Berges'},
{w:'Wald',g:'masculine',en:'forest',gen:'Waldes'},
{w:'Bahnhof',g:'masculine',en:'station',gen:'Bahnhofs'},
{w:'Flughafen',g:'masculine',en:'airport',gen:'Flughafens'},
{w:'Schrank',g:'masculine',en:'cupboard',gen:'Schranks'},
{w:'Boden',g:'masculine',en:'floor',gen:'Bodens'},
{w:'Supermarkt',g:'masculine',en:'supermarket',gen:'Supermarkts'},
{w:'Keller',g:'masculine',en:'cellar',gen:'Kellers'},
{w:'Wand',g:'feminine',en:'wall'},
{w:'Küche',g:'feminine',en:'kitchen'},
{w:'Wohnung',g:'feminine',en:'flat'},
{w:'Tafel',g:'feminine',en:'blackboard'},
{w:'Wiese',g:'feminine',en:'meadow'},
{w:'Tasche',g:'feminine',en:'bag'},
{w:'Kirche',g:'feminine',en:'church'},
{w:'Bibliothek',g:'feminine',en:'library'},
{w:'Bank',g:'feminine',en:'bench'},
{w:'Bett',g:'neuter',en:'bed',gen:'Bettes'},
{w:'Zimmer',g:'neuter',en:'room',gen:'Zimmers'},
{w:'Regal',g:'neuter',en:'shelf',gen:'Regals'},
{w:'Sofa',g:'neuter',en:'sofa',gen:'Sofas'},
{w:'Meer',g:'neuter',en:'sea',gen:'Meeres'},
{w:'Dorf',g:'neuter',en:'village',gen:'Dorfes'},
{w:'Bild',g:'neuter',en:'picture',gen:'Bildes'},
{w:'Wasser',g:'neuter',en:'water',gen:'Wassers',noIndef:true},
];
const NOUN_BY_NAME = Object.fromEntries(NOUNS.map(n => [n.w, n]));
export const nounForm = (noun, cas) =>
@@ -146,6 +173,7 @@ const PLACES_IN = ['Park','Garten','Schule','Stadt','Haus','Hotel','Museum','Bü
const POSSESSABLE = new Set([
...PERSONS, 'Hund','Katze','Haus','Garten','Auto','Buch','Tisch','Stuhl',
'Büro','Schule','Stadt','Fenster','Tür','Musik',
'Wohnung','Zimmer','Küche','Tasche','Bett','Sofa','Schrank','Regal','Wand','Bild',
]);
export const PRONOUNS = [
@@ -204,6 +232,24 @@ export const VERBS = {
hängen: {de:{Er:'hängt',Sie:'hängt',Wir:'hängen',Ich:'hänge'}, en:{Er:'hangs',Sie:'hangs',Wir:'hang',Ich:'hang'}},
legen: {de:{Er:'legt',Sie:'legt',Wir:'legen',Ich:'lege'}, en:{Er:'puts',Sie:'puts',Wir:'put',Ich:'put'}},
stellen: {de:{Er:'stellt',Sie:'stellt',Wir:'stellen',Ich:'stelle'}, en:{Er:'puts',Sie:'puts',Wir:'put',Ich:'put'}},
rennen: {de:{Er:'rennt',Sie:'rennt',Wir:'rennen',Ich:'renne'}, en:{Er:'runs',Sie:'runs',Wir:'run',Ich:'run'}},
klettern:{de:{Er:'klettert',Sie:'klettert',Wir:'klettern',Ich:'klettere'}, en:{Er:'climbs',Sie:'climbs',Wir:'climb',Ich:'climb'}},
fliegen: {de:{Er:'fliegt',Sie:'fliegt',Wir:'fliegen',Ich:'fliege'}, en:{Er:'flies',Sie:'flies',Wir:'fly',Ich:'fly'}},
schwimmen:{de:{Er:'schwimmt',Sie:'schwimmt',Wir:'schwimmen',Ich:'schwimme'}, en:{Er:'swims',Sie:'swims',Wir:'swim',Ich:'swim'}},
steigen: {de:{Er:'steigt',Sie:'steigt',Wir:'steigen',Ich:'steige'}, en:{Er:'climbs',Sie:'climbs',Wir:'climb',Ich:'climb'}},
werfen: {de:{Er:'wirft',Sie:'wirft',Wir:'werfen',Ich:'werfe'}, en:{Er:'throws',Sie:'throws',Wir:'throw',Ich:'throw'}},
stecken: {de:{Er:'steckt',Sie:'steckt',Wir:'stecken',Ich:'stecke'}, en:{Er:'puts',Sie:'puts',Wir:'put',Ich:'put'}},
setzen: {de:{Er:'setzt',Sie:'setzt',Wir:'setzen',Ich:'setze'}, en:{Er:'sits',Sie:'sits',Wir:'sit',Ich:'sit'}},
kleben: {de:{Er:'klebt',Sie:'klebt',Wir:'kleben',Ich:'klebe'}, en:{Er:'sticks',Sie:'sticks',Wir:'stick',Ich:'stick'}},
schreiben:{de:{Er:'schreibt',Sie:'schreibt',Wir:'schreiben',Ich:'schreibe'}, en:{Er:'writes',Sie:'writes',Wir:'write',Ich:'write'}},
fallen: {de:{Er:'fällt',Sie:'fällt',Wir:'fallen',Ich:'falle'}, en:{Er:'falls',Sie:'falls',Wir:'fall',Ich:'fall'}},
ziehen: {de:{Er:'zieht',Sie:'zieht',Wir:'ziehen',Ich:'ziehe'}, en:{Er:'moves',Sie:'moves',Wir:'move',Ich:'move'}},
tragen: {de:{Er:'trägt',Sie:'trägt',Wir:'tragen',Ich:'trage'}, en:{Er:'carries',Sie:'carries',Wir:'carry',Ich:'carry'}},
schieben:{de:{Er:'schiebt',Sie:'schiebt',Wir:'schieben',Ich:'schiebe'}, en:{Er:'pushes',Sie:'pushes',Wir:'push',Ich:'push'}},
packen: {de:{Er:'packt',Sie:'packt',Wir:'packen',Ich:'packe'}, en:{Er:'packs',Sie:'packs',Wir:'pack',Ich:'pack'}},
tanzen: {de:{Er:'tanzt',Sie:'tanzt',Wir:'tanzen',Ich:'tanze'}, en:{Er:'is dancing',Sie:'is dancing',Wir:'are dancing',Ich:'am dancing'}},
leben: {de:{Er:'lebt',Sie:'lebt',Wir:'leben',Ich:'lebe'}, en:{Er:'lives',Sie:'lives',Wir:'live',Ich:'live'}},
ankommen:{de:{Er:'kommt',Sie:'kommt',Wir:'kommen',Ich:'komme'}, en:{Er:'arrives',Sie:'arrives',Wir:'arrive',Ich:'arrive'}},
};
export const SUBJ_EN = {Er:'He',Sie:'She',Wir:'We',Ich:'I'};
@@ -307,6 +353,8 @@ export const WECHSEL_SET = new Set(PREPS_BY_CASE.wechsel);
// poss — false: skip this frame for possessive questions
// Wechsel preps use {motion:[...], location:[...]} instead of a flat list.
// ═══════════════════════════════════════════
const ZW_POST = {accusative:' und das Fenster', dative:' und dem Fenster', en:' and the window'};
export const PREP_FRAMES = {
durch: [{verbs:['gehen','laufen','fahren'], nouns:['Park','Stadt','Straße','Garten','Tür']}],
für: [{verbs:['kaufen'], mid:{de:'das Geschenk',en:'the gift'}, nouns:[...PERSONS,'Hund','Katze']}],
@@ -338,69 +386,158 @@ export const PREP_FRAMES = {
außerhalb: [{verbs:['wohnen','arbeiten'], nouns:['Stadt']}],
innerhalb: [{verbs:['wohnen','arbeiten'], nouns:['Stadt']}],
// ── Wechselpräpositionen ────────────────────────────────────
// Split by what DECIDES the case, not by "is there movement?".
// motion → the phrase is the goal: the thing wasn't there, now it is.
// location → the phrase is the arena: the thing was already there.
// Motion verbs deliberately appear on the DATIVE side too (schwimmen im See,
// tanzen in der Küche, spielen auf der Straße) — that is the whole lesson.
// Every frame is one-way: no verb+noun pairing may license both readings,
// because level 18 blanks the article and the learner has only the verb.
an: {
motion: [{verbs:['gehen','laufen'], nouns:['Tür','Fenster']}],
location:[{verbs:['stehen','warten','sitzen'], nouns:['Tür','Fenster']}],
motion: [
{verbs:['gehen','laufen'], nouns:['Tür','Fenster','Tafel']},
{verbs:['hängen'], mid:{de:'das Bild',en:'the picture'}, nouns:['Wand','Tür']},
{verbs:['schreiben'], mid:{de:'das Wort',en:'the word'}, nouns:['Tafel','Wand']},
{verbs:['kleben'], mid:{de:'den Zettel',en:'the note'}, nouns:['Tür','Fenster','Schrank']},
{verbs:['stellen'], mid:{de:'den Stuhl',en:'the chair'}, nouns:['Fenster','Wand','Tür']},
{verbs:['fahren'], nouns:['See','Fluss','Meer']},
],
location: [
{verbs:['stehen','warten','sitzen'], nouns:['Tür','Fenster','Tafel']},
{verbs:['hängen'], fixedSubj:{de:'Das Bild',en:'The picture'}, nouns:['Wand','Tür'], enVerb:{Er:'is hanging',Sie:'is hanging',Wir:'are hanging',Ich:'am hanging'}},
{verbs:['wohnen','leben'], nouns:['See','Fluss']},
{verbs:['arbeiten'], nouns:['Tisch','Fenster']},
{verbs:['spielen','schlafen'], nouns:['See','Fluss','Meer']},
// ankommen takes DATIVE although you obviously arrived — the an- prefix
// already carries the arrival, so the phrase only locates it.
{verbs:['ankommen'], nouns:['Bahnhof','Flughafen'], post:{dative:' an', en:''}},
],
},
auf: {
motion: [
{verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Tisch','Stuhl']},
{verbs:['gehen'], nouns:['Straße']},
{verbs:['springen'], fixedSubj:{de:'Die Katze',en:'The cat'}, nouns:['Tisch','Dach']},
{verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Tisch','Stuhl','Regal','Bett','Sofa','Boden']},
{verbs:['stellen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch','Regal','Boden','Schrank']},
{verbs:['setzen'], mid:{de:'das Kind',en:'the child'}, nouns:['Stuhl','Sofa','Bank','Bett']},
{verbs:['werfen'], mid:{de:'den Ball',en:'the ball'}, nouns:['Dach','Boden','Bett','Sofa']},
{verbs:['klettern','steigen'], nouns:['Berg','Baum','Dach','Schrank']},
{verbs:['springen'], fixedSubj:{de:'Die Katze',en:'The cat'}, nouns:['Tisch','Dach','Sofa','Schrank']},
{verbs:['gehen','laufen'], nouns:['Straße','Wiese','Brücke']},
],
location: [
{verbs:['sitzen'], nouns:['Stuhl']},
{verbs:['liegen'], fixedSubj:{de:'Das Buch',en:'The book'}, nouns:['Tisch']},
{verbs:['stehen','warten'], nouns:['Straße']},
{verbs:['sitzen'], nouns:['Stuhl','Sofa','Bank','Boden','Bett']},
{verbs:['liegen'], fixedSubj:{de:'Das Buch',en:'The book'}, nouns:['Tisch','Bett','Regal','Boden','Sofa']},
{verbs:['stehen','warten'], nouns:['Straße','Dach','Berg','Brücke','Wiese']},
{verbs:['spielen','tanzen'], nouns:['Straße','Wiese','Boden','Brücke']},
{verbs:['schlafen'], nouns:['Sofa','Boden','Bett']},
{verbs:['arbeiten'], nouns:['Dach','Wiese','Straße']},
],
},
hinter: {
motion: [{verbs:['gehen','laufen'], nouns:['Haus','Baum','Garten','Schule']}],
location:[{verbs:['stehen','warten'], nouns:['Haus','Baum','Tür','Schule']}],
motion: [
{verbs:['gehen','laufen','fahren'], nouns:['Haus','Baum','Garten','Schule','Berg','Kirche']},
{verbs:['stellen'], mid:{de:'den Stuhl',en:'the chair'}, nouns:['Tisch','Tür','Schrank','Sofa']},
{verbs:['werfen'], mid:{de:'den Ball',en:'the ball'}, nouns:['Haus','Baum','Schrank','Sofa']},
{verbs:['legen'], mid:{de:'die Tasche',en:'the bag'}, nouns:['Tür','Schrank','Sofa']},
],
location: [
{verbs:['stehen','warten'], nouns:['Haus','Baum','Tür','Schule','Kirche','Bahnhof']},
{verbs:['liegen'], fixedSubj:{de:'Der Ball',en:'The ball'}, nouns:['Schrank','Sofa','Baum','Haus']},
{verbs:['spielen'], nouns:['Haus','Schule','Garten','Kirche']},
{verbs:['sitzen'], nouns:['Tisch','Baum','Schrank']},
{verbs:['wohnen','leben'], nouns:['Bahnhof','Kirche','Supermarkt','Wald']},
],
},
in: {
motion: [{verbs:['gehen','fahren','kommen','laufen'], nouns:PLACES_IN}],
motion: [
{verbs:['gehen','fahren','kommen','laufen','rennen'], nouns:[...PLACES_IN,'Kirche','Bibliothek','Supermarkt','Wald','Dorf','Küche','Zimmer','Keller']},
{verbs:['springen','fallen'], nouns:['See','Fluss','Wasser','Meer']},
{verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Tasche','Schrank','Regal']},
{verbs:['stecken'], mid:{de:'den Schlüssel',en:'the key'}, nouns:['Tasche','Tür','Schrank']},
{verbs:['packen'], mid:{de:'das Buch',en:'the book'}, nouns:['Tasche','Schrank']},
{verbs:['tragen'], mid:{de:'die Tasche',en:'the bag'}, nouns:['Küche','Zimmer','Keller','Haus']},
{verbs:['ziehen'], nouns:['Wohnung','Stadt','Dorf','Haus']},
],
location: [
{verbs:['wohnen'], nouns:['Stadt','Haus','Hotel']},
{verbs:['sein','warten','sitzen','arbeiten'], nouns:PLACES_IN},
{verbs:['wohnen','leben'], nouns:['Stadt','Haus','Hotel','Dorf','Wohnung','Wald']},
{verbs:['sein','warten','sitzen','arbeiten'], nouns:[...PLACES_IN,'Kirche','Bibliothek','Supermarkt','Küche','Zimmer','Keller']},
{verbs:['schwimmen'], nouns:['See','Fluss','Meer','Wasser']},
{verbs:['tanzen','spielen'], nouns:['Küche','Zimmer','Garten','Wohnung','Wald']},
{verbs:['schlafen'], nouns:['Bett','Zimmer','Hotel','Keller']},
{verbs:['liegen'], fixedSubj:{de:'Das Buch',en:'The book'}, nouns:['Tasche','Schrank','Regal']},
{verbs:['stecken'], fixedSubj:{de:'Der Schlüssel',en:'The key'}, nouns:['Tür','Tasche','Schrank'], enVerb:{Er:'is stuck',Sie:'is stuck',Wir:'are stuck',Ich:'am stuck'}},
{verbs:['ankommen'], nouns:['Stadt','Hotel','Dorf'], post:{dative:' an', en:''}},
],
},
neben: {
motion: [
{verbs:['stellen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch','Stuhl','Tür']},
{verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Stuhl','Tisch']},
{verbs:['stellen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch','Stuhl','Tür','Bett','Sofa','Schrank']},
{verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Stuhl','Tisch','Bett','Sofa']},
{verbs:['setzen'], mid:{de:'das Kind',en:'the child'}, nouns:['Fenster','Tür','Schrank']},
{verbs:['schieben'], mid:{de:'den Stuhl',en:'the chair'}, nouns:['Tisch','Bett','Regal']},
],
location: [
{verbs:['stehen','sitzen'], nouns:['Tisch','Tür','Fenster']},
{verbs:['wohnen'], nouns:['Schule','Park','Museum']},
{verbs:['stehen','sitzen'], nouns:['Tisch','Tür','Fenster','Bett','Schrank','Sofa']},
{verbs:['wohnen','leben'], nouns:['Schule','Park','Museum','Kirche','Bahnhof','Supermarkt','Bibliothek']},
{verbs:['liegen'], fixedSubj:{de:'Das Buch',en:'The book'}, nouns:['Bett','Sofa','Tisch','Regal']},
{verbs:['warten'], nouns:['Tür','Bahnhof','Kirche']},
{verbs:['arbeiten'], nouns:['Fenster','Tür']},
],
},
über: {
motion: [
{verbs:['gehen','laufen'], nouns:['Straße','Brücke']},
{verbs:['hängen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch']},
{verbs:['gehen','laufen','fahren','rennen'], nouns:['Straße','Brücke','Wiese']},
{verbs:['hängen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch','Bett','Sofa']},
{verbs:['fliegen'], nouns:['Stadt','Berg','Meer','Wald','See']},
{verbs:['werfen'], mid:{de:'den Ball',en:'the ball'}, nouns:['Wand','Brücke','Schrank']},
],
location: [
{verbs:['hängen'], fixedSubj:{de:'Die Lampe',en:'The lamp'}, nouns:['Tisch','Bett','Sofa'], enVerb:{Er:'is hanging',Sie:'is hanging',Wir:'are hanging',Ich:'am hanging'}},
{verbs:['wohnen','leben'], nouns:['Restaurant','Supermarkt','Büro','Bibliothek']},
{verbs:['arbeiten'], nouns:['Restaurant','Büro','Supermarkt']},
],
location:[{verbs:['hängen'], fixedSubj:{de:'Die Lampe',en:'The lamp'}, nouns:['Tisch']}],
},
unter: {
motion: [
{verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Tisch','Stuhl']},
{verbs:['laufen'], fixedSubj:{de:'Die Katze',en:'The cat'}, nouns:['Tisch','Baum']},
{verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Tisch','Stuhl','Bett','Sofa','Regal']},
{verbs:['stellen'], mid:{de:'die Tasche',en:'the bag'}, nouns:['Tisch','Bett','Stuhl','Regal']},
{verbs:['werfen'], mid:{de:'den Ball',en:'the ball'}, nouns:['Sofa','Bett','Tisch','Schrank']},
{verbs:['schieben'], mid:{de:'die Tasche',en:'the bag'}, nouns:['Bett','Sofa','Tisch']},
{verbs:['laufen','rennen'], fixedSubj:{de:'Die Katze',en:'The cat'}, nouns:['Tisch','Baum','Bett','Sofa']},
],
location: [
{verbs:['sitzen','stehen','schlafen'], nouns:['Baum','Brücke']},
{verbs:['schlafen'], fixedSubj:{de:'Die Katze',en:'The cat'}, nouns:['Tisch','Stuhl']},
{verbs:['sitzen','stehen','schlafen'], nouns:['Baum','Brücke','Dach']},
{verbs:['liegen'], fixedSubj:{de:'Das Buch',en:'The book'}, nouns:['Bett','Sofa','Tisch','Stuhl','Regal']},
{verbs:['spielen','tanzen'], nouns:['Baum','Brücke','Dach']},
{verbs:['warten'], nouns:['Brücke','Baum','Dach']},
{verbs:['wohnen'], nouns:['Dach']},
],
},
vor: {
motion: [{verbs:['fahren','gehen'], nouns:['Haus','Tür','Schule','Hotel']}],
location:[{verbs:['stehen','warten'], nouns:['Tür','Haus','Schule','Hotel','Museum','Restaurant']}],
motion: [
{verbs:['fahren','gehen','laufen'], nouns:['Haus','Tür','Schule','Hotel','Kirche','Bahnhof','Supermarkt']},
{verbs:['stellen'], mid:{de:'den Stuhl',en:'the chair'}, nouns:['Fenster','Tür','Schrank','Regal']},
{verbs:['legen'], mid:{de:'die Tasche',en:'the bag'}, nouns:['Tür','Bett','Sofa']},
],
location: [
{verbs:['stehen','warten'], nouns:['Tür','Haus','Schule','Hotel','Museum','Restaurant','Kirche','Bahnhof','Supermarkt','Bibliothek']},
{verbs:['sitzen'], nouns:['Fenster','Haus','Tür']},
{verbs:['spielen'], nouns:['Haus','Schule','Kirche','Garten']},
{verbs:['tanzen'], nouns:['Haus','Kirche','Hotel']},
{verbs:['liegen'], fixedSubj:{de:'Die Tasche',en:'The bag'}, nouns:['Tür','Bett','Sofa','Schrank']},
],
},
zwischen: {
motion: [{verbs:['stellen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch','Stuhl'],
post:{accusative:' und das Fenster', dative:' und dem Fenster', en:' and the window'}}],
location: [{verbs:['stehen','sitzen'], nouns:['Tisch','Stuhl'],
post:{accusative:' und das Fenster', dative:' und dem Fenster', en:' and the window'}}],
motion: [
{verbs:['stellen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch','Stuhl','Sofa','Schrank'], post:ZW_POST},
{verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Tisch','Stuhl','Bett'], post:ZW_POST},
{verbs:['setzen'], mid:{de:'das Kind',en:'the child'}, nouns:['Stuhl','Sofa'], post:ZW_POST},
],
location: [
{verbs:['stehen','sitzen'], nouns:['Tisch','Stuhl','Sofa','Schrank'], post:ZW_POST},
{verbs:['liegen'], fixedSubj:{de:'Das Buch',en:'The book'}, nouns:['Tisch','Stuhl','Regal'], post:ZW_POST},
{verbs:['warten'], nouns:['Tisch','Schrank'], post:ZW_POST},
],
},
};
@@ -644,9 +781,18 @@ export function genWechselContextQ(level, state = newState()) {
const cas = isMotion ? 'accusative' : 'dative';
const r = realizeFrame(prepKey, cas, isMotion, state);
const art = DEF[r.noun.g][cas];
const sentence = `${r.subjDe} ${r.verbDe}${r.midDe} ${prepKey} ${art} ${nounForm(r.noun, cas)}${r.postDe}.`;
const head = `${r.subjDe} ${r.verbDe}${r.midDe}`;
const tail = `${nounForm(r.noun, cas)}${r.postDe}.`;
// The article is BLANKED. With "unter dem Stuhl" on screen the answer is just
// article recognition; blanked, the only evidence is the verb (and whether it
// has a direct object) — which is the actual wohin/wo skill.
const sentence = `${head} ${prepKey} _____ ${tail}`;
// The solved sentence contracts where German really contracts (in dem → im).
const fused = CONTRACTABLE[prepKey]?.[art] || null;
const answerSentence = fused ? `${head} ${fused} ${tail}` : `${head} ${prepKey} ${art} ${tail}`;
const translation = `${r.subjEn} ${r.verbEn}${r.midEn} ${r.enPrep} ${enNoun(r, r.enDet)}${r.postEn}.`;
return {type:'wechselContext', prepKey, ans:cas, isMotion, verb:r.verbKey, sentence, translation, nounEn:r.noun.en};
return {type:'wechselContext', prepKey, ans:cas, isMotion, verb:r.verbKey, noun:r.noun,
article:art, contraction:fused, sentence, answerSentence, translation, nounEn:r.noun.en};
}
const POSS_ACC_TEMPLATES = [
+70 -1
View File
@@ -3,7 +3,7 @@ import {
DEF, INDEF, PREPS, PREPS_BY_CASE, COMMON_PREPS, RARE_PREPS, ALL_PREPS,
NOUNS, nounForm, LEVELS, CONTRACTIONS, CONTRACTABLE, CHUNK_PREPS,
POSS_ENDINGS, combinePoss, PRONOUNS, VERBS, PREP_FRAMES,
createSession, gen, genContractionContextQ, newState, buildChunkChoices,
createSession, gen, genContractionContextQ, newState, buildChunkChoices, PREPS_BY_CASE as _PBC,
bumpStreak, currentStreak, weekDots,
} from './engine.js';
@@ -189,6 +189,75 @@ describe('wechsel in context (level 18)', () => {
expect(q.sentence.endsWith('.')).toBe(true);
}
});
it('blanks the article so the verb is the only evidence', () => {
for (const q of runSession(18, 300)) {
expect(q.sentence).toContain('_____');
expect(q.article).toBe(DEF[q.noun.g][q.ans]);
// the blanked sentence must never leak the answer article next to the prep
expect(q.sentence).not.toContain(` ${q.prepKey} ${q.article} `);
}
});
it('the solved sentence contracts exactly where German contracts', () => {
for (const q of runSession(18, 400)) {
const fused = CONTRACTABLE[q.prepKey]?.[q.article] || null;
expect(q.contraction).toBe(fused);
expect(q.answerSentence).not.toContain('_____');
if (fused) expect(q.answerSentence).toContain(` ${fused} `);
else expect(q.answerSentence).toContain(` ${q.prepKey} ${q.article} `);
}
});
it('drills far more than the handful of cats and lamps it used to', () => {
const qs = runSession(18, 400);
expect(new Set(qs.map(q => q.verb)).size).toBeGreaterThanOrEqual(20);
expect(new Set(qs.map(q => q.noun.w)).size).toBeGreaterThanOrEqual(25);
// no single sentence should dominate the round
const counts = {};
for (const q of qs) counts[q.answerSentence] = (counts[q.answerSentence] || 0) + 1;
expect(Math.max(...Object.values(counts))).toBeLessThanOrEqual(12);
});
it('teaches that movement alone is not Akkusativ', () => {
const qs = runSession(18, 600);
// motion verbs used with a dative "arena" reading
const arena = qs.filter(q => ['schwimmen','tanzen','spielen','fliegen'].includes(q.verb) && q.ans === 'dative');
expect(arena.length).toBeGreaterThan(0);
// ankommen is the lexical exception: always dative
const ank = qs.filter(q => q.verb === 'ankommen');
expect(ank.length).toBeGreaterThan(0);
for (const q of ank) expect(q.ans).toBe('dative');
});
it('the put/be verb pairs land on the case their transitivity demands', () => {
// Unambiguously transitive "put it there" verbs vs unambiguously stative ones.
// hängen and stecken are deliberately excluded — see the next test.
const PUT = ['legen','stellen','setzen','werfen','schieben','packen','tragen','kleben','schreiben'];
const BE = ['liegen','stehen','sitzen','wohnen','leben','schlafen','warten','arbeiten','sein'];
for (const q of runSession(18, 600)) {
if (PUT.includes(q.verb)) expect(q.ans, `${q.answerSentence}`).toBe('accusative');
if (BE.includes(q.verb)) expect(q.ans, `${q.answerSentence}`).toBe('dative');
}
});
it('hängen and stecken appear in BOTH cases — only the direct object decides', () => {
const qs = runSession(18, 800);
for (const v of ['hängen', 'stecken']) {
const mine = qs.filter(q => q.verb === v);
expect(mine.some(q => q.ans === 'accusative'), `${v} never showed transitive`).toBe(true);
expect(mine.some(q => q.ans === 'dative'), `${v} never showed intransitive`).toBe(true);
// transitive = something is being moved, so the sentence carries an object
// before the preposition; intransitive sentences start with that thing.
for (const q of mine) {
const beforePrep = q.answerSentence.split(` ${q.prepKey} `)[0];
const fusedHead = q.contraction ? q.answerSentence.split(` ${q.contraction} `)[0] : beforePrep;
const head = q.contraction ? fusedHead : beforePrep;
const hasObject = /\b(das|den|die|dem)\b/.test(head);
expect(hasObject, `${q.answerSentence} (${q.ans})`).toBe(q.ans === 'accusative');
}
}
});
});
// ── Chunk / contraction questions ───────────────────────────