From 2dd317e0dcf914679864c21e93c9e1a9c75e2db2 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Wed, 15 Oct 2025 18:28:55 +0100 Subject: [PATCH] working on useScore --- example/src/App.tsx | 51 +++++++++++++++++++++++++++++-- example/src/GameOverlay.tsx | 46 +++++++++++++++------------- example/src/MobileGameOverlay.tsx | 29 ++++++++---------- src/sudoku.css | 8 +++++ src/useGame/useGame.ts | 14 ++++++--- src/useScore/useScore.ts | 27 ++++++++++++++-- 6 files changed, 128 insertions(+), 47 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 735b57d..068cce8 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,15 +1,60 @@ import { useGame } from "../../src/useGame/useGame"; import { useKeyBindings } from './useKeyBindings'; +import { useMounted } from './useMounted'; +import { GameOverlay } from './GameOverlay'; 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() { - const { containerRef, initGame, solveBoard } = useGame(useKeyBindings); + const mounted = useMounted(); + const { + containerRef, + initGame, + solveBoard, + scoreManager, + keyLog, + gameStatus, + } = useGame(useKeyBindings); + + const { timeValue, finalScore } = scoreManager; return (

VIMazing Sudoku

-
-
+ + {/* Scoreboard */} +
+
+ Time: {formatTime(timeValue)} +
+
+ Keystrokes: {keyLog.length} +
+ {gameStatus === "game-won" && finalScore !== null && ( +
+ Score: {finalScore.toLocaleString()} +
+ )} +
+ +
+
+ {mounted && } +
+ +
diff --git a/example/src/GameOverlay.tsx b/example/src/GameOverlay.tsx index d9d8c59..f03c57f 100644 --- a/example/src/GameOverlay.tsx +++ b/example/src/GameOverlay.tsx @@ -1,5 +1,4 @@ -import type { GameStatus } from "@vimazing/vim-maze"; -import type { UseScoreType } from "@vimazing/vim-maze"; +import type { GameStatus, UseScoreType } from "../../src/types"; import { useIsMobile } from "./useIsMobile"; import { MobileGameOverlay } from "./MobileGameOverlay"; @@ -12,9 +11,7 @@ export const GameOverlay = ({ gameStatus, scoreManager }: GameOverlayProps) => { const isMobile = useIsMobile(); if (isMobile) return - // 🧠 Only show overlay when the game has ended - if (['started', 'hasKey'].includes(gameStatus)) return null; - // console.log('gameStatus:', gameStatus) + if (gameStatus === "started") return null; return ( @@ -32,13 +29,20 @@ export const GameWaitingContent = () => { style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }} >
-

Press i to play...

-

h,j,k,l to move around

-

try other vim motions
you're familiar with

-

- $,^,0,G,gg only valid if
- the edges are reachable -

+

Press New Puzzle to start

+
+

Navigation Mode (default):

+

h,j,k,l to move around

+

R or C to enter edit mode

+ +

Edit Mode:

+

Type 1-9 to fill cell

+

Esc or Enter to exit edit mode

+ +

+ Try vim motions: $, ^, 0, counted moves +

+
); @@ -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" style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }} > -

Congratulations!

+

🎉 Puzzle Solved!

Final Score:

{finalScore.toLocaleString()}

- min + max ( + 0 + , 100000 - , ( - optimalTime - / - actualTime - ) * - 100000 + - + timeInSeconds + * + 10 )

-

Press i to play again

+

Click New Puzzle to play again

); }; diff --git a/example/src/MobileGameOverlay.tsx b/example/src/MobileGameOverlay.tsx index f804e3f..8d03524 100644 --- a/example/src/MobileGameOverlay.tsx +++ b/example/src/MobileGameOverlay.tsx @@ -1,5 +1,4 @@ -import type { GameStatus } from "@vimazing/vim-maze"; -import type { UseScoreType } from "@vimazing/vim-maze"; +import type { GameStatus, UseScoreType } from "../../src/types"; type MobileGameOverlayProps = { gameStatus: GameStatus; @@ -7,9 +6,7 @@ type MobileGameOverlayProps = { }; export const MobileGameOverlay = ({ gameStatus, scoreManager }: MobileGameOverlayProps) => { - // 🧠 Only show overlay when the game has ended - if (['started', 'hasKey'].includes(gameStatus)) return null; - // console.log('gameStatus:', gameStatus) + if (gameStatus === "started") return null; return ( @@ -27,8 +24,8 @@ export const GameWaitingContent = () => { style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }} >
-

Press here to play

-

all the vim motions
available on desktop

+

Tap New Puzzle to start

+

VIM motions available
via on-screen keyboard

); @@ -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" style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }} > -

Congratulations!

+

🎉 Solved!

Final Score:

{finalScore.toLocaleString()}

- min + max ( + 0 + , 100000 - , ( - optimalTime - / - actualTime - ) * - 100000 + - + time + * + 10 )

-

Press to play again

+

Tap to play again

); }; diff --git a/src/sudoku.css b/src/sudoku.css index c7eb2c1..11131c0 100644 --- a/src/sudoku.css +++ b/src/sudoku.css @@ -8,6 +8,14 @@ --box-separator: 3px; } + +:root { + --cell-size: 96px; + --cell-font-size: 48px; + --grid-gap: 6px; + --box-separator: 9px; +} + /* ========================================================= Container & Grid Base ========================================================= */ diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts index e962539..914be9e 100644 --- a/src/useGame/useGame.ts +++ b/src/useGame/useGame.ts @@ -16,10 +16,6 @@ export function useGame(platformHook?: unknown) { const boardManager = useBoardState(cursorManager); const { board, initialBoard, solution, setDigit, erase, reset, solveBoard } = boardManager; - const scoreManager = useScore({ - gameContext: { gameStatus, setGameStatus }, - }); - // --------------------------- // Keybindings // --------------------------- @@ -33,7 +29,15 @@ export function useGame(platformHook?: unknown) { initialBoard, }); - const { editMode } = keyBindings; + const { editMode, keyLog } = keyBindings; + + // --------------------------- + // Scoring (needs keyLog from keyBindings) + // --------------------------- + const scoreManager = useScore({ + gameContext: { gameStatus, setGameStatus }, + keystrokeCount: keyLog.length, + }); // --------------------------- // Rendering diff --git a/src/useScore/useScore.ts b/src/useScore/useScore.ts index f1917e8..2365d40 100644 --- a/src/useScore/useScore.ts +++ b/src/useScore/useScore.ts @@ -3,6 +3,9 @@ import { useTimer } from "./hooks/useTimer"; import { useScoreTime } from "./useScoreTime"; import type { GameStatus } from "../types"; +const TIME_LIMIT_MS = 60000; // 60 seconds +const KEYSTROKE_LIMIT = 100; + type GameScoreContext = { gameStatus: GameStatus; setGameStatus: (status: GameStatus) => void; @@ -10,17 +13,35 @@ type GameScoreContext = { type UseScoreParams = { gameContext: GameScoreContext; + keystrokeCount: number; }; -export function useScore({ gameContext }: UseScoreParams) { +export function useScore({ gameContext, keystrokeCount }: UseScoreParams) { const timer = useTimer(); - const { gameStatus } = gameContext; + const { gameStatus, setGameStatus } = gameContext; const { timeValue, startTimer, stopTimer, resetTimer } = timer; const [finalScore, setFinalScore] = useState(null); 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(() => { if (gameStatus !== "game-won") return; @@ -38,6 +59,8 @@ export function useScore({ gameContext }: UseScoreParams) { stopTimer, resetTimer, finalScore, + timeLimit: TIME_LIMIT_MS, + keystrokeLimit: KEYSTROKE_LIMIT, }; }