AT Mot — a bilingual (EN/FR) daily word game native to the AT Protocol.
1import { describe, it, expect } from 'vitest';
2import { computeNextStats, statsForLang, type StatsRecord } from '../src/engine/stats.js';
3import type { GameResult } from '../src/engine/types.js';
4
5function game(lang: 'en' | 'fr', puzzleNumber: number, solved: boolean, guessCount?: number): GameResult {
6 return { lang, puzzleNumber, solved, guessCount, rows: [] };
7}
8
9const T = '2026-06-23T00:00:00.000Z';
10
11describe('computeNextStats', () => {
12 it('creates a fresh lang bucket on first play', () => {
13 const s = computeNextStats(null, game('en', 1, true, 3), T);
14 const en = statsForLang(s, 'en')!;
15 expect(en.gamesPlayed).toBe(1);
16 expect(en.gamesWon).toBe(1);
17 expect(en.currentStreak).toBe(1);
18 expect(en.maxStreak).toBe(1);
19 expect(en.lastPuzzleNumber).toBe(1);
20 expect(en.guessDistribution).toEqual([{ guesses: 3, count: 1 }]);
21 });
22
23 it('extends streak on consecutive wins and tracks max', () => {
24 let s: StatsRecord | null = null;
25 s = computeNextStats(s, game('en', 1, true, 4), T);
26 s = computeNextStats(s, game('en', 2, true, 3), T);
27 s = computeNextStats(s, game('en', 3, true, 2), T);
28 const en = statsForLang(s, 'en')!;
29 expect(en.currentStreak).toBe(3);
30 expect(en.maxStreak).toBe(3);
31 expect(en.gamesWon).toBe(3);
32 });
33
34 it('resets streak to 0 on a loss, keeps max', () => {
35 let s: StatsRecord | null = null;
36 s = computeNextStats(s, game('en', 1, true, 4), T);
37 s = computeNextStats(s, game('en', 2, true, 3), T);
38 s = computeNextStats(s, game('en', 3, false), T);
39 const en = statsForLang(s, 'en')!;
40 expect(en.currentStreak).toBe(0);
41 expect(en.maxStreak).toBe(2);
42 expect(en.gamesPlayed).toBe(3);
43 expect(en.gamesWon).toBe(2);
44 });
45
46 it('restarts streak at 1 after a gap', () => {
47 let s: StatsRecord | null = null;
48 s = computeNextStats(s, game('en', 1, true, 4), T);
49 s = computeNextStats(s, game('en', 5, true, 3), T); // gap
50 expect(statsForLang(s, 'en')!.currentStreak).toBe(1);
51 });
52
53 it('keeps EN and FR streaks independent', () => {
54 let s: StatsRecord | null = null;
55 s = computeNextStats(s, game('en', 1, true, 4), T);
56 s = computeNextStats(s, game('fr', 1, true, 5), T);
57 s = computeNextStats(s, game('en', 2, true, 3), T);
58 expect(statsForLang(s, 'en')!.currentStreak).toBe(2);
59 expect(statsForLang(s, 'fr')!.currentStreak).toBe(1);
60 });
61
62 it('does not disturb streak when replaying an older puzzle', () => {
63 let s: StatsRecord | null = null;
64 s = computeNextStats(s, game('en', 10, true, 4), T);
65 s = computeNextStats(s, game('en', 3, true, 2), T); // older puzzle
66 const en = statsForLang(s, 'en')!;
67 expect(en.lastPuzzleNumber).toBe(10);
68 expect(en.currentStreak).toBe(1);
69 expect(en.gamesPlayed).toBe(2);
70 expect(en.gamesWon).toBe(2);
71 });
72});