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:
+170
-34
@@ -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
@@ -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
@@ -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 ───────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user