diff --git a/example/src/App.tsx b/example/src/App.tsx
index 4d25c6a..d5f92e1 100755
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -3,12 +3,17 @@ import "@vimazing/vim-snake/game.css";
import { useKeyBindings } from "./useKeyBindings";
function App() {
- const { containerRef } = useGame(30, 20, useKeyBindings);
+ const { containerRef, gameStatus } = useGame(30, 20, useKeyBindings);
return (
VIMazing Snake
+
+ {gameStatus === "waiting" &&
Press space to start
}
+ {gameStatus === "started" &&
Use hjkl to move • Press q to quit
}
+ {gameStatus === "game-over" &&
Game Over! Press space to restart
}
+
);
}
diff --git a/example/src/useKeyBindings.ts b/example/src/useKeyBindings.ts
index 91e6895..60bbc65 100755
--- a/example/src/useKeyBindings.ts
+++ b/example/src/useKeyBindings.ts
@@ -1,21 +1,22 @@
import { useEffect } from "react";
-import type { UseGameType } from "@vimazing/vim-maze";
+import type { UseGameType } from "@vimazing/vim-snake";
export const useKeyBindings = (gameManager: UseGameType) => {
- const { gameStatus, initGame, stopGame, clearLog, resetCount } = gameManager;
+ const { gameStatus, startGame, stopGame, clearLog, resetCount } = gameManager;
useEffect(() => {
const handler = (e: KeyboardEvent) => {
+ console.log("key pressed:", e.key, e.code);
// start game
- if (e.key === "i" || e.key === "I") {
+ if (e.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(() => initGame(true), 0);
} else {
- initGame(true);
+ startGame();
}
return;
}
@@ -41,6 +42,6 @@ export const useKeyBindings = (gameManager: UseGameType) => {
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
- }, [gameStatus, initGame, stopGame, clearLog, resetCount]);
+ }, [gameStatus, startGame, stopGame, clearLog, resetCount]);
};
diff --git a/src/game.css b/src/game.css
index 5ebff21..78d6c4d 100755
--- a/src/game.css
+++ b/src/game.css
@@ -29,5 +29,21 @@
background-color: var(--muted, #2f334d);
border: 1px solid var(--border, #3b3f5c);
box-sizing: border-box;
+ transition: background-color 0.1s ease;
+}
+
+/* =========================================================
+ Snake Rendering
+ ========================================================= */
+.snake-cell.snake-head {
+ background-color: var(--primary, #82aaff);
+ border-radius: 30%;
+ box-shadow: 0 0 8px color-mix(in srgb, var(--primary, #82aaff) 70%, transparent);
+}
+
+.snake-cell.snake-body {
+ background-color: var(--primary, #82aaff);
+ opacity: 0.7;
+ box-shadow: inset 0 0 3px color-mix(in srgb, var(--primary, #82aaff) 50%, transparent);
}
diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts
index e0ad4c7..56fba49 100755
--- a/src/useGame/useGame.ts
+++ b/src/useGame/useGame.ts
@@ -1,17 +1,39 @@
import { useEffect } from "react";
import { useRenderer } from "./useRenderer";
+import { useSnake } from "./useSnake";
+import { useGameStatus } from "./useGameStatus";
+import { useKeyBindings, type UseKeyBindingsType } from "./useKeyBindings";
export function useGame(cols: number, rows: number, platformHook?: unknown) {
const rendererManager = useRenderer();
const { containerRef, renderBoard } = rendererManager;
+ const snakeManager = useSnake(cols, rows, rendererManager);
+ const { changeDirection } = snakeManager;
+
+ const gameManager = useGameStatus(rendererManager, snakeManager);
+ const { gameStatus, startGame, stopGame } = gameManager;
+
useEffect(() => {
renderBoard(cols, rows);
}, [cols, rows]);
+ const keyBindings: UseKeyBindingsType = useKeyBindings({
+ gameManager: {
+ gameStatus,
+ startGame,
+ stopGame,
+ changeDirection,
+ },
+ });
+
const fullGameManager = {
containerRef,
- renderBoard,
+ gameStatus,
+ startGame,
+ stopGame,
+ ...snakeManager,
+ ...keyBindings,
};
if (typeof platformHook === "function") {
diff --git a/src/useGame/useGameStatus.ts b/src/useGame/useGameStatus.ts
index 76e0b6b..1398730 100755
--- a/src/useGame/useGameStatus.ts
+++ b/src/useGame/useGameStatus.ts
@@ -1 +1,37 @@
+import { useState } from "react";
+import type { GameStatus } from "../types";
+import type { UseRendererType } from "./useRenderer";
+import type { UseSnakeType } from "./useSnake";
+
+export function useGameStatus(
+ rendererManager: UseRendererType,
+ snakeManager: UseSnakeType
+) {
+ const { containerRef } = rendererManager;
+ const { initSnake, clearSnake } = snakeManager;
+ const [gameStatus, setGameStatus] = useState("waiting");
+
+ const startGame = () => {
+ initSnake();
+ setGameStatus("started");
+ };
+
+ const stopGame = () => {
+ clearSnake();
+ setGameStatus("waiting");
+ const container = containerRef.current;
+ container?.querySelectorAll(".snake-head, .snake-body").forEach((el) => {
+ el.classList.remove("snake-head", "snake-body");
+ });
+ };
+
+ return {
+ gameStatus,
+ setGameStatus,
+ startGame,
+ stopGame,
+ };
+}
+
+export type UseGameStatusType = ReturnType;
export type { GameStatus } from "../types";
diff --git a/src/useGame/useKeyBindings.ts b/src/useGame/useKeyBindings.ts
index a92bf7d..5f75d14 100755
--- a/src/useGame/useKeyBindings.ts
+++ b/src/useGame/useKeyBindings.ts
@@ -1,3 +1,49 @@
-export function useKeyBindings() {
+import { useEffect } from "react";
+import type { GameStatus, Direction } from "../types";
+
+export type GameBindingContext = {
+ gameStatus: GameStatus;
+ startGame: () => void;
+ stopGame: () => void;
+ changeDirection: (dir: Direction) => void;
+};
+
+type UseKeyBindingsParams = {
+ gameManager: GameBindingContext;
+};
+
+export function useKeyBindings({ gameManager }: UseKeyBindingsParams) {
+ const { gameStatus, startGame, stopGame, changeDirection } = gameManager;
+
+ useEffect(() => {
+ const handler = (ev: KeyboardEvent) => {
+ if (!["started"].includes(gameStatus)) return;
+
+ switch (ev.key) {
+ case "h":
+ case "H":
+ changeDirection("left");
+ break;
+ case "j":
+ case "J":
+ changeDirection("down");
+ break;
+ case "k":
+ case "K":
+ changeDirection("up");
+ break;
+ case "l":
+ case "L":
+ changeDirection("right");
+ break;
+ }
+ };
+
+ window.addEventListener("keydown", handler);
+ return () => window.removeEventListener("keydown", handler);
+ }, [gameStatus, startGame, stopGame, changeDirection]);
+
return {};
}
+
+export type UseKeyBindingsType = ReturnType;
diff --git a/src/useGame/useSnake.ts b/src/useGame/useSnake.ts
index 246cb4a..d4d5fae 100755
--- a/src/useGame/useSnake.ts
+++ b/src/useGame/useSnake.ts
@@ -1,3 +1,80 @@
-export function useSnake() {
- return {};
+import { useRef, useState } from "react";
+import type { Direction, SnakeBody } from "../types";
+import type { UseRendererType } from "./useRenderer";
+
+export function useSnake(
+ cols: number,
+ rows: number,
+ rendererManager: UseRendererType
+) {
+ const { containerRef } = rendererManager;
+ const [snakeBody, setSnakeBody] = useState([]);
+ const [direction, setDirection] = useState("up");
+ const directionRef = useRef("up");
+
+ const initSnake = () => {
+ const centerR = Math.floor(rows / 2);
+ const centerC = Math.floor(cols / 2);
+
+ const initialBody: SnakeBody = [
+ { r: centerR, c: centerC },
+ { r: centerR + 1, c: centerC },
+ { r: centerR + 2, c: centerC },
+ ];
+
+ setSnakeBody(initialBody);
+ setDirection("up");
+ directionRef.current = "up";
+ renderSnake(initialBody);
+ };
+
+ const clearSnake = () => {
+ setSnakeBody([]);
+ setDirection("up");
+ directionRef.current = "up";
+ };
+
+ const renderSnake = (body: SnakeBody) => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ container.querySelectorAll(".snake-head, .snake-body").forEach((el) => {
+ el.classList.remove("snake-head", "snake-body");
+ });
+
+ body.forEach((segment, idx) => {
+ const cell = container.querySelector(
+ `.snake-cell[data-r="${segment.r}"][data-c="${segment.c}"]`
+ );
+ if (cell) {
+ cell.classList.add(idx === 0 ? "snake-head" : "snake-body");
+ }
+ });
+ };
+
+ const changeDirection = (newDirection: Direction) => {
+ const opposites: Record = {
+ up: "down",
+ down: "up",
+ left: "right",
+ right: "left",
+ };
+
+ if (opposites[directionRef.current] !== newDirection) {
+ setDirection(newDirection);
+ directionRef.current = newDirection;
+ }
+ };
+
+ return {
+ snakeBody,
+ direction,
+ directionRef,
+ initSnake,
+ clearSnake,
+ changeDirection,
+ renderSnake,
+ };
}
+
+export type UseSnakeType = ReturnType;