Fix host reconnection losing player state and first-answer bonus

- Persist firstCorrectPlayerId to backend during state sync
- Restore full players array (not just host) on host reconnect
- Add default values to WELCOME payload for new/unmatched players
- Add migration for first_correct_player_id column in game_sessions
This commit is contained in:
Joey Yakimowich-Payne 2026-01-14 10:04:51 -07:00
commit 035ea57274
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
4 changed files with 22 additions and 3 deletions

View file

@ -71,7 +71,8 @@ export const useGame = () => {
const gameConfigRef = useRef<GameConfig>(DEFAULT_GAME_CONFIG); const gameConfigRef = useRef<GameConfig>(DEFAULT_GAME_CONFIG);
const gamePinRef = useRef<string | null>(null); const gamePinRef = useRef<string | null>(null);
const hostSecretRef = useRef<string | null>(null); const hostSecretRef = useRef<string | null>(null);
const gameStateRef = useRef<GameState>('LANDING'); const gameStateRef = useRef<GameState>("LANDING");
const firstCorrectPlayerIdRef = useRef<string | null>(null);
useEffect(() => { timeLeftRef.current = timeLeft; }, [timeLeft]); useEffect(() => { timeLeftRef.current = timeLeft; }, [timeLeft]);
useEffect(() => { playersRef.current = players; }, [players]); useEffect(() => { playersRef.current = players; }, [players]);
@ -81,6 +82,7 @@ export const useGame = () => {
useEffect(() => { gamePinRef.current = gamePin; }, [gamePin]); useEffect(() => { gamePinRef.current = gamePin; }, [gamePin]);
useEffect(() => { hostSecretRef.current = hostSecret; }, [hostSecret]); useEffect(() => { hostSecretRef.current = hostSecret; }, [hostSecret]);
useEffect(() => { gameStateRef.current = gameState; }, [gameState]); useEffect(() => { gameStateRef.current = gameState; }, [gameState]);
useEffect(() => { firstCorrectPlayerIdRef.current = firstCorrectPlayerId; }, [firstCorrectPlayerId]);
const generateGamePin = () => Math.floor(Math.random() * 900000) + 100000 + ""; const generateGamePin = () => Math.floor(Math.random() * 900000) + 100000 + "";
@ -98,6 +100,7 @@ export const useGame = () => {
body: JSON.stringify({ body: JSON.stringify({
gameState: gameStateRef.current, gameState: gameStateRef.current,
currentQuestionIndex: currentQuestionIndexRef.current, currentQuestionIndex: currentQuestionIndexRef.current,
firstCorrectPlayerId: firstCorrectPlayerIdRef.current,
players: playersRef.current, players: playersRef.current,
}), }),
}); });
@ -296,6 +299,7 @@ export const useGame = () => {
setQuiz(hostData.quiz); setQuiz(hostData.quiz);
setGameConfig(hostData.gameConfig); setGameConfig(hostData.gameConfig);
setCurrentQuestionIndex(hostData.currentQuestionIndex || 0); setCurrentQuestionIndex(hostData.currentQuestionIndex || 0);
setFirstCorrectPlayerId(hostData.firstCorrectPlayerId || null);
const hostPlayer = (hostData.players || []).find((p: Player) => p.id === 'host'); const hostPlayer = (hostData.players || []).find((p: Player) => p.id === 'host');
if (hostPlayer) { if (hostPlayer) {
@ -303,7 +307,7 @@ export const useGame = () => {
setCurrentPlayerName('Host'); setCurrentPlayerName('Host');
setCurrentPlayerScore(hostPlayer.score); setCurrentPlayerScore(hostPlayer.score);
setCurrentStreak(hostPlayer.streak); setCurrentStreak(hostPlayer.streak);
setPlayers([hostPlayer]); setPlayers(hostData.players || []);
if (hostPlayer.lastAnswerCorrect !== null) { if (hostPlayer.lastAnswerCorrect !== null) {
setHasAnswered(true); setHasAnswered(true);

View file

@ -45,6 +45,7 @@ const runMigrations = () => {
game_state TEXT NOT NULL DEFAULT 'LOBBY', game_state TEXT NOT NULL DEFAULT 'LOBBY',
current_question_index INTEGER NOT NULL DEFAULT 0, current_question_index INTEGER NOT NULL DEFAULT 0,
players_data TEXT NOT NULL DEFAULT '[]', players_data TEXT NOT NULL DEFAULT '[]',
first_correct_player_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
@ -52,6 +53,13 @@ const runMigrations = () => {
`); `);
console.log("Migration: Created game_sessions table"); console.log("Migration: Created game_sessions table");
} }
const sessionTableInfo = db.prepare("PRAGMA table_info(game_sessions)").all() as { name: string }[];
const hasFirstCorrect = sessionTableInfo.some(col => col.name === "first_correct_player_id");
if (!hasFirstCorrect) {
db.exec("ALTER TABLE game_sessions ADD COLUMN first_correct_player_id TEXT");
console.log("Migration: Added first_correct_player_id to game_sessions");
}
}; };
runMigrations(); runMigrations();

View file

@ -50,6 +50,7 @@ CREATE TABLE IF NOT EXISTS game_sessions (
game_state TEXT NOT NULL DEFAULT 'LOBBY', game_state TEXT NOT NULL DEFAULT 'LOBBY',
current_question_index INTEGER NOT NULL DEFAULT 0, current_question_index INTEGER NOT NULL DEFAULT 0,
players_data TEXT NOT NULL DEFAULT '[]', players_data TEXT NOT NULL DEFAULT '[]',
first_correct_player_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );

View file

@ -15,6 +15,7 @@ interface GameSession {
game_state: string; game_state: string;
current_question_index: number; current_question_index: number;
players_data: string; players_data: string;
first_correct_player_id: string | null;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
@ -112,6 +113,7 @@ router.get('/:pin/host', (req: Request, res: Response) => {
gameState: session.game_state, gameState: session.game_state,
currentQuestionIndex: session.current_question_index, currentQuestionIndex: session.current_question_index,
players: JSON.parse(session.players_data), players: JSON.parse(session.players_data),
firstCorrectPlayerId: session.first_correct_player_id,
}); });
} catch (err) { } catch (err) {
console.error('Error getting host session:', err); console.error('Error getting host session:', err);
@ -123,7 +125,7 @@ router.patch('/:pin', (req: Request, res: Response) => {
try { try {
const { pin } = req.params; const { pin } = req.params;
const hostSecret = req.headers['x-host-secret'] as string; const hostSecret = req.headers['x-host-secret'] as string;
const { hostPeerId, gameState, currentQuestionIndex, players } = req.body; const { hostPeerId, gameState, currentQuestionIndex, players, firstCorrectPlayerId } = req.body;
if (!hostSecret) { if (!hostSecret) {
res.status(401).json({ error: 'Host secret required' }); res.status(401).json({ error: 'Host secret required' });
@ -156,6 +158,10 @@ router.patch('/:pin', (req: Request, res: Response) => {
updates.push('players_data = ?'); updates.push('players_data = ?');
values.push(JSON.stringify(players)); values.push(JSON.stringify(players));
} }
if (firstCorrectPlayerId !== undefined) {
updates.push("first_correct_player_id = ?");
values.push(firstCorrectPlayerId);
}
if (updates.length === 0) { if (updates.length === 0) {
res.status(400).json({ error: 'No updates provided' }); res.status(400).json({ error: 'No updates provided' });