From 0749aba6481b14ec43908e936a851dadcddf462e Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Sun, 2 Nov 2025 17:04:21 +0000 Subject: [PATCH] refactor: separate score calculation from score management BREAKING CHANGES: - Remove timeLimit from GameOptions (use gameOver function instead) - Remove finalScore from ScoreManager (pure data collection only) - Remove rows/cols parameters from useScore - Remove finalScore calculation logic from useScore hook ADDITIONS: - Create calculateMazeScore() utility function - ScoreManager now only returns raw metrics: timeValue, keystrokes, optimalSteps, efficiency, distToKey, distToExit - Score calculation moved to consumer (calculateMazeScore takes scoreManager + mazeSize) - Example app now uses calculateMazeScore to compute final score on game-won BENEFITS: - ScoreManager is now a pure data collector with no side effects - Scoring algorithm is reusable and testable in isolation - Game-over conditions fully controlled via gameOver option - Clear separation of concerns: collection vs calculation --- example/src/App.tsx | 11 +++++++-- src/index.ts | 1 + src/types.ts | 1 - src/useGame.ts | 4 ++-- src/useScore/calculateMazeScore.ts | 30 +++++++++++++++++++++++ src/useScore/index.ts | 1 + src/useScore/types.ts | 1 - src/useScore/useScore.ts | 38 +++--------------------------- 8 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 src/useScore/calculateMazeScore.ts diff --git a/example/src/App.tsx b/example/src/App.tsx index 45c2c98..043af5e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,6 +1,7 @@ -import { useGame, version } from "../../src"; +import { useGame, version, calculateMazeScore } from "../../src"; import { useKeyBindings } from "./useKeyBindings"; import { useIsMobile } from "./useIsMobile"; +import { useMemo } from "react"; import '../../src/game.css'; function App() { @@ -10,7 +11,13 @@ function App() { const gameManager = useGame({ cols, rows }, useKeyBindings); const { containerRef, gameStatus, scoreManager } = gameManager; - const { timeValue, keystrokes, optimalSteps, efficiency, finalScore } = scoreManager; + const { timeValue, keystrokes, optimalSteps, efficiency } = scoreManager; + + const mazeSize = rows * cols; + const finalScore = useMemo( + () => gameStatus === "game-won" ? calculateMazeScore(scoreManager, mazeSize) : null, + [gameStatus, scoreManager, mazeSize] + ); const formatTime = (ms: number) => { const seconds = Math.floor(ms / 1000); diff --git a/src/index.ts b/src/index.ts index 9d104fc..5f641b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from './types'; export * from './useGame'; export * from './useGameStatus'; export { gameInfo } from './gameInfo'; +export { calculateMazeScore } from './useScore'; import { version } from '../package.json'; export type { GameInfo } from './gameInfo'; export { version }; diff --git a/src/types.ts b/src/types.ts index 49ae5cc..517d7b7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,7 +11,6 @@ export * from './useScore/types'; export type GameOptions = { rows: number; cols: number; - timeLimit?: number; gameOver?: boolean | ((scoreManager: ScoreManager) => boolean); }; diff --git a/src/useGame.ts b/src/useGame.ts index b3008fd..f2f7fb4 100644 --- a/src/useGame.ts +++ b/src/useGame.ts @@ -6,7 +6,7 @@ import { useHeroRender } from './useCursor'; import { useScore } from './useScore'; export function useGame(options: GameOptions, platformHook?: unknown): GameManager { - const { cols, rows, timeLimit = 600, gameOver } = options; + const { cols, rows, gameOver } = options; const board = useBoard(cols, rows); const { containerRef, renderBoard } = board; const gameStatusManager = useGameStatus(); @@ -14,7 +14,7 @@ export function useGame(options: GameOptions, platformHook?: unknown): GameManag const hero = cursor.hero; const renderer = useHeroRender({ gameStatus: gameStatusManager.gameStatus, board, hero }); - const scoreManager = useScore({ board, hero, gameStatusManager, keyManager, rows, cols, timeLimit, gameOver }); + const scoreManager = useScore({ board, hero, gameStatusManager, keyManager, gameOver }); const gameManager: GameManager = { containerRef, diff --git a/src/useScore/calculateMazeScore.ts b/src/useScore/calculateMazeScore.ts new file mode 100644 index 0000000..5c03db4 --- /dev/null +++ b/src/useScore/calculateMazeScore.ts @@ -0,0 +1,30 @@ +import type { ScoreManager } from './types'; + +/** + * Calculate the final maze score based on performance metrics + * + * Formula: + * Base Score = 1000 - (time penalty) - (keystroke penalty) + * Size Multiplier = max(1.0, (mazeSize) / 500) + * Final Score = min(1000, max(0, round(Base Score × Size Multiplier))) + * + * Penalties: + * - Time Penalty = seconds / 10 + * - Keystroke Penalty = keystrokes / 2 + * + * @param scoreManager - Current score metrics + * @param mazeSize - Total cells in maze (rows * cols) + * @returns Final score between 0-1000 + */ +export function calculateMazeScore(scoreManager: ScoreManager, mazeSize: number): number { + const seconds = scoreManager.timeValue / 1000; + + const timePenalty = seconds / 10; + const keystrokePenalty = scoreManager.keystrokes / 2; + const sizeMultiplier = Math.max(1.0, mazeSize / 500); + + const baseScore = 1000 - timePenalty - keystrokePenalty; + const score = Math.min(1000, Math.max(0, Math.round(baseScore * sizeMultiplier))); + + return score; +} diff --git a/src/useScore/index.ts b/src/useScore/index.ts index cb6c144..a2550b3 100644 --- a/src/useScore/index.ts +++ b/src/useScore/index.ts @@ -3,3 +3,4 @@ export * from './useScore'; export * from './useScorePaths'; export * from './useScoreTime'; export * from './useTimer'; +export * from './calculateMazeScore'; diff --git a/src/useScore/types.ts b/src/useScore/types.ts index b02a135..c7084f4 100644 --- a/src/useScore/types.ts +++ b/src/useScore/types.ts @@ -22,5 +22,4 @@ export type ScoreManager = { keystrokes: number; optimalSteps: number; efficiency: number; - finalScore: number | null; }; diff --git a/src/useScore/useScore.ts b/src/useScore/useScore.ts index 155950b..bf80764 100644 --- a/src/useScore/useScore.ts +++ b/src/useScore/useScore.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { useTimer } from "./useTimer"; import { useScoreTime } from "./useScoreTime"; import { useScorePaths } from "./useScorePaths"; @@ -12,13 +12,10 @@ type UseScoreParams = { hero: UseHeroType; gameStatusManager: GameStatusManager; keyManager?: { keyLog: any[] }; - rows: number; - cols: number; - timeLimit: number; gameOver?: boolean | ((scoreManager: ScoreManager) => boolean); }; -export function useScore({ board, hero, gameStatusManager, keyManager, rows, cols, timeLimit, gameOver }: UseScoreParams) { +export function useScore({ board, hero, gameStatusManager, keyManager, gameOver }: UseScoreParams) { const timer = useTimer(); const { gameStatus, setGameStatus } = gameStatusManager; const { timeValue, startTimer, stopTimer, resetTimer } = timer; @@ -33,7 +30,6 @@ export function useScore({ board, hero, gameStatusManager, keyManager, rows, col const distToKey = entranceToKey || heroToKey; const distToExit = entranceToExit || heroToExit; - const [finalScore, setFinalScore] = useState(null); const optimalRef = useRef(0); const keystrokes = keyManager ? keyManager.keyLog.length : 0; @@ -56,16 +52,6 @@ export function useScore({ board, hero, gameStatusManager, keyManager, rows, col const optimalSteps = optimalRef.current; const efficiency = optimalSteps > 0 ? Math.round((keystrokes / optimalSteps) * 100) : 0; - useEffect(() => { - if (gameStatus !== "started" && gameStatus !== "has-key") return; - - const timeLimitMs = timeLimit * 1000; - if (timeValue >= timeLimitMs) { - setGameStatus("game-over"); - return; - } - }, [gameStatus, timeValue, timeLimit, setGameStatus]); - // Check custom gameOver condition if provided useEffect(() => { if (gameStatus !== "started" && gameStatus !== "has-key") return; @@ -81,30 +67,13 @@ export function useScore({ board, hero, gameStatusManager, keyManager, rows, col keystrokes, optimalSteps, efficiency, - finalScore, }; const shouldGameOver = typeof gameOver === "function" ? gameOver(scoreManager) : gameOver; if (shouldGameOver) { setGameStatus("game-over"); } - }, [gameStatus, gameOver, timeValue, keystrokes, optimalSteps, efficiency, distToKey, distToExit, startTimer, stopTimer, resetTimer, finalScore, setGameStatus]); - - useEffect(() => { - if (gameStatus !== "game-won") return; - - const seconds = timeValue / 1000; - const mazeSize = rows * cols; - - const timePenalty = seconds / 10; - const keystrokePenalty = keystrokes / 2; - const sizeMultiplier = Math.max(1.0, mazeSize / 500); - - const baseScore = 1000 - timePenalty - keystrokePenalty; - const score = Math.min(1000, Math.max(0, Math.round(baseScore * sizeMultiplier))); - - setFinalScore(score); - }, [gameStatus, timeValue, keystrokes, rows, cols]); + }, [gameStatus, gameOver, timeValue, keystrokes, optimalSteps, efficiency, distToKey, distToExit, startTimer, stopTimer, resetTimer, setGameStatus]); return { timeValue, @@ -116,6 +85,5 @@ export function useScore({ board, hero, gameStatusManager, keyManager, rows, col keystrokes, optimalSteps, efficiency, - finalScore, }; }