starting new structure

This commit is contained in:
2025-10-19 17:59:12 +01:00
parent 4c6bd0d4d4
commit d0a51d7079
50 changed files with 2219 additions and 432 deletions
+3 -58
View File
@@ -1,70 +1,15 @@
import { useEffect } from "react";
import { useGame } from "../../src/useGame/useGame";
import { useGame } from "../../src/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')}`;
}
import "../../src/game.css";
export default function App() {
const mounted = useMounted();
const {
containerRef,
initGame,
solveBoard,
scoreManager,
boardManager,
keyLog,
gameStatus,
} = useGame(useKeyBindings);
const { timeValue, finalScore } = scoreManager;
const stringed = boardManager.boardToString(true);
useEffect(() => {
console.log(stringed);
}, [stringed]);
const { containerRef } = useGame({}, useKeyBindings);
return (
<div style={{ padding: 16 }}>
<h1>VIMazing Sudoku</h1>
{/* 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={solveBoard}>Solve</button>
</div>
</div>
);
-122
View File
@@ -1,122 +0,0 @@
import type { GameStatus, UseScoreType } from "../../src/types";
import { useIsMobile } from "./useIsMobile";
import { MobileGameOverlay } from "./MobileGameOverlay";
type GameOverlayProps = {
gameStatus: GameStatus;
scoreManager: UseScoreType;
};
export const GameOverlay = ({ gameStatus, scoreManager }: GameOverlayProps) => {
const isMobile = useIsMobile();
if (isMobile) return <MobileGameOverlay gameStatus={gameStatus} scoreManager={scoreManager} />
if (gameStatus === "started") return null;
return (
<GameOverlayContainer>
{gameStatus === "waiting" && <GameWaitingContent />}
{gameStatus === "game-over" && <GameOverContent />}
{gameStatus === "game-won" && <GameWonContent scoreManager={scoreManager} />}
</GameOverlayContainer>
);
};
export const GameWaitingContent = () => {
return (
<div
className="text-[#c8d3f5] font-mono text-center select-none"
style={{
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
fontSize: "calc(2rem * var(--sudoku-size-multiplier))"
}}
>
<div className="flex flex-col" style={{ gap: "calc(2.5rem * var(--sudoku-size-multiplier))" }}>
<p style={{ fontSize: "calc(3rem * var(--sudoku-size-multiplier))" }}>Press <strong className="text-[#82aaff]">New Puzzle</strong> to start</p>
<div style={{ fontSize: "calc(1.2rem * var(--sudoku-size-multiplier))" }}>
<p style={{ marginBottom: "calc(1rem * var(--sudoku-size-multiplier))" }}><strong className="text-[#ffc777]">Navigation Mode</strong> (default):</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>
<p style={{ marginTop: "calc(0.5rem * var(--sudoku-size-multiplier))" }}><strong className="text-[#82aaff]">R</strong> or <strong className="text-[#82aaff]">C</strong> to enter edit mode</p>
<p style={{ marginTop: "calc(1.5rem * var(--sudoku-size-multiplier))", marginBottom: "calc(1rem * var(--sudoku-size-multiplier))" }}><strong className="text-[#ffc777]">Edit Mode</strong>:</p>
<p>Type <strong className="text-[#82aaff]">1-9</strong> to fill cell</p>
<p style={{ marginTop: "calc(0.5rem * var(--sudoku-size-multiplier))" }}><strong className="text-[#82aaff]">Esc</strong> or <strong className="text-[#82aaff]">Enter</strong> to exit edit mode</p>
<p className="text-[#828bb8]" style={{ marginTop: "calc(1.5rem * var(--sudoku-size-multiplier))", fontSize: "calc(0.8rem * var(--sudoku-size-multiplier))" }}>
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>
);
};
export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => {
const finalScore = scoreManager.finalScore ?? 0;
return (
<div
className="text-[#c8d3f5] grid font-mono uppercase text-center select-none"
style={{
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
fontSize: "calc(2rem * var(--sudoku-size-multiplier))",
gap: "calc(1rem * var(--sudoku-size-multiplier))"
}}
>
<p className="text-[#c3e88d]">🎉 Puzzle Solved!</p>
<p>Final Score:</p>
<p className="text-[#82aaff]" style={{ fontSize: "calc(2.5rem * var(--sudoku-size-multiplier))" }}>{finalScore.toLocaleString()}</p>
<p className="text-[#828bb8] normal-case" style={{ fontSize: "calc(0.65rem * var(--sudoku-size-multiplier))" }}>
<span className="text-[#c099ff]">max</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-[#c8d3f5]"> - </span>
<span className="text-[#82aaff]">timeInSeconds</span>
<span className="text-[#c8d3f5]"> * </span>
<span className="text-[#ff966c]">10</span>
<span className="text-[#c8d3f5]">)</span>
</p>
<p className="text-[#828bb8]">Click <strong className="text-[#ffc777] normal-case">New Puzzle</strong> to play again</p>
</div>
);
};
export const GameOverContent = () => {
return (
<div
className="text-[#ff757f] font-mono uppercase text-center select-none"
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
>
<p style={{ fontSize: "calc(5rem * var(--sudoku-size-multiplier))" }}>Game Over</p>
<p className="text-[#828bb8]" style={{ fontSize: "calc(2rem * var(--sudoku-size-multiplier))" }}>Press <strong className="text-[#ffc777] normal-case">i</strong> to play again</p>
</div>
);
};
export const GameOverlayContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
return (
<div
className="
absolute inset-0
flex justify-center items-center
z-40
pointer-events-none
"
style={{
backgroundColor: 'rgba(27, 29, 43, 0.75)',
backdropFilter: 'blur(1px)'
}}
>
{children}
</div>
);
};
-106
View File
@@ -1,106 +0,0 @@
import type { GameStatus, UseScoreType } from "../../src/types";
type MobileGameOverlayProps = {
gameStatus: GameStatus;
scoreManager: UseScoreType;
};
export const MobileGameOverlay = ({ gameStatus, scoreManager }: MobileGameOverlayProps) => {
if (gameStatus === "started") return null;
return (
<GameOverlayContainer>
{gameStatus === "waiting" && <GameWaitingContent />}
{gameStatus === "game-over" && <GameOverContent />}
{gameStatus === "game-won" && <GameWonContent scoreManager={scoreManager} />}
</GameOverlayContainer>
);
};
export const GameWaitingContent = () => {
return (
<div
className="text-[#c8d3f5] font-mono text-center select-none"
style={{
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
fontSize: "calc(1rem * var(--sudoku-size-multiplier))"
}}
>
<div className="flex flex-col" style={{ gap: "calc(0.75rem * var(--sudoku-size-multiplier))" }}>
<p style={{ fontSize: "calc(1.5rem * var(--sudoku-size-multiplier))" }}>Tap New Puzzle to start</p>
<p>VIM motions available <br /> via on-screen keyboard</p>
</div>
</div>
);
};
export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => {
const finalScore = scoreManager.finalScore ?? 0;
return (
<div
className="text-[#c8d3f5] grid font-mono uppercase text-center select-none"
style={{
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
fontSize: "calc(1rem * var(--sudoku-size-multiplier))",
gap: "calc(1rem * var(--sudoku-size-multiplier))"
}}
>
<p className="text-[#c3e88d]">🎉 Solved!</p>
<p>Final Score:</p>
<p className="text-[#82aaff]" style={{ fontSize: "calc(2.5rem * var(--sudoku-size-multiplier))" }}>{finalScore.toLocaleString()}</p>
<p className="text-[#828bb8] normal-case" style={{ fontSize: "calc(0.65rem * var(--sudoku-size-multiplier))" }}>
<span className="text-[#c099ff]">max</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-[#c8d3f5]"> - </span>
<span className="text-[#82aaff]">time</span>
<span className="text-[#c8d3f5]"> * </span>
<span className="text-[#ff966c]">10</span>
<span className="text-[#c8d3f5]">)</span>
</p>
<p className="text-[#828bb8]">Tap to play again</p>
</div>
);
};
export const GameOverContent = () => {
return (
<div
className="text-[#ff757f] font-mono uppercase text-center select-none"
style={{
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
fontSize: "calc(5rem * var(--sudoku-size-multiplier))"
}}
>
Game Over
</div>
);
};
export const GameOverlayContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
return (
<div
className="
absolute inset-0
flex justify-center items-center
z-40
pointer-events-none
"
style={{
backgroundColor: 'rgba(27, 29, 43, 0.75)',
backdropFilter: 'blur(1px)'
}}
>
{children}
</div>
);
};
+13 -22
View File
@@ -1,22 +1,21 @@
import { useEffect } from "react";
import type { UseGameType } from "../../src/";
import type { GameManager } from "../../src";
export const useKeyBindings = (gameManager: UseGameType) => {
const { gameStatus, initGame, stopGame, clearLog, resetCount } = gameManager;
export const useKeyBindings = (gameManager: GameManager) => {
const { gameStatus, renderBoard, startGame, quitGame, clearKeyLog } = gameManager;
useEffect(() => {
const handler = (e: KeyboardEvent) => {
console.log(e.key);
const handler = (ev: KeyboardEvent) => {
// start game
if (e.code === "Space") {
if (ev.code === "Space") {
if (["waiting", "game-over", "game-won"].includes(gameStatus)) {
clearLog();
resetCount();
clearKeyLog();
renderBoard();
if (gameStatus === "game-over" || gameStatus === "game-won") {
stopGame();
setTimeout(() => initGame(), 0);
quitGame();
setTimeout(() => startGame(), 0);
} else {
initGame();
startGame();
}
return;
}
@@ -25,23 +24,15 @@ export const useKeyBindings = (gameManager: UseGameType) => {
// ignore other keys while waiting
if (gameStatus === "waiting") return;
// cancel count or stop game
if (e.key === "Escape") {
stopGame();
resetCount();
return;
}
// quit
if (e.key === "q" || e.key === "Q") {
stopGame();
resetCount();
if (ev.key === "q" || ev.key === "Q") {
quitGame();
return;
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [gameStatus, initGame, stopGame, clearLog, resetCount]);
}, [gameStatus]);
};
+2 -2
View File
@@ -31,8 +31,8 @@
/* === Path aliases (optional) === */
"baseUrl": ".",
"paths": {
"@vimazing/vim-maze": ["../dist/index.d.ts"],
"@vimazing/vim-maze/*": ["../dist/*"]
"@vimazing/vim-sudoku": ["../src/index.ts"],
"@vimazing/vim-sudoku/*": ["../src/*"]
}
},
"include": ["src", "vite.config.ts", "global.d.ts"],
+1 -1
View File
@@ -11,7 +11,7 @@ export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@vimazing/vim-maze": libSource, // use source during dev
"@vimazing/vim-sudoku": libSource, // use source during dev
},
},
server: {