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 (
);
};
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);
+};