Add presenter role for game flow control

This commit is contained in:
Joey Yakimowich-Payne 2026-01-19 14:02:28 -07:00
commit 9ef8f7343d
No known key found for this signature in database
GPG key ID: DDF6AF5B21B407D4
10 changed files with 1412 additions and 17 deletions

View file

@ -124,6 +124,7 @@ export const useGame = () => {
const [firstCorrectPlayerId, setFirstCorrectPlayerId] = useState<string | null>(null);
const [hostSecret, setHostSecret] = useState<string | null>(null);
const [isReconnecting, setIsReconnecting] = useState(false);
const [presenterId, setPresenterId] = useState<string | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const syncTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
@ -141,6 +142,7 @@ export const useGame = () => {
const gameStateRef = useRef<GameState>("LANDING");
const firstCorrectPlayerIdRef = useRef<string | null>(null);
const currentCorrectShapeRef = useRef<string | null>(null);
const presenterIdRef = useRef<string | null>(null);
useEffect(() => { timeLeftRef.current = timeLeft; }, [timeLeft]);
useEffect(() => { playersRef.current = players; }, [players]);
@ -152,6 +154,7 @@ export const useGame = () => {
useEffect(() => { gameStateRef.current = gameState; }, [gameState]);
useEffect(() => { firstCorrectPlayerIdRef.current = firstCorrectPlayerId; }, [firstCorrectPlayerId]);
useEffect(() => { currentCorrectShapeRef.current = currentCorrectShape; }, [currentCorrectShape]);
useEffect(() => { presenterIdRef.current = presenterId; }, [presenterId]);
const isInitializingFromUrl = useRef(false);
@ -817,6 +820,9 @@ export const useGame = () => {
updatedPlayers = playersRef.current.map(p => p.id === reconnectedPlayer.id ? { ...p, id: conn.peer } : p);
setPlayers(updatedPlayers);
assignedName = reconnectedPlayer.name;
if (presenterIdRef.current === reconnectedPlayer.id) {
setPresenterId(conn.peer);
}
} else if (!playersRef.current.find(p => p.id === conn.peer)) {
const colorIndex = playersRef.current.length % PLAYER_COLORS.length;
newPlayer = {
@ -834,6 +840,12 @@ export const useGame = () => {
};
updatedPlayers = [...playersRef.current, newPlayer];
setPlayers(updatedPlayers);
const realPlayers = updatedPlayers.filter(p => p.id !== 'host');
if (!gameConfigRef.current.hostParticipates && realPlayers.length === 1 && !presenterIdRef.current) {
setPresenterId(conn.peer);
broadcast({ type: 'PRESENTER_CHANGED', payload: { presenterId: conn.peer } });
}
}
const currentState = gameStateRef.current;
@ -855,6 +867,7 @@ export const useGame = () => {
lastAnswerCorrect: null,
selectedShape: null,
assignedName,
presenterId: presenterIdRef.current,
};
if (currentQuestion) {
@ -933,6 +946,26 @@ export const useGame = () => {
endQuestion();
}
}
if (data.type === 'ADVANCE') {
const { action } = data.payload;
if (conn.peer !== presenterIdRef.current) {
console.log('[HOST] ADVANCE rejected - not from presenter');
return;
}
if (action === 'START' && gameStateRef.current === 'LOBBY') {
startHostGame();
} else if (action === 'NEXT') {
if (gameStateRef.current === 'REVEAL') {
showScoreboard();
} else if (gameStateRef.current === 'SCOREBOARD') {
nextQuestion();
}
} else if (action === 'SCOREBOARD' && gameStateRef.current === 'REVEAL') {
showScoreboard();
}
}
};
useEffect(() => {
@ -1287,6 +1320,9 @@ export const useGame = () => {
setSelectedOption(matchedOption);
}
}
if (payload.presenterId !== undefined) {
setPresenterId(payload.presenterId);
}
if (payload.questionText && payload.options && payload.totalQuestions !== undefined) {
const questions: Question[] = [];
@ -1406,6 +1442,10 @@ export const useGame = () => {
setPlayers(data.payload.players);
clearStoredSession();
}
if (data.type === 'PRESENTER_CHANGED') {
setPresenterId(data.payload.presenterId);
}
};
const handleAnswer = (arg: boolean | AnswerOption) => {
@ -1489,10 +1529,21 @@ export const useGame = () => {
}
}, [gameState, players, role]);
const setPresenterPlayer = (playerId: string | null) => {
if (role !== 'HOST') return;
setPresenterId(playerId);
broadcast({ type: 'PRESENTER_CHANGED', payload: { presenterId: playerId } });
};
const sendAdvance = (action: 'START' | 'NEXT' | 'SCOREBOARD') => {
if (role !== 'CLIENT' || !hostConnectionRef.current) return;
hostConnectionRef.current.send({ type: 'ADVANCE', payload: { action } });
};
return {
role, gameState, quiz, players, currentQuestionIndex, timeLeft, error, gamePin, hasAnswered, lastPointsEarned, lastAnswerCorrect, currentCorrectShape, selectedOption, currentPlayerScore, currentStreak, currentPlayerId, gameConfig,
pendingQuizToSave, dismissSavePrompt, sourceQuizId, isReconnecting, currentPlayerName,
pendingQuizToSave, dismissSavePrompt, sourceQuizId, isReconnecting, currentPlayerName, presenterId,
startQuizGen, startManualCreation, cancelCreation, finalizeManualQuiz, loadSavedQuiz, joinGame, startGame: startHostGame, handleAnswer, nextQuestion, showScoreboard,
updateQuizFromEditor, startGameFromEditor, backFromEditor, endGame, attemptReconnect, goHomeFromDisconnected, resumeGame
updateQuizFromEditor, startGameFromEditor, backFromEditor, endGame, attemptReconnect, goHomeFromDisconnected, resumeGame, setPresenterPlayer, sendAdvance
};
};