Add the Adjectives section, and explain why the noun itself changes
Two gaps, both visible from one screenshot of the Genitiv level. 1. Adjective endings were half-present and entirely untested. STRONG and WEAK existed in engine.js with correct data, but only as the S/W columns of a reference chart — nothing generated them, nothing tested them, and the MIXED table (the one "in seinem eigenen Kontext" actually needs) did not exist at all. There was not a single adjective in the codebase. 2. Nothing explained why "Regen" becomes "Regens" after trotz. New Adjectives tab, six lessons plus Daily Practice: Schwach after der/diese/jede — only -e and -en Gemischt after ein/kein/mein — weak plus three exceptions Stark no determiner — the adjective carries the case Welche Tabelle? der-word, ein-word or nothing? (the meta-skill) Nach Präposition the full chain: preposition -> case -> table Alles gemischt all three tables, including Genitiv The three tables are built on one idea: the case has to be signalled once in the noun phrase, and whoever can show it does. MIXED is therefore not hand-typed — it is DERIVED from POSS_ENDINGS as "weak, except in the three slots where the ein-word has no ending of its own, where it borrows the strong one". A test asserts that relationship as well as all 48 cells, so the rule cannot silently rot. Two info panels: - "Which ending table?" — the one-signal idea, the three tables, plus the two traps: strong genitive masc/neut is -en not -es (the noun already carries the -(e)s), and predicate adjectives take no ending at all. - "Why Regens?" — appears wherever the case changes the noun itself: the -es/-s split, that feminines and plurals never change, the dative plural -n, and a note on the N-declension nouns Beug does not have yet. Notable decisions: - Nominative is now tested, for adjectives only. der gute Mann vs ein guter Mann is the clearest illustration of the whole weak/mixed split and it exists only in the nominative, so the app-wide "no nominative" rule is explicitly scoped to non-adjective levels rather than quietly dropped. - The tab bar measured 369px of content in a 350px bar with four tabs, so a fifth could not fit. The row now scrolls instead of squeezing labels. - Adjectives are gated like Possessives and Pronouns. The corpus caught its keep again. Adjective sentences are enumerated into corpus.txt keyed on the semantic pairing (frame x adjective x noun), which keeps it at ~2000 lines instead of exploding over every determiner and case. Reading it surfaced, and this commit fixes: "I live in the old lake" (places split into buildings/settlements/landscape), "I play with the blue sofa", "I like the bad kitchen" (frames that assert liking now exclude negative adjectives), "I buy the good time" and "I cook with the good money" (pools narrowed to what you can buy and cook with), grosser/kleiner of a person rendering as "big/small" rather than "tall/short", and "in dem alten Hotel" failing to fuse into "im alten Hotel". 130 tests pass; 12000 generated questions across all 30 levels. Known gap, stated in the rule panel: Beug has no plural nouns, so the plural column of all three tables is shown for reference but not drilled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
This commit is contained in:
+306
@@ -168,6 +168,14 @@ export const NOUNS = [
|
||||
{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},
|
||||
{w:'Brot',g:'neuter',en:'bread',gen:'Brotes',noIndef:true},
|
||||
{w:'Bier',g:'neuter',en:'beer',gen:'Bieres',noIndef:true},
|
||||
{w:'Geld',g:'neuter',en:'money',gen:'Geldes',noIndef:true},
|
||||
{w:'Kaffee',g:'masculine',en:'coffee',gen:'Kaffees',noIndef:true},
|
||||
{w:'Tee',g:'masculine',en:'tea',gen:'Tees',noIndef:true},
|
||||
{w:'Wein',g:'masculine',en:'wine',gen:'Weines',noIndef:true},
|
||||
{w:'Milch',g:'feminine',en:'milk',noIndef:true},
|
||||
{w:'Zeit',g:'feminine',en:'time',noIndef:true},
|
||||
];
|
||||
const NOUN_BY_NAME = Object.fromEntries(NOUNS.map(n => [n.w, n]));
|
||||
export const nounForm = (noun, cas) =>
|
||||
@@ -220,6 +228,174 @@ export const POSS_ENDINGS = {
|
||||
};
|
||||
export const combinePoss = (stem, ending) => (stem === 'euer' && ending) ? 'eur' + ending : stem + ending;
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// ADJECTIVE ENDINGS
|
||||
// Three tables, chosen by what stands in front of the adjective. The idea the
|
||||
// whole system rests on (the "one signal" principle American courses teach):
|
||||
// the case/gender signal has to appear ONCE in the noun phrase. If the
|
||||
// determiner already carries it, the adjective goes limp (-e/-en). If there is
|
||||
// no determiner, the adjective has to carry it itself.
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
// der-word endings: der/die/das, dieser, jeder, welcher, jener, alle.
|
||||
// Note genitive masc/neut is -es here but -en on an adjective — the one place
|
||||
// the two tables part company.
|
||||
export const DER_ENDINGS = {
|
||||
masculine:{nominative:'er',accusative:'en',dative:'em',genitive:'es'},
|
||||
feminine: {nominative:'e', accusative:'e', dative:'er',genitive:'er'},
|
||||
neuter: {nominative:'es',accusative:'es',dative:'em',genitive:'es'},
|
||||
plural: {nominative:'e', accusative:'e', dative:'en',genitive:'er'},
|
||||
};
|
||||
|
||||
const ADJ_GENDERS = ['masculine','feminine','neuter','plural'];
|
||||
const ADJ_CASES = ['nominative','accusative','dative','genitive'];
|
||||
|
||||
// STRONG/WEAK already exist above as single signal letters; as adjective
|
||||
// endings they are those letters with a leading -e where needed.
|
||||
const ending = letter => (letter === 'e' ? 'e' : 'e' + letter);
|
||||
export const ADJ_STRONG = Object.fromEntries(ADJ_GENDERS.map(g =>
|
||||
[g, Object.fromEntries(ADJ_CASES.map(c => [c, ending(STRONG[g][c])]))]));
|
||||
export const ADJ_WEAK = Object.fromEntries(ADJ_GENDERS.map(g =>
|
||||
[g, Object.fromEntries(ADJ_CASES.map(c => [c, ending(WEAK[g][c])]))]));
|
||||
|
||||
// MIXED is not a third thing to memorise: it IS weak, except in the three
|
||||
// slots where the ein-word itself has no ending (ein_ guter, ein_ gutes,
|
||||
// ein_ gutes). There the adjective picks up the strong ending instead.
|
||||
// Derived from POSS_ENDINGS so the rule is enforced, never duplicated.
|
||||
export const ADJ_MIXED = Object.fromEntries(ADJ_GENDERS.map(g =>
|
||||
[g, Object.fromEntries(ADJ_CASES.map(c =>
|
||||
[c, POSS_ENDINGS[g][c] === '' ? ADJ_STRONG[g][c] : ADJ_WEAK[g][c]]))]));
|
||||
|
||||
export const ADJ_ENDINGS = { strong: ADJ_STRONG, weak: ADJ_WEAK, mixed: ADJ_MIXED };
|
||||
export const ADJ_CLASS_LABEL = { weak:'Schwach', mixed:'Gemischt', strong:'Stark' };
|
||||
export const ADJ_CLASS_HINT = {
|
||||
weak: 'after a der-word — it already shows the case',
|
||||
mixed: 'after an ein-word — sometimes it shows nothing',
|
||||
strong:'no determiner — the adjective must show the case',
|
||||
};
|
||||
// Every distinct ending, so wrong answers are always plausible endings.
|
||||
export const ADJ_ALL_ENDINGS = ['e','en','er','es','em'];
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// ADJECTIVES
|
||||
// Adjectives declare which kinds of noun they may describe, the same
|
||||
// discipline the preposition frames use — "ein grünes Kind" has to be
|
||||
// impossible to build, not merely unlikely to come up.
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
export const ADJ_NOUN_GROUPS = {
|
||||
people: ['Mann','Frau','Arzt','Ärztin','Freund','Freundin','Lehrer','Lehrerin',
|
||||
'Bruder','Schwester','Vater','Mutter','Tochter','Kind','Mädchen'],
|
||||
animals: ['Hund','Katze'],
|
||||
things: ['Buch','Auto','Tisch','Stuhl','Fenster','Tür','Bett','Sofa','Schrank',
|
||||
'Regal','Tasche','Bild','Zug'],
|
||||
// Buildings you can be inside; settlements you can live in; landscape you
|
||||
// cannot ("Ich wohne in dem alten See" was reachable before this split).
|
||||
buildings: ['Haus','Hotel','Museum','Büro','Restaurant','Kirche','Bibliothek',
|
||||
'Supermarkt','Schule','Bahnhof','Zimmer','Küche','Keller','Wohnung'],
|
||||
settlements:['Stadt','Dorf'],
|
||||
landscape: ['Park','Garten','Wald','See','Fluss','Berg','Meer','Wiese','Brücke','Straße'],
|
||||
mass: ['Wasser','Milch','Brot','Kaffee','Tee','Bier','Wein','Geld','Zeit','Musik','Arbeit'],
|
||||
weather: ['Wetter','Regen'],
|
||||
};
|
||||
|
||||
// groups = broad compatibility; nouns = an explicit list where the adjective is
|
||||
// fussy and a whole group would let nonsense through.
|
||||
// enPeople: German groß/klein mean tall/short of a person.
|
||||
// neg: excluded from frames that assert liking ("I like the bad kitchen").
|
||||
export const ADJECTIVES = [
|
||||
{de:'groß', en:'big', enPeople:'tall', groups:['people','animals','things','buildings','settlements','landscape']},
|
||||
{de:'klein', en:'small', enPeople:'short', groups:['people','animals','things','buildings','settlements','landscape']},
|
||||
{de:'alt', en:'old', groups:['people','animals','things','buildings','settlements']},
|
||||
{de:'neu', en:'new', groups:['things','buildings']},
|
||||
{de:'jung', en:'young', groups:['people','animals']},
|
||||
{de:'schön', en:'beautiful', groups:['things','buildings','settlements','landscape'], nouns:['Musik','Wetter']},
|
||||
{de:'gut', en:'good', groups:['things','buildings','people','mass','weather']},
|
||||
{de:'schlecht', en:'bad', neg:true, groups:['things','buildings','mass','weather']},
|
||||
{de:'freundlich', en:'friendly', groups:['people','animals']},
|
||||
{de:'müde', en:'tired', neg:true, groups:['people','animals']},
|
||||
{de:'krank', en:'sick', neg:true, groups:['people','animals']},
|
||||
{de:'interessant', en:'interesting', nouns:['Buch','Bild','Museum','Arbeit','Musik','Konzert']},
|
||||
{de:'wichtig', en:'important', nouns:['Buch','Arbeit','Zeit','Geld','Konzert']},
|
||||
{de:'billig', en:'cheap', nouns:['Auto','Buch','Tasche','Zug','Hotel','Restaurant','Kaffee','Bier','Wein','Bild']},
|
||||
{de:'kalt', en:'cold', nouns:['Wasser','Milch','Kaffee','Tee','Bier','Wetter','Zimmer','Keller','Haus','Nacht']},
|
||||
{de:'warm', en:'warm', nouns:['Wasser','Milch','Kaffee','Tee','Brot','Wetter','Zimmer','Haus']},
|
||||
{de:'laut', en:'loud', neg:true, nouns:['Musik','Stadt','Straße','Restaurant','Hund','Konzert','Kind']},
|
||||
{de:'ruhig', en:'quiet', nouns:['Zimmer','Dorf','Garten','Wald','Straße','Hund','Katze','Kind']},
|
||||
{de:'lang', en:'long', nouns:['Straße','Brücke','Fluss','Buch','Nacht','Woche','Arbeit']},
|
||||
{de:'rot', en:'red', nouns:['Auto','Buch','Tasche','Tür','Sofa','Bild','Wein','Katze']},
|
||||
{de:'grün', en:'green', nouns:['Auto','Buch','Tasche','Tür','Sofa','Wiese','Wald','Tee']},
|
||||
{de:'schwarz', en:'black', nouns:['Auto','Buch','Tasche','Tür','Sofa','Hund','Katze','Kaffee','Tee','Bild']},
|
||||
{de:'weiß', en:'white', nouns:['Auto','Haus','Tür','Sofa','Tasche','Hund','Katze','Milch','Brot','Wein']},
|
||||
{de:'blau', en:'blue', nouns:['Auto','Buch','Tasche','Tür','Sofa','See','Meer']},
|
||||
];
|
||||
|
||||
export const adjNouns = adj => [...new Set([
|
||||
...(adj.groups || []).flatMap(g => ADJ_NOUN_GROUPS[g]),
|
||||
...(adj.nouns || []),
|
||||
])];
|
||||
const PEOPLE_SET = new Set(ADJ_NOUN_GROUPS.people);
|
||||
export const adjEn = (adj, noun) => (adj.enPeople && PEOPLE_SET.has(noun.w)) ? adj.enPeople : adj.en;
|
||||
|
||||
export const DER_WORD_DETS = [
|
||||
{stem:'d', en:'the', definite:true},
|
||||
{stem:'dies', en:'this'},
|
||||
{stem:'jed', en:'every'},
|
||||
];
|
||||
export const EIN_WORD_DETS = [
|
||||
{stem:'ein', en:'a'},
|
||||
{stem:'kein', en:'no'},
|
||||
{stem:'mein', en:'my', poss:true},
|
||||
{stem:'dein', en:'your', poss:true},
|
||||
{stem:'sein', en:'his', poss:true},
|
||||
{stem:'ihr', en:'her', poss:true},
|
||||
{stem:'unser', en:'our', poss:true},
|
||||
];
|
||||
|
||||
export function determinerFor(cls, det, g, cas) {
|
||||
if (cls === 'strong') return '';
|
||||
if (cls === 'weak') return det.definite ? DEF[g][cas] : det.stem + DER_ENDINGS[g][cas];
|
||||
return combinePoss(det.stem, POSS_ENDINGS[g][cas]);
|
||||
}
|
||||
|
||||
// Sentence carriers. `prep` is kept OUT of dePre so it can fuse with the
|
||||
// article (in + dem -> im). deCap/enCap mark which sentence the noun phrase opens.
|
||||
// noNeg drops adjectives that would contradict the frame ("I like the bad hotel").
|
||||
export const ADJ_FRAMES = [
|
||||
{cas:'nominative', dePre:'', dePost:' gefällt mir.', enPre:'I like ', enPost:'.', deCap:true, noNeg:true,
|
||||
groups:['people','animals','things','buildings','settlements','landscape'],
|
||||
nouns:['Musik','Wetter','Wein','Kaffee','Brot']},
|
||||
{cas:'nominative', dePre:'', dePost:' ist da.', enPre:'', enPost:' is there.', deCap:true, enCap:true,
|
||||
groups:['people','animals','things']},
|
||||
{cas:'accusative', dePre:'Ich sehe ', dePost:'.', enPre:'I see ', enPost:'.',
|
||||
groups:['people','animals','things','buildings','settlements','landscape']},
|
||||
{cas:'accusative', dePre:'Ich kaufe ', dePost:'.', enPre:'I buy ', enPost:'.',
|
||||
groups:['things'], nouns:['Brot','Wein','Bier','Kaffee','Tee','Milch']},
|
||||
{cas:'accusative', dePre:'Ich mag ', dePost:'.', enPre:'I like ', enPost:'.', noNeg:true,
|
||||
groups:['people','animals','things','buildings','settlements','landscape'],
|
||||
nouns:['Musik','Kaffee','Tee','Wein','Bier','Brot']},
|
||||
{cas:'dative', dePre:'Ich helfe ', dePost:'.', enPre:'I help ', enPost:'.',
|
||||
groups:['people','animals']},
|
||||
{cas:'dative', dePre:'Ich spiele ', prep:'mit', dePost:'.', enPre:'I play with ', enPost:'.',
|
||||
groups:['animals']},
|
||||
{cas:'dative', dePre:'Ich wohne ', prep:'in', dePost:'.', enPre:'I live in ', enPost:'.',
|
||||
groups:['buildings','settlements']},
|
||||
{cas:'dative', dePre:'Ich koche ', prep:'mit', dePost:'.', enPre:'I cook with ', enPost:'.',
|
||||
nouns:['Wasser','Milch','Brot','Wein']},
|
||||
{cas:'dative', dePre:'Ich warte ', prep:'vor', dePost:'.', enPre:'I wait in front of ', enPost:'.',
|
||||
groups:['buildings']},
|
||||
{cas:'genitive', dePre:'Ich arbeite ', prep:'trotz', dePost:'.', enPre:'I work despite ', enPost:'.',
|
||||
nouns:['Wetter','Regen','Musik','Arbeit'], classes:['weak','strong']},
|
||||
{cas:'genitive', dePre:'Ich bleibe ', prep:'wegen', dePost:'.', enPre:'I stay because of ', enPost:'.',
|
||||
nouns:['Wetter','Regen','Musik'], classes:['weak','strong']},
|
||||
];
|
||||
ADJ_FRAMES.forEach((f, i) => { f.id = i; });
|
||||
const MASS_LIKE = new Set([...ADJ_NOUN_GROUPS.mass, ...ADJ_NOUN_GROUPS.weather]);
|
||||
const framePool = f => [...new Set([
|
||||
...(f.groups || []).flatMap(g => ADJ_NOUN_GROUPS[g]),
|
||||
...(f.nouns || []),
|
||||
])];
|
||||
|
||||
export const CL = {nominative:'Nominativ',accusative:'Akkusativ',dative:'Dativ',genitive:'Genitiv',wechsel:'Wechsel'};
|
||||
export const CASE_C = {nominative:'#2563eb',accusative:'#dc2626',dative:'#ea580c',genitive:'#16a34a',wechsel:'#7c3aed'};
|
||||
export const GENDER_C = {masculine:'#16a34a',feminine:'#db2777',neuter:'#2563eb',plural:'#7c3aed'};
|
||||
@@ -324,6 +500,17 @@ export const LEVELS = [
|
||||
{id:16, tab:'pronoun', name:'Sie (formal)', desc:'Formal Sie — singular and plural', pronouns:FORMAL_PRONOUN_KEYS, cases:['accusative','dative'], type:'pronoun'},
|
||||
{id:17, tab:'pronoun', name:'Alle gemischt', desc:'All 10 pronouns, Akkusativ & Dativ', pronouns:ALL_PRONOUN_KEYS, cases:['accusative','dative'], type:'pronoun'},
|
||||
{id:17.5, tab:'pronoun', name:'Daily Practice', desc:'Every pronoun lesson, mixed · one-tap', daily:true},
|
||||
|
||||
// Adjective endings. Nominative IS tested here, unlike everywhere else in the
|
||||
// app: der gute Mann vs ein guter Mann is the clearest illustration of the
|
||||
// whole weak/mixed split, and skipping it would gut the section.
|
||||
{id:20, tab:'adj', name:'Schwach', desc:'After der, diese, jede — only -e and -en', classes:['weak'], cases:['nominative','accusative','dative'], type:'adjective'},
|
||||
{id:21, tab:'adj', name:'Gemischt', desc:'After ein, kein, mein — weak plus three exceptions', classes:['mixed'], cases:['nominative','accusative','dative'], type:'adjective'},
|
||||
{id:22, tab:'adj', name:'Stark', desc:'No determiner — the adjective carries the case', classes:['strong'], cases:['nominative','accusative','dative'], type:'adjective'},
|
||||
{id:23, tab:'adj', name:'Welche Tabelle?', desc:'der-word, ein-word or nothing at all?', classes:['weak','mixed','strong'], cases:['nominative','accusative','dative'], type:'adjClass'},
|
||||
{id:24, tab:'adj', name:'Nach Präposition', desc:'The full chain: preposition → case → table', classes:['weak','mixed','strong'], cases:['accusative','dative','genitive'], type:'adjective', prepOnly:true},
|
||||
{id:25, tab:'adj', name:'Alles gemischt', desc:'All three tables, including Genitiv', classes:['weak','mixed','strong'], cases:['nominative','accusative','dative','genitive'], type:'adjective'},
|
||||
{id:25.5, tab:'adj', name:'Daily Practice', desc:'Every adjective lesson, mixed · one-tap', daily:true},
|
||||
];
|
||||
|
||||
// Contractions (preposition + article). pref:true = preferred even in writing.
|
||||
@@ -1014,8 +1201,97 @@ export function genContractionContextQ(state = newState()) {
|
||||
return makeChunkQ(c.prep, cas, tw, state, genders);
|
||||
}
|
||||
|
||||
// Build one adjective question. Order matters: the class picks the determiner,
|
||||
// the determiner constrains the nouns ("ein Wasser" is not a thing), the noun
|
||||
// picks the adjective.
|
||||
function buildAdjective(lv, state) {
|
||||
for (let attempt = 0; attempt < 16; attempt++) {
|
||||
const cls = drawBag(state, 'adjcls', lv.classes, state.last.adjcls);
|
||||
let frames = ADJ_FRAMES.filter(f =>
|
||||
lv.cases.includes(f.cas) &&
|
||||
(!f.classes || f.classes.includes(cls)) &&
|
||||
(!lv.prepOnly || f.prep));
|
||||
// Strong only occurs where German really drops the determiner: mass nouns.
|
||||
if (cls === 'strong') frames = frames.filter(f => framePool(f).some(n => MASS_LIKE.has(n)));
|
||||
if (!frames.length) continue;
|
||||
|
||||
const frame = pick(frames);
|
||||
const detObj = cls === 'strong' ? null : pick(cls === 'weak' ? DER_WORD_DETS : EIN_WORD_DETS);
|
||||
|
||||
let pool = framePool(frame);
|
||||
if (cls === 'strong') pool = pool.filter(n => MASS_LIKE.has(n));
|
||||
// "ein Wasser" is not a thing, but "kein Geld" and "meine Zeit" are.
|
||||
if (detObj?.stem === 'ein') pool = pool.filter(n => !NOUN_BY_NAME[n].noIndef);
|
||||
if (!pool.length) continue;
|
||||
|
||||
let candidates = ADJECTIVES.filter(a => adjNouns(a).some(n => pool.includes(n)));
|
||||
if (frame.noNeg) candidates = candidates.filter(a => !a.neg);
|
||||
if (!candidates.length) continue;
|
||||
|
||||
const adj = pickAvoid(candidates, state.last.adj);
|
||||
const nounName = pickAvoid(adjNouns(adj).filter(n => pool.includes(n)), state.last.noun);
|
||||
state.last.adj = adj; state.last.noun = nounName; state.last.adjcls = cls;
|
||||
|
||||
const noun = NOUN_BY_NAME[nounName];
|
||||
const cas = frame.cas;
|
||||
const det = determinerFor(cls, detObj, noun.g, cas);
|
||||
const ending = ADJ_ENDINGS[cls][noun.g][cas];
|
||||
const nounDe = nounForm(noun, cas);
|
||||
|
||||
// Fuse the preposition with a definite article the way German really does.
|
||||
const fused = frame.prep && det ? CONTRACTABLE[frame.prep]?.[det] : null;
|
||||
const lead = !frame.prep ? (det ? det + ' ' : '')
|
||||
: fused ? fused + ' '
|
||||
: `${frame.prep} ${det ? det + ' ' : ''}`;
|
||||
|
||||
let blanked = `${lead}${adj.de}_____ ${nounDe}`;
|
||||
let solved = `${lead}${adj.de}${ending} ${nounDe}`;
|
||||
if (frame.deCap) { blanked = cap(blanked); solved = cap(solved); }
|
||||
|
||||
let enDet = cls === 'strong' ? '' : (detObj.definite ? 'the' : detObj.en);
|
||||
const adjE = adjEn(adj, noun);
|
||||
if (enDet === 'a' && /^[aeiou]/i.test(adjE)) enDet = 'an';
|
||||
const nounEn = detObj?.poss ? possNounEn(noun) : noun.en;
|
||||
let enPhrase = `${enDet}${enDet ? ' ' : ''}${adjE} ${nounEn}`;
|
||||
if (frame.enCap) enPhrase = cap(enPhrase);
|
||||
|
||||
return {
|
||||
cls, cas, frame, adj, noun, det, detObj, ending, fused,
|
||||
sentence: `${frame.dePre}${blanked}${frame.dePost}`,
|
||||
answerSentence: `${frame.dePre}${solved}${frame.dePost}`,
|
||||
translation: `${frame.enPre}${enPhrase}${frame.enPost}`,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function genAdjectiveQ(level, state = newState()) {
|
||||
const lv = LEVELS.find(l => l.id === level);
|
||||
const b = buildAdjective(lv, state);
|
||||
return {
|
||||
type:'adjective', cls:b.cls, cas:b.cas, frameId:b.frame.id, prepKey:b.frame.prep || null,
|
||||
adj:b.adj, noun:b.noun, det:b.det, ans:b.ending,
|
||||
sentence:b.sentence, answerSentence:b.answerSentence, translation:b.translation,
|
||||
choices: pickChoices(b.ending, ADJ_ALL_ENDINGS, 4),
|
||||
};
|
||||
}
|
||||
|
||||
// The meta-skill: name the table before you can pick the ending.
|
||||
export function genAdjClassQ(level, state = newState()) {
|
||||
const lv = LEVELS.find(l => l.id === level);
|
||||
const b = buildAdjective(lv, state);
|
||||
return {
|
||||
type:'adjClass', cls:b.cls, cas:b.cas, frameId:b.frame.id, prepKey:b.frame.prep || null,
|
||||
adj:b.adj, noun:b.noun, det:b.det, ending:b.ending, ans:b.cls,
|
||||
sentence:b.sentence, answerSentence:b.answerSentence, translation:b.translation,
|
||||
choices:['weak','mixed','strong'],
|
||||
};
|
||||
}
|
||||
|
||||
export function gen(level, state = newState()) {
|
||||
const lv = LEVELS.find(l=>l.id===level);
|
||||
if (lv.type==='adjective') return genAdjectiveQ(level, state);
|
||||
if (lv.type==='adjClass') return genAdjClassQ(level, state);
|
||||
if (lv.type==='chunk') return genContractionContextQ(state);
|
||||
if (lv.type==='contraction') return genContractionQ(state);
|
||||
if (lv.type==='prep') return genPrepQ(level, state);
|
||||
@@ -1118,6 +1394,34 @@ export function enumerateCorpus() {
|
||||
}
|
||||
}
|
||||
|
||||
// Adjective sentences. Keyed on the SEMANTIC pairing — frame x adjective x
|
||||
// noun — because that is what carries the risk ("kalter Kaffee" fine,
|
||||
// "kalte Mutter" not). The determiner, class and ending are mechanical and
|
||||
// covered exhaustively by the ending-table tests instead, which keeps this
|
||||
// from exploding into hundreds of thousands of lines.
|
||||
for (const frame of ADJ_FRAMES) {
|
||||
const pool = framePool(frame);
|
||||
for (const adj of ADJECTIVES) {
|
||||
if (frame.noNeg && adj.neg) continue;
|
||||
for (const n of adjNouns(adj)) {
|
||||
if (!pool.includes(n)) continue;
|
||||
const noun = NOUN_BY_NAME[n];
|
||||
const cas = frame.cas;
|
||||
const det = DEF[noun.g][cas];
|
||||
const ending = ADJ_ENDINGS.weak[noun.g][cas];
|
||||
const fused = frame.prep ? CONTRACTABLE[frame.prep]?.[det] : null;
|
||||
const lead = !frame.prep ? det + ' ' : fused ? fused + ' ' : `${frame.prep} ${det} `;
|
||||
let solved = `${lead}${adj.de}${ending} ${nounForm(noun, cas)}`;
|
||||
if (frame.deCap) solved = cap(solved);
|
||||
let enPhrase = `the ${adjEn(adj, noun)} ${noun.en}`;
|
||||
if (frame.enCap) enPhrase = cap(enPhrase);
|
||||
add({ key: `ADJ|${frame.id}|${adj.de}|${n}`,
|
||||
de: `${frame.dePre}${solved}${frame.dePost}`,
|
||||
en: `${frame.enPre}${enPhrase}${frame.enPost}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Possessive sentences with no preposition at all.
|
||||
for (const t of POSS_ACC_TEMPLATES) {
|
||||
for (const n of t.nouns.filter(x => POSSESSABLE.has(x))) {
|
||||
@@ -1145,6 +1449,8 @@ export function corpusKeyFor(q) {
|
||||
if (q.type === 'wechselContext') return `WECH|${q.prepKey}|${q.cas}|${q.verb}|${n}`;
|
||||
if (q.type === 'chunk') return `CHUNK|${q.prepKey}|${q.cas}|${q.verb}|${n}`;
|
||||
if (q.type === 'article') return `${q.artType === 'indefinite' ? 'INDEF' : 'ART'}|${q.prepKey}|${q.cas}|${q.verb}|${n}`;
|
||||
if (q.type === 'adjective' || q.type === 'adjClass')
|
||||
return `ADJ|${q.frameId}|${q.adj.de}|${n}`;
|
||||
if (q.type === 'poss') return q.prepKey
|
||||
? `POSS|${q.prepKey}|${q.cas}|${q.verb}|${n}`
|
||||
: `POSSTPL|-|${q.cas}|${q.verb}|${n}`;
|
||||
|
||||
Reference in New Issue
Block a user