├── .eslintrc.json
├── .gitignore
├── .idea
├── inspectionProfiles
│ └── Project_Default.xml
├── minesweeper-ts.iml
├── misc.xml
├── modules.xml
├── vcs.xml
└── workspace.xml
├── LICENSE
├── README.md
├── minesweeper-blueprint.png
├── minesweeper.png
├── minesweeper2.png
├── minesweeper3.png
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── components
│ ├── App
│ │ ├── App.scss
│ │ └── index.tsx
│ ├── Button
│ │ ├── Button.scss
│ │ └── index.tsx
│ └── NumberDisplay
│ │ ├── NumberDisplay.scss
│ │ └── index.tsx
├── constants
│ └── index.ts
├── index.scss
├── index.tsx
├── react-app-env.d.ts
├── styles
│ └── index.scss
├── types
│ └── index.ts
└── utils
│ └── index.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["prettier"],
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "prettier/prettier": ["error"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/minesweeper-ts.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | 1578192091555
89 |
90 |
91 | 1578192091555
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | MIT License
3 |
4 | Copyright (c) 2020 Justin Kim
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Minesweeper 💣 😵
2 |
3 | React Minesweeper made using React, Typescript, and SASS!
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/minesweeper-blueprint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angle943/react-minesweeper-ts/b82b7defcdcb9b6b2d057b62b9e5213b696e78cf/minesweeper-blueprint.png
--------------------------------------------------------------------------------
/minesweeper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angle943/react-minesweeper-ts/b82b7defcdcb9b6b2d057b62b9e5213b696e78cf/minesweeper.png
--------------------------------------------------------------------------------
/minesweeper2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angle943/react-minesweeper-ts/b82b7defcdcb9b6b2d057b62b9e5213b696e78cf/minesweeper2.png
--------------------------------------------------------------------------------
/minesweeper3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angle943/react-minesweeper-ts/b82b7defcdcb9b6b2d057b62b9e5213b696e78cf/minesweeper3.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minesweeper-ts",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "@types/jest": "^24.0.0",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^16.9.0",
12 | "@types/react-dom": "^16.9.0",
13 | "eslint-config-prettier": "^6.9.0",
14 | "eslint-plugin-prettier": "^3.1.2",
15 | "node-sass": "^4.13.0",
16 | "prettier": "^1.19.1",
17 | "react": "^16.12.0",
18 | "react-dom": "^16.12.0",
19 | "react-scripts": "3.3.0",
20 | "typescript": "~3.7.2"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": "react-app"
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angle943/react-minesweeper-ts/b82b7defcdcb9b6b2d057b62b9e5213b696e78cf/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angle943/react-minesweeper-ts/b82b7defcdcb9b6b2d057b62b9e5213b696e78cf/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angle943/react-minesweeper-ts/b82b7defcdcb9b6b2d057b62b9e5213b696e78cf/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/components/App/App.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles/index.scss";
2 |
3 | .App {
4 | @include borders(white, #999);
5 | background: #c2c2c2;
6 | padding: 16px;
7 | }
8 |
9 | .Header {
10 | @include borders(#7b7b7b, white);
11 | align-items: center;
12 | background: #c0c0c0;
13 | display: flex;
14 | justify-content: space-between;
15 | padding: 10px 12px;
16 | }
17 |
18 | .Body {
19 | @include borders(#7b7b7b, white);
20 | display: grid;
21 | grid-template-columns: repeat(9, 1fr);
22 | grid-template-rows: repeat(9, 1fr);
23 | margin-top: 16px;
24 | }
25 |
26 | .Face {
27 | @include borders;
28 | @include buttons;
29 | align-items: center;
30 | cursor: pointer;
31 | display: flex;
32 | font-size: 35px;
33 | height: 52px;
34 | justify-content: center;
35 | width: 52px;
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/App/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import Button from "../Button";
4 | import NumberDisplay from "../NumberDisplay";
5 | import { generateCells, openMultipleCells } from "../../utils";
6 | import { Cell, CellState, CellValue, Face } from "../../types";
7 | import { MAX_COLS, MAX_ROWS } from "../../constants";
8 |
9 | import "./App.scss";
10 |
11 | const App: React.FC = () => {
12 | const [cells, setCells] = useState(generateCells());
13 | const [face, setFace] = useState(Face.smile);
14 | const [time, setTime] = useState(0);
15 | const [live, setLive] = useState(false);
16 | const [bombCounter, setBombCounter] = useState(10);
17 | const [hasLost, setHasLost] = useState(false);
18 | const [hasWon, setHasWon] = useState(false);
19 |
20 | useEffect(() => {
21 | const handleMouseDown = (): void => {
22 | setFace(Face.oh);
23 | };
24 |
25 | const handleMouseUp = (): void => {
26 | setFace(Face.smile);
27 | };
28 |
29 | window.addEventListener("mousedown", handleMouseDown);
30 | window.addEventListener("mouseup", handleMouseUp);
31 |
32 | return () => {
33 | window.removeEventListener("mousedown", handleMouseDown);
34 | window.removeEventListener("mouseup", handleMouseUp);
35 | };
36 | }, []);
37 |
38 | useEffect(() => {
39 | if (live && time < 999) {
40 | const timer = setInterval(() => {
41 | setTime(time + 1);
42 | }, 1000);
43 |
44 | return () => {
45 | clearInterval(timer);
46 | };
47 | }
48 | }, [live, time]);
49 |
50 | useEffect(() => {
51 | if (hasLost) {
52 | setLive(false);
53 | setFace(Face.lost);
54 | }
55 | }, [hasLost]);
56 |
57 | useEffect(() => {
58 | if (hasWon) {
59 | setLive(false);
60 | setFace(Face.won);
61 | }
62 | }, [hasWon]);
63 |
64 | const handleCellClick = (rowParam: number, colParam: number) => (): void => {
65 | let newCells = cells.slice();
66 |
67 | // start the game
68 | if (!live) {
69 | let isABomb = newCells[rowParam][colParam].value === CellValue.bomb;
70 | while (isABomb) {
71 | newCells = generateCells();
72 | if (newCells[rowParam][colParam].value !== CellValue.bomb) {
73 | isABomb = false;
74 | break;
75 | }
76 | }
77 | setLive(true);
78 | }
79 |
80 | const currentCell = newCells[rowParam][colParam];
81 |
82 | if ([CellState.flagged, CellState.visible].includes(currentCell.state)) {
83 | return;
84 | }
85 |
86 | if (currentCell.value === CellValue.bomb) {
87 | setHasLost(true);
88 | newCells[rowParam][colParam].red = true;
89 | newCells = showAllBombs();
90 | setCells(newCells);
91 | return;
92 | } else if (currentCell.value === CellValue.none) {
93 | newCells = openMultipleCells(newCells, rowParam, colParam);
94 | } else {
95 | newCells[rowParam][colParam].state = CellState.visible;
96 | }
97 |
98 | // Check to see if you have won
99 | let safeOpenCellsExists = false;
100 | for (let row = 0; row < MAX_ROWS; row++) {
101 | for (let col = 0; col < MAX_COLS; col++) {
102 | const currentCell = newCells[row][col];
103 |
104 | if (
105 | currentCell.value !== CellValue.bomb &&
106 | currentCell.state === CellState.open
107 | ) {
108 | safeOpenCellsExists = true;
109 | break;
110 | }
111 | }
112 | }
113 |
114 | if (!safeOpenCellsExists) {
115 | newCells = newCells.map(row =>
116 | row.map(cell => {
117 | if (cell.value === CellValue.bomb) {
118 | return {
119 | ...cell,
120 | state: CellState.flagged
121 | };
122 | }
123 | return cell;
124 | })
125 | );
126 | setHasWon(true);
127 | }
128 |
129 | setCells(newCells);
130 | };
131 |
132 | const handleCellContext = (rowParam: number, colParam: number) => (
133 | e: React.MouseEvent
134 | ): void => {
135 | e.preventDefault();
136 |
137 | if (!live) {
138 | return;
139 | }
140 |
141 | const currentCells = cells.slice();
142 | const currentCell = cells[rowParam][colParam];
143 |
144 | if (currentCell.state === CellState.visible) {
145 | return;
146 | } else if (currentCell.state === CellState.open) {
147 | currentCells[rowParam][colParam].state = CellState.flagged;
148 | setCells(currentCells);
149 | setBombCounter(bombCounter - 1);
150 | } else if (currentCell.state === CellState.flagged) {
151 | currentCells[rowParam][colParam].state = CellState.open;
152 | setCells(currentCells);
153 | setBombCounter(bombCounter + 1);
154 | }
155 | };
156 |
157 | const handleFaceClick = (): void => {
158 | setLive(false);
159 | setTime(0);
160 | setCells(generateCells());
161 | setHasLost(false);
162 | setHasWon(false);
163 | };
164 |
165 | const renderCells = (): React.ReactNode => {
166 | return cells.map((row, rowIndex) =>
167 | row.map((cell, colIndex) => (
168 |
178 | ))
179 | );
180 | };
181 |
182 | const showAllBombs = (): Cell[][] => {
183 | const currentCells = cells.slice();
184 | return currentCells.map(row =>
185 | row.map(cell => {
186 | if (cell.value === CellValue.bomb) {
187 | return {
188 | ...cell,
189 | state: CellState.visible
190 | };
191 | }
192 |
193 | return cell;
194 | })
195 | );
196 | };
197 |
198 | return (
199 |
200 |
201 |
202 |
203 |
204 | {face}
205 |
206 |
207 |
208 |
209 | {renderCells()}
210 |
211 | );
212 | };
213 |
214 | export default App;
215 |
--------------------------------------------------------------------------------
/src/components/Button/Button.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles/index.scss";
2 |
3 | .Button {
4 | @include borders;
5 | @include buttons;
6 | align-items: center;
7 | display: flex;
8 | font-weight: bold;
9 | height: 30px;
10 | justify-content: center;
11 | width: 30px;
12 |
13 | &.visible {
14 | border-color: #7b7b7b;
15 | border-width: 1px;
16 | }
17 |
18 | &.red {
19 | background: red;
20 | }
21 |
22 | span {
23 | font-size: 12px;
24 | margin-left: 2px;
25 | }
26 |
27 | &.value-1 {
28 | color: blue;
29 | }
30 | &.value-2 {
31 | color: green;
32 | }
33 | &.value-3 {
34 | color: red;
35 | }
36 | &.value-4 {
37 | color: purple;
38 | }
39 | &.value-5 {
40 | color: maroon;
41 | }
42 | &.value-6 {
43 | color: turquoise;
44 | }
45 | &.value-7 {
46 | color: black;
47 | }
48 | &.value-8 {
49 | color: gray;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { CellState, CellValue } from "../../types";
3 |
4 | import "./Button.scss";
5 |
6 | interface ButtonProps {
7 | col: number;
8 | onClick(rowParam: number, colParam: number): (...args: any[]) => void;
9 | onContext(rowParam: number, colParam: number): (...args: any[]) => void;
10 | red?: boolean;
11 | row: number;
12 | state: CellState;
13 | value: CellValue;
14 | }
15 |
16 | const Button: React.FC = ({
17 | col,
18 | onClick,
19 | onContext,
20 | red,
21 | row,
22 | state,
23 | value
24 | }) => {
25 | const renderContent = (): React.ReactNode => {
26 | if (state === CellState.visible) {
27 | if (value === CellValue.bomb) {
28 | return (
29 |
30 | 💣
31 |
32 | );
33 | } else if (value === CellValue.none) {
34 | return null;
35 | }
36 |
37 | return value;
38 | } else if (state === CellState.flagged) {
39 | return (
40 |
41 | 🚩
42 |
43 | );
44 | }
45 |
46 | return null;
47 | };
48 |
49 | return (
50 |
57 | {renderContent()}
58 |
59 | );
60 | };
61 |
62 | export default Button;
63 |
--------------------------------------------------------------------------------
/src/components/NumberDisplay/NumberDisplay.scss:
--------------------------------------------------------------------------------
1 | .NumberDisplay {
2 | width: 80px;
3 | height: 48px;
4 | color: #ff0701;
5 | background: black;
6 | text-align: center;
7 | font-size: 40px;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/NumberDisplay/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./NumberDisplay.scss";
4 |
5 | interface NumberDisplayProps {
6 | value: number;
7 | }
8 |
9 | const NumberDisplay: React.FC = ({ value }) => {
10 | return (
11 |
12 | {value < 0
13 | ? `-${Math.abs(value)
14 | .toString()
15 | .padStart(2, "0")}`
16 | : value.toString().padStart(3, "0")}
17 |
18 | );
19 | };
20 |
21 | export default NumberDisplay;
22 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const MAX_ROWS = 9;
2 | export const MAX_COLS = 9;
3 | export const NO_OF_BOMBS = 10;
4 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
8 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
9 | sans-serif;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | height: 100vh;
16 | }
17 |
18 | code {
19 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
20 | monospace;
21 | }
22 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import App from "./components/App";
5 |
6 | import "./index.scss";
7 |
8 | ReactDOM.render(, document.getElementById("root"));
9 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @mixin borders($left-top: white, $right-bottom: #7b7b7b) {
2 | border-bottom-color: $right-bottom;
3 | border-left-color: $left-top;
4 | border-right-color: $right-bottom;
5 | border-style: solid;
6 | border-top-color: $left-top;
7 | border-width: 4px;
8 | }
9 |
10 | @mixin buttons {
11 | &:active {
12 | border-bottom-color: white;
13 | border-left-color: #7b7b7b;
14 | border-right-color: white;
15 | border-top-color: #7b7b7b;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export enum CellValue {
2 | none,
3 | one,
4 | two,
5 | three,
6 | four,
7 | five,
8 | six,
9 | seven,
10 | eight,
11 | bomb
12 | }
13 |
14 | export enum CellState {
15 | open,
16 | visible,
17 | flagged
18 | }
19 |
20 | export type Cell = { value: CellValue; state: CellState; red?: boolean };
21 |
22 | export enum Face {
23 | smile = "😁",
24 | oh = "😮",
25 | lost = "😵",
26 | won = "😎"
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { MAX_COLS, MAX_ROWS, NO_OF_BOMBS } from "../constants";
2 | import { Cell, CellState, CellValue } from "../types";
3 |
4 | const grabAllAdjacentCells = (
5 | cells: Cell[][],
6 | rowParam: number,
7 | colParam: number
8 | ): {
9 | topLeftCell: Cell | null;
10 | topCell: Cell | null;
11 | topRightCell: Cell | null;
12 | leftCell: Cell | null;
13 | rightCell: Cell | null;
14 | bottomLeftCell: Cell | null;
15 | bottomCell: Cell | null;
16 | bottomRightCell: Cell | null;
17 | } => {
18 | const topLeftCell =
19 | rowParam > 0 && colParam > 0 ? cells[rowParam - 1][colParam - 1] : null;
20 | const topCell = rowParam > 0 ? cells[rowParam - 1][colParam] : null;
21 | const topRightCell =
22 | rowParam > 0 && colParam < MAX_COLS - 1
23 | ? cells[rowParam - 1][colParam + 1]
24 | : null;
25 | const leftCell = colParam > 0 ? cells[rowParam][colParam - 1] : null;
26 | const rightCell =
27 | colParam < MAX_COLS - 1 ? cells[rowParam][colParam + 1] : null;
28 | const bottomLeftCell =
29 | rowParam < MAX_ROWS - 1 && colParam > 0
30 | ? cells[rowParam + 1][colParam - 1]
31 | : null;
32 | const bottomCell =
33 | rowParam < MAX_ROWS - 1 ? cells[rowParam + 1][colParam] : null;
34 | const bottomRightCell =
35 | rowParam < MAX_ROWS - 1 && colParam < MAX_COLS - 1
36 | ? cells[rowParam + 1][colParam + 1]
37 | : null;
38 |
39 | return {
40 | topLeftCell,
41 | topCell,
42 | topRightCell,
43 | leftCell,
44 | rightCell,
45 | bottomLeftCell,
46 | bottomCell,
47 | bottomRightCell
48 | };
49 | };
50 |
51 | export const generateCells = (): Cell[][] => {
52 | let cells: Cell[][] = [];
53 |
54 | // generating all cells
55 | for (let row = 0; row < MAX_ROWS; row++) {
56 | cells.push([]);
57 | for (let col = 0; col < MAX_COLS; col++) {
58 | cells[row].push({
59 | value: CellValue.none,
60 | state: CellState.open
61 | });
62 | }
63 | }
64 |
65 | // randomly put 10 bombs
66 | let bombsPlaced = 0;
67 | while (bombsPlaced < NO_OF_BOMBS) {
68 | const randomRow = Math.floor(Math.random() * MAX_ROWS);
69 | const randomCol = Math.floor(Math.random() * MAX_COLS);
70 |
71 | const currentCell = cells[randomRow][randomCol];
72 | if (currentCell.value !== CellValue.bomb) {
73 | cells = cells.map((row, rowIndex) =>
74 | row.map((cell, colIndex) => {
75 | if (randomRow === rowIndex && randomCol === colIndex) {
76 | return {
77 | ...cell,
78 | value: CellValue.bomb
79 | };
80 | }
81 |
82 | return cell;
83 | })
84 | );
85 | bombsPlaced++;
86 | }
87 | }
88 |
89 | // calculate the numbers for each cell
90 | for (let rowIndex = 0; rowIndex < MAX_ROWS; rowIndex++) {
91 | for (let colIndex = 0; colIndex < MAX_COLS; colIndex++) {
92 | const currentCell = cells[rowIndex][colIndex];
93 | if (currentCell.value === CellValue.bomb) {
94 | continue;
95 | }
96 |
97 | let numberOfBombs = 0;
98 | const {
99 | topLeftCell,
100 | topCell,
101 | topRightCell,
102 | leftCell,
103 | rightCell,
104 | bottomLeftCell,
105 | bottomCell,
106 | bottomRightCell
107 | } = grabAllAdjacentCells(cells, rowIndex, colIndex);
108 |
109 | if (topLeftCell?.value === CellValue.bomb) {
110 | numberOfBombs++;
111 | }
112 | if (topCell?.value === CellValue.bomb) {
113 | numberOfBombs++;
114 | }
115 | if (topRightCell?.value === CellValue.bomb) {
116 | numberOfBombs++;
117 | }
118 | if (leftCell?.value === CellValue.bomb) {
119 | numberOfBombs++;
120 | }
121 | if (rightCell?.value === CellValue.bomb) {
122 | numberOfBombs++;
123 | }
124 | if (bottomLeftCell?.value === CellValue.bomb) {
125 | numberOfBombs++;
126 | }
127 | if (bottomCell?.value === CellValue.bomb) {
128 | numberOfBombs++;
129 | }
130 | if (bottomRightCell?.value === CellValue.bomb) {
131 | numberOfBombs++;
132 | }
133 |
134 | if (numberOfBombs > 0) {
135 | cells[rowIndex][colIndex] = {
136 | ...currentCell,
137 | value: numberOfBombs
138 | };
139 | }
140 | }
141 | }
142 |
143 | return cells;
144 | };
145 |
146 | export const openMultipleCells = (
147 | cells: Cell[][],
148 | rowParam: number,
149 | colParam: number
150 | ): Cell[][] => {
151 | const currentCell = cells[rowParam][colParam];
152 |
153 | if (
154 | currentCell.state === CellState.visible ||
155 | currentCell.state === CellState.flagged
156 | ) {
157 | return cells;
158 | }
159 |
160 | let newCells = cells.slice();
161 | newCells[rowParam][colParam].state = CellState.visible;
162 |
163 | const {
164 | topLeftCell,
165 | topCell,
166 | topRightCell,
167 | leftCell,
168 | rightCell,
169 | bottomLeftCell,
170 | bottomCell,
171 | bottomRightCell
172 | } = grabAllAdjacentCells(cells, rowParam, colParam);
173 |
174 | if (
175 | topLeftCell?.state === CellState.open &&
176 | topLeftCell.value !== CellValue.bomb
177 | ) {
178 | if (topLeftCell.value === CellValue.none) {
179 | newCells = openMultipleCells(newCells, rowParam - 1, colParam - 1);
180 | } else {
181 | newCells[rowParam - 1][colParam - 1].state = CellState.visible;
182 | }
183 | }
184 |
185 | if (topCell?.state === CellState.open && topCell.value !== CellValue.bomb) {
186 | if (topCell.value === CellValue.none) {
187 | newCells = openMultipleCells(newCells, rowParam - 1, colParam);
188 | } else {
189 | newCells[rowParam - 1][colParam].state = CellState.visible;
190 | }
191 | }
192 |
193 | if (
194 | topRightCell?.state === CellState.open &&
195 | topRightCell.value !== CellValue.bomb
196 | ) {
197 | if (topRightCell.value === CellValue.none) {
198 | newCells = openMultipleCells(newCells, rowParam - 1, colParam + 1);
199 | } else {
200 | newCells[rowParam - 1][colParam + 1].state = CellState.visible;
201 | }
202 | }
203 |
204 | if (leftCell?.state === CellState.open && leftCell.value !== CellValue.bomb) {
205 | if (leftCell.value === CellValue.none) {
206 | newCells = openMultipleCells(newCells, rowParam, colParam - 1);
207 | } else {
208 | newCells[rowParam][colParam - 1].state = CellState.visible;
209 | }
210 | }
211 |
212 | if (
213 | rightCell?.state === CellState.open &&
214 | rightCell.value !== CellValue.bomb
215 | ) {
216 | if (rightCell.value === CellValue.none) {
217 | newCells = openMultipleCells(newCells, rowParam, colParam + 1);
218 | } else {
219 | newCells[rowParam][colParam + 1].state = CellState.visible;
220 | }
221 | }
222 |
223 | if (
224 | bottomLeftCell?.state === CellState.open &&
225 | bottomLeftCell.value !== CellValue.bomb
226 | ) {
227 | if (bottomLeftCell.value === CellValue.none) {
228 | newCells = openMultipleCells(newCells, rowParam + 1, colParam - 1);
229 | } else {
230 | newCells[rowParam + 1][colParam - 1].state = CellState.visible;
231 | }
232 | }
233 |
234 | if (
235 | bottomCell?.state === CellState.open &&
236 | bottomCell.value !== CellValue.bomb
237 | ) {
238 | if (bottomCell.value === CellValue.none) {
239 | newCells = openMultipleCells(newCells, rowParam + 1, colParam);
240 | } else {
241 | newCells[rowParam + 1][colParam].state = CellState.visible;
242 | }
243 | }
244 |
245 | if (
246 | bottomRightCell?.state === CellState.open &&
247 | bottomRightCell.value !== CellValue.bomb
248 | ) {
249 | if (bottomRightCell.value === CellValue.none) {
250 | newCells = openMultipleCells(newCells, rowParam + 1, colParam + 1);
251 | } else {
252 | newCells[rowParam + 1][colParam + 1].state = CellState.visible;
253 | }
254 | }
255 |
256 | return newCells;
257 | };
258 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
|