From 9033ef8f681e0b075932e81ff279af3f396df488 Mon Sep 17 00:00:00 2001 From: UnEpicier Date: Mon, 12 Feb 2024 16:11:09 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(game):=20Added=20Bombs=20&=20Flags?= =?UTF-8?q?=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added placed flags by bombs count. Fix timer startng only when first click is triggered --- package-lock.json | 25 ++++++++ package.json | 1 + src/App.scss | 15 ++++- src/App.tsx | 29 ++++++++-- src/assets/flag.tsx | 20 +++++++ src/assets/logo.tsx | 34 +++++++++++ src/components/Grid/Grid.tsx | 57 +++++++++++-------- .../Stats/MinesCounter/MinesCounter.tsx | 28 +++++++++ src/components/{ => Stats}/Timer/Timer.tsx | 40 ++++++++----- src/components/{Timer => Stats}/styles.scss | 2 +- src/contexts/gameState.ts | 10 ++++ src/index.scss | 5 ++ src/utils/game.ts | 39 ++++++++++++- 13 files changed, 259 insertions(+), 46 deletions(-) create mode 100644 src/assets/flag.tsx create mode 100644 src/assets/logo.tsx create mode 100644 src/components/Stats/MinesCounter/MinesCounter.tsx rename src/components/{ => Stats}/Timer/Timer.tsx (52%) rename src/components/{Timer => Stats}/styles.scss (91%) create mode 100644 src/contexts/gameState.ts diff --git a/package-lock.json b/package-lock.json index 00bd4e6..ca0065e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", + "recoil": "^0.7.7", "sass": "^1.70.0", "typescript": "^4.9.5" }, @@ -8963,6 +8964,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -15337,6 +15343,25 @@ "node": ">=8.10.0" } }, + "node_modules/recoil": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", + "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", diff --git a/package.json b/package.json index 90de419..cc84f8a 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", + "recoil": "^0.7.7", "sass": "^1.70.0", "typescript": "^4.9.5" }, diff --git a/src/App.scss b/src/App.scss index 0386a36..4ebb6d3 100644 --- a/src/App.scss +++ b/src/App.scss @@ -4,6 +4,17 @@ justify-content: center; align-items: center; gap: 20px; - width: 100%; - height: 100%; + + & > .stats { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + gap: 15px; + width: 100%; + + & > .logo { + width: 75px; + } + } } diff --git a/src/App.tsx b/src/App.tsx index 2e93215..f3b2992 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,33 @@ -import './App.scss'; +// --------------------------------------------------------------------------------------------------------------------- +//! Imports +// --------------------------------------------------------------------------------------------------------------------- + +// ----------------------------------------------------- Context ------------------------------------------------------- +import { RecoilRoot } from 'recoil'; +// --------------------------------------------------------------------------------------------------------------------- + +// --------------------------------------------------- Components ------------------------------------------------------ import Grid from './components/Grid/Grid'; -import Timer from './components/Timer/Timer'; +import Timer from './components/Stats/Timer/Timer'; +import MinesCounter from './components/Stats/MinesCounter/MinesCounter'; +// --------------------------------------------------------------------------------------------------------------------- + +// ----------------------------------------------------- Styles -------------------------------------------------------- +import './App.scss'; +import Logo from './assets/logo'; +// --------------------------------------------------------------------------------------------------------------------- const App = () => { return (
- - + +
+ + + +
+ +
); }; diff --git a/src/assets/flag.tsx b/src/assets/flag.tsx new file mode 100644 index 0000000..dc44558 --- /dev/null +++ b/src/assets/flag.tsx @@ -0,0 +1,20 @@ +const Flag = (props: any) => { + return ( + + + + ); +}; + +export default Flag; diff --git a/src/assets/logo.tsx b/src/assets/logo.tsx new file mode 100644 index 0000000..2a46dfc --- /dev/null +++ b/src/assets/logo.tsx @@ -0,0 +1,34 @@ +const Logo = (props: any) => { + return ( + + {/* Edges */} + + {/* Main circle */} + + {/* Light on main circle */} + + + ); +}; + +export default Logo; diff --git a/src/components/Grid/Grid.tsx b/src/components/Grid/Grid.tsx index c91dcce..7847905 100644 --- a/src/components/Grid/Grid.tsx +++ b/src/components/Grid/Grid.tsx @@ -3,12 +3,17 @@ // --------------------------------------------------------------------------------------------------------------------- // ------------------------------------------------------ React -------------------------------------------------------- -import { MouseEvent, useCallback, useEffect, useState } from 'react'; +import { MouseEvent, useCallback, useState } from 'react'; +// --------------------------------------------------------------------------------------------------------------------- + +// ----------------------------------------------------- Context ------------------------------------------------------- +import { useRecoilState } from 'recoil'; +import { gameStateAtom } from '../../contexts/gameState'; // --------------------------------------------------------------------------------------------------------------------- // ------------------------------------------------------ Utils -------------------------------------------------------- import { Difficulty } from '../../types/game'; -import { discoverAroundCell, genBombs, genGrid, genNumbers } from '../../utils/game'; +import { discoverAroundCell, genGrid, startGame } from '../../utils/game'; // --------------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------- Assets -------------------------------------------------------- @@ -18,8 +23,9 @@ import './styles.scss'; // --------------------------------------------------------------------------------------------------------------------- const Grid = ({ size = 12, difficulty = 'beginner' }) => { + const [gameState, setGameState] = useRecoilState(gameStateAtom); + const [grid, setGrid] = useState(genGrid(size)); - const [gameStatus, setGameStatus] = useState('idle'); const [, updateGrid] = useState({}); const forceUpdate = useCallback(() => updateGrid({}), []); @@ -30,11 +36,15 @@ const Grid = ({ size = 12, difficulty = 'beginner' }) => { if (grid[rowIndex][colIndex].hidden && !grid[rowIndex][colIndex].flag) { setGrid((prev) => { - if (gameStatus === 'idle') { - do { - prev = genNumbers(genBombs(prev, difficulty as Difficulty)); - } while (prev[rowIndex][colIndex].value !== 'empty'); - setGameStatus('started'); + if (gameState.status === 'idle') { + const { grid: newGrid, bombsCount } = startGame(prev, difficulty as Difficulty, true, rowIndex, colIndex); + prev = newGrid; + + setGameState((prevGameState) => ({ + ...prevGameState, + status: 'playing', + bombs: bombsCount, + })); } return discoverAroundCell(prev, rowIndex, colIndex); @@ -48,10 +58,26 @@ const Grid = ({ size = 12, difficulty = 'beginner' }) => { if (grid[rowIndex][colIndex].hidden) { setGrid((prev) => { + if (gameState.status === 'idle') { + const { grid: newGrid, bombsCount } = startGame(prev, difficulty as Difficulty, false); + prev = newGrid; + + setGameState((prevGameState) => ({ + ...prevGameState, + status: 'playing', + bombs: bombsCount, + })); + } + return prev.map((row, idx) => { if (idx === rowIndex) { return row.map((col, idy) => { if (idy === colIndex) { + setGameState((prevGameState) => ({ + ...prevGameState, + placedFlags: col.flag ? prevGameState.placedFlags - 1 : prevGameState.placedFlags + 1, + })); + return { ...col, flag: !col.flag, @@ -65,21 +91,6 @@ const Grid = ({ size = 12, difficulty = 'beginner' }) => { }); } }; - - // --- Development - useEffect(() => { - if (process.env.NODE_ENV === 'development') { - document.addEventListener('keyup', (ev) => { - if (ev.key === 'v') { - setGrid(() => { - let newGrid = genGrid(size); - - return genNumbers(genBombs(newGrid, difficulty as Difficulty)); - }); - } - }); - } - }, [size, difficulty]); // --------------------------------------------------------------------------------------------------------------------- return ( diff --git a/src/components/Stats/MinesCounter/MinesCounter.tsx b/src/components/Stats/MinesCounter/MinesCounter.tsx new file mode 100644 index 0000000..ffe38f9 --- /dev/null +++ b/src/components/Stats/MinesCounter/MinesCounter.tsx @@ -0,0 +1,28 @@ +// --------------------------------------------------------------------------------------------------------------------- +//! Imports +// --------------------------------------------------------------------------------------------------------------------- + +// ----------------------------------------------------- Recoil -------------------------------------------------------- +import { useRecoilState } from 'recoil'; +import { gameStateAtom } from '../../../contexts/gameState'; +// --------------------------------------------------------------------------------------------------------------------- + +// ----------------------------------------------------- Assets -------------------------------------------------------- +import Flag from '../../../assets/flag'; +import '../styles.scss'; +// --------------------------------------------------------------------------------------------------------------------- + +const MinesCounter = () => { + const [{ bombs, placedFlags }] = useRecoilState(gameStateAtom); + + return ( +
+ +

+ {placedFlags} / {bombs} +

+
+ ); +}; + +export default MinesCounter; diff --git a/src/components/Timer/Timer.tsx b/src/components/Stats/Timer/Timer.tsx similarity index 52% rename from src/components/Timer/Timer.tsx rename to src/components/Stats/Timer/Timer.tsx index 673fd21..6fa880d 100644 --- a/src/components/Timer/Timer.tsx +++ b/src/components/Stats/Timer/Timer.tsx @@ -7,32 +7,46 @@ import { useEffect, useState } from 'react'; // --------------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------- Assets -------------------------------------------------------- -import Clock from '../../assets/clock'; -import './styles.scss'; +import Clock from '../../../assets/clock'; +import '../styles.scss'; +import { useRecoilState } from 'recoil'; +import { gameStateAtom } from '../../../contexts/gameState'; // --------------------------------------------------------------------------------------------------------------------- const Timer = () => { - const [startedTime] = useState(Date.now()); + const [{ status }] = useRecoilState(gameStateAtom); + + const [startedTime, setStartedTime] = useState(null); const [displayedTime, setDisplayedTime] = useState('00:00'); useEffect(() => { - const interval = setInterval(() => { - const elapsedTime = (Date.now() - startedTime) / 1000; + if (status === 'playing') { + setStartedTime(Date.now()); + } else if (status === 'win' || status === 'loose') { + setStartedTime(null); + } + }, [status]); - const seconds = Math.floor(elapsedTime % 60); - const strSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`; + useEffect(() => { + if (startedTime) { + const interval = setInterval(() => { + const elapsedTime = (Date.now() - startedTime) / 1000; - const minutes = Math.floor(elapsedTime / 60); - const strMinute = minutes < 10 ? `0${minutes}` : `${minutes}`; + const seconds = Math.floor(elapsedTime % 60); + const strSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`; - setDisplayedTime(`${strMinute}:${strSeconds}`); - }, 500); + const minutes = Math.floor(elapsedTime / 60); + const strMinute = minutes < 10 ? `0${minutes}` : `${minutes}`; - return () => clearInterval(interval); + setDisplayedTime(`${strMinute}:${strSeconds}`); + }, 500); + + return () => clearInterval(interval); + } }, [startedTime]); return ( -
+

{displayedTime}

diff --git a/src/components/Timer/styles.scss b/src/components/Stats/styles.scss similarity index 91% rename from src/components/Timer/styles.scss rename to src/components/Stats/styles.scss index b4ff216..af45820 100644 --- a/src/components/Timer/styles.scss +++ b/src/components/Stats/styles.scss @@ -1,4 +1,4 @@ -.timer { +.statContainer { display: grid; grid-template-columns: 25px 40px; align-items: center; diff --git a/src/contexts/gameState.ts b/src/contexts/gameState.ts new file mode 100644 index 0000000..af46068 --- /dev/null +++ b/src/contexts/gameState.ts @@ -0,0 +1,10 @@ +import { atom } from 'recoil'; + +export const gameStateAtom = atom({ + key: 'gameState', + default: { + bombs: 0, + placedFlags: 0, + status: 'idle', + }, +}); diff --git a/src/index.scss b/src/index.scss index 1b0969f..5829c73 100644 --- a/src/index.scss +++ b/src/index.scss @@ -9,6 +9,11 @@ } #root { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; width: 100svw; height: 100svh; background: #598b8e; diff --git a/src/utils/game.ts b/src/utils/game.ts index e8c0d92..dcae4ca 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -5,6 +5,7 @@ export const genGrid = (size: number): GridType => { let grid: GridType = Array.apply(null, Array(size)).map(() => { return Array.apply(null, Array(size)).map(() => { return { + // hidden: false, hidden: true, value: 'empty', flag: false, @@ -12,6 +13,9 @@ export const genGrid = (size: number): GridType => { }); }); + // grid = genBombs(grid, 'beginner'); + // grid = genNumbers(grid); + return grid; }; @@ -28,11 +32,11 @@ export const genBombs = (grid: GridType, difficulty: Difficulty) => { } bombs = Math.floor(bombs); - - for (let i = 0; i <= bombs; i++) { + for (let i = 1; i <= bombs; i++) { let needRegenerate = false; do { + needRegenerate = false; const row = getRandomArbitrary(0, grid.length); const col = getRandomArbitrary(0, grid.length); let bombsArounds = 0; @@ -69,7 +73,7 @@ export const genBombs = (grid: GridType, difficulty: Difficulty) => { } while (needRegenerate); } - return grid; + return { grid: grid, bombsCount: bombs }; }; export const genNumbers = (grid: GridType): GridType => { @@ -141,3 +145,32 @@ export const revealAllGrid = (grid: GridType, excludeGoodFlags: boolean = true) }); }); }; + +export const startGame = (grid: GridType, difficulty: Difficulty, needEmpty: boolean = true, rowIndex?: number, colIndex?: number): { grid: GridType; bombsCount: number } => { + if (needEmpty) { + if (rowIndex === undefined || rowIndex < 0 || rowIndex >= grid.length) { + throw Error('Missing rowIndex and/or colIndex parameters.'); + } else if (colIndex === undefined || colIndex < 0 || colIndex >= grid[rowIndex].length) { + throw Error('Missing rowIndex and/or colIndex parameters.'); + } + + let bombsCount = 0; + + do { + const { grid: bombsGrid, bombsCount: bombsCountFromGen } = genBombs(grid, difficulty as Difficulty); + grid = bombsGrid; + + bombsCount = bombsCountFromGen; + } while (grid[rowIndex][colIndex].value !== 'empty'); + + grid = genNumbers(grid); + return { grid, bombsCount }; + } + + const { grid: bombsGrid, bombsCount } = genBombs(grid, difficulty); + grid = bombsGrid; + + grid = genNumbers(grid); + + return { grid, bombsCount }; +};