(game): Added Bombs & Flags count

Added placed flags by bombs count. Fix timer startng only when first click is triggered
This commit is contained in:
2024-02-12 16:11:09 +01:00
parent 3a6be56841
commit 9033ef8f68
13 changed files with 259 additions and 46 deletions
+25
View File
@@ -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",
+1
View File
@@ -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"
},
+12 -1
View File
@@ -4,6 +4,17 @@
justify-content: center;
align-items: center;
gap: 20px;
& > .stats {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
gap: 15px;
width: 100%;
height: 100%;
& > .logo {
width: 75px;
}
}
}
+23 -2
View File
@@ -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 (
<div className='App'>
<RecoilRoot>
<div className='stats'>
<Timer />
<Logo className={'logo'} />
<MinesCounter />
</div>
<Grid />
</RecoilRoot>
</div>
);
};
+20
View File
@@ -0,0 +1,20 @@
const Flag = (props: any) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
{...props}
>
<path
d='M80 464V68.14a8 8 0 014-6.9C91.81 56.66 112.92 48 160 48c64 0 145 48 192 48a199.53 199.53 0 0077.23-15.77 2 2 0 012.77 1.85v219.36a4 4 0 01-2.39 3.65C421.37 308.7 392.33 320 352 320c-48 0-128-32-192-32s-80 16-80 16'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeMiterlimit='10'
strokeWidth='32'
/>
</svg>
);
};
export default Flag;
+34
View File
@@ -0,0 +1,34 @@
const Logo = (props: any) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
{...props}
>
{/* Edges */}
<path
d='M256 32 l0 448 M32 256 l448 0 M112 112 l286 286 M400 112 L112 400'
stroke='currentColor'
strokeLinecap='round'
strokeMiterlimit='10'
strokeWidth={32}
/>
{/* Main circle */}
<circle
cx={256}
cy={256}
r={150}
fill='currentColor'
/>
{/* Light on main circle */}
<circle
cx={206}
cy={206}
r={40}
fill='#ffffff'
/>
</svg>
);
};
export default Logo;
+34 -23
View File
@@ -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 (
@@ -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 (
<div className='statContainer'>
<Flag />
<p>
{placedFlags} / {bombs}
</p>
</div>
);
};
export default MinesCounter;
@@ -7,15 +7,28 @@ 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<number>(Date.now());
const [{ status }] = useRecoilState(gameStateAtom);
const [startedTime, setStartedTime] = useState<number | null>(null);
const [displayedTime, setDisplayedTime] = useState<string>('00:00');
useEffect(() => {
if (status === 'playing') {
setStartedTime(Date.now());
} else if (status === 'win' || status === 'loose') {
setStartedTime(null);
}
}, [status]);
useEffect(() => {
if (startedTime) {
const interval = setInterval(() => {
const elapsedTime = (Date.now() - startedTime) / 1000;
@@ -29,10 +42,11 @@ const Timer = () => {
}, 500);
return () => clearInterval(interval);
}
}, [startedTime]);
return (
<div className='timer'>
<div className='statContainer'>
<Clock />
<p>{displayedTime}</p>
</div>
@@ -1,4 +1,4 @@
.timer {
.statContainer {
display: grid;
grid-template-columns: 25px 40px;
align-items: center;
+10
View File
@@ -0,0 +1,10 @@
import { atom } from 'recoil';
export const gameStateAtom = atom({
key: 'gameState',
default: {
bombs: 0,
placedFlags: 0,
status: 'idle',
},
});
+5
View File
@@ -9,6 +9,11 @@
}
#root {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
width: 100svw;
height: 100svh;
background: #598b8e;
+36 -3
View File
@@ -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 };
};