diff --git a/packages/game-client/package.json b/packages/game-client/package.json
index 9270d66..6533211 100644
--- a/packages/game-client/package.json
+++ b/packages/game-client/package.json
@@ -24,6 +24,7 @@
"webpack-dev-server": "^4.15.1"
},
"dependencies": {
+ "antd": "^5.11.0",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0"
diff --git a/packages/game-client/src/App.jsx b/packages/game-client/src/App.jsx
index d982121..de0c0f8 100644
--- a/packages/game-client/src/App.jsx
+++ b/packages/game-client/src/App.jsx
@@ -1,74 +1,54 @@
-import React, { useState } from 'react';
-import { 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';
-
-// function App() {
-// const [mounted, setMounted] = useState(true);
-// return (
-//
-//
-//
-//
-//
{mounted && }
-//
-// );
-// }
+import React, { Fragment, useState } from 'react';
+import Game from './Game';
+import { Button } from 'antd';
+import { Divider, Space, Checkbox, Switch } from 'antd';
+import { stringToBoolean } from './utils';
function App() {
- const [snakeId, setSnakeId] = useState(1);
-
- // 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);
-
- useInput({ snakes, onUp, onDown, onLeft, onRight, snakeId });
-
- const getSnakeCells = () => allSnakeCells();
-
- const { food, getFood, removeFood, spawnFood, isFood, setFood } = useFood({ initialFoodState, getSnakeCells });
-
- const getTracks = () => {
- return { addSnakeToTrack, removeSnakeFromTracks, resetSnakeTrack };
- };
-
- // Don't keep direction of the snakes inside of useState()...
- const {
- snakes,
- moveForward,
- getSnakeCells: allSnakeCells,
- getAllSnakeIds,
- } = useSnakes({
- initialSnakesState,
- getDirection,
- getFood,
- removeFood,
- setDirection,
- isFood,
- setFood,
- getTracks,
- });
-
- const { addSnakeToTrack, removeSnakeFromTracks, resetSnakeTrack } = useTicks({
- moveForward,
- spawnFood,
- getAllSnakeIds,
- });
+ const [recordInLocalStorage, setRecordInLocalStorage] = useState(
+ stringToBoolean(localStorage.getItem('recordInLocalStorage')) ?? false,
+ );
+ const [isGamePaused, setIsGamePaused] = useState(stringToBoolean(localStorage.getItem('isGamePaused') ?? true));
+ const [showCellId, setShowCellId] = useState(stringToBoolean(localStorage.getItem('showCellId')) ?? false);
return (
-
- {/* */}
-
-
+
+
+ {
+ const val = e.target.checked;
+ localStorage.setItem('isGamePaused', val);
+ setIsGamePaused(val);
+ }}
+ >
+ Is Game paused?
+
+ {
+ const val = Boolean(e.target.checked);
+ localStorage.setItem('recordInLocalStorage', val);
+ setRecordInLocalStorage(val);
+ }}
+ >
+ Record game in localStorage?
+
+ {
+ const val = Boolean(e.target.checked);
+ localStorage.setItem('showCellId', val);
+ setShowCellId(val);
+ }}
+ >
+ Show cell ID?
+
+
+
+
+
+
);
}
diff --git a/packages/game-client/src/Game.jsx b/packages/game-client/src/Game.jsx
new file mode 100644
index 0000000..b902b92
--- /dev/null
+++ b/packages/game-client/src/Game.jsx
@@ -0,0 +1,77 @@
+import React, { Fragment, useState } from 'react';
+import { 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';
+
+// function App() {
+// const [mounted, setMounted] = useState(true);
+// return (
+//
+//
+//
+//
+//
{mounted && }
+//
+// );
+// }
+
+function Game(props) {
+ const { showCellId, isGamePaused } = props;
+ const [snakeId, setSnakeId] = useState(1);
+
+ // 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);
+
+ useInput({ snakes, onUp, onDown, onLeft, onRight, snakeId });
+
+ const getSnakeCells = () => allSnakeCells();
+
+ const { food, getFood, removeFood, spawnFood, isFood, setFood } = useFood({ initialFoodState, getSnakeCells });
+
+ const getTracks = () => {
+ return { addSnakeToTrack, removeSnakeFromTracks, resetSnakeTrack };
+ };
+
+ // Don't keep direction of the snakes inside of useState()...
+ const {
+ snakes,
+ moveForward,
+ getSnakeCells: allSnakeCells,
+ getAllSnakeIds,
+ } = useSnakes({
+ initialSnakesState,
+ getDirection,
+ getFood,
+ removeFood,
+ setDirection,
+ isFood,
+ setFood,
+ getTracks,
+ });
+
+ const { addSnakeToTrack, removeSnakeFromTracks, resetSnakeTrack } = useTicks({
+ moveForward,
+ spawnFood,
+ getAllSnakeIds,
+ isGamePaused,
+ });
+
+ return (
+
+ {/* */}
+
+
+ );
+}
+
+export default Game;
diff --git a/packages/game-client/src/Grid.jsx b/packages/game-client/src/Grid.jsx
index 54493fd..751a1a7 100644
--- a/packages/game-client/src/Grid.jsx
+++ b/packages/game-client/src/Grid.jsx
@@ -1,11 +1,12 @@
import React from 'react';
import styles from './grid.module.css';
-import { CELL_DIMENSION, GRID_HEIGHT, GRID_WIDTH } from './constants';
+import { CELL_DIMENSION, GRID_HEIGHT, GRID_WIDTH, FOOD_TYPES } from './constants';
+import animation from './animations.module.css';
-function Cell({ x, y, color }) {
+function Cell({ x, y, color, showCellId, animationClass }) {
return (
- );
-}
-
-function Food({ x, y, type }) {
- return (
-
+ >
+ {showCellId && `${x}-${y}`}
+
);
}
-function Grid({ snakes, food }) {
+function Grid({ snakes, food, showCellId }) {
return (
{Object.values(snakes).map((snake) => {
@@ -39,16 +28,24 @@ function Grid({ snakes, food }) {
const [headKey] = snake.list;
return Object.entries(hash).map(([key, value]) => {
const { x, y } = value;
- if (key == headKey) {
- return | ;
- } else {
- return | ;
- }
+ return (
+ |
+ );
});
})}
{Object.entries(food).map(([key, value]) => {
- const { x, y, type } = value;
- return ;
+ const { x, y, type: foodType } = value;
+ const { color, animationClass } = FOOD_TYPES[foodType];
+ return (
+ |
+ );
})}
);
diff --git a/packages/game-client/src/animations.module.css b/packages/game-client/src/animations.module.css
new file mode 100644
index 0000000..a859b13
--- /dev/null
+++ b/packages/game-client/src/animations.module.css
@@ -0,0 +1,121 @@
+.frog {
+ /* Enter */
+ -webkit-animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+ animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+
+ /* Pulsate */
+ -webkit-animation: pulsate-fwd 0.5s ease-in-out infinite both;
+ animation: pulsate-fwd 0.5s ease-in-out infinite both;
+}
+
+.red-bull {
+ -webkit-animation: vibrate 0.3s linear infinite both;
+ animation: vibrate 0.3s linear infinite both;
+}
+
+@-webkit-keyframes scale-in-center {
+ 0% {
+ -webkit-transform: scale(0);
+ transform: scale(0);
+ opacity: 1;
+ }
+ 100% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+@keyframes scale-in-center {
+ 0% {
+ -webkit-transform: scale(0);
+ transform: scale(0);
+ opacity: 1;
+ }
+ 100% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+@-webkit-keyframes pulsate-fwd {
+ 0% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+ 50% {
+ -webkit-transform: scale(1.1);
+ transform: scale(1.1);
+ }
+ 100% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+}
+@keyframes pulsate-fwd {
+ 0% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+ 50% {
+ -webkit-transform: scale(1.1);
+ transform: scale(1.1);
+ }
+ 100% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+}
+
+@-webkit-keyframes vibrate {
+ 0% {
+ -webkit-transform: translate(0);
+ transform: translate(0);
+ }
+ 20% {
+ -webkit-transform: translate(-1px, 1px);
+ transform: translate(-1px, 1px);
+ }
+ 40% {
+ -webkit-transform: translate(-1px, -1px);
+ transform: translate(-1px, -1px);
+ }
+ 60% {
+ -webkit-transform: translate(1px, 1px);
+ transform: translate(1px, 1px);
+ }
+ 80% {
+ -webkit-transform: translate(1px, -1px);
+ transform: translate(1px, -1px);
+ }
+ 100% {
+ -webkit-transform: translate(0);
+ transform: translate(0);
+ }
+}
+@keyframes vibrate {
+ 0% {
+ -webkit-transform: translate(0);
+ transform: translate(0);
+ }
+ 20% {
+ -webkit-transform: translate(-2px, 2px);
+ transform: translate(-2px, 2px);
+ }
+ 40% {
+ -webkit-transform: translate(-2px, -2px);
+ transform: translate(-2px, -2px);
+ }
+ 60% {
+ -webkit-transform: translate(2px, 2px);
+ transform: translate(2px, 2px);
+ }
+ 80% {
+ -webkit-transform: translate(2px, -2px);
+ transform: translate(2px, -2px);
+ }
+ 100% {
+ -webkit-transform: translate(0);
+ transform: translate(0);
+ }
+}
diff --git a/packages/game-client/src/app.module.css b/packages/game-client/src/app.module.css
index f5ab743..a23a64a 100644
--- a/packages/game-client/src/app.module.css
+++ b/packages/game-client/src/app.module.css
@@ -4,7 +4,3 @@
justify-content: center;
height: 100vh;
}
-
-:global(body) {
- margin: 0px;
-}
diff --git a/packages/game-client/src/computed.js b/packages/game-client/src/computed.js
index 9f37e88..88df464 100644
--- a/packages/game-client/src/computed.js
+++ b/packages/game-client/src/computed.js
@@ -1,5 +1,5 @@
import { NUMBER_OF_COLUMNS, NUMBER_OF_ROWS } from './constants';
-import { generateKey, generateValue } from './helpers';
+import { generateKey, generateValue, isCellValid } from './helpers';
const generateGridMap = () => {
const hash = {};
@@ -30,13 +30,12 @@ const initialSnakesState = {
},
2: {
headColor: 'blue',
- bodyColor: 'yellow',
+ bodyColor: 'purple',
hash: {
'0-11': { x: 0, y: 11 },
'0-12': { x: 0, y: 12 },
- '0-13': { x: 0, y: 13 },
},
- list: ['0-11', '0-12', '0-13'],
+ list: ['0-11', '0-12'],
},
3: {
headColor: 'yellow',
@@ -67,6 +66,21 @@ const initialSnakesState = {
},
};
+// TODO: Move this logic into a unit test later.
+
+Object.values(initialSnakesState).forEach((snake) => {
+ const { hash, list } = snake;
+ for (const cell of list) {
+ if (!(cell in hash)) {
+ throw new Error('Snake initial data is corrupt! list and hash are not in sync.');
+ }
+ const { x, y } = hash[cell];
+ if (!isCellValid(x, y)) {
+ throw new Error(`A cell is outside of the grid! x - ${x}, y - ${y}`);
+ }
+ }
+});
+
const initialFoodState = {};
export { GRID_MAP, initialSnakesState, initialFoodState };
diff --git a/packages/game-client/src/constants.js b/packages/game-client/src/constants.js
index 0810668..0b212d2 100644
--- a/packages/game-client/src/constants.js
+++ b/packages/game-client/src/constants.js
@@ -47,7 +47,7 @@ const FOOD_TICKS = {
// if they are unique.
areValuesUnique(SNAKE_TICKS);
-const DEFAULT_TRACK = SNAKE_TICKS.QUARTER.TYPE;
+const DEFAULT_TRACK = SNAKE_TICKS.ONE.TYPE;
const FOOD_EFFECTS = {
GROW: 'grow',
@@ -69,13 +69,23 @@ const FOOD_TYPES = {
TYPE: 'FROG',
chance: 95,
effects: { ...grow(1) },
+ color: 'green',
+ animationClass: 'frog',
},
RED_BULL: {
TYPE: 'RED_BULL',
chance: 5,
effects: { ...speed(SNAKE_TICKS.ONE_TENTH.TYPE, 30) }, // Lasts for 30 ticks.
+ color: 'silver',
+ animationClass: 'red-bull',
+ },
+ FILLET: {
+ TYPE: 'FILLET',
+ chance: 0,
+ effects: { ...grow(3) },
+ color: 'red',
+ animationClass: 'fillet',
},
- FILLET: { TYPE: 'FILLET', chance: 0, effects: { ...grow(3) } },
};
const defaultDirections = {
diff --git a/packages/game-client/src/grid.module.css b/packages/game-client/src/grid.module.css
index aac0e28..d91f850 100644
--- a/packages/game-client/src/grid.module.css
+++ b/packages/game-client/src/grid.module.css
@@ -5,142 +5,9 @@
.cell {
position: absolute;
-}
-
-:global(*) {
- outline: 1px solid black !important;
-}
-
-.food {
- box-sizing: border-box;
- border: 1px solid black;
-}
-
-.food.frog {
- background-color: green;
-
- /* Enter */
- -webkit-animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
- animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
-
- /* Pulsate */
- -webkit-animation: pulsate-fwd 0.5s ease-in-out infinite both;
- animation: pulsate-fwd 0.5s ease-in-out infinite both;
-}
-
-.food.fillet {
- background-color: #d62828;
-}
-
-.food.red_bull {
- -webkit-animation: vibrate 0.3s linear infinite both;
- animation: vibrate 0.3s linear infinite both;
- background-color: blue;
-}
-
-@-webkit-keyframes scale-in-center {
- 0% {
- -webkit-transform: scale(0);
- transform: scale(0);
- opacity: 1;
- }
- 100% {
- -webkit-transform: scale(1);
- transform: scale(1);
- opacity: 1;
- }
-}
-@keyframes scale-in-center {
- 0% {
- -webkit-transform: scale(0);
- transform: scale(0);
- opacity: 1;
- }
- 100% {
- -webkit-transform: scale(1);
- transform: scale(1);
- opacity: 1;
- }
-}
-
-@-webkit-keyframes pulsate-fwd {
- 0% {
- -webkit-transform: scale(1);
- transform: scale(1);
- }
- 50% {
- -webkit-transform: scale(1.1);
- transform: scale(1.1);
- }
- 100% {
- -webkit-transform: scale(1);
- transform: scale(1);
- }
-}
-@keyframes pulsate-fwd {
- 0% {
- -webkit-transform: scale(1);
- transform: scale(1);
- }
- 50% {
- -webkit-transform: scale(1.1);
- transform: scale(1.1);
- }
- 100% {
- -webkit-transform: scale(1);
- transform: scale(1);
- }
-}
-
-@-webkit-keyframes vibrate {
- 0% {
- -webkit-transform: translate(0);
- transform: translate(0);
- }
- 20% {
- -webkit-transform: translate(-1px, 1px);
- transform: translate(-1px, 1px);
- }
- 40% {
- -webkit-transform: translate(-1px, -1px);
- transform: translate(-1px, -1px);
- }
- 60% {
- -webkit-transform: translate(1px, 1px);
- transform: translate(1px, 1px);
- }
- 80% {
- -webkit-transform: translate(1px, -1px);
- transform: translate(1px, -1px);
- }
- 100% {
- -webkit-transform: translate(0);
- transform: translate(0);
- }
-}
-@keyframes vibrate {
- 0% {
- -webkit-transform: translate(0);
- transform: translate(0);
- }
- 20% {
- -webkit-transform: translate(-2px, 2px);
- transform: translate(-2px, 2px);
- }
- 40% {
- -webkit-transform: translate(-2px, -2px);
- transform: translate(-2px, -2px);
- }
- 60% {
- -webkit-transform: translate(2px, 2px);
- transform: translate(2px, 2px);
- }
- 80% {
- -webkit-transform: translate(2px, -2px);
- transform: translate(2px, -2px);
- }
- 100% {
- -webkit-transform: translate(0);
- transform: translate(0);
- }
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: white;
+ font-size: 10px;
}
diff --git a/packages/game-client/src/hooks/useTicks.js b/packages/game-client/src/hooks/useTicks.js
index 2dafa1c..6b218b3 100644
--- a/packages/game-client/src/hooks/useTicks.js
+++ b/packages/game-client/src/hooks/useTicks.js
@@ -1,7 +1,8 @@
import React, { useEffect, useRef } from 'react';
import { SNAKE_TICKS, FOOD_TICKS, DEFAULT_TRACK } from '../constants';
-const useTicks = ({ moveForward, spawnFood, getSnakeCells, getAllSnakeIds }) => {
+const useTicks = ({ moveForward, spawnFood, getSnakeCells, getAllSnakeIds, isGamePaused }) => {
+ const isGamePausedRef = useRef(isGamePaused);
const timersRef = useRef([]);
const trackRef = useRef(
Object.keys(SNAKE_TICKS).reduce((tracks, tick) => {
@@ -48,29 +49,27 @@ const useTicks = ({ moveForward, spawnFood, getSnakeCells, getAllSnakeIds }) =>
}
};
- // For food.
+ useEffect(() => {
+ isGamePausedRef.current = isGamePaused;
+ }, [isGamePaused]);
+
useEffect(() => {
for (const { DURATION: duration } of Object.values(FOOD_TICKS)) {
const timer = setInterval(() => {
- spawnFood(getSnakeCells);
+ if (!isGamePausedRef.current) {
+ spawnFood(getSnakeCells);
+ }
}, duration);
timersRef.current.push(timer);
}
- return () => {
- for (const timer of timersRef.current) {
- clearInterval(timer);
- }
- };
- }, []);
-
- // For snakes.
- useEffect(() => {
for (const [key, value] of Object.entries(SNAKE_TICKS)) {
const { DURATION: duration } = value;
const timer = setInterval(() => {
- onTick(key);
+ if (!isGamePausedRef.current) {
+ onTick(key);
+ }
}, duration);
timersRef.current.push(timer);
diff --git a/packages/game-client/src/index.css b/packages/game-client/src/index.css
index e69de29..c957491 100644
--- a/packages/game-client/src/index.css
+++ b/packages/game-client/src/index.css
@@ -0,0 +1,7 @@
+body {
+ margin: 0px;
+}
+
+* {
+ outline: 1px dashed black !important;
+}
diff --git a/packages/game-client/src/utils.js b/packages/game-client/src/utils.js
index 599e525..6b41da9 100644
--- a/packages/game-client/src/utils.js
+++ b/packages/game-client/src/utils.js
@@ -23,4 +23,16 @@ const findKeyByValue = (object, value) => {
throw new Error("The key you supplied doesn't exist in the hash.");
};
-export { generateRandomNumber, findKeyByValue, areValuesUnique };
+const stringToBoolean = (val) => {
+ if (val === 'true' || val === true) {
+ return true;
+ } else if (val === 'false' || val === false) {
+ return false;
+ } else if (val === null || val === undefined) {
+ return null;
+ } else {
+ throw new Error(`Invalid string passed! ${val}`);
+ }
+};
+
+export { generateRandomNumber, findKeyByValue, areValuesUnique, stringToBoolean };