starting new structure
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export type UseTimerReturn = {
|
||||
timeValue: number;
|
||||
startTimer: () => void;
|
||||
stopTimer: () => void;
|
||||
resetTimer: () => void;
|
||||
};
|
||||
|
||||
export type UseTimerOptions = {
|
||||
interval?: number;
|
||||
};
|
||||
|
||||
export function useTimer(options: UseTimerOptions = {}): UseTimerReturn {
|
||||
const { interval = 100 } = options;
|
||||
const [timeValue, setTimeValue] = useState(0);
|
||||
|
||||
const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const offsetRef = useRef(0);
|
||||
const startTimeRef = useRef<number | null>(null);
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (startTimeRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
setTimeValue(offsetRef.current + (now - startTimeRef.current));
|
||||
}, []);
|
||||
|
||||
const startTimer = useCallback(() => {
|
||||
if (intervalIdRef.current !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (startTimeRef.current === null) {
|
||||
startTimeRef.current = performance.now();
|
||||
}
|
||||
|
||||
update();
|
||||
intervalIdRef.current = setInterval(update, Math.max(16, interval));
|
||||
}, [interval, update]);
|
||||
|
||||
const stopTimer = useCallback(() => {
|
||||
if (intervalIdRef.current !== null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
|
||||
if (startTimeRef.current !== null) {
|
||||
update();
|
||||
offsetRef.current += performance.now() - startTimeRef.current;
|
||||
startTimeRef.current = null;
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
const resetTimer = useCallback(() => {
|
||||
if (intervalIdRef.current !== null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
|
||||
startTimeRef.current = null;
|
||||
offsetRef.current = 0;
|
||||
setTimeValue(0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (intervalIdRef.current !== null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
timeValue,
|
||||
startTimer,
|
||||
stopTimer,
|
||||
resetTimer,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useScore';
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTimer } from "./hooks/useTimer";
|
||||
import { useScoreTime } from "./useScoreTime";
|
||||
import type { GameStatus } from "../types";
|
||||
|
||||
const TIME_LIMIT_MS = Infinity; // No time limit by default
|
||||
const KEYSTROKE_LIMIT = Infinity; // No keystroke limit by default
|
||||
|
||||
type GameScoreContext = {
|
||||
gameStatus: GameStatus;
|
||||
setGameStatus: (status: GameStatus) => void;
|
||||
};
|
||||
|
||||
type UseScoreParams = {
|
||||
gameContext: GameScoreContext;
|
||||
keystrokeCount: number;
|
||||
board: string;
|
||||
isComplete: (board: string) => boolean;
|
||||
isFullyValid: (board: string) => boolean;
|
||||
initialBoard: string;
|
||||
};
|
||||
|
||||
export function useScore({
|
||||
gameContext,
|
||||
keystrokeCount,
|
||||
board,
|
||||
isComplete,
|
||||
isFullyValid,
|
||||
initialBoard
|
||||
}: UseScoreParams) {
|
||||
const timer = useTimer();
|
||||
const { gameStatus, setGameStatus } = gameContext;
|
||||
const { timeValue, startTimer, stopTimer, resetTimer } = timer;
|
||||
|
||||
const [finalScore, setFinalScore] = useState<number | null>(null);
|
||||
|
||||
const minMovesRequired = initialBoard.split("").filter(c => c === ".").length;
|
||||
const actualMoves = keystrokeCount;
|
||||
const efficiency = minMovesRequired > 0
|
||||
? Math.round((actualMoves / minMovesRequired) * 100)
|
||||
: 0;
|
||||
|
||||
useScoreTime({ gameStatus, timer });
|
||||
|
||||
useEffect(() => {
|
||||
if (gameStatus !== "started") return;
|
||||
|
||||
if (isComplete(board) && isFullyValid(board)) {
|
||||
setGameStatus("game-won");
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeValue >= TIME_LIMIT_MS) {
|
||||
setGameStatus("game-over");
|
||||
return;
|
||||
}
|
||||
|
||||
if (keystrokeCount >= KEYSTROKE_LIMIT) {
|
||||
setGameStatus("game-over");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Enable efficiency-based game-over in future
|
||||
// if (efficiency > 200) {
|
||||
// setGameStatus("game-over");
|
||||
// return;
|
||||
// }
|
||||
}, [gameStatus, timeValue, keystrokeCount, board, setGameStatus, isComplete, isFullyValid, efficiency]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleWin = () => {
|
||||
if (gameStatus === "started") {
|
||||
setGameStatus("game-won");
|
||||
}
|
||||
};
|
||||
window.addEventListener("sudoku-game-won", handleWin);
|
||||
return () => window.removeEventListener("sudoku-game-won", handleWin);
|
||||
}, [gameStatus, setGameStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (gameStatus !== "game-won") return;
|
||||
|
||||
const timeInSeconds = timeValue / 1000;
|
||||
const baseScore = 100000;
|
||||
const timePenalty = Math.floor(timeInSeconds * 10);
|
||||
const extraMoves = Math.max(0, actualMoves - minMovesRequired);
|
||||
const movePenalty = extraMoves * 50;
|
||||
const score = Math.max(0, baseScore - timePenalty - movePenalty);
|
||||
|
||||
setFinalScore(score);
|
||||
}, [gameStatus, timeValue, actualMoves, minMovesRequired]);
|
||||
|
||||
return {
|
||||
timeValue,
|
||||
startTimer,
|
||||
stopTimer,
|
||||
resetTimer,
|
||||
finalScore,
|
||||
timeLimit: TIME_LIMIT_MS,
|
||||
keystrokeLimit: KEYSTROKE_LIMIT,
|
||||
minMovesRequired,
|
||||
actualMoves,
|
||||
efficiency,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseScoreType = ReturnType<typeof useScore>;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { UseTimerReturn } from "./hooks/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 "game-over":
|
||||
case "game-won":
|
||||
case "waiting":
|
||||
stopTimer();
|
||||
break;
|
||||
}
|
||||
}, [gameStatus, startTimer, stopTimer, resetTimer]);
|
||||
}
|
||||
Reference in New Issue
Block a user