fix: Prevent replaying already solved puzzles across domains

- Add checks in handleGuess, handleSkip, and handleGiveUp to prevent actions on solved/failed puzzles
- Add protection in addGuess to prevent adding guesses to solved puzzles
- Fix and simplify backend state loading logic
- Ensure solved puzzles cannot be replayed when switching domains
This commit is contained in:
Hördle Bot
2025-12-01 20:22:28 +01:00
parent 61846a6982
commit 27fa689b18
2 changed files with 97 additions and 47 deletions

View File

@@ -108,6 +108,10 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
const handleGuess = (song: any) => {
if (isProcessingGuess) return;
// Prevent guessing if already solved or failed
if (gameState?.isSolved || gameState?.isFailed) {
return;
}
setIsProcessingGuess(true);
setLastAction('GUESS');
@@ -159,6 +163,9 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
};
const handleSkip = () => {
// Prevent skipping if already solved or failed
if (gameState?.isSolved || gameState?.isFailed) return;
// If user hasn't played audio yet on first attempt, start it instead of skipping
if (gameState.guesses.length === 0 && !hasPlayedAudio) {
handleStartAudio();
@@ -187,6 +194,9 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
};
const handleGiveUp = () => {
// Prevent giving up if already solved or failed
if (gameState?.isSolved || gameState?.isFailed) return;
setLastAction('SKIP');
addGuess("SKIPPED", false);
giveUp(); // Ensure game is marked as failed and score reset to 0

View File

@@ -61,6 +61,17 @@ export function useGameState(
yearGuessed: false
});
const createNewStatistics = (): Statistics => ({
solvedIn1: 0,
solvedIn2: 0,
solvedIn3: 0,
solvedIn4: 0,
solvedIn5: 0,
solvedIn6: 0,
solvedIn7: 0,
failed: 0,
});
useEffect(() => {
const today = getTodayISOString();
@@ -86,63 +97,90 @@ export function useGameState(
await savePlayerState(genreKey, newState, loadedStats);
return;
}
} else {
// No backend state found - check localStorage before creating new state
const storageKey = getStorageKey();
const stored = localStorage.getItem(storageKey);
if (stored) {
const parsed = JSON.parse(stored);
if (parsed.date === today) {
// Migration for existing states without score
if (parsed.score === undefined) {
parsed.score = INITIAL_SCORE;
parsed.replayCount = 0;
parsed.skipCount = 0;
parsed.scoreBreakdown = [{ value: INITIAL_SCORE, reason: 'Start value' }];
parsed.yearGuessed = false;
}
setGameState(parsed as GameState);
// Also save to backend for cross-domain sync
const statsKey = getStatsKey();
const storedStats = localStorage.getItem(statsKey);
const stats = storedStats ? JSON.parse(storedStats) : createNewStatistics();
if (!statistics) {
setStatistics(stats);
}
await savePlayerState(genreKey, parsed as GameState, stats);
return;
}
}
// No state found - create new state
const newState = createNewState(today);
setGameState(newState);
localStorage.setItem(getStorageKey(), JSON.stringify(newState));
const newStats = createNewStatistics();
setStatistics(newStats);
localStorage.setItem(getStatsKey(), JSON.stringify(newStats));
await savePlayerState(genreKey, newState, newStats);
return;
}
} catch (error) {
console.warn('[gameState] Failed to load from backend, falling back to localStorage:', error);
}
// Fallback to localStorage
const storageKey = getStorageKey();
const stored = localStorage.getItem(storageKey);
// Fallback to localStorage only on error
const storageKey = getStorageKey();
const stored = localStorage.getItem(storageKey);
if (stored) {
const parsed = JSON.parse(stored);
if (parsed.date === today) {
// Migration for existing states without score
if (parsed.score === undefined) {
parsed.score = INITIAL_SCORE;
parsed.replayCount = 0;
parsed.skipCount = 0;
parsed.scoreBreakdown = [{ value: INITIAL_SCORE, reason: 'Start value' }];
parsed.yearGuessed = false;
if (stored) {
const parsed = JSON.parse(stored);
if (parsed.date === today) {
// Migration for existing states without score
if (parsed.score === undefined) {
parsed.score = INITIAL_SCORE;
parsed.replayCount = 0;
parsed.skipCount = 0;
parsed.scoreBreakdown = [{ value: INITIAL_SCORE, reason: 'Start value' }];
parsed.yearGuessed = false;
}
setGameState(parsed as GameState);
// Load statistics from localStorage
const statsKey = getStatsKey();
const storedStats = localStorage.getItem(statsKey);
if (storedStats) {
const parsedStats = JSON.parse(storedStats);
if (parsedStats.solvedIn7 === undefined) {
parsedStats.solvedIn7 = 0;
}
setStatistics(parsedStats);
} else {
const newStats = createNewStatistics();
setStatistics(newStats);
localStorage.setItem(statsKey, JSON.stringify(newStats));
}
return;
}
setGameState(parsed as GameState);
} else {
// New day
const newState = createNewState(today);
setGameState(newState);
localStorage.setItem(storageKey, JSON.stringify(newState));
}
} else {
// No state
// No state found - create new state
const newState = createNewState(today);
setGameState(newState);
localStorage.setItem(storageKey, JSON.stringify(newState));
}
// Load statistics from localStorage
const statsKey = getStatsKey();
const storedStats = localStorage.getItem(statsKey);
if (storedStats) {
const parsedStats = JSON.parse(storedStats);
// Migration for existing stats without solvedIn7
if (parsedStats.solvedIn7 === undefined) {
parsedStats.solvedIn7 = 0;
}
setStatistics(parsedStats);
} else {
const newStats: Statistics = {
solvedIn1: 0,
solvedIn2: 0,
solvedIn3: 0,
solvedIn4: 0,
solvedIn5: 0,
solvedIn6: 0,
solvedIn7: 0,
failed: 0,
};
const newStats = createNewStatistics();
setStatistics(newStats);
localStorage.setItem(statsKey, JSON.stringify(newStats));
localStorage.setItem(getStatsKey(), JSON.stringify(newStats));
}
};
@@ -203,6 +241,8 @@ export function useGameState(
const addGuess = (guess: string, correct: boolean) => {
if (!gameState) return;
// Prevent adding guesses if already solved or failed
if (gameState.isSolved || gameState.isFailed) return;
const newGuesses = [...gameState.guesses, guess];
const isSolved = correct;