✨ (game): Generate grid & User Interactions
Generate grid with bombs & numbers, user interactions like, click on a cell, pin a flag
This commit is contained in:
+5
-1
@@ -1,3 +1,7 @@
|
|||||||
.App {
|
.App {
|
||||||
text-align: center;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-14
@@ -1,22 +1,10 @@
|
|||||||
import React from 'react';
|
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
import Grid from './components/Grid/Grid';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<div className='App'>
|
<div className='App'>
|
||||||
<header className='App-header'>
|
<Grid />
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to reload.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
className='App-link'
|
|
||||||
href='https://reactjs.org'
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
Learn React
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,134 @@
|
|||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
//! Imports
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ------------------------------------------------------ React --------------------------------------------------------
|
||||||
|
import { MouseEvent, useCallback, useEffect, useState } from 'react';
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ------------------------------------------------------ Utils --------------------------------------------------------
|
||||||
|
import { Difficulty } from '../../types/game';
|
||||||
|
import { discoverAroundCell, genBombs, genGrid, genNumbers } from '../../utils/game';
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ----------------------------------------------------- Assets --------------------------------------------------------
|
||||||
|
import Bomb from '../../assets/bomb.png';
|
||||||
|
import Flag from '../../assets/flag.png';
|
||||||
|
import './styles.scss';
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const Grid = ({ size = 12, difficulty = 'beginner' }) => {
|
||||||
|
const [grid, setGrid] = useState(genGrid(size));
|
||||||
|
const [gameStatus, setGameStatus] = useState('idle');
|
||||||
|
|
||||||
|
const [, updateGrid] = useState({});
|
||||||
|
const forceUpdate = useCallback(() => updateGrid({}), []);
|
||||||
|
|
||||||
|
// ----------------------------------------------------- Actions -------------------------------------------------------
|
||||||
|
const handleLeftClick = (ev: MouseEvent, rowIndex: number, colIndex: number) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
return discoverAroundCell(prev, rowIndex, colIndex);
|
||||||
|
});
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRightClick = (ev: MouseEvent, rowIndex: number, colIndex: number) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (grid[rowIndex][colIndex].hidden) {
|
||||||
|
setGrid((prev) => {
|
||||||
|
return prev.map((row, idx) => {
|
||||||
|
if (idx === rowIndex) {
|
||||||
|
return row.map((col, idy) => {
|
||||||
|
if (idy === colIndex) {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
flag: !col.flag,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 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 (
|
||||||
|
<div
|
||||||
|
className='grid'
|
||||||
|
style={{
|
||||||
|
gridTemplateRows: `repeat(${size}, var(--size))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{grid.map((row, rowIndex) => (
|
||||||
|
<div
|
||||||
|
key={`row${rowIndex}`}
|
||||||
|
className='row'
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${size}, var(--size))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.map((cell, cellIndex) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`cell${cellIndex}`}
|
||||||
|
className={`cell ${cell.hidden ? 'hidden' : cell.value !== 'empty' ? `v${cell.value}` : ''}`}
|
||||||
|
onClick={(ev) => handleLeftClick(ev, rowIndex, cellIndex)}
|
||||||
|
onContextMenu={(ev) => handleRightClick(ev, rowIndex, cellIndex)}
|
||||||
|
>
|
||||||
|
{cell.hidden && cell.flag && (
|
||||||
|
<img
|
||||||
|
src={Flag}
|
||||||
|
alt='flat'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!cell.hidden &&
|
||||||
|
(cell.value === 'bomb' ? (
|
||||||
|
<img
|
||||||
|
src={Bomb}
|
||||||
|
alt='bomb'
|
||||||
|
/>
|
||||||
|
) : cell.value !== 'empty' ? (
|
||||||
|
cell.value
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
))}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Grid;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
.grid {
|
||||||
|
--size: 30px;
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #ffff;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& > .row {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
height: var(--size);
|
||||||
|
|
||||||
|
& > .cell {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: var(--size);
|
||||||
|
border: none;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-weight: 700;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
background-color: #dedede;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.v1 {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.v2 {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.v3 {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.v4 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.v5 {
|
||||||
|
color: purple;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > img {
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child > .cell {
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:last-child > .cell {
|
||||||
|
&:first-child {
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,12 @@
|
|||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100svw;
|
||||||
|
height: 100svh;
|
||||||
|
background: #598b8e;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
export type Difficulty = 'beginner' | 'intermediate' | 'expert';
|
||||||
|
export type CellValue = 'empty' | 'bomb' | number;
|
||||||
|
export type GridType = { hidden: boolean; value: CellValue; flag: boolean }[][];
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { Difficulty, CellValue, GridType } from '../types/game';
|
||||||
|
import { getRandomArbitrary } from './generics';
|
||||||
|
|
||||||
|
export const genGrid = (size: number): GridType => {
|
||||||
|
let grid: GridType = Array.apply(null, Array(size)).map(() => {
|
||||||
|
return Array.apply(null, Array(size)).map(() => {
|
||||||
|
return {
|
||||||
|
hidden: true,
|
||||||
|
value: 'empty',
|
||||||
|
flag: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const genBombs = (grid: GridType, difficulty: Difficulty) => {
|
||||||
|
const surface = grid.length * grid.length;
|
||||||
|
|
||||||
|
let bombs = 0;
|
||||||
|
if (difficulty === 'beginner') {
|
||||||
|
bombs = surface * 0.05;
|
||||||
|
} else if (difficulty === 'intermediate') {
|
||||||
|
bombs = surface * 0.1;
|
||||||
|
} else if (difficulty === 'expert') {
|
||||||
|
bombs = surface * 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
bombs = Math.floor(bombs);
|
||||||
|
|
||||||
|
for (let i = 0; i <= bombs; i++) {
|
||||||
|
let needRegenerate = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const row = getRandomArbitrary(0, grid.length);
|
||||||
|
const col = getRandomArbitrary(0, grid.length);
|
||||||
|
let bombsArounds = 0;
|
||||||
|
|
||||||
|
for (let testRowIndex = -1; testRowIndex <= 1; testRowIndex++) {
|
||||||
|
const testedRow = row + testRowIndex;
|
||||||
|
|
||||||
|
if (testedRow < 0) continue;
|
||||||
|
if (testedRow >= grid.length) continue;
|
||||||
|
|
||||||
|
for (let testColIndex = -1; testColIndex <= 1; testColIndex++) {
|
||||||
|
const testedCol = col + testColIndex;
|
||||||
|
|
||||||
|
if (testedCol < 0) continue;
|
||||||
|
if (testedCol >= grid.length) continue;
|
||||||
|
if (testedRow === row && testedCol === col) continue;
|
||||||
|
|
||||||
|
if (grid[testedRow][testedCol].value === 'bomb') {
|
||||||
|
bombsArounds++;
|
||||||
|
|
||||||
|
if (bombsArounds > 3) {
|
||||||
|
needRegenerate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needRegenerate) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bombsArounds <= 3) {
|
||||||
|
grid[row][col].value = 'bomb';
|
||||||
|
}
|
||||||
|
} while (needRegenerate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const genNumbers = (grid: GridType): GridType => {
|
||||||
|
for (let row = 0; row < grid.length; row++) {
|
||||||
|
for (let col = 0; col < grid[row].length; col++) {
|
||||||
|
let bombsArounds = 0;
|
||||||
|
|
||||||
|
checkAroundCell(grid, row, col, (testedCell: { hidden: boolean; value: CellValue }, testedRow: number, testedCol: number) => {
|
||||||
|
if (testedCell.value === 'bomb') {
|
||||||
|
bombsArounds++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bombsArounds > 0) {
|
||||||
|
grid[row][col].value = bombsArounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkAroundCell = (grid: GridType, row: number, col: number, callback: Function): void => {
|
||||||
|
for (let testRowIndex = -1; testRowIndex <= 1; testRowIndex++) {
|
||||||
|
const testedRow = row + testRowIndex;
|
||||||
|
|
||||||
|
if (testedRow < 0) continue;
|
||||||
|
if (testedRow >= grid.length) continue;
|
||||||
|
|
||||||
|
for (let testColIndex = -1; testColIndex <= 1; testColIndex++) {
|
||||||
|
const testedCol = col + testColIndex;
|
||||||
|
|
||||||
|
if (testedCol < 0) continue;
|
||||||
|
if (testedCol >= grid.length) continue;
|
||||||
|
if (testedRow === row && testedCol === col) continue;
|
||||||
|
|
||||||
|
callback(grid[testedRow][testedCol], testedRow, testedCol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const discoverAroundCell = (grid: GridType, rowIndex: number, colIndex: number): GridType => {
|
||||||
|
grid[rowIndex][colIndex].hidden = false;
|
||||||
|
|
||||||
|
checkAroundCell(grid, rowIndex, colIndex, (cell: { hidden: boolean; value: CellValue; flag: boolean }, testedRowIndex: number, testedColIndex: number) => {
|
||||||
|
if (!cell.flag && cell.hidden) {
|
||||||
|
if (cell.value === 'empty') {
|
||||||
|
grid = discoverAroundCell(grid, testedRowIndex, testedColIndex);
|
||||||
|
} else if (typeof cell.value === 'number') {
|
||||||
|
grid[testedRowIndex][testedColIndex].hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const revealAllGrid = (grid: GridType, excludeGoodFlags: boolean = true) => {
|
||||||
|
return grid.map((row) => {
|
||||||
|
return row.map((col) => {
|
||||||
|
if (excludeGoodFlags && col.value === 'bomb' && col.flag) {
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
hidden: false,
|
||||||
|
flag: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const getRandomArbitrary = (min: number, max: number): number => {
|
||||||
|
return Math.floor(Math.random() * (max - min) + min);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user