diff --git a/.eslintrc.json b/.eslintrc.json
index 9671fde..4f75410 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -3,5 +3,8 @@
"env": {
"es6": true,
"browser": true
+ },
+ "rules":{
+ "react-hooks/exhaustive-deps": "error"
}
}
diff --git a/pages/index.js b/pages/index.js
index de83024..8b0668e 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -1,5 +1,5 @@
import dynamic from "next/dynamic";
-import { useEffect, useState, useRef } from "react";
+import { useEffect, useState, useCallback, useRef } from "react";
import styles from "../styles/Snake.module.css";
const Config = {
@@ -12,6 +12,7 @@ const CellType = {
Snake: "snake",
Food: "food",
Empty: "empty",
+ Poison: "poison",
};
const Direction = {
@@ -21,7 +22,7 @@ const Direction = {
Bottom: { x: 0, y: 1 },
};
-const Cell = ({ x, y, type }) => {
+const Cell = ({ x, y, type, remaining }) => {
const getStyles = () => {
switch (type) {
case CellType.Snake:
@@ -33,18 +34,29 @@ const Cell = ({ x, y, type }) => {
case CellType.Food:
return {
- backgroundColor: "darkorange",
+ backgroundColor: "tomato",
borderRadius: 20,
width: 32,
height: 32,
+ transform: `scale(${0.5 + remaining / 20})`,
+ };
+ case CellType.Poison:
+ return {
+ backgroundColor: "black",
+ borderRadius: 20,
+ width: 32,
+ height: 32,
+ transform: `scale(${0.5 + remaining / 20})`,
};
default:
return {};
}
};
+
return (
{
height: Config.cellSize,
}}
>
-
+
+ {remaining}
+
);
};
@@ -61,81 +75,182 @@ const Cell = ({ x, y, type }) => {
const getRandomCell = () => ({
x: Math.floor(Math.random() * Config.width),
y: Math.floor(Math.random() * Config.width),
+ createdAt: Date.now(),
});
-const Snake = () => {
+const getInitialDirection = () => Direction.Right;
+
+const useInterval = (callback, duration) => {
+ const time = useRef(0);
+
+ const wrappedCallback = useCallback(() => {
+ // don't call callback() more than once within `duration`
+ if (Date.now() - time.current >= duration) {
+ time.current = Date.now();
+ callback();
+ }
+ }, [callback, duration]);
+
+ useEffect(() => {
+ const interval = setInterval(wrappedCallback, 1000 / 60);
+ return () => clearInterval(interval);
+ }, [wrappedCallback, duration]);
+};
+
+const useSnake = () => {
const getDefaultSnake = () => [
{ x: 8, y: 12 },
{ x: 7, y: 12 },
{ x: 6, y: 12 },
];
- const grid = useRef();
+
// snake[0] is head and snake[snake.length - 1] is tail
const [snake, setSnake] = useState(getDefaultSnake());
- const [direction, setDirection] = useState(Direction.Right);
+ const [direction, setDirection] = useState(getInitialDirection());
- const [food, setFood] = useState({ x: 4, y: 10 });
- const [score, setScore] = useState(0);
+ const [foods, setFoods] = useState([]);
+ const [poison, setPoison] = useState({});
- // move the snake
- useEffect(() => {
- const runSingleStep = () => {
- setSnake((snake) => {
- const head = snake[0];
- const newHead = { x: head.x + direction.x, y: head.y + direction.y };
+ const score = snake.length - 3;
- // make a new snake by extending head
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
- const newSnake = [newHead, ...snake];
+ // useCallback() prevents instantiation of a function on each rerender
+ // based on the dependency array
- // remove tail
- newSnake.pop();
+
+
+
+ // resets the snake ,foods, direction to initial values
+ const resetGame = useCallback(() => {
+
+ setFoods([getEmptyCell()]);
+ setDirection(getInitialDirection());
+ setPoison({});
- return newSnake;
- });
- };
+ }, [getEmptyCell]);
- runSingleStep();
- const timer = setInterval(runSingleStep, 500);
+ const removeFoods = useCallback(() => {
+ // only keep those foods which were created within last 10s.
+ setFoods((currentFoods) =>
+ currentFoods.filter((food) => Date.now() - food.createdAt <= 10 * 1000)
+ );
+ }, []);
- return () => clearInterval(timer);
- }, [direction, food]);
+ // ?. is called optional chaining
+ // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
+ const isFood = useCallback(
+ ({ x, y }) => foods.some((food) => food.x === x && food.y === y),
+ [foods]
+ );
- // update score whenever head touches a food
- useEffect(() => {
- const head = snake[0];
- if (isFood(head)) {
- setScore((score) => {
- return score + 1;
- });
+ const isSnake = useCallback(
+ ({ x, y }) =>
+ snake.find((position) => position.x === x && position.y === y),
+ [snake]
+ );
- let newFood = getRandomCell();
- while (isSnake(newFood)) {
- newFood = getRandomCell();
- }
+ const isPoison = useCallback( ({x,y}) => { return poison.x === x && poison.y === y}, [poison])
+ const isOccupied = useCallback((cell) => isFood(cell) || isSnake(cell) || isPoison(cell), [isFood, isSnake, isPoison]);
- setFood(newFood);
+ const getEmptyCell = useCallback(() =>{
+ let newCell = getRandomCell();
+ while (isOccupied(newCell)) {
+ newCell = getRandomCell();
}
- }, [snake]);
+ return newCell;
+ },[isOccupied]);
+
+ const addFood = useCallback(() => {
+ setFoods((currentFoods) => [...currentFoods, getEmptyCell()]);
+ }, [getEmptyCell]);
+
+ const addPoison = useCallback(() => {
+ setPoison(getEmptyCell());
+ },[getEmptyCell])
+
+ const addObject = (typeOfObject = "food")=>{
+ if(typeOfObject === "food"){
+ addFood()
+ }
+ else if(typeOfObject === "poison"){
+ addPoison()
+ }
+ }
+
+
+ // move the snake
+ const runSingleStep = useCallback(() => {
+ setSnake((snake) => {
+ const head = snake[0];
+
+ // 0 <= a % b < b
+ // so new x will always be inside the grid
+ const newHead = {
+ x: (head.x + direction.x + Config.height) % Config.height,
+ y: (head.y + direction.y + Config.width) % Config.width,
+ };
+
+ // make a new snake by extending head
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
+ const newSnake = [newHead, ...snake];
+
+ // reset the game if the snake hit itself
+ if (isSnake(newHead)) {
+ resetGame();
+ return getDefaultSnake();
+ }
+
+ if (isPoison(newHead)) {
+ resetGame();
+ return getDefaultSnake();
+ }
+
+ // remove tail from the increased size snake
+ // only if the newHead isn't a food
+ if (!isFood(newHead)) {
+ newSnake.pop();
+ } else {
+ setFoods((currentFoods) =>
+ currentFoods.filter(
+ (food) => !(food.x === newHead.x && food.y === newHead.y)
+ )
+ );
+ }
+
+ return newSnake;
+ });
+ }, [direction, isFood, isSnake, isPoison, resetGame]);
+
+ useInterval(runSingleStep, 200);
+ useInterval(()=>addObject("food"), 3000);
+ useInterval(()=>addObject("poison"), 5000);
+ useInterval(removeFoods, 100);
useEffect(() => {
+ const handleDirection = (direction, oppositeDirection) => {
+ setDirection((currentDirection) => {
+ if (currentDirection === oppositeDirection) {
+ return currentDirection;
+ } else return direction;
+ });
+ };
+
const handleNavigation = (event) => {
switch (event.key) {
case "ArrowUp":
- setDirection(Direction.Top);
+ handleDirection(Direction.Top, Direction.Bottom);
break;
case "ArrowDown":
- setDirection(Direction.Bottom);
+ handleDirection(Direction.Bottom, Direction.Top);
break;
case "ArrowLeft":
- setDirection(Direction.Left);
+ handleDirection(Direction.Left, Direction.Right);
break;
case "ArrowRight":
- setDirection(Direction.Right);
+ handleDirection(Direction.Right, Direction.Left);
break;
}
};
@@ -144,26 +259,48 @@ const Snake = () => {
return () => window.removeEventListener("keydown", handleNavigation);
}, []);
- // ?. is called optional chaining
- // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
- const isFood = ({ x, y }) => food?.x === x && food?.y === y;
-
- const isSnake = ({ x, y }) =>
- snake.find((position) => position.x === x && position.y === y);
-
const cells = [];
for (let x = 0; x < Config.width; x++) {
for (let y = 0; y < Config.height; y++) {
- let type = CellType.Empty;
+ let type = CellType.Empty,
+ remaining = undefined;
if (isFood({ x, y })) {
type = CellType.Food;
+ remaining =
+ 10 -
+ Math.round(
+ (Date.now() -
+ foods.find((food) => food.x === x && food.y === y).createdAt) /
+ 1000
+ );
} else if (isSnake({ x, y })) {
type = CellType.Snake;
}
- cells.push( | );
+ else if (isPoison({x,y})) {
+ type = CellType.Poison;
+ remaining =
+ 5 -
+ Math.round(
+ (Date.now() -
+ poison.createdAt) /
+ 1000
+ );
+ }
+ cells.push(
+ |
+ );
}
}
+ return {
+ snake,
+ cells,
+ score,
+ };
+};
+
+const Snake = () => {
+ const { cells, score } = useSnake();
return (