Skip to content

Commit

Permalink
fix: refactor to custom hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Shibi Suriya committed Oct 2, 2023
1 parent ed0eb50 commit 8c558ce
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 254 deletions.
243 changes: 27 additions & 216 deletions packages/game-client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,234 +1,45 @@
import React, { useState, useEffect } from 'react';
import { generateKey, getOppositeDirection, generateRandomNumber } from './utils';
import { NUMBER_OF_COLUMNS, NUMBER_OF_ROWS, DIRECTIONS, defaultDirections } from './constants';
import { GRID_MAP, initialSnakesState } from './computed';
import { useDirection, useFood, useTicks } from './hooks';
import { generateRandomNumber } from './utils';
import { DIRECTIONS, defaultDirections } from './constants';
import { initialSnakesState, initialFoodState } from './computed';
import { useDirection, useFood, useTicks, useSnakes, useInput, useSocket } from './hooks';
import Grid from './Grid';
import styles from './app.module.css';

const SPEED = 1 * 100;
const FOOD_SPAWN_INTERVAL = 1 * 1000;

function App() {
// const socket = useRef();
useTicks();
useEffect(() => {
// For now I am going to be using a simple websocket server for
// developer & testing purposes.
// const { hostname, port } = window.location;
// const WS_PORT = 8085;
// socket.current = new WebSocket(`ws://${hostname}:${WS_PORT}`);
// socket.current.addEventListener('open', (event) => {
// console.log('WebSocket connection opened:', event);
// });
// socket.current.addEventListener('message', (event) => {
// const message = event.data;
// console.log('message -> ', message);
// });
// socket.current.addEventListener('close', (event) => {
// console.log('WebSocket connection closed:', event);
// });
// socket.current.addEventListener('error', (event) => {
// console.error('WebSocket error:', event);
// });
// return () => {
// // Disconnect...
// if (socket.current) {
// socket.current.close();
// }
// };
}, []);
const [mounted, setMounted] = useState(true);
return (
<div>
<div>
<button onClick={() => setMounted((prev) => !prev)}>Mount / Unmount</button>
</div>
<div>{mounted && <Game />}</div>
</div>
);
}

function Game() {
const [snakeId, setSnakeId] = useState(1);
const { food, setFood, removeFood } = useFood();
// const playerId = useRef(2);

// Keep the direction of the snakes inside useRef since we don't
// want to force rerender of the component when the user changes
// the direction.
const { getDirection, onLeft, onRight, onUp, onDown, setDirection } = useDirection(defaultDirections, 1);

const { getDirection, setDown, setLeft, setRight, setUp, setDirection } = useDirection(defaultDirections);

// Don't keep direction of the snakes insides of useState()...
const [snakes, setSnakes] = useState(initialSnakesState);

const getSnakeCells = () => {
// return cells that are occupied by snakes.
return Object.values(snakes).reduce((hash, snake) => {
// TODO: check if the data is consistent here.
Object.assign(hash, snake.hash);
return hash;
}, {});
};

const removeSnake = (snakeId, prevSnakes) => {
const snakes = { ...prevSnakes };
delete snakes[snakeId];
return snakes;
};

const spawnFood = () => {
const snakeCells = getSnakeCells();
const emptyCells = {};

for (const [key, value] of Object.entries(GRID_MAP)) {
if (!(key in snakeCells) && !(key in food)) {
Object.assign(emptyCells, { [key]: value });
}
}
const keys = Object.keys(emptyCells);
if (keys.length > 0) {
const randomEmptyCell = emptyCells[keys[generateRandomNumber(keys.length)]];
const { x, y } = randomEmptyCell;
setFood(x, y);
}
};

useEffect(() => {
const timer = setInterval(spawnFood, FOOD_SPAWN_INTERVAL);
return () => {
clearInterval(timer);
};
}, [food]);

const moveSnakeForward = (snakeId) => {
setSnakes((prevSnakes) => {
const resetSnake = (snakeId) => {
debugger;
setDirection(snakeId, defaultDirections[snakeId]); // Set to the default initial direction.
return { ...snakes, [snakeId]: initialSnakesState[snakeId] };
};

if (snakeId in prevSnakes) {
const updatedHash = { ...prevSnakes[snakeId].hash };
const updatedList = [...prevSnakes[snakeId].list];

// Create new head using prev head.
const [headKey] = updatedList;
const head = updatedHash[headKey];

const direction = getDirection(snakeId);

let newHead;
let newHeadKey;
if (direction == DIRECTIONS.RIGHT) {
newHead = { x: head.x, y: head.y + 1 };
newHeadKey = generateKey(newHead.x, newHead.y);
updatedHash[newHeadKey] = newHead;
updatedList.unshift(newHeadKey);
} else if (direction == DIRECTIONS.UP) {
newHead = { x: head.x - 1, y: head.y };
newHeadKey = generateKey(newHead.x, newHead.y);
updatedHash[newHeadKey] = newHead;
updatedList.unshift(newHeadKey);
} else if (direction == DIRECTIONS.DOWN) {
newHead = { x: head.x + 1, y: head.y };
newHeadKey = generateKey(newHead.x, newHead.y);
updatedHash[newHeadKey] = newHead;
updatedList.unshift(newHeadKey);
} else if (direction == DIRECTIONS.LEFT) {
newHead = { x: head.x, y: head.y - 1 };
newHeadKey = generateKey(newHead.x, newHead.y);
updatedHash[newHeadKey] = newHead;
updatedList.unshift(newHeadKey);
}

// Remove tail.
if (newHeadKey in food) {
removeFood(newHead.x, newHead.y);
return { ...prevSnakes, [snakeId]: { ...snakes[snakeId], hash: updatedHash, list: updatedList } };
} else if (newHeadKey in prevSnakes[snakeId].hash) {
// Snake collided with itself.
return resetSnake(snakeId, prevSnakes);
} else if (
newHead.x < NUMBER_OF_ROWS &&
newHead.x >= 0 &&
newHead.y >= 0 &&
newHead.y < NUMBER_OF_COLUMNS
) {
// Snake moved.
const tailKey = updatedList.pop(); // mutates.
delete updatedHash[tailKey];
return { ...prevSnakes, [snakeId]: { ...snakes[snakeId], hash: updatedHash, list: updatedList } };
} else {
// Snake collided with the wall...
return resetSnake(snakeId, prevSnakes);
}
} else {
throw new Error('The id mentioned is not in the hash!');
}
});
};

const up = (snakeId) => {
const direction = getDirection(snakeId);
if (direction == DIRECTIONS.UP) {
// moving up only.
return;
} else if (getOppositeDirection(direction) !== DIRECTIONS.UP) {
setUp(snakeId);
}
};

const down = (snakeId) => {
const direction = getDirection(snakeId);
if (direction == DIRECTIONS.DOWN) {
return;
} else if (getOppositeDirection(direction) !== DIRECTIONS.DOWN) {
setDown(snakeId);
}
};

const right = (snakeId) => {
const direction = getDirection(snakeId);
if (direction == DIRECTIONS.RIGHT) {
return;
} else if (getOppositeDirection(direction) !== DIRECTIONS.RIGHT) {
setRight(snakeId);
}
};

const left = () => {
const direction = getDirection(snakeId);
if (direction == DIRECTIONS.LEFT) {
return;
} else if (getOppositeDirection(direction) !== DIRECTIONS.LEFT) {
setLeft(snakeId);
}
};
useInput({ snakes, onUp, onDown, onLeft, onRight, snakeId });

useEffect(() => {
const timer = setInterval(() => {
Object.keys(snakes).forEach((snakeId) => {
moveSnakeForward(snakeId);
});
}, SPEED);
const abortController = new AbortController();
const { food, removeFood, spawnFood } = useFood({ initialFoodState });

// Can only change the direction, won't make the snake move in
// a particular direction.
document.addEventListener(
'keydown',
(event) => {
const key = event.key.toLowerCase();
if (['w', 'arrowup'].includes(key)) {
up(snakeId);
} else if (['s', 'arrowdown'].includes(key)) {
down(snakeId);
} else if (['a', 'arrowleft'].includes(key)) {
left(snakeId);
} else if (['d', 'arrowright'].includes(key)) {
right(snakeId);
}
},
{ signal: abortController.signal },
);
// Don't keep direction of the snakes inside of useState()...
const { snakes, updateSnake, removeSnake, resetSnake } = useSnakes({
initialSnakesState,
getDirection,
food,
removeFood,
setDirection,
});

return () => {
abortController.abort();
clearInterval(timer);
};
}, [snakes]);
useTicks({ updateSnake, snakes, food, spawnFood });

return (
<div className={styles.game}>
Expand Down
28 changes: 28 additions & 0 deletions packages/game-client/src/Test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useEffect, useState, useRef } from 'react';

function Test() {
// const [count, setCount] = useState(0);
const counterRef = useRef(10);

useEffect(() => {
const timer = setInterval(() => {
console.log(Date.now(), ' Hello from inside of the timer. count => ', counterRef.current);
}, 500);
return () => {
clearInterval(timer);
};
}, []);

const increment = () => {
counterRef.current++;
};

return (
<div>
<h1>count: {counterRef.current}</h1>
<button onClick={increment}>++</button>
</div>
);
}

export default Test;
4 changes: 3 additions & 1 deletion packages/game-client/src/computed.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,6 @@ const initialSnakesState = {
// },
};

export { GRID_MAP, initialSnakesState };
const initialFoodState = {};

export { GRID_MAP, initialSnakesState, initialFoodState };
25 changes: 21 additions & 4 deletions packages/game-client/src/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// in px (pixels)
const GRID_WIDTH = 30 * 30;
const GRID_HEIGHT = 30 * 30;
const GRID_HEIGHT = 30 * 10;
const CELL_DIMENSION = 30;

if (GRID_HEIGHT % CELL_DIMENSION !== 0) {
Expand All @@ -27,19 +27,34 @@ const FOOD_TYPES = {
PROTEIN: 'protein',
};

const TICK_TYPES = {
SNAKES: 'snakes',
FOOD: 'food',
};

const TICKS = {
1: 1 * 1000,
0.5: 0.5 * 1000,
0.25: 0.25 * 1000,
[TICK_TYPES.FOOD]: {
0.1: 1000 * 0.1,
},
[TICK_TYPES.SNAKES]: {
// 1: 1 * 1000,
// 0.5: 0.5 * 1000,
// 0.25: 0.25 * 1000,
0.1: 1000 * 0.1,
},
};

const SPEED = 1 * 100;

const defaultDirections = {
1: DIRECTIONS.DOWN,
2: DIRECTIONS.RIGHT,
3: DIRECTIONS.RIGHT,
4: DIRECTIONS.RIGHT,
};

const FOOD_SPAWN_INTERVAL = 1 * 1000;

export {
defaultDirections,
GRID_HEIGHT,
Expand All @@ -51,4 +66,6 @@ export {
DEFAULT_DIRECTION,
FOOD_TYPES,
TICKS,
TICK_TYPES,
SPEED,
};
5 changes: 4 additions & 1 deletion packages/game-client/src/grid.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
position: absolute;
}

:global(*) {
outline: 1px solid black !important;
}

.food {
box-sizing: border-box;
padding: 1px;
border: 1px solid black;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/game-client/src/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export { useFood } from './useFood';
export { useDirection } from './useDirection';
export { useSnakes } from './useSnakes';
export { useInput } from './useInput';
export { useSocket } from './useSocket';
export { useTicks } from './useTicks';
Loading

0 comments on commit 8c558ce

Please sign in to comment.