152 lines
4.8 KiB
TypeScript
152 lines
4.8 KiB
TypeScript
import { DIFFICULTIES } from "./game-engine";
|
|
import type { DailyProgress, Difficulty, GlobalStats } from "./types";
|
|
|
|
const STATS_KEY = "hidden11-global-stats";
|
|
|
|
export function getStorageKey(dateKey: string): string {
|
|
return `hidden11-progress-${dateKey}`;
|
|
}
|
|
|
|
export function createInitialProgress(): DailyProgress {
|
|
return DIFFICULTIES.reduce<DailyProgress>((progress, difficulty, index) => {
|
|
progress[difficulty] = {
|
|
attempts: [],
|
|
elapsedSeconds: 0,
|
|
solved: false,
|
|
locked: index !== 0,
|
|
};
|
|
return progress;
|
|
}, {} as DailyProgress);
|
|
}
|
|
|
|
export function loadProgress(dateKey: string): DailyProgress {
|
|
if (typeof window === "undefined") return createInitialProgress();
|
|
|
|
try {
|
|
const raw = window.localStorage.getItem(getStorageKey(dateKey));
|
|
if (!raw) return createInitialProgress();
|
|
return normalizeProgress(JSON.parse(raw) as DailyProgress);
|
|
} catch {
|
|
return createInitialProgress();
|
|
}
|
|
}
|
|
|
|
export function saveProgress(dateKey: string, progress: DailyProgress): void {
|
|
if (typeof window === "undefined") return;
|
|
window.localStorage.setItem(getStorageKey(dateKey), JSON.stringify(progress));
|
|
}
|
|
|
|
export function createInitialStats(): GlobalStats {
|
|
return {
|
|
averageSeconds: 0,
|
|
bestSeconds: null,
|
|
currentStreak: 0,
|
|
gamesPlayed: 0,
|
|
lastCompletedDate: null,
|
|
maxStreak: 0,
|
|
perfectDays: 0,
|
|
totalSeconds: 0,
|
|
winDistribution: {
|
|
1: 0,
|
|
2: 0,
|
|
3: 0,
|
|
4: 0,
|
|
5: 0,
|
|
6: 0,
|
|
7: 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function loadStats(): GlobalStats {
|
|
if (typeof window === "undefined") return createInitialStats();
|
|
|
|
try {
|
|
const raw = window.localStorage.getItem(STATS_KEY);
|
|
if (!raw) return createInitialStats();
|
|
return normalizeStats(JSON.parse(raw) as GlobalStats);
|
|
} catch {
|
|
return createInitialStats();
|
|
}
|
|
}
|
|
|
|
export function saveStats(stats: GlobalStats): void {
|
|
if (typeof window === "undefined") return;
|
|
window.localStorage.setItem(STATS_KEY, JSON.stringify(stats));
|
|
}
|
|
|
|
export function recordDailyStats(dateKey: string, progress: DailyProgress): GlobalStats {
|
|
const current = loadStats();
|
|
if (current.lastCompletedDate === dateKey) return current;
|
|
|
|
const solvedCount = DIFFICULTIES.filter((difficulty) => progress[difficulty].solved).length;
|
|
const totalSeconds = DIFFICULTIES.reduce((sum, difficulty) => sum + progress[difficulty].elapsedSeconds, 0);
|
|
const solvedAttempts = DIFFICULTIES.filter((difficulty) => progress[difficulty].solved).map(
|
|
(difficulty) => progress[difficulty].attempts.length,
|
|
);
|
|
const averageAttempts =
|
|
solvedAttempts.length > 0
|
|
? solvedAttempts.reduce((sum, attempts) => sum + attempts, 0) / solvedAttempts.length
|
|
: null;
|
|
const bucket =
|
|
solvedCount === DIFFICULTIES.length && averageAttempts !== null
|
|
? Math.min(6, Math.max(1, Math.round(averageAttempts)))
|
|
: 7;
|
|
const previousDate = current.lastCompletedDate ? new Date(`${current.lastCompletedDate}T00:00:00.000Z`) : null;
|
|
const today = new Date(`${dateKey}T00:00:00.000Z`);
|
|
const daysSincePrevious = previousDate ? Math.round((today.getTime() - previousDate.getTime()) / 86400000) : null;
|
|
const currentStreak = daysSincePrevious === 1 ? current.currentStreak + 1 : 1;
|
|
const gamesPlayed = current.gamesPlayed + 1;
|
|
const nextTotalSeconds = current.totalSeconds + totalSeconds;
|
|
|
|
const next: GlobalStats = {
|
|
...current,
|
|
averageSeconds: Math.round(nextTotalSeconds / gamesPlayed),
|
|
bestSeconds: current.bestSeconds === null ? totalSeconds : Math.min(current.bestSeconds, totalSeconds),
|
|
currentStreak,
|
|
gamesPlayed,
|
|
lastCompletedDate: dateKey,
|
|
maxStreak: Math.max(current.maxStreak, currentStreak),
|
|
perfectDays: current.perfectDays + (solvedCount === DIFFICULTIES.length ? 1 : 0),
|
|
totalSeconds: nextTotalSeconds,
|
|
winDistribution: {
|
|
...current.winDistribution,
|
|
[bucket]: (current.winDistribution[bucket] ?? 0) + 1,
|
|
},
|
|
};
|
|
|
|
saveStats(next);
|
|
return next;
|
|
}
|
|
|
|
export function nextDifficulty(difficulty: Difficulty): Difficulty | null {
|
|
if (difficulty === "easy") return "medium";
|
|
if (difficulty === "medium") return "hard";
|
|
return null;
|
|
}
|
|
|
|
function normalizeProgress(progress: DailyProgress): DailyProgress {
|
|
const initial = createInitialProgress();
|
|
DIFFICULTIES.forEach((difficulty) => {
|
|
initial[difficulty] = {
|
|
attempts: progress[difficulty]?.attempts ?? [],
|
|
elapsedSeconds: progress[difficulty]?.elapsedSeconds ?? 0,
|
|
solved: progress[difficulty]?.solved ?? false,
|
|
locked: progress[difficulty]?.locked ?? difficulty !== "easy",
|
|
};
|
|
});
|
|
return initial;
|
|
}
|
|
|
|
function normalizeStats(stats: GlobalStats): GlobalStats {
|
|
const initial = createInitialStats();
|
|
return {
|
|
...initial,
|
|
...stats,
|
|
winDistribution: {
|
|
...initial.winDistribution,
|
|
...(stats.winDistribution ?? {}),
|
|
},
|
|
};
|
|
}
|