Add Word Wrangler demos

This commit is contained in:
Mark Backman
2025-04-24 14:29:19 -04:00
parent 09ff836ef6
commit c80d09f66c
54 changed files with 12209 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import { useEffect, useCallback } from 'react';
import {
useRTVIClient,
useRTVIClientTransportState,
} from '@pipecat-ai/client-react';
import { CONNECTION_STATES } from '@/constants/gameConstants';
export function useConnectionState(
onConnected?: () => void,
onDisconnected?: () => void
) {
const client = useRTVIClient();
const transportState = useRTVIClientTransportState();
const isConnected = CONNECTION_STATES.ACTIVE.includes(transportState);
const isConnecting = CONNECTION_STATES.CONNECTING.includes(transportState);
const isDisconnecting =
CONNECTION_STATES.DISCONNECTING.includes(transportState);
// Handle connection changes
useEffect(() => {
if (isConnected && onConnected) {
onConnected();
}
if (!isConnected && !isConnecting && onDisconnected) {
onDisconnected();
}
}, [isConnected, isConnecting, onConnected, onDisconnected]);
// Toggle connection state
const toggleConnection = useCallback(async () => {
if (!client) return;
try {
if (isConnected) {
await client.disconnect();
} else {
await client.connect();
}
} catch (error) {
console.error('Connection error:', error);
}
}, [client, isConnected]);
return {
isConnected,
isConnecting,
isDisconnecting,
toggleConnection,
transportState,
client, // Expose the client for direct access when needed
};
}

View File

@@ -0,0 +1,98 @@
import { GAME_CONFIG, GAME_STATES, GameState } from "@/constants/gameConstants";
import { getRandomCatchPhraseWords } from "@/data/wordWranglerWords";
import { useCallback, useState } from "react";
export function useGameState() {
// Game state
const [gameState, setGameState] = useState<GameState>(GAME_STATES.IDLE);
const [timeLeft, setTimeLeft] = useState(GAME_CONFIG.GAME_DURATION);
const [score, setScore] = useState(0);
const [words, setWords] = useState<string[]>([]);
const [currentWordIndex, setCurrentWordIndex] = useState(0);
const [skipsRemaining, setSkipsRemaining] = useState(GAME_CONFIG.MAX_SKIPS);
const [bestScore, _setBestScore] = useState(0);
// Initialize or reset game state
const initializeGame = useCallback(() => {
const freshWords = getRandomCatchPhraseWords(GAME_CONFIG.WORD_POOL_SIZE);
setWords(freshWords);
setGameState(GAME_STATES.ACTIVE);
setTimeLeft(GAME_CONFIG.GAME_DURATION);
setScore(0);
setCurrentWordIndex(0);
setSkipsRemaining(GAME_CONFIG.MAX_SKIPS);
// Get best score from local storage
const storedScore = localStorage.getItem("bestScore");
if (storedScore) {
_setBestScore(Number(storedScore) || 0);
}
return freshWords;
}, []);
// End game
const finishGame = useCallback(() => {
setGameState(GAME_STATES.FINISHED);
}, []);
// Handle scoring
const incrementScore = useCallback(() => {
setScore((prev) => prev + 1);
}, []);
// Handle best score
const setBestScore = useCallback((newBestScore: number) => {
_setBestScore(newBestScore);
localStorage.setItem("bestScore", newBestScore.toString());
}, []);
// Handle word navigation
const moveToNextWord = useCallback(() => {
setCurrentWordIndex((prev) => {
if (prev >= words.length - 1) {
// If we're at the end of the word list, get new words
setWords(getRandomCatchPhraseWords(GAME_CONFIG.WORD_POOL_SIZE));
return 0;
}
return prev + 1;
});
}, [words]);
// Handle skipping
const useSkip = useCallback(() => {
if (skipsRemaining <= 0) return false;
setSkipsRemaining((prev) => prev - 1);
return true;
}, [skipsRemaining]);
// Update timer
const decrementTimer = useCallback(() => {
return setTimeLeft((prev) => {
if (prev <= 1) {
return 0;
}
return prev - 1;
});
}, []);
return {
// State
gameState,
setGameState,
timeLeft,
score,
bestScore,
words,
currentWord: words[currentWordIndex] || "",
skipsRemaining,
// Actions
initializeGame,
finishGame,
incrementScore,
setBestScore,
moveToNextWord,
useSkip,
decrementTimer,
};
}

View File

@@ -0,0 +1,59 @@
import { GAME_CONFIG } from "@/constants/gameConstants";
import { clearTimer } from "@/utils/timerUtils";
import { useCallback, useEffect, useRef, useState } from "react";
export function useGameTimer(onTimeUp: () => void) {
const [timeLeft, setTimeLeft] = useState(GAME_CONFIG.GAME_DURATION);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const hasCalledTimeUpRef = useRef(false);
// Start the game timer with initial duration
const startTimer = useCallback(() => {
// Reset time left and timeUp flag
setTimeLeft(GAME_CONFIG.GAME_DURATION);
hasCalledTimeUpRef.current = false;
// Clear any existing timer
timerRef.current = clearTimer(timerRef.current);
// Start a new timer
timerRef.current = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1 && !hasCalledTimeUpRef.current) {
// Time's up - clear the interval and call the callback
timerRef.current = clearTimer(timerRef.current);
hasCalledTimeUpRef.current = true;
onTimeUp();
return 0;
}
return prev - 1;
});
}, GAME_CONFIG.TIMER_INTERVAL);
}, [onTimeUp]);
// Stop the timer
const stopTimer = useCallback(() => {
timerRef.current = clearTimer(timerRef.current);
hasCalledTimeUpRef.current = false;
}, []);
// Reset the timer to initial value without starting it
const resetTimer = useCallback(() => {
setTimeLeft(GAME_CONFIG.GAME_DURATION);
hasCalledTimeUpRef.current = false;
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
timerRef.current = clearTimer(timerRef.current);
};
}, []);
return {
timeLeft,
startTimer,
stopTimer,
resetTimer,
};
}

View File

@@ -0,0 +1,62 @@
import { useState, useRef, useCallback } from 'react';
import { clearTimer } from '@/utils/timerUtils';
import { GAME_CONFIG } from '@/constants/gameConstants';
export function useVisualFeedback() {
// Visual feedback state
const [showAutoDetected, setShowAutoDetected] = useState(false);
const [showIncorrect, setShowIncorrect] = useState(false);
const autoDetectTimerRef = useRef<NodeJS.Timeout | null>(null);
// Reset all visual states
const resetVisuals = useCallback(() => {
setShowAutoDetected(false);
setShowIncorrect(false);
autoDetectTimerRef.current = clearTimer(autoDetectTimerRef.current);
}, []);
// Show correct animation
const showCorrect = useCallback((onComplete?: () => void) => {
// Clear any existing animation
autoDetectTimerRef.current = clearTimer(autoDetectTimerRef.current);
// Show correct animation
setShowAutoDetected(true);
setShowIncorrect(false);
// Set timeout to hide animation
autoDetectTimerRef.current = setTimeout(() => {
setShowAutoDetected(false);
if (onComplete) onComplete();
}, GAME_CONFIG.ANIMATION_DURATION);
}, []);
// Show incorrect animation
const showIncorrectAnimation = useCallback(() => {
// Clear any existing animation
autoDetectTimerRef.current = clearTimer(autoDetectTimerRef.current);
// Show incorrect animation
setShowIncorrect(true);
setShowAutoDetected(false);
// Set timeout to hide animation
autoDetectTimerRef.current = setTimeout(() => {
setShowIncorrect(false);
}, GAME_CONFIG.ANIMATION_DURATION);
}, []);
// Clean up function
const cleanup = useCallback(() => {
autoDetectTimerRef.current = clearTimer(autoDetectTimerRef.current);
}, []);
return {
showAutoDetected,
showIncorrect,
resetVisuals,
showCorrect,
showIncorrectAnimation,
cleanup,
};
}

View File

@@ -0,0 +1,62 @@
import { useRef } from 'react';
import { useRTVIClientEvent } from '@pipecat-ai/client-react';
import { RTVIEvent } from '@pipecat-ai/client-js';
import { detectWordGuess } from '@/utils/wordDetection';
import { GAME_STATES, GameState } from '@/constants/gameConstants';
interface UseWordDetectionProps {
gameState: GameState;
currentWord: string;
onCorrectGuess: () => void;
onIncorrectGuess: () => void;
}
export function useWordDetection({
gameState,
currentWord,
onCorrectGuess,
onIncorrectGuess,
}: UseWordDetectionProps) {
const lastProcessedMessageRef = useRef('');
// Reset the last processed message
const resetLastProcessedMessage = () => {
lastProcessedMessageRef.current = '';
};
// Listen for bot transcripts to detect correct answers
useRTVIClientEvent(RTVIEvent.BotTranscript, (data) => {
if (gameState !== GAME_STATES.ACTIVE) {
return;
}
if (!currentWord) {
return;
}
if (!data.text) {
return;
}
// Skip if this is a repeat of the same transcript
if (data.text === lastProcessedMessageRef.current) {
return;
}
lastProcessedMessageRef.current = data.text;
// Use the utility function to detect word guesses
const result = detectWordGuess(data.text, currentWord);
if (result.isCorrect) {
onCorrectGuess();
} else if (result.isExplicitGuess) {
onIncorrectGuess();
} else {
}
});
return {
resetLastProcessedMessage,
};
}