You can play the game here
To move you can use following keys
up
: up arrow key orw
down
: down arrow key ors
left
: left arrow key ord
right
: right arrow key ora
- 15x15 grid
- Snake should be controlled with cursor keys (or WASD if you prefer)
- Snake should start with a length of 3
- One apple at a time should appear in a random position on the grid. When collected, it should increase the score by one, increase the snake length by one, and change to another random position
- Display a score for how many apples have been collected
- If the snake head collides with the rest of the body, the game should end
- If the snake head collides with the borders, the game should end
React
All the game logic is encapsulated in useSnakeGame hook.
I utilize the power of useReducer
hook to manage the state of the game.
There can be two type of action
for user
- Start Game
- Move i.e. move the snake with controls
a
,s
,d
,w
or witharrow
keys
There are three possible state
for the game
when user presses space
we start game by dispatching starGame
event
dispatch({
type: "startGame",
});
I capture the space event via useSpaceKeydown
hook.
There is no restart
action as I utilize the same action for restarting the game as well. I only dispatch this event if game is not inprogress
state i.e. either idle
or end
state. If space is pressed during a ongoing game, it will be ignored.
useSpaceKeydown(() => {
if (state.status === "inprogress") {
return;
}
dispatch({
type: "startGame",
});
});
When user press control keys there can be two types move
- If snake is moving in
right
direction and user pressesa
or< left arrow key
key - If snake is moving in
left
direction and user pressesd
or> right arrow key
key - If snake is moving in
up
direction and user pressess
orv down arrow key
key - If snake is moving in
down
direction and user pressesw
or^ up arrow key
key
If any move is illegal then we just return current state of the game to ignore that move. We check this with isValidDirection
defined in gameUtils
const isAllowedDirection = isValidDirection(currentDirection, state.direction);
All the moves which are not illegal is valid moves. For all such move following can happen
Snake can move to a empty space If snake move to empty space we keep the score same and generate the new state of the game and return
Snake can eat the fruit If snake has eaten the fruit then we can increase the score and increase the snake length and calculate the new state and return
Snake can collide with wall If this happens then the game should end. We can end the game and return the state.
Snake can collide with itself If the snake is collided with itself that also a game end scenario. We can end the game and return the state.
All the components are in scr/components folder
There are two components
- Grid
- Grid cell
- Render grid
- Render snake and fruit
- Show score and highest score
- Show information to start the game
- Show Information at the end of the game
const GridProps = {
grid,
status,
score,
};
- grid: Which is a
2d
array ofrows
andcol
- status: Game status
- score: Score of current game
It is the lower level component of game. It maps to each cell in the grid. Depending upon the position of snake and cell. Grid can be in three state
const GridCellProps = { content };
content Content can be one of the followings
- Empty
- Snake
- Snake head - Just to show different color for head
- Fruit
I set the background color of the grid based on these three contents via css variables
/**
* There are css variable in index.css for
* matching content refer index.css
* --snake-head: #f27195;
* --snake: #f19fb6;
* --fruit: #006d77;
* --empty: #fff;
*/
let style = {
backgroundColor: `var(--${content})`,
};
All the hooks are in src/hooks folder.
There are following hooks
useEventListener
This is a lower level hook to capture any kind of event
import { useEffect, useRef } from "react";
export const useEventListener = (
eventName,
handler,
element = window,
options = {
once: false,
}
) => {
const savedEventHandler = useRef();
const { once } = options;
useEffect(() => {
savedEventHandler.current = handler;
}, [handler]);
useEffect(() => {
const listener = (event) => savedEventHandler.current(event);
const opt = { once };
element.addEventListener(eventName, listener, opt);
return () => {
element.removeEventListener(eventName, listener);
};
}, [eventName, element, once]);
};
useIntervalHook This is used for moving the snake at specified interval.
import { useEffect, useRef } from "react";
export const useInterval = (cb, interval) => {
const savedCallback = useRef(cb);
const intervalIdRef = useRef(null);
useEffect(() => {
savedCallback.current = cb;
}, [cb]);
useEffect(() => {
const tick = () => savedCallback.current();
if (typeof interval === "number") {
intervalIdRef.current = setInterval(tick, interval);
return () => window.clearInterval(intervalIdRef.current);
}
}, [interval]);
return intervalIdRef;
};
useSpaceKeyDown
This is a hook which uses useEventListener
to capture space
event so that we can start
and restart
the game.
import { useCallback } from "react";
import { useEventListener } from "./useEventListener";
export const useSpaceKeydown = (cb) => {
const handleSpaceKey = useCallback(
(e) => {
e.preventDefault();
if (e.code.toLowerCase() !== "space") {
return;
}
cb();
},
[cb]
);
return useEventListener("keydown", handleSpaceKey);
};
useDirectionControl
This is the hook which again uses useEventListener
to capture the control key presses.
import React, { useEffect } from "react";
import { useEventListener } from "./useEventListener";
export const useGameDirectionControl = (init) => {
const directionRef = React.useRef(init);
useEffect(() => {
directionRef.current = init;
}, [init]);
const handleKeyEvents = React.useCallback((e) => {
e.preventDefault();
switch (e.code) {
case "ArrowUp":
case "KeyW": {
directionRef.current = "up";
break;
}
case "ArrowDown":
case "KeyS": {
directionRef.current = "down";
break;
}
case "ArrowRight":
case "KeyD": {
directionRef.current = "right";
break;
}
case "ArrowLeft":
case "KeyA": {
directionRef.current = "left";
break;
}
}
}, []);
useEventListener("keydown", handleKeyEvents);
return directionRef;
};
useLocalStorage
This is used for setting and reading the values from local storage. This is used for highest score
import React from "react";
export const useLocalStorage = (key, defaultValue) => {
const [storedValued, setStoredValue] = React.useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error);
return defaultValue;
}
});
React.useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(storedValued));
}, [key, storedValued]);
return [storedValued, setStoredValue];
};