Files
alexandrev-tibco f4157f6b82
Build and push image / build (push) Successful in 1m15s
Improve puzzle difficulty and sharing
2026-05-04 21:04:27 +02:00

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 ?? {}),
},
};
}