Harden saved game restore

This commit is contained in:
2026-06-27 08:54:03 +03:00
parent cc7aec8db5
commit 480d1019c4
+41 -12
View File
@@ -1,7 +1,7 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
import type { GameState, QuestionRef } from '$lib/types';
import { getQuestionSetById, getTotalQuestions } from '$lib/question-sets';
import { findQuestion, getQuestionSetById, getTotalQuestions } from '$lib/question-sets';
/**
* Игровое состояние полностью отделено от компонентов.
@@ -32,21 +32,50 @@ function loadState(): GameState {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return initialState();
const parsed = JSON.parse(raw) as Partial<GameState>;
// Защита от повреждённых/чужих данных: проверяем базовую структуру.
if (
parsed &&
(parsed.questionSetId === null || typeof parsed.questionSetId === 'string') &&
typeof parsed.score === 'number' &&
Array.isArray(parsed.played) &&
(parsed.current === null || typeof parsed.current === 'string')
!parsed ||
(parsed.questionSetId !== null && typeof parsed.questionSetId !== 'string') ||
typeof parsed.score !== 'number' ||
!Array.isArray(parsed.played) ||
(parsed.current !== null && typeof parsed.current !== 'string')
) {
return {
questionSetId: parsed.questionSetId ?? null,
score: parsed.score ?? 0,
played: parsed.played as QuestionRef[],
current: parsed.current ?? null
};
return initialState();
}
const setId = parsed.questionSetId;
// Набор не выбран — экран выбора, возвращаем чистое состояние.
if (setId === null) return initialState();
// Набор неизвестен (удалён/переименован) — полный сброс партии.
const set = getQuestionSetById(setId);
if (!set) return initialState();
// Счёт должен быть конечным неотрицательным числом, иначе сброс.
if (!Number.isFinite(parsed.score) || parsed.score < 0) return initialState();
// Оставляем только уникальные ссылки на реально существующие вопросы.
const played: QuestionRef[] = [];
const seen = new Set<QuestionRef>();
for (const ref of parsed.played) {
if (typeof ref !== 'string' || seen.has(ref)) continue;
if (!findQuestion(set, ref)) continue;
seen.add(ref);
played.push(ref);
}
// Текущий вопрос сбрасываем, если он не существует или уже сыгран.
const current = parsed.current;
const safeCurrent =
current !== null && findQuestion(set, current) && !seen.has(current) ? current : null;
return {
questionSetId: setId,
score: parsed.score,
played,
current: safeCurrent
};
} catch {
// Повреждённый JSON — просто начинаем заново.
}