Some checks failed
Test and deploy staging / staging (push) Has been cancelled
Dutch and Korean (explained in English) and English (explained in Dutch) from A1 to C1, with a Hangul level for Korean. Lessons with notes, words and sentences generate multiple-choice, typing, sentence building, cloze, listening and matching exercises. Endless challenges: a seeded daily challenge, sprint, survival, mixed practice, level tests, spaced-repetition reviews and generated drills (numbers, clock times, de/het, plurals, Dutch and Korean conjugation, particles, Hangul reading, irregular verbs). Progress is stored in localStorage with export/import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
1.9 KiB
TypeScript
77 lines
1.9 KiB
TypeScript
/** A simplified SM-2 schedule. Days are counted as local calendar days. */
|
|
|
|
export type Card = {
|
|
due: number;
|
|
interval: number;
|
|
ease: number;
|
|
reps: number;
|
|
lapses: number;
|
|
seen: number;
|
|
right: number;
|
|
last: number;
|
|
};
|
|
|
|
export function dayKey(date = new Date()): string {
|
|
const y = date.getFullYear();
|
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
const d = String(date.getDate()).padStart(2, '0');
|
|
return `${y}-${m}-${d}`;
|
|
}
|
|
|
|
export function dayNumber(key = dayKey()): number {
|
|
const [y, m, d] = key.split('-').map(Number);
|
|
return Math.round(Date.UTC(y, m - 1, d) / 86_400_000);
|
|
}
|
|
|
|
export const newCard = (today: number): Card => ({
|
|
due: today,
|
|
interval: 0,
|
|
ease: 2.5,
|
|
reps: 0,
|
|
lapses: 0,
|
|
seen: 0,
|
|
right: 0,
|
|
last: -1
|
|
});
|
|
|
|
export function review(card: Card, correct: boolean, today: number): Card {
|
|
const next = { ...card, seen: card.seen + 1, right: card.right + (correct ? 1 : 0) };
|
|
if (!correct) {
|
|
return {
|
|
...next,
|
|
reps: 0,
|
|
interval: 0,
|
|
lapses: card.lapses + 1,
|
|
ease: Math.max(1.3, card.ease - 0.2),
|
|
due: today,
|
|
last: today
|
|
};
|
|
}
|
|
// Grinding the same card again on one day should not push it months ahead.
|
|
if (card.last === today && card.reps > 0) return next;
|
|
const reps = card.reps + 1;
|
|
const interval =
|
|
reps === 1 ? 1 : reps === 2 ? 3 : Math.round(Math.max(card.interval, 1) * card.ease);
|
|
return {
|
|
...next,
|
|
reps,
|
|
interval,
|
|
ease: Math.min(3, card.ease + 0.05),
|
|
due: today + interval,
|
|
last: today
|
|
};
|
|
}
|
|
|
|
export type Mastery = 'new' | 'learning' | 'familiar' | 'known' | 'mastered';
|
|
|
|
export function mastery(card: Card | undefined): Mastery {
|
|
if (!card || card.seen === 0) return 'new';
|
|
if (card.interval < 3) return 'learning';
|
|
if (card.interval < 14) return 'familiar';
|
|
if (card.interval < 60) return 'known';
|
|
return 'mastered';
|
|
}
|
|
|
|
/** 0 for new cards, approaching 1 for cards reviewed months apart. */
|
|
export const strength = (card: Card | undefined) => (card ? Math.min(1, card.interval / 30) : 0);
|