working on useScore

This commit is contained in:
2025-10-15 18:28:55 +01:00
parent 5f3e3706fa
commit 2dd317e0dc
6 changed files with 128 additions and 47 deletions
+48 -3
View File
@@ -1,15 +1,60 @@
import { useGame } from "../../src/useGame/useGame"; import { useGame } from "../../src/useGame/useGame";
import { useKeyBindings } from './useKeyBindings'; import { useKeyBindings } from './useKeyBindings';
import { useMounted } from './useMounted';
import { GameOverlay } from './GameOverlay';
import "../../src/sudoku.css"; import "../../src/sudoku.css";
function formatTime(ms: number): string {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
export default function App() { export default function App() {
const { containerRef, initGame, solveBoard } = useGame(useKeyBindings); const mounted = useMounted();
const {
containerRef,
initGame,
solveBoard,
scoreManager,
keyLog,
gameStatus,
} = useGame(useKeyBindings);
const { timeValue, finalScore } = scoreManager;
return ( return (
<div style={{ padding: 16 }}> <div style={{ padding: 16 }}>
<h1>VIMazing Sudoku</h1> <h1>VIMazing Sudoku</h1>
<div ref={containerRef} />
<div className="mt-20"> {/* Scoreboard */}
<div style={{
display: 'flex',
gap: 32,
marginBottom: 16,
fontFamily: 'monospace',
fontSize: 18,
}}>
<div>
<strong>Time:</strong> {formatTime(timeValue)}
</div>
<div>
<strong>Keystrokes:</strong> {keyLog.length}
</div>
{gameStatus === "game-won" && finalScore !== null && (
<div style={{ color: '#82aaff', fontWeight: 'bold' }}>
<strong>Score:</strong> {finalScore.toLocaleString()}
</div>
)}
</div>
<div className="relative w-fit mx-auto">
<div ref={containerRef} />
{mounted && <GameOverlay gameStatus={gameStatus} scoreManager={scoreManager} />}
</div>
<div style={{ marginTop: 20, display: 'flex', gap: 8 }}>
<button onClick={initGame}>New Puzzle</button> <button onClick={initGame}>New Puzzle</button>
<button onClick={solveBoard}>Solve</button> <button onClick={solveBoard}>Solve</button>
</div> </div>
+25 -21
View File
@@ -1,5 +1,4 @@
import type { GameStatus } from "@vimazing/vim-maze"; import type { GameStatus, UseScoreType } from "../../src/types";
import type { UseScoreType } from "@vimazing/vim-maze";
import { useIsMobile } from "./useIsMobile"; import { useIsMobile } from "./useIsMobile";
import { MobileGameOverlay } from "./MobileGameOverlay"; import { MobileGameOverlay } from "./MobileGameOverlay";
@@ -12,9 +11,7 @@ export const GameOverlay = ({ gameStatus, scoreManager }: GameOverlayProps) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (isMobile) return <MobileGameOverlay gameStatus={gameStatus} scoreManager={scoreManager} /> if (isMobile) return <MobileGameOverlay gameStatus={gameStatus} scoreManager={scoreManager} />
// 🧠 Only show overlay when the game has ended if (gameStatus === "started") return null;
if (['started', 'hasKey'].includes(gameStatus)) return null;
// console.log('gameStatus:', gameStatus)
return ( return (
<GameOverlayContainer> <GameOverlayContainer>
@@ -32,13 +29,20 @@ export const GameWaitingContent = () => {
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }} style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
> >
<div className="flex flex-col gap-10"> <div className="flex flex-col gap-10">
<p className="text-[3rem]">Press <strong className="text-[#ffc777]">i</strong> to play...</p> <p className="text-[3rem]">Press <strong className="text-[#82aaff]">New Puzzle</strong> to start</p>
<p><strong className="text-[#ffc777]">h</strong>,<strong className="text-[#ffc777]">j</strong>,<strong className="text-[#ffc777]">k</strong>,<strong className="text-[#ffc777]">l</strong> to move around</p> <div className="text-[1.2em]">
<p>try other vim motions <br /> you're familiar with</p> <p className="mb-4"><strong className="text-[#ffc777]">Navigation Mode</strong> (default):</p>
<p> <p><strong className="text-[#82aaff]">h</strong>,<strong className="text-[#82aaff]">j</strong>,<strong className="text-[#82aaff]">k</strong>,<strong className="text-[#82aaff]">l</strong> to move around</p>
<strong className="text-[#ffc777]">$</strong>,<strong className="text-[#ffc777]">^</strong>,<strong className="text-[#ffc777]">0</strong>,<strong className="text-[#ffc777]">G</strong>,<strong className="text-[#ffc777]">gg</strong> only valid if <br /> <p className="mt-2"><strong className="text-[#82aaff]">R</strong> or <strong className="text-[#82aaff]">C</strong> to enter edit mode</p>
the edges are reachable
</p> <p className="mt-6 mb-4"><strong className="text-[#ffc777]">Edit Mode</strong>:</p>
<p>Type <strong className="text-[#82aaff]">1-9</strong> to fill cell</p>
<p className="mt-2"><strong className="text-[#82aaff]">Esc</strong> or <strong className="text-[#82aaff]">Enter</strong> to exit edit mode</p>
<p className="mt-6 text-[0.8em] text-[#828bb8]">
Try vim motions: <strong className="text-[#82aaff]">$</strong>, <strong className="text-[#82aaff]">^</strong>, <strong className="text-[#82aaff]">0</strong>, counted moves
</p>
</div>
</div> </div>
</div> </div>
); );
@@ -52,22 +56,22 @@ export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType })
className="text-[#c8d3f5] text-[2em] grid gap-4 font-mono uppercase text-center select-none" className="text-[#c8d3f5] text-[2em] grid gap-4 font-mono uppercase text-center select-none"
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }} style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
> >
<p className="text-[#c3e88d]">Congratulations!</p> <p className="text-[#c3e88d]">🎉 Puzzle Solved!</p>
<p>Final Score:</p> <p>Final Score:</p>
<p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p> <p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p>
<p className="text-[#828bb8] text-[0.65em] normal-case"> <p className="text-[#828bb8] text-[0.65em] normal-case">
<span className="text-[#c099ff]">min</span> <span className="text-[#c099ff]">max</span>
<span className="text-[#c8d3f5]">(</span> <span className="text-[#c8d3f5]">(</span>
<span className="text-[#ff966c]">0</span>
<span className="text-[#c8d3f5]">, </span>
<span className="text-[#ff966c]">100000</span> <span className="text-[#ff966c]">100000</span>
<span className="text-[#c8d3f5]">, (</span> <span className="text-[#c8d3f5]"> - </span>
<span className="text-[#82aaff]">optimalTime</span> <span className="text-[#82aaff]">timeInSeconds</span>
<span className="text-[#c8d3f5]"> / </span> <span className="text-[#c8d3f5]"> * </span>
<span className="text-[#82aaff]">actualTime</span> <span className="text-[#ff966c]">10</span>
<span className="text-[#c8d3f5]">) * </span>
<span className="text-[#ff966c]">100000</span>
<span className="text-[#c8d3f5]">)</span> <span className="text-[#c8d3f5]">)</span>
</p> </p>
<p className="text-[#828bb8]">Press <strong className="text-[#ffc777] normal-case">i</strong> to play again</p> <p className="text-[#828bb8]">Click <strong className="text-[#ffc777] normal-case">New Puzzle</strong> to play again</p>
</div> </div>
); );
}; };
+13 -16
View File
@@ -1,5 +1,4 @@
import type { GameStatus } from "@vimazing/vim-maze"; import type { GameStatus, UseScoreType } from "../../src/types";
import type { UseScoreType } from "@vimazing/vim-maze";
type MobileGameOverlayProps = { type MobileGameOverlayProps = {
gameStatus: GameStatus; gameStatus: GameStatus;
@@ -7,9 +6,7 @@ type MobileGameOverlayProps = {
}; };
export const MobileGameOverlay = ({ gameStatus, scoreManager }: MobileGameOverlayProps) => { export const MobileGameOverlay = ({ gameStatus, scoreManager }: MobileGameOverlayProps) => {
// 🧠 Only show overlay when the game has ended if (gameStatus === "started") return null;
if (['started', 'hasKey'].includes(gameStatus)) return null;
// console.log('gameStatus:', gameStatus)
return ( return (
<GameOverlayContainer> <GameOverlayContainer>
@@ -27,8 +24,8 @@ export const GameWaitingContent = () => {
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }} style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
> >
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<p className="text-[1.5rem]">Press here to play</p> <p className="text-[1.5rem]">Tap New Puzzle to start</p>
<p>all the vim motions <br /> available on desktop</p> <p>VIM motions available <br /> via on-screen keyboard</p>
</div> </div>
</div> </div>
); );
@@ -42,22 +39,22 @@ export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType })
className="text-[#c8d3f5] text-[1em] grid gap-4 font-mono uppercase text-center select-none" className="text-[#c8d3f5] text-[1em] grid gap-4 font-mono uppercase text-center select-none"
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }} style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
> >
<p className="text-[#c3e88d]">Congratulations!</p> <p className="text-[#c3e88d]">🎉 Solved!</p>
<p>Final Score:</p> <p>Final Score:</p>
<p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p> <p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p>
<p className="text-[#828bb8] text-[0.65em] normal-case"> <p className="text-[#828bb8] text-[0.65em] normal-case">
<span className="text-[#c099ff]">min</span> <span className="text-[#c099ff]">max</span>
<span className="text-[#c8d3f5]">(</span> <span className="text-[#c8d3f5]">(</span>
<span className="text-[#ff966c]">0</span>
<span className="text-[#c8d3f5]">, </span>
<span className="text-[#ff966c]">100000</span> <span className="text-[#ff966c]">100000</span>
<span className="text-[#c8d3f5]">, (</span> <span className="text-[#c8d3f5]"> - </span>
<span className="text-[#82aaff]">optimalTime</span> <span className="text-[#82aaff]">time</span>
<span className="text-[#c8d3f5]"> / </span> <span className="text-[#c8d3f5]"> * </span>
<span className="text-[#82aaff]">actualTime</span> <span className="text-[#ff966c]">10</span>
<span className="text-[#c8d3f5]">) * </span>
<span className="text-[#ff966c]">100000</span>
<span className="text-[#c8d3f5]">)</span> <span className="text-[#c8d3f5]">)</span>
</p> </p>
<p className="text-[#828bb8]">Press to play again</p> <p className="text-[#828bb8]">Tap to play again</p>
</div> </div>
); );
}; };
+8
View File
@@ -8,6 +8,14 @@
--box-separator: 3px; --box-separator: 3px;
} }
:root {
--cell-size: 96px;
--cell-font-size: 48px;
--grid-gap: 6px;
--box-separator: 9px;
}
/* ========================================================= /* =========================================================
Container & Grid Base Container & Grid Base
========================================================= */ ========================================================= */
+9 -5
View File
@@ -16,10 +16,6 @@ export function useGame(platformHook?: unknown) {
const boardManager = useBoardState(cursorManager); const boardManager = useBoardState(cursorManager);
const { board, initialBoard, solution, setDigit, erase, reset, solveBoard } = boardManager; const { board, initialBoard, solution, setDigit, erase, reset, solveBoard } = boardManager;
const scoreManager = useScore({
gameContext: { gameStatus, setGameStatus },
});
// --------------------------- // ---------------------------
// Keybindings // Keybindings
// --------------------------- // ---------------------------
@@ -33,7 +29,15 @@ export function useGame(platformHook?: unknown) {
initialBoard, initialBoard,
}); });
const { editMode } = keyBindings; const { editMode, keyLog } = keyBindings;
// ---------------------------
// Scoring (needs keyLog from keyBindings)
// ---------------------------
const scoreManager = useScore({
gameContext: { gameStatus, setGameStatus },
keystrokeCount: keyLog.length,
});
// --------------------------- // ---------------------------
// Rendering // Rendering
+25 -2
View File
@@ -3,6 +3,9 @@ import { useTimer } from "./hooks/useTimer";
import { useScoreTime } from "./useScoreTime"; import { useScoreTime } from "./useScoreTime";
import type { GameStatus } from "../types"; import type { GameStatus } from "../types";
const TIME_LIMIT_MS = 60000; // 60 seconds
const KEYSTROKE_LIMIT = 100;
type GameScoreContext = { type GameScoreContext = {
gameStatus: GameStatus; gameStatus: GameStatus;
setGameStatus: (status: GameStatus) => void; setGameStatus: (status: GameStatus) => void;
@@ -10,17 +13,35 @@ type GameScoreContext = {
type UseScoreParams = { type UseScoreParams = {
gameContext: GameScoreContext; gameContext: GameScoreContext;
keystrokeCount: number;
}; };
export function useScore({ gameContext }: UseScoreParams) { export function useScore({ gameContext, keystrokeCount }: UseScoreParams) {
const timer = useTimer(); const timer = useTimer();
const { gameStatus } = gameContext; const { gameStatus, setGameStatus } = gameContext;
const { timeValue, startTimer, stopTimer, resetTimer } = timer; const { timeValue, startTimer, stopTimer, resetTimer } = timer;
const [finalScore, setFinalScore] = useState<number | null>(null); const [finalScore, setFinalScore] = useState<number | null>(null);
useScoreTime({ gameStatus, timer }); useScoreTime({ gameStatus, timer });
// Check for game-over conditions
useEffect(() => {
if (gameStatus !== "started") return;
// Time limit exceeded
if (timeValue >= TIME_LIMIT_MS) {
setGameStatus("game-over");
return;
}
// Keystroke limit exceeded
if (keystrokeCount >= KEYSTROKE_LIMIT) {
setGameStatus("game-over");
return;
}
}, [gameStatus, timeValue, keystrokeCount, setGameStatus]);
useEffect(() => { useEffect(() => {
if (gameStatus !== "game-won") return; if (gameStatus !== "game-won") return;
@@ -38,6 +59,8 @@ export function useScore({ gameContext }: UseScoreParams) {
stopTimer, stopTimer,
resetTimer, resetTimer,
finalScore, finalScore,
timeLimit: TIME_LIMIT_MS,
keystrokeLimit: KEYSTROKE_LIMIT,
}; };
} }