proper keybindings

This commit is contained in:
2025-10-16 13:21:22 +01:00
parent 6a3cfad409
commit 20ac6b9349
6 changed files with 127 additions and 39 deletions
+20 -2
View File
@@ -3,15 +3,33 @@ import "@vimazing/vim-snake/game.css";
import { useKeyBindings } from "./useKeyBindings";
function App() {
const { containerRef, gameStatus } = useGame(30, 20, useKeyBindings);
const { containerRef, gameStatus, scoreManager, keyLog } = useGame(30, 20, useKeyBindings);
const formatTime = (ms: number) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
};
return (
<div className="relative mx-auto my-4 w-fit space-y-4">
<h1 className="text-2xl font-bold text-center">VIMazing Snake</h1>
<div className="flex gap-4 justify-center text-sm font-mono">
<div className="px-3 py-1 bg-muted rounded">
Time: {formatTime(scoreManager.timeValue)}
</div>
<div className="px-3 py-1 bg-muted rounded">
Keys: {keyLog.length}
</div>
</div>
<div ref={containerRef} className="relative" />
<div className="text-center text-sm text-muted-foreground">
{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 move • 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>}
</div>
</div>
+14 -22
View File
@@ -5,16 +5,16 @@ export const useKeyBindings = (gameManager: UseGameType) => {
const { gameStatus, startGame, stopGame, clearLog, resetCount } = gameManager;
useEffect(() => {
const handler = (e: KeyboardEvent) => {
console.log("key pressed:", e.key, e.code);
const handler = (ev: KeyboardEvent) => {
// start game
if (e.code === "Space") {
if (ev.code === "Space") {
if (["waiting", "game-over", "game-won"].includes(gameStatus)) {
// clearLog();
// resetCount();
clearLog();
resetCount();
if (gameStatus === "game-over" || gameStatus === "game-won") {
stopGame();
// setTimeout(() => initGame(true), 0);
setTimeout(() => startGame(), 0);
} else {
startGame();
}
@@ -22,26 +22,18 @@ 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();
return;
if (ev.key === "q" || ev.key === "Q") {
if (gameStatus === "started") {
stopGame();
resetCount();
return;
}
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
window.addEventListener("keydown", handler, { capture: true });
return () => window.removeEventListener("keydown", handler, { capture: true });
}, [gameStatus, startGame, stopGame, clearLog, resetCount]);
};
+4 -2
View File
@@ -3,6 +3,7 @@ import { useRenderer } from "./useRenderer";
import { useSnake } from "./useSnake";
import { useGameStatus } from "./useGameStatus";
import { useKeyBindings, type UseKeyBindingsType } from "./useKeyBindings";
import { useScore } from "../useScore";
export function useGame(cols: number, rows: number, platformHook?: unknown) {
const rendererManager = useRenderer();
@@ -21,12 +22,12 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) {
const keyBindings: UseKeyBindingsType = useKeyBindings({
gameManager: {
gameStatus,
startGame,
stopGame,
changeDirection,
},
});
const scoreManager = useScore({ gameStatus });
const fullGameManager = {
containerRef,
gameStatus,
@@ -34,6 +35,7 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) {
stopGame,
...snakeManager,
...keyBindings,
scoreManager,
};
if (typeof platformHook === "function") {
+42 -6
View File
@@ -1,10 +1,10 @@
import { useEffect } from "react";
import { useEffect, useRef, useState } from "react";
import type { GameStatus, Direction } from "../types";
export type KeyLogEntry = { key: string; timestamp: number };
export type GameBindingContext = {
gameStatus: GameStatus;
startGame: () => void;
stopGame: () => void;
changeDirection: (dir: Direction) => void;
};
@@ -13,37 +13,73 @@ type UseKeyBindingsParams = {
};
export function useKeyBindings({ gameManager }: UseKeyBindingsParams) {
const { gameStatus, startGame, stopGame, changeDirection } = gameManager;
const { gameStatus, changeDirection } = gameManager;
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
const logRef = useRef<KeyLogEntry[]>([]);
const countRef = useRef<string>("");
const recordKey = (key: string) => {
const entry = { key, timestamp: performance.now() };
logRef.current.push(entry);
setKeyLog([...logRef.current]);
};
const clearLog = () => {
logRef.current = [];
setKeyLog([]);
};
const resetCount = () => {
countRef.current = "";
};
useEffect(() => {
const handler = (ev: KeyboardEvent) => {
if (!["started"].includes(gameStatus)) return;
recordKey(ev.key);
let handled = false;
switch (ev.key) {
case "h":
case "H":
changeDirection("left");
handled = true;
break;
case "j":
case "J":
changeDirection("down");
handled = true;
break;
case "k":
case "K":
changeDirection("up");
handled = true;
break;
case "l":
case "L":
changeDirection("right");
handled = true;
break;
}
if (handled) {
ev.stopPropagation();
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [gameStatus, startGame, stopGame, changeDirection]);
}, [gameStatus, changeDirection]);
return {};
return {
keyLog,
clearLog,
resetCount,
getLog: () => logRef.current,
};
}
export type UseKeyBindingsType = ReturnType<typeof useKeyBindings>;
+18 -5
View File
@@ -1,8 +1,21 @@
export function useScore() {
import { useTimer } from "./hooks/useTimer";
import { useScoreTime } from "./useScoreTime";
import type { GameStatus } from "../types";
export type UseScoreParams = {
gameStatus: GameStatus;
};
export function useScore({ gameStatus }: UseScoreParams) {
const timer = useTimer();
const { timeValue, startTimer, stopTimer, resetTimer } = timer;
useScoreTime({ gameStatus, timer });
return {
timeValue: 0,
startTimer: () => {},
stopTimer: () => {},
resetTimer: () => {},
timeValue,
startTimer,
stopTimer,
resetTimer,
};
}
+29 -2
View File
@@ -1,3 +1,30 @@
export function useScoreTime() {
return {};
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]);
}