Extract state machine logic into dedicated hook and move test scenarios to usePlatformHook
This commit is contained in:
+8
-54
@@ -1,8 +1,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState } from "react";
|
||||
import { useGame, type GameMode } from "@vimazing/typing-chud";
|
||||
import "@vimazing/typing-chud/game.css";
|
||||
import { usePlatformHook } from "./usePlatformHook";
|
||||
import { testScenarios, getScenario, getScenarioNames, type TestScenario } from "./testStateMachines";
|
||||
import { usePlatformHook, useStateMachine, getScenario, getScenarioNames } from "./usePlatformHook";
|
||||
|
||||
function App() {
|
||||
const [scenarioId, setScenarioId] = useState<string>("progressive");
|
||||
@@ -10,58 +9,13 @@ function App() {
|
||||
const [gameMode, setGameMode] = useState<GameMode>(scenario.initialMode);
|
||||
const gameManager = useGame({ gameMode }, usePlatformHook);
|
||||
const { containerRef, gameStatus, scoreManager } = gameManager;
|
||||
const lastTransitionIndexRef = useRef<number>(-1);
|
||||
const triggeredScoreTransitionsRef = useRef<Set<number>>(new Set());
|
||||
const lastTriggeredModeRef = useRef<GameMode | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (gameStatus !== "started") return;
|
||||
|
||||
// Handle score-based transitions
|
||||
if (scenario.scoreTransitions) {
|
||||
const currentScore = scoreManager.score;
|
||||
const nextTransition = scenario.scoreTransitions.find(
|
||||
(t) => currentScore >= t.scoreThreshold && !triggeredScoreTransitionsRef.current.has(t.scoreThreshold)
|
||||
);
|
||||
|
||||
if (nextTransition) {
|
||||
triggeredScoreTransitionsRef.current.add(nextTransition.scoreThreshold);
|
||||
setGameMode(nextTransition.targetMode);
|
||||
gameManager.changeGameMode({ gameMode: nextTransition.targetMode });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle count-based transitions
|
||||
if (!scenario.transitions) return;
|
||||
|
||||
// Skip if we already triggered a transition for this mode
|
||||
if (lastTriggeredModeRef.current === gameMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTransitionIdx = scenario.transitions.findIndex((t) => t.fromMode === gameMode);
|
||||
|
||||
if (currentTransitionIdx === -1 || currentTransitionIdx <= lastTransitionIndexRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTransition = scenario.transitions[currentTransitionIdx];
|
||||
const currentCount = scoreManager.currentCount;
|
||||
|
||||
if (currentCount >= currentTransition.threshold) {
|
||||
lastTransitionIndexRef.current = currentTransitionIdx;
|
||||
lastTriggeredModeRef.current = gameMode; // Remember we triggered from this mode
|
||||
setGameMode(currentTransition.targetMode);
|
||||
gameManager.changeGameMode({ gameMode: currentTransition.targetMode });
|
||||
}
|
||||
}, [scoreManager.currentCount, gameMode, gameManager, scenario, gameStatus, scoreManager.score]);
|
||||
|
||||
useEffect(() => {
|
||||
lastTransitionIndexRef.current = -1;
|
||||
triggeredScoreTransitionsRef.current.clear();
|
||||
lastTriggeredModeRef.current = null;
|
||||
}, [scenarioId]);
|
||||
useStateMachine({
|
||||
gameManager,
|
||||
gameMode,
|
||||
scenario,
|
||||
onModeChange: setGameMode,
|
||||
});
|
||||
|
||||
const formatTime = (ms: number) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './usePlatformHook';
|
||||
export * from './useStateMachine';
|
||||
export * from './testStateMachines';
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import type { GameMode } from '@vimazing/typing-chud';
|
||||
|
||||
export type ModeTransition = {
|
||||
fromMode: GameMode;
|
||||
threshold: number;
|
||||
targetMode: GameMode;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type ScoreTransition = {
|
||||
scoreThreshold: number;
|
||||
targetMode: GameMode;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type TestScenario = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
initialMode: GameMode;
|
||||
transitions?: ModeTransition[];
|
||||
scoreTransitions?: ScoreTransition[];
|
||||
};
|
||||
|
||||
export const testScenarios: Record<string, TestScenario> = {
|
||||
scoreBasedProgression: {
|
||||
id: 'scoreBasedProgression',
|
||||
name: 'Score-Based Progression',
|
||||
description: 'Change modes based on score: score 10 → words, score 50 → sentences, score 100 → paragraphs. Tests delayed application until current item completes.',
|
||||
initialMode: 'letters',
|
||||
scoreTransitions: [
|
||||
{
|
||||
scoreThreshold: 10,
|
||||
targetMode: 'words',
|
||||
description: 'At score 10, request switch to words',
|
||||
},
|
||||
{
|
||||
scoreThreshold: 50,
|
||||
targetMode: 'sentences',
|
||||
description: 'At score 50, request switch to sentences',
|
||||
},
|
||||
{
|
||||
scoreThreshold: 100,
|
||||
targetMode: 'paragraphs',
|
||||
description: 'At score 100, request switch to paragraphs',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
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,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { GameMode, GameManager } from "../../../src";
|
||||
import type { TestScenario } from "./testStateMachines";
|
||||
|
||||
interface UseStateMachineProps {
|
||||
gameManager: GameManager;
|
||||
gameMode: GameMode;
|
||||
scenario: TestScenario;
|
||||
onModeChange: (newMode: GameMode) => void;
|
||||
}
|
||||
|
||||
export const useStateMachine = ({
|
||||
gameManager,
|
||||
gameMode,
|
||||
scenario,
|
||||
onModeChange,
|
||||
}: UseStateMachineProps) => {
|
||||
const lastTransitionIndexRef = useRef<number>(-1);
|
||||
const triggeredScoreTransitionsRef = useRef<Set<number>>(new Set());
|
||||
const lastTriggeredModeRef = useRef<GameMode | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const { gameStatus, scoreManager } = gameManager;
|
||||
|
||||
if (gameStatus !== "started") return;
|
||||
|
||||
// Handle score-based transitions
|
||||
if (scenario.scoreTransitions) {
|
||||
const currentScore = scoreManager.score;
|
||||
const nextTransition = scenario.scoreTransitions.find(
|
||||
(t) => currentScore >= t.scoreThreshold && !triggeredScoreTransitionsRef.current.has(t.scoreThreshold)
|
||||
);
|
||||
|
||||
if (nextTransition) {
|
||||
triggeredScoreTransitionsRef.current.add(nextTransition.scoreThreshold);
|
||||
onModeChange(nextTransition.targetMode);
|
||||
gameManager.changeGameMode({ gameMode: nextTransition.targetMode });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle count-based transitions
|
||||
if (!scenario.transitions) return;
|
||||
|
||||
// Skip if we already triggered a transition for this mode
|
||||
if (lastTriggeredModeRef.current === gameMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTransitionIdx = scenario.transitions.findIndex((t) => t.fromMode === gameMode);
|
||||
|
||||
if (currentTransitionIdx === -1 || currentTransitionIdx <= lastTransitionIndexRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTransition = scenario.transitions[currentTransitionIdx];
|
||||
const currentCount = scoreManager.currentCount;
|
||||
|
||||
if (currentCount >= currentTransition.threshold) {
|
||||
lastTransitionIndexRef.current = currentTransitionIdx;
|
||||
lastTriggeredModeRef.current = gameMode;
|
||||
onModeChange(currentTransition.targetMode);
|
||||
gameManager.changeGameMode({ gameMode: currentTransition.targetMode });
|
||||
}
|
||||
}, [scoreManager.currentCount, gameMode, gameManager, scenario, gameStatus, scoreManager.score]);
|
||||
|
||||
useEffect(() => {
|
||||
lastTransitionIndexRef.current = -1;
|
||||
triggeredScoreTransitionsRef.current.clear();
|
||||
lastTriggeredModeRef.current = null;
|
||||
}, [scenario.id]);
|
||||
};
|
||||
Reference in New Issue
Block a user