scoreManager implemented flawlessly
This commit is contained in:
+45
-2
@@ -11,10 +11,52 @@ function App() {
|
|||||||
const rows = isMobile ? 8 : 24;
|
const rows = isMobile ? 8 : 24;
|
||||||
|
|
||||||
const gameManager = useGame({ cols, rows }, useKeyBindings);
|
const gameManager = useGame({ cols, rows }, useKeyBindings);
|
||||||
const { containerRef, gameStatus } = gameManager;
|
const { containerRef, gameStatus, scoreManager } = gameManager;
|
||||||
|
const { timeValue, keystrokes, efficiency, optimalSteps, finalScore } = scoreManager;
|
||||||
|
|
||||||
|
const formatTime = (ms: number) => {
|
||||||
|
const seconds = Math.floor(ms / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col items-center justify-around min-h-[100vh]">
|
||||||
|
<h1 className="text-6xl">VIMazing vim-maze example</h1>
|
||||||
|
<div className="mx-auto my-4 w-fit bg-muted rounded-lg p-4">
|
||||||
|
{(gameStatus === "started" || gameStatus === "has-key" || gameStatus === "game-won") && (
|
||||||
|
<div className="flex gap-8 text-sm font-mono items-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground">Time</div>
|
||||||
|
<div className="text-lg font-bold">{formatTime(timeValue)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground">Keystrokes</div>
|
||||||
|
<div className="text-lg font-bold">{keystrokes}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground">Efficiency</div>
|
||||||
|
<div className={`text-lg font-bold ${efficiency > 150 ? 'text-red-500' : efficiency > 100 ? 'text-yellow-500' : 'text-green-500'}`}>
|
||||||
|
{efficiency}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground">Optimal</div>
|
||||||
|
<div className="text-lg font-bold">{optimalSteps}</div>
|
||||||
|
</div>
|
||||||
|
{gameStatus === "game-won" && finalScore && (
|
||||||
|
<>
|
||||||
|
<div className="w-px h-12 bg-muted-foreground/20" />
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-muted-foreground">Final Score</div>
|
||||||
|
<div className="text-3xl font-bold bg-gradient-to-r from-green-400 to-emerald-500 bg-clip-text text-transparent">{finalScore}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="relative mx-auto my-4 w-fit" id="maze_container">
|
<div className="relative mx-auto my-4 w-fit" id="maze_container">
|
||||||
<div ref={containerRef} className="relative" />
|
<div ref={containerRef} className="relative" />
|
||||||
</div>
|
</div>
|
||||||
@@ -22,6 +64,7 @@ function App() {
|
|||||||
{gameStatus === "waiting" && <p>Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to start</p>}
|
{gameStatus === "waiting" && <p>Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to start</p>}
|
||||||
{gameStatus === "started" && <p>Use <kbd className="px-2 py-1 bg-muted rounded">hjkl</kbd> to change direction • Press <kbd className="px-2 py-1 bg-muted rounded">q</kbd> to quit</p>}
|
{gameStatus === "started" && <p>Use <kbd className="px-2 py-1 bg-muted rounded">hjkl</kbd> to change direction • Press <kbd className="px-2 py-1 bg-muted rounded">q</kbd> to quit</p>}
|
||||||
{gameStatus === "game-over" && <p>Game Over! Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to restart</p>}
|
{gameStatus === "game-over" && <p>Game Over! Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to restart</p>}
|
||||||
|
{gameStatus === "game-won" && <p>You Won! Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to play again</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import { MazeRenderer, MazeGenerator } from "./useBoard";
|
import { MazeRenderer, MazeGenerator } from "./useBoard";
|
||||||
|
import type { ScoreManager } from './useScore';
|
||||||
export * from './useGame';
|
export * from './useGame';
|
||||||
export * from './useBoard';
|
export * from './useBoard';
|
||||||
export * from './useCursor';
|
export * from './useCursor';
|
||||||
|
export * from './useScore';
|
||||||
|
|
||||||
export type GameStatus = 'waiting' | 'started' | 'has-key' | 'paused' | 'game-over' | 'game-won'
|
export type GameStatus = 'waiting' | 'started' | 'has-key' | 'paused' | 'game-over' | 'game-won'
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ export type GameManager = {
|
|||||||
cursor: CursorManager;
|
cursor: CursorManager;
|
||||||
hero: HeroManager;
|
hero: HeroManager;
|
||||||
renderer: HeroRenderer;
|
renderer: HeroRenderer;
|
||||||
|
scoreManager: ScoreManager;
|
||||||
} & GameStatusManager & GameKeyManager;
|
} & GameStatusManager & GameKeyManager;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -3,6 +3,7 @@ import { useBoard } from './useBoard';
|
|||||||
import { useCursor } from './useCursor';
|
import { useCursor } from './useCursor';
|
||||||
import { useGameStatus } from './useGameStatus';
|
import { useGameStatus } from './useGameStatus';
|
||||||
import { useHeroRender } from './useCursor';
|
import { useHeroRender } from './useCursor';
|
||||||
|
import { useScore } from './useScore';
|
||||||
|
|
||||||
export function useGame(options: GameOptions, platformHook?: unknown): GameManager {
|
export function useGame(options: GameOptions, platformHook?: unknown): GameManager {
|
||||||
const { cols, rows } = options;
|
const { cols, rows } = options;
|
||||||
@@ -13,8 +14,9 @@ export function useGame(options: GameOptions, platformHook?: unknown): GameManag
|
|||||||
|
|
||||||
const hero = cursor.hero;
|
const hero = cursor.hero;
|
||||||
const renderer = useHeroRender({ gameStatus: gameStatusManager.gameStatus, board, hero });
|
const renderer = useHeroRender({ gameStatus: gameStatusManager.gameStatus, board, hero });
|
||||||
|
const scoreManager = useScore({ board, hero, gameStatusManager, keyManager });
|
||||||
|
|
||||||
const gameManager = {
|
const gameManager: GameManager = {
|
||||||
containerRef,
|
containerRef,
|
||||||
...keyManager,
|
...keyManager,
|
||||||
...gameStatusManager,
|
...gameStatusManager,
|
||||||
@@ -22,6 +24,7 @@ export function useGame(options: GameOptions, platformHook?: unknown): GameManag
|
|||||||
cursor,
|
cursor,
|
||||||
hero,
|
hero,
|
||||||
renderer,
|
renderer,
|
||||||
|
scoreManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof platformHook === 'function') {
|
if (typeof platformHook === 'function') {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './useScore';
|
||||||
|
export * from './useScorePaths';
|
||||||
|
export * from './useScoreTime';
|
||||||
|
export * from './useTimer';
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTimer } from "./useTimer";
|
||||||
|
import { useScoreTime } from "./useScoreTime";
|
||||||
|
import { useScorePaths } from "./useScorePaths";
|
||||||
|
import type { BoardManager, GameStatusManager, UseHeroType } from "../types";
|
||||||
|
|
||||||
|
type UseScoreParams = {
|
||||||
|
board: BoardManager;
|
||||||
|
hero: UseHeroType;
|
||||||
|
gameStatusManager: GameStatusManager;
|
||||||
|
keyManager?: { keyLog: any[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useScore({ board, hero, gameStatusManager, keyManager }: UseScoreParams) {
|
||||||
|
const timer = useTimer();
|
||||||
|
const { gameStatus, setGameStatus } = gameStatusManager;
|
||||||
|
const { timeValue, startTimer, stopTimer, resetTimer } = timer;
|
||||||
|
|
||||||
|
const {
|
||||||
|
entranceToKey = 0,
|
||||||
|
entranceToExit = 0,
|
||||||
|
heroToKey = 0,
|
||||||
|
heroToExit = 0,
|
||||||
|
} = useScorePaths({ board, heroPos: hero.heroPos });
|
||||||
|
|
||||||
|
const distToKey = entranceToKey || heroToKey;
|
||||||
|
const distToExit = entranceToExit || heroToExit;
|
||||||
|
|
||||||
|
const [finalScore, setFinalScore] = useState<number | null>(null);
|
||||||
|
const optimalRef = useRef<number>(0);
|
||||||
|
const keystrokes = keyManager ? keyManager.keyLog.length : 0;
|
||||||
|
|
||||||
|
useScoreTime({ gameStatus, timer });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const maze = board.mazeInstanceRef.current;
|
||||||
|
if (!maze) return;
|
||||||
|
|
||||||
|
const k = maze.getDistance("entrance", "key");
|
||||||
|
const e = maze.getDistance("key", "exit");
|
||||||
|
const total = k + e;
|
||||||
|
|
||||||
|
if (Number.isFinite(total) && total > 0) {
|
||||||
|
optimalRef.current = total;
|
||||||
|
}
|
||||||
|
}, [gameStatus, board.mazeInstanceRef]);
|
||||||
|
|
||||||
|
const optimalSteps = optimalRef.current;
|
||||||
|
|
||||||
|
const efficiency =
|
||||||
|
optimalSteps > 0 ? Math.round((keystrokes / optimalSteps) * 100) : 0;
|
||||||
|
|
||||||
|
const [logicFirst, setLogicFirst] = useState<number | null>(null);
|
||||||
|
const [logicLast, setLogicLast] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameStatus === "started") {
|
||||||
|
setLogicFirst(null);
|
||||||
|
setLogicLast(null);
|
||||||
|
setPlayerSteps(0);
|
||||||
|
prevPosRef.current = null;
|
||||||
|
}
|
||||||
|
}, [gameStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onTick = (e: Event) => {
|
||||||
|
const t =
|
||||||
|
(e as CustomEvent<{ t?: number }>).detail?.t ?? performance.now();
|
||||||
|
setLogicFirst((prev) => prev ?? t);
|
||||||
|
setLogicLast(t);
|
||||||
|
};
|
||||||
|
window.addEventListener("maze-logic-tick", onTick as EventListener);
|
||||||
|
return () =>
|
||||||
|
window.removeEventListener("maze-logic-tick", onTick as EventListener);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [, setPlayerSteps] = useState(0);
|
||||||
|
const prevPosRef = useRef<{ row: number; col: number } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pos = hero.heroPos;
|
||||||
|
if (!pos) {
|
||||||
|
prevPosRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["started", "has-key"].includes(gameStatus)) {
|
||||||
|
prevPosRef.current = pos;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = prevPosRef.current;
|
||||||
|
prevPosRef.current = pos;
|
||||||
|
if (!prev) return;
|
||||||
|
|
||||||
|
const delta = Math.abs(pos.row - prev.row) + Math.abs(pos.col - prev.col);
|
||||||
|
if (delta > 0) setPlayerSteps((s) => s + delta);
|
||||||
|
}, [gameStatus, hero.heroPos]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameStatus === "started" && efficiency > 150) {
|
||||||
|
setGameStatus("game-over");
|
||||||
|
}
|
||||||
|
}, [efficiency, gameStatus, setGameStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameStatus !== "game-won") return;
|
||||||
|
|
||||||
|
const logicMs =
|
||||||
|
logicFirst != null && logicLast != null
|
||||||
|
? Math.max(1, logicLast - logicFirst)
|
||||||
|
: timeValue;
|
||||||
|
|
||||||
|
const optimalMs = optimalSteps * 200;
|
||||||
|
const ratio = Math.max(0.1, optimalMs / logicMs);
|
||||||
|
const score = Math.min(100000, Math.round(ratio * 100000));
|
||||||
|
|
||||||
|
setFinalScore(score);
|
||||||
|
}, [gameStatus, logicFirst, logicLast, timeValue, optimalSteps]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
timeValue,
|
||||||
|
startTimer,
|
||||||
|
stopTimer,
|
||||||
|
resetTimer,
|
||||||
|
distToKey,
|
||||||
|
distToExit,
|
||||||
|
keystrokes,
|
||||||
|
optimalSteps,
|
||||||
|
efficiency,
|
||||||
|
finalScore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScoreManager = ReturnType<typeof useScore>;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { BoardManager } from "../types";
|
||||||
|
|
||||||
|
type UseScorePathsParams = {
|
||||||
|
board: BoardManager;
|
||||||
|
heroPos: { row: number; col: number } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useScorePaths({ board, heroPos }: UseScorePathsParams) {
|
||||||
|
const { mazeInstanceRef } = board;
|
||||||
|
|
||||||
|
const [distances, setDistances] = useState({
|
||||||
|
entranceToKey: Infinity,
|
||||||
|
entranceToExit: Infinity,
|
||||||
|
heroToKey: Infinity,
|
||||||
|
heroToExit: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const maze = mazeInstanceRef.current;
|
||||||
|
if (!maze) return;
|
||||||
|
|
||||||
|
setDistances((prev) => ({
|
||||||
|
...prev,
|
||||||
|
entranceToKey: maze.getDistance("entrance", "key"),
|
||||||
|
entranceToExit: maze.getDistance("entrance", "exit"),
|
||||||
|
}));
|
||||||
|
}, [mazeInstanceRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const maze = mazeInstanceRef.current;
|
||||||
|
if (!maze || !heroPos) return;
|
||||||
|
|
||||||
|
setDistances((prev) => ({
|
||||||
|
...prev,
|
||||||
|
heroToKey: maze.getDistance("hero", "key", heroPos),
|
||||||
|
heroToExit: maze.getDistance("hero", "exit", heroPos),
|
||||||
|
}));
|
||||||
|
}, [heroPos, mazeInstanceRef]);
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseScorePathsReturn = ReturnType<typeof useScorePaths>;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import type { UseTimerReturn } from "./useTimer";
|
||||||
|
import type { GameStatus } from "../types";
|
||||||
|
|
||||||
|
type UseScoreTimeParams = {
|
||||||
|
gameStatus: GameStatus;
|
||||||
|
timer: UseTimerReturn;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useScoreTime({ gameStatus, timer }: UseScoreTimeParams) {
|
||||||
|
const prevStatusRef = useRef<GameStatus | null>(null);
|
||||||
|
const { startTimer, stopTimer, resetTimer } = timer;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
prevStatusRef.current = gameStatus;
|
||||||
|
|
||||||
|
switch (gameStatus) {
|
||||||
|
case "started":
|
||||||
|
resetTimer();
|
||||||
|
startTimer();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "has-key":
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "game-over":
|
||||||
|
case "game-won":
|
||||||
|
case "waiting":
|
||||||
|
stopTimer();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [gameStatus]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
export function useTimer() {
|
||||||
|
const [timeValue, setTimeValue] = useState(0);
|
||||||
|
const startTimeRef = useRef<number | null>(null);
|
||||||
|
const frameRef = useRef<number | null>(null);
|
||||||
|
const runningRef = useRef(false);
|
||||||
|
|
||||||
|
const startTimer = () => {
|
||||||
|
if (runningRef.current) return;
|
||||||
|
runningRef.current = true;
|
||||||
|
startTimeRef.current = performance.now();
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (!runningRef.current) return;
|
||||||
|
const now = performance.now();
|
||||||
|
const elapsed = now - (startTimeRef.current ?? now);
|
||||||
|
setTimeValue(elapsed);
|
||||||
|
frameRef.current = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
|
||||||
|
frameRef.current = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopTimer = () => {
|
||||||
|
runningRef.current = false;
|
||||||
|
if (frameRef.current) {
|
||||||
|
cancelAnimationFrame(frameRef.current);
|
||||||
|
frameRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetTimer = () => {
|
||||||
|
stopTimer();
|
||||||
|
setTimeValue(0);
|
||||||
|
startTimeRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (frameRef.current) cancelAnimationFrame(frameRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { timeValue, startTimer, stopTimer, resetTimer };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseTimerReturn = ReturnType<typeof useTimer>;
|
||||||
Reference in New Issue
Block a user