diff --git a/example/src/App.tsx b/example/src/App.tsx index 1585e16..dd083f7 100755 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,19 +1,44 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useGame, type GameMode } from "@vimazing/typing-chud"; import "@vimazing/typing-chud/game.css"; import { useKeyBindings } from "./useKeyBindings"; +import { testScenarios, getScenario, getScenarioNames, type TestScenario } from "./testStateMachines"; function App() { - const [gameMode, setGameMode] = useState("letters"); - const gameManager = useGame({ gameMode }, useKeyBindings); - const { containerRef, gameStatus, scoreManager } = gameManager; + const [scenarioId, setScenarioId] = useState("progressive"); + const scenario = getScenario(scenarioId); + const [gameMode, setGameMode] = useState(scenario.initialMode); + const gameManager = useGame({ gameMode }, useKeyBindings); + const { containerRef, gameStatus, scoreManager } = gameManager; + const lastTransitionIndexRef = useRef(-1); - useEffect(() => { - if (scoreManager.score === 5 && gameMode === "letters") { - setGameMode("words"); - gameManager.changeGameMode({ gameMode: "words" }); - } - }, [scoreManager.score, gameMode, gameManager]); + useEffect(() => { + if (gameStatus !== "started") return; + + const currentTransitionIdx = scenario.transitions.findIndex((t) => t.fromMode === gameMode); + console.log(`[DEBUG] gameMode=${gameMode}, currentTransitionIdx=${currentTransitionIdx}, lastTransitionIndex=${lastTransitionIndexRef.current}`); + + if (currentTransitionIdx === -1 || currentTransitionIdx <= lastTransitionIndexRef.current) { + console.log(`[DEBUG] SKIPPING - already processed or no transition found`); + return; + } + + const currentTransition = scenario.transitions[currentTransitionIdx]; + const currentCount = scoreManager.currentCount; + + console.log(`[DEBUG] currentCount=${currentCount}, threshold=${currentTransition.threshold}, lettersCompleted=${scoreManager.lettersCompleted}, wordsCompleted=${scoreManager.wordsCompleted}, sentencesCompleted=${scoreManager.sentencesCompleted}`); + + if (currentCount >= currentTransition.threshold) { + console.log(`[DEBUG] TRIGGERING TRANSITION: ${currentTransition.fromMode} -> ${currentTransition.targetMode}`); + lastTransitionIndexRef.current = currentTransitionIdx; + setGameMode(currentTransition.targetMode); + gameManager.changeGameMode({ gameMode: currentTransition.targetMode }); + } + }, [scoreManager.currentCount, gameMode, gameManager, scenario, gameStatus, scoreManager.lettersCompleted, scoreManager.wordsCompleted, scoreManager.sentencesCompleted]); + + useEffect(() => { + lastTransitionIndexRef.current = -1; + }, [scenarioId]); const formatTime = (ms: number) => { const seconds = Math.floor(ms / 1000); @@ -22,56 +47,110 @@ function App() { return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; }; - return ( -
-
-

Typing Chud

-
- {(['letters', 'words', 'sentences', 'paragraphs'] as const).map((mode) => ( - - ))} -
-
+ return ( +
+
+

Typing Chud

+ +
+
+ + +

{scenario.description}

+
-
-
-
Time
-
{formatTime(scoreManager.timeValue)}
-
-
-
Score
-
{scoreManager.score}
-
-
-
Correct
-
{scoreManager.correct}
-
-
-
Missed
-
{scoreManager.missed}
-
-
-
Deaths
-
{scoreManager.deaths}
-
-
-
Keystrokes
-
{scoreManager.totalKeystrokes}
-
-
+
+
Mode Selection:
+
+ {(['letters', 'words', 'sentences', 'paragraphs'] as const).map((mode) => ( + + ))} +
+
+
+
+ +
+
+
Time
+
{formatTime(scoreManager.timeValue)}
+
+
+
Score
+
{scoreManager.score}
+
+
+
Correct
+
{scoreManager.correct}
+
+
+
Missed
+
{scoreManager.missed}
+
+
+
Deaths
+
{scoreManager.deaths}
+
+
+
Current
+
{scoreManager.currentCount}
+
+
+
Total
+
{scoreManager.lettersCompleted + scoreManager.wordsCompleted + scoreManager.sentencesCompleted + scoreManager.paragraphsCompleted}
+
+
+
Keystrokes
+
{scoreManager.totalKeystrokes}
+
+
+ +
+
+
Letters
+
{scoreManager.lettersCompleted}
+
+
+
Words
+
{scoreManager.wordsCompleted}
+
+
+
Sentences
+
{scoreManager.sentencesCompleted}
+
+
+
Paragraphs
+
{scoreManager.paragraphsCompleted}
+
+
diff --git a/example/src/testStateMachines.ts b/example/src/testStateMachines.ts new file mode 100644 index 0000000..690a2bc --- /dev/null +++ b/example/src/testStateMachines.ts @@ -0,0 +1,136 @@ +import type { GameMode } from '@vimazing/typing-chud'; + +export type ModeTransition = { + fromMode: GameMode; + threshold: number; + targetMode: GameMode; + description: string; +}; + +export type TestScenario = { + id: string; + name: string; + description: string; + initialMode: GameMode; + transitions: ModeTransition[]; +}; + +export const testScenarios: Record = { + progressive: { + id: 'progressive', + name: 'Progressive Mode Ladder', + description: 'Automatically progress through all modes: 10 letters → 10 words → 5 sentences → 3 paragraphs', + initialMode: 'letters', + transitions: [ + { + fromMode: 'letters', + threshold: 10, + targetMode: 'words', + description: 'After 10 letters, switch to words', + }, + { + fromMode: 'words', + threshold: 10, + targetMode: 'sentences', + description: 'After 10 words, switch to sentences', + }, + { + fromMode: 'sentences', + threshold: 5, + targetMode: 'paragraphs', + description: 'After 5 sentences, switch to paragraphs', + }, + ], + }, + + lettersOnly: { + id: 'lettersOnly', + name: 'Letters Only', + description: 'Stay in letters mode (baseline)', + initialMode: 'letters', + transitions: [], + }, + + wordsOnly: { + id: 'wordsOnly', + name: 'Words Only', + description: 'Start in words mode (baseline)', + initialMode: 'words', + transitions: [], + }, + + sentencesOnly: { + id: 'sentencesOnly', + name: 'Sentences Only', + description: 'Start in sentences mode (baseline)', + initialMode: 'sentences', + transitions: [], + }, + + paragraphsOnly: { + id: 'paragraphsOnly', + name: 'Paragraphs Only', + description: 'Start in paragraphs mode (baseline)', + initialMode: 'paragraphs', + transitions: [], + }, + + rapidTransitions: { + id: 'rapidTransitions', + name: 'Rapid Transitions', + description: 'Quick transitions: 3 letters → 3 words → 2 sentences → 1 paragraph', + initialMode: 'letters', + transitions: [ + { + fromMode: 'letters', + threshold: 3, + targetMode: 'words', + description: 'After 3 letters, switch to words', + }, + { + fromMode: 'words', + threshold: 3, + targetMode: 'sentences', + description: 'After 3 words, switch to sentences', + }, + { + fromMode: 'sentences', + threshold: 2, + targetMode: 'paragraphs', + description: 'After 2 sentences, switch to paragraphs', + }, + ], + }, + + backAndForth: { + id: 'backAndForth', + name: 'Back and Forth', + description: 'Oscillate between modes: 5 letters ↔ 5 words ↔ 5 letters', + initialMode: 'letters', + transitions: [ + { + fromMode: 'letters', + threshold: 5, + targetMode: 'words', + description: 'After 5 letters, switch to words', + }, + { + fromMode: 'words', + threshold: 5, + targetMode: 'letters', + description: 'After 5 words, switch back to letters', + }, + ], + }, +}; + +export function getScenario(id: string): TestScenario { + return testScenarios[id] || testScenarios.progressive; +} + +export function getScenarioNames(): { id: string; name: string }[] { + return Object.entries(testScenarios).map(([id, scenario]) => ({ + id, + name: scenario.name, + })); +} diff --git a/src/types.ts b/src/types.ts index 75437cb..b4862cb 100755 --- a/src/types.ts +++ b/src/types.ts @@ -147,6 +147,7 @@ export type GameStatusManager = { wordsCompleted: number; sentencesCompleted: number; paragraphsCompleted: number; + currentGameMode: GameMode; getCurrentCount: () => number; }; diff --git a/src/useGameStatus/useGameStatus.ts b/src/useGameStatus/useGameStatus.ts index ed7f9aa..a5cc501 100755 --- a/src/useGameStatus/useGameStatus.ts +++ b/src/useGameStatus/useGameStatus.ts @@ -45,6 +45,7 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal const [wordsCompleted, setWordsCompleted] = useState(0); const [sentencesCompleted, setSentencesCompleted] = useState(0); const [paragraphsCompleted, setParagraphsCompleted] = useState(0); + const [currentGameMode, setCurrentGameMode] = useState(gameMode); const gameLoopRef = useRef(null); const lastFrameTimeRef = useRef(0); const lettersRef = useRef([]); @@ -325,35 +326,43 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal onGameLoopTickRef.current = callback; }; - const setGameMode = (mode: GameMode) => { - gameModeRef.current = mode; - }; + const setGameMode = (mode: GameMode) => { + gameModeRef.current = mode; + setCurrentGameMode(mode); + }; const setPendingOptions = (newOptions: GameOptions) => { pendingOptionsRef.current = newOptions; + + // Update currentGameMode immediately so getCurrentCount() returns the correct value + if (newOptions.gameMode) { + setCurrentGameMode(newOptions.gameMode); + gameModeRef.current = newOptions.gameMode; + } }; - const applyPendingOptions = () => { - if (!pendingOptionsRef.current) return; - - const pending = pendingOptionsRef.current; - pendingOptionsRef.current = null; - - // Update game mode - if (pending.gameMode) { - gameModeRef.current = pending.gameMode; - } - - // Update speed ref - if (pending.downwardSpeed !== undefined) { - downwardSpeedRef.current = pending.downwardSpeed; - } - - // Update word list ref - if (pending.wordList !== undefined) { - wordListRef.current = pending.wordList; - } - }; + const applyPendingOptions = () => { + if (!pendingOptionsRef.current) return; + + const pending = pendingOptionsRef.current; + pendingOptionsRef.current = null; + + // Update game mode + if (pending.gameMode) { + gameModeRef.current = pending.gameMode; + setCurrentGameMode(pending.gameMode); + } + + // Update speed ref + if (pending.downwardSpeed !== undefined) { + downwardSpeedRef.current = pending.downwardSpeed; + } + + // Update word list ref + if (pending.wordList !== undefined) { + wordListRef.current = pending.wordList; + } + }; const addCorrect = () => { setScore(prev => prev + 1); @@ -382,7 +391,7 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal }; const getCurrentCount = (): number => { - const mode = gameModeRef.current; + const mode = currentGameMode; if (mode === 'letters') { return lettersCompleted; } else if (mode === 'words') { @@ -419,6 +428,7 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal wordsCompleted, sentencesCompleted, paragraphsCompleted, + currentGameMode, getCurrentCount, }; } diff --git a/src/useScore/useScore.ts b/src/useScore/useScore.ts index 12ea1fa..9cc3331 100755 --- a/src/useScore/useScore.ts +++ b/src/useScore/useScore.ts @@ -14,6 +14,8 @@ export function useScore(gameStatusManager: any): ScoreManager { return () => clearInterval(interval); }, [gameStatusManager.gameStatus]); + const currentCount = gameStatusManager.getCurrentCount(); + return { timeValue, totalKeystrokes: 0, // TODO: Track keystrokes from keyboard input @@ -25,7 +27,7 @@ export function useScore(gameStatusManager: any): ScoreManager { wordsCompleted: gameStatusManager.wordsCompleted, sentencesCompleted: gameStatusManager.sentencesCompleted, paragraphsCompleted: gameStatusManager.paragraphsCompleted, - currentCount: gameStatusManager.getCurrentCount(), + currentCount, }; }