✨ (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:
Generated
+25
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const gameStateAtom = atom({
|
||||
key: 'gameState',
|
||||
default: {
|
||||
bombs: 0,
|
||||
placedFlags: 0,
|
||||
status: 'idle',
|
||||
},
|
||||
});
|
||||
@@ -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
@@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user