Add Word Wrangler demos
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user