Remove nominative testing, add hideable hints and week streak dots

- Remove the Nominativ level and drop nominative-case testing from mixed/
  possessive/pronoun levels (nominative kept only in reference charts)
- Add persistent hide/show toggles on the two giveaway hint lines in
  practice (noun gender, case/preposition info); state persists in localStorage
- De-spoil "Wechsel im Kontext": remove motion/location hint from answer
  buttons; put sentence translation + preposition meaning behind a toggle
- Add a 7-day practice-history dot strip beside the streak counter
- Extend engine + tests (61 vitest tests)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fightme-cmd
2026-07-14 12:36:08 +02:00
co-authored by Claude Opus 4.8
parent d30e213104
commit cd0b6c800d
3 changed files with 179 additions and 103 deletions
+40 -4
View File
@@ -4,7 +4,7 @@ import {
NOUNS, nounForm, LEVELS, CONTRACTIONS, CONTRACTABLE, CHUNK_PREPS,
POSS_ENDINGS, combinePoss, PRONOUNS, VERBS, PREP_FRAMES,
createSession, gen, genContractionContextQ, newState, buildChunkChoices,
bumpStreak, currentStreak,
bumpStreak, currentStreak, weekDots,
} from './engine.js';
const runSession = (levelId, n) => {
@@ -238,9 +238,22 @@ describe('pronoun questions', () => {
it.each([14, 15, 16, 17])('level %s: answer matches the pronoun table', (id) => {
for (const q of runSession(id, 200)) {
const p = PRONOUNS.find(x => x.key === q.pronKey);
const expected = p[{nominative:'nom',accusative:'acc',dative:'dat'}[q.cas]];
const want = q.cas === 'nominative' ? expected.charAt(0).toUpperCase() + expected.slice(1) : expected;
expect(q.ans).toBe(want);
expect(q.ans).toBe(p[{accusative:'acc',dative:'dat'}[q.cas]]);
}
});
});
// ── Nominative is never tested ──────────────────────────────
describe('no nominative anywhere', () => {
it('no level tests the nominative — you already know der/die/das', () => {
expect(LEVELS.some(l => l.name === 'Nominativ')).toBe(false);
for (const lv of LEVELS) {
if (lv.cases) expect(lv.cases, `level ${lv.id} still lists nominative`).not.toContain('nominative');
for (const q of runSession(lv.id, 150)) {
if (q.cas) expect(q.cas, `level ${lv.id} generated a nominative question`).not.toBe('nominative');
expect(q.ans).not.toBe('nominative');
}
}
});
});
@@ -289,6 +302,29 @@ describe('streaks', () => {
expect(st.streak).toBe(10);
});
it('records practice days and prunes old ones', () => {
let st = bumpStreak({ streak: 0, ld: null }, '2026-07-01');
expect(st.days['2026-07-01']).toBe(true);
st = bumpStreak(st, '2026-07-01'); // same day, already recorded
expect(Object.keys(st.days).length).toBe(1);
st = bumpStreak(st, '2026-07-02');
expect(st.days['2026-07-01']).toBe(true);
expect(st.days['2026-07-02']).toBe(true);
st = bumpStreak(st, '2026-07-20'); // far in the future → old days pruned
expect(st.days['2026-07-01']).toBeUndefined();
expect(st.days['2026-07-20']).toBe(true);
});
it('weekDots reports the last 7 days, today last', () => {
const st = { days: { '2026-07-10': true, '2026-07-12': true, '2026-07-14': true } };
const dots = weekDots(st, '2026-07-14');
expect(dots.length).toBe(7);
expect(dots[0].date).toBe('2026-07-08');
expect(dots[6].date).toBe('2026-07-14');
expect(dots[6].isToday).toBe(true);
expect(dots.map(d => d.practiced)).toEqual([false, false, true, false, true, false, true]);
});
it('displays 0 once the streak has lapsed, without needing a new answer', () => {
const st = { streak: 7, ld: '2026-07-01' };
expect(currentStreak(st, '2026-07-01')).toBe(7);