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
Show file tree
Hide file tree
Changes from all 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
224 changes: 165 additions & 59 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
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,
};

const CellType = {
Snake: "snake",
Food: "food",
Empty: "empty",
Poison: "poison",
};

const Direction = {
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -53,117 +64,212 @@ const Cell = ({ x, y, type }) => {
height: Config.cellSize,
}}
>
<div className={styles.cell} style={getStyles()}></div>
<div className={styles.cell} style={getStyles()}>
{remaining}
</div>
</div>
);
};

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) => {

Choose a reason for hiding this comment

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

doesn't it look cleaner ?

  const isObjectOfType = useCallback(
    ({ x, y }, type) => {
      const _obj = isObject({ x, y });
      return _obj && _obj.type === type;
    },
    [isObject]
  );

Copy link
Author

Choose a reason for hiding this comment

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

I'm using the same function for snake, food, and poison to find. So I think this is better -

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
    );
  };

  //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]
  );

//for snake
if (type === undefined)
return arr.find((position) => position.x === x && position.y === y);
return arr.find(
Copy link
Author

Choose a reason for hiding this comment

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

@royantar0311 @MahdiMurshed Check this commit. Is it good? Do I need to modify anything?

(position) =>
position.x === x && position.y === y && position.type === type
);
};

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.

// 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);
},

Choose a reason for hiding this comment

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

If we just want to know whether the cell contains an object or not, do we need to know the type too?

Copy link
Author

Choose a reason for hiding this comment

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

My 'objects' array contains both poison and food. So, I need to find out whether an object is a food or poison when the snake eats it. I need to update the length and the score according to the type of the object. I've been using the function in this purpose, so I need it.

Choose a reason for hiding this comment

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

I don't think you are taking full advantage of objects state

Choose a reason for hiding this comment

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

isObject can be more simpler

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

[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
)
);

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.

}, []);

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;
}
};
window.addEventListener("keydown", handleNavigation);

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 =
Copy link
Author

Choose a reason for hiding this comment

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

@royantar0311 I forgot to add the remaining time to the food, so I tried it out today. But the time shows undefined. I can't figure out the problem :")

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(<Cell key={`${x}-${y}`} x={x} y={y} type={type} />);
cells.push(
<Cell key={`${x}-${y}`} x={x} y={y} type={type} remaining={remaining} />
);
}
}
return { score, isObjectOrSnake, cells };
};

//view
const Snake = () => {
const { score, cells } = GetSnake();
return (
<div className={styles.container}>
<div
Expand Down
6 changes: 5 additions & 1 deletion styles/Snake.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@
.cell {
width: 100%;
height: 100%;
}
display: flex;
justify-content: center;
align-items: center;
color: white;
}