diff --git a/src/App.scss b/src/App.scss index c735afc..eb442da 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,3 +1,7 @@ .App { - text-align: center; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; } diff --git a/src/App.tsx b/src/App.tsx index 169f89a..4f0cd48 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,10 @@ -import React from 'react'; import './App.scss'; +import Grid from './components/Grid/Grid'; const App = () => { return (
-
-

- Edit src/App.tsx and save to reload. -

- - Learn React - -
+
); }; diff --git a/src/assets/bomb.png b/src/assets/bomb.png new file mode 100644 index 0000000..88d67e9 Binary files /dev/null and b/src/assets/bomb.png differ diff --git a/src/assets/flag.png b/src/assets/flag.png new file mode 100644 index 0000000..f751de7 Binary files /dev/null and b/src/assets/flag.png differ diff --git a/src/components/Grid/Grid.tsx b/src/components/Grid/Grid.tsx new file mode 100644 index 0000000..c91dcce --- /dev/null +++ b/src/components/Grid/Grid.tsx @@ -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 ( +
+ {grid.map((row, rowIndex) => ( +
+ {row.map((cell, cellIndex) => { + return ( + + ); + })} +
+ ))} +
+ ); +}; + +export default Grid; diff --git a/src/components/Grid/styles.scss b/src/components/Grid/styles.scss new file mode 100644 index 0000000..33273ab --- /dev/null +++ b/src/components/Grid/styles.scss @@ -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; + } + } + } +} diff --git a/src/index.scss b/src/index.scss index 1270a2d..1b0969f 100644 --- a/src/index.scss +++ b/src/index.scss @@ -5,5 +5,12 @@ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + user-select: none; } +#root { + width: 100svw; + height: 100svh; + background: #598b8e; + overflow: hidden; +} diff --git a/src/types/game.d.ts b/src/types/game.d.ts new file mode 100644 index 0000000..2ac084a --- /dev/null +++ b/src/types/game.d.ts @@ -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 }[][]; diff --git a/src/utils/game.ts b/src/utils/game.ts new file mode 100644 index 0000000..e8c0d92 --- /dev/null +++ b/src/utils/game.ts @@ -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, + }; + }); + }); +}; diff --git a/src/utils/generics.ts b/src/utils/generics.ts new file mode 100644 index 0000000..36c5950 --- /dev/null +++ b/src/utils/generics.ts @@ -0,0 +1,3 @@ +export const getRandomArbitrary = (min: number, max: number): number => { + return Math.floor(Math.random() * (max - min) + min); +};