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 (