Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module #2 (Nowshin Owishi) #29

Open
wants to merge 11 commits into
base: module-2
Choose a base branch
from
Open
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 149 additions & 44 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -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,
};

Expand Down Expand Up @@ -39,6 +39,14 @@ const Cell = ({ x, y, type }) => {
height: 32,
};

case CellType.Poison:
return {
backgroundColor: "red",
borderRadius: 20,
width: 32,
height: 32,
};

default:
return {};
}
Expand All @@ -61,9 +69,26 @@ const Cell = ({ x, y, type }) => {
const getRandomCell = () => ({
x: Math.floor(Math.random() * Config.width),
y: Math.floor(Math.random() * Config.width),
start: Date.now(),
});

const Snake = () => {
//custom hook
//controller
const UseInterval = (func, dir) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

custom hooks name should start with use with a lowercase u and a react components name should start with a capital letter.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

custom hooks name should start with use with a lowercase u and a react components name should start with a capital letter.

@owishiboo

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 UseSnake = () => {
Copy link

@MahdiMurshed MahdiMurshed Apr 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix naming convention

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UseSnake is not a hook here. I'm not sure, should I remove the term 'Use'?

const getDefaultSnake = () => [
{ x: 8, y: 12 },
{ x: 7, y: 12 },
Expand All @@ -74,68 +99,147 @@ const Snake = () => {
// snake[0] is head and snake[snake.length - 1] is tail
const [snake, setSnake] = useState(getDefaultSnake());
const [direction, setDirection] = useState(Direction.Right);
const [foods, setFoods] = useState([]);
const [poisons, setPoison] = useState([]);
const score = snake.length - 3;

const [food, setFood] = useState({ x: 4, y: 10 });
const [score, setScore] = useState(0);
// ?. is called optional chaining
const isFood = useCallback(
({ x, y }) =>
foods.find((position) => position.x === x && position.y === y),
[foods]
);

// 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 isPoison = useCallback(
({ x, y }) =>
poisons.find((position) => position.x === x && position.y === y),
[poisons]
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as snake state is different from objects state,it makes sense to have a different function for checking snake.And it also makes the code more readable

  const isSnake = useCallback(
    ({ x, y }) =>
      snake.find((position) => position.x === x && position.y === y),
    [snake]
  );

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I give an upvote to this. @owishiboo your function can work with all three types but loses simplicity. poison and food are similiar and we put them to one object state, but snake is different so we can keep it seperate fully.
well there is no right or wrong here, it comes down to your opinion.

const isSnake = useCallback(
({ x, y }) =>
snake.find((position) => position.x === x && position.y === y),
[snake]
);

//restart the game
const resetGame = useCallback(() => {
setSnake(getDefaultSnake());
setDirection(Direction.Right);
setFoods([]);
setPoison([]);
}, []);

// make a new snake by extending head
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
const newSnake = [newHead, ...snake];
//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,
};

// remove tail
// make a new snake by extending head
const newSnake = [newHead, ...snake];

// remove tail when head doesnt eat food
if (!isFood(newHead)) {
newSnake.pop();
}
//remove again when eats poison
if (isPoison(newHead)) {
newSnake.pop();
}
if (isSnake(newHead) || score < 0) {
Copy link

@royantar0311 royantar0311 Mar 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how weird would that be to play with a snake of length < 3 ? haha
also one more thing, you are popping the snake here but does the score gets updated here? no, you will get the updated value of score only on the next rerender. so you cannot rely on the score here to get the snake length, use newSnake.length here to avoid potential bugs.

resetGame();
}

return newSnake;
});
};
return newSnake;
});
}, [direction.x, direction.y, isFood, isPoison, isSnake, resetGame, score]);

//add new food
const addFood = () => {
let newFood = getRandomCell();
while (isSnake(newFood) || isFood(newFood) || isPoison(newFood)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an object can be put to a cell only if it isn't occupied yet.
why not create a reusable function like

const isOccupied =  (cell) => isSnake(cell) || isFood(cell) || isPoison(cell);

newFood = getRandomCell();
}
setFoods((currentFoods) => [...currentFoods, newFood]);
};

runSingleStep();
const timer = setInterval(runSingleStep, 500);
//add poison
const addPoison = () => {
let newPoison = getRandomCell();
while (isSnake(newPoison) || isFood(newPoison) || isPoison(newPoison)) {
newPoison = getRandomCell();
}
setPoison((currentPoison) => [...currentPoison, newPoison]);
};

return () => clearInterval(timer);
}, [direction, food]);
//remove food
const removeFood = useCallback(() => {
// console.log("delete food auto");
setFoods((currentFoods) =>
currentFoods.filter((food) => (Date.now() - food.start) < 10000)
);
}, []);
//remove poison
const removePoison = useCallback(() => {
// console.log("delete poison auto");
setPoison((currentPoison) =>
currentPoison.filter((poison) => (Date.now() - poison.start) < 10000)
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addFoods and addPoison, removeFood and removePoison works the same, you can merge them to have one addObject function, and one removeObject function.

}, []);

// update score whenever head touches a food
// update foods and poisons whenever head touches a food or a poison
useEffect(() => {
const head = snake[0];
if (isFood(head)) {
setScore((score) => {
return score + 1;
});
console.log("ate food");
setFoods((currentFoods) =>
currentFoods.filter((food) => food.x !== head.x && food.y !== head.y)
);
}
if (isPoison(head)) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Corei13
Thanks for the previous help.
Whenever the snake eats one poison, other poisons of the same row OR same column get deleted. But I'm checking both row AND column every time. The same happens with the food also. How to fix it? Everything else is working fine.

console.log("ate poison");
setPoison((currentPoison) =>
currentPoison.filter(
(poison) => poison.x !== head.x && poison.y !== head.y
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@owishiboo poison.x !== head.x && poison.y !== head.y is equivalent to !(poison.x === head.x || poison.y === head.y)

See the issue? It will remove a poison even when it matches either x or y.

)
);
}
}, [isFood, isPoison, snake]);

let newFood = getRandomCell();
while (isSnake(newFood)) {
newFood = getRandomCell();
}
UseInterval(addFood, 3000);
UseInterval(runSingleStep, 300);
UseInterval(removeFood, 50);
UseInterval(addPoison, 4000);
UseInterval(removePoison, 100);

setFood(newFood);
}
}, [snake]);
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;
}
};
Expand All @@ -144,13 +248,12 @@ 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);
return { score, isFood, isSnake, isPoison };
};

//view
const Snake = () => {
const { score, isFood, isSnake, isPoison } = UseSnake();
const cells = [];
for (let x = 0; x < Config.width; x++) {
for (let y = 0; y < Config.height; y++) {
Expand All @@ -159,6 +262,8 @@ const Snake = () => {
type = CellType.Food;
} else if (isSnake({ x, y })) {
type = CellType.Snake;
} else if (isPoison({ x, y })) {
type = CellType.Poison;
}
cells.push(<Cell key={`${x}-${y}`} x={x} y={y} type={type} />);
}
Expand Down