diff --git a/pages/index.js b/pages/index.js index de83024..2b62d72 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,10 +1,10 @@ import dynamic from "next/dynamic"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState, useRef, useCallback } from "react"; import styles from "../styles/Snake.module.css"; const Config = { - height: 25, - width: 25, + height: 15, + width: 15, cellSize: 32, }; @@ -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,10 +34,20 @@ 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: "red", + borderRadius: 20, + width: 32, + height: 32, + transform: `scale(${0.5 + remaining / 20})`, }; default: @@ -53,89 +64,172 @@ const Cell = ({ x, y, type }) => { height: Config.cellSize, }} > -
+
+ {remaining} +
); }; -const getRandomCell = () => ({ +const getRandomCell = (type) => ({ x: Math.floor(Math.random() * Config.width), y: Math.floor(Math.random() * Config.width), + start: Date.now(), + type: type, }); -const Snake = () => { +//custom hook +//controller +const useInterval = (func, dir) => { + const timer = useRef(Date.now()); + const createCallback = useCallback(() => { + if (Date.now() - timer.current > dir) { + timer.current = Date.now(); + func(); + } + }, [dir, func]); + + useEffect(() => { + const interval = setInterval(createCallback, 1000 / 60); + return () => clearInterval(interval); + }, [createCallback]); +}; +const GetSnake = () => { 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 [objects, setObjects] = useState([]); + const score = snake.length - 3; - const [food, setFood] = useState({ x: 4, y: 10 }); - const [score, setScore] = useState(0); + const finding = ({ x, y }, arr, type) => { + //for snake + if (type === undefined) + return arr.find((position) => position.x === x && position.y === y); + return arr.find( + (position) => + position.x === x && position.y === y && position.type === type + ); + }; - // move the snake - useEffect(() => { - const runSingleStep = () => { - setSnake((snake) => { - const head = snake[0]; - const newHead = { x: head.x + direction.x, y: head.y + direction.y }; + //checking cells + const isObjectOrSnake = useCallback( + ({ x, y }, type) => { + if (type === CellType.Snake) return finding({ x, y }, snake); + else return finding({ x, y }, objects, type); + }, + [objects, snake] + ); - // make a new snake by extending head - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax - const newSnake = [newHead, ...snake]; + //restart the game + const resetGame = useCallback(() => { + setSnake(getDefaultSnake()); + setDirection(Direction.Right); + setObjects([]); + }, []); - // remove tail - newSnake.pop(); + //moving the snake + const runSingleStep = useCallback(() => { + setSnake((snake) => { + const head = snake[0]; + const newHead = { + x: (head.x + direction.x + Config.width) % Config.width, + y: (head.y + direction.y + Config.height) % Config.height, + }; - return newSnake; - }); - }; + // make a new snake by extending head + const newSnake = [newHead, ...snake]; - runSingleStep(); - const timer = setInterval(runSingleStep, 500); + // remove tail when head doesnt eat food + if (!isObjectOrSnake(newHead, CellType.Food)) { + newSnake.pop(); + } + //remove again when eats poison + if (isObjectOrSnake(newHead, CellType.Poison)) { + newSnake.pop(); + } + if (isObjectOrSnake(newHead, CellType.Snake) || score < 0) { + resetGame(); + } - return () => clearInterval(timer); - }, [direction, food]); + return newSnake; + }); + }, [direction.x, direction.y, isObjectOrSnake, resetGame, score]); - // update score whenever head touches a food - useEffect(() => { - const head = snake[0]; - if (isFood(head)) { - setScore((score) => { - return score + 1; - }); - - let newFood = getRandomCell(); - while (isSnake(newFood)) { - newFood = getRandomCell(); + const addObject = useCallback( + (type) => { + let newObject = getRandomCell(type); + while ( + isObjectOrSnake( + newObject, + CellType.Snake || + isObjectOrSnake(newObject, CellType.Food) || + isObjectOrSnake(isObjectOrSnake, CellType.Poison) + ) + ) { + newObject = getRandomCell(type); } + console.log(newObject.type); + setObjects((currentObjects) => [...currentObjects, newObject]); + }, + [isObjectOrSnake] + ); + const removeObject = useCallback(() => { + setObjects((currentObjects) => + currentObjects.filter( + (currentObject) => Date.now() - currentObject.start < 10000 + ) + ); + }, []); - setFood(newFood); + // update foods and poisons whenever head touches a food or a poison + useEffect(() => { + const head = snake[0]; + if (isObjectOrSnake(head, CellType.Food)) { + console.log("ate food"); + setObjects((currentObjects) => + currentObjects.filter( + (currentObject) => + !(currentObject.x === head.x && currentObject.y === head.y) + ) + ); } - }, [snake]); + }, [isObjectOrSnake, snake]); + + useInterval(() => addObject(CellType.Food), 3000); + useInterval(() => addObject(CellType.Poison), 1000); + useInterval(runSingleStep, 300); + useInterval(() => removeObject(), 50); + useInterval(() => removeObject(), 100); + + const changeDir = (checkDir, newDir) => { + setDirection((direction) => { + if (direction != checkDir) return newDir; + return direction; + }); + }; useEffect(() => { const handleNavigation = (event) => { switch (event.key) { case "ArrowUp": - setDirection(Direction.Top); + changeDir(Direction.Bottom, Direction.Top); break; case "ArrowDown": - setDirection(Direction.Bottom); + changeDir(Direction.Top, Direction.Bottom); break; case "ArrowLeft": - setDirection(Direction.Left); + changeDir(Direction.Right, Direction.Left); break; case "ArrowRight": - setDirection(Direction.Right); + changeDir(Direction.Left, Direction.Right); break; } }; @@ -143,27 +237,39 @@ 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; - if (isFood({ x, y })) { + let type = CellType.Empty, + remaining = undefined; + if (isObjectOrSnake({ x, y }, CellType.Food)) { type = CellType.Food; - } else if (isSnake({ x, y })) { + remaining = + 10 - + Math.round( + (Date.now() - finding({ x, y }, objects, type).start) / 1000 + ); + } else if (isObjectOrSnake({ x, y }, CellType.Snake)) { type = CellType.Snake; + } else if (isObjectOrSnake({ x, y }, CellType.Poison)) { + type = CellType.Poison; + remaining = + 10 - + Math.round( + (Date.now() - finding({ x, y }, objects, type).start) / 1000 + ); } - cells.push(); + cells.push( + + ); } } + return { score, isObjectOrSnake, cells }; +}; +//view +const Snake = () => { + const { score, cells } = GetSnake(); return (