├── src ├── react-app-env.d.ts ├── constants │ └── index.ts ├── components │ ├── NumberDisplay │ │ ├── NumberDisplay.scss │ │ └── index.tsx │ ├── App │ │ ├── App.scss │ │ └── index.tsx │ └── Button │ │ ├── Button.scss │ │ └── index.tsx ├── index.tsx ├── types │ └── index.ts ├── styles │ └── index.scss ├── index.scss └── utils │ └── index.ts ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── minesweeper.png ├── minesweeper2.png ├── minesweeper3.png ├── minesweeper-blueprint.png ├── .eslintrc.json ├── .idea ├── misc.xml ├── vcs.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── minesweeper-ts.iml └── workspace.xml ├── README.md ├── .gitignore ├── tsconfig.json ├── LICENSE └── package.json /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /minesweeper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angle943/react-minesweeper-ts/HEAD/minesweeper.png -------------------------------------------------------------------------------- /minesweeper2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angle943/react-minesweeper-ts/HEAD/minesweeper2.png -------------------------------------------------------------------------------- /minesweeper3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angle943/react-minesweeper-ts/HEAD/minesweeper3.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angle943/react-minesweeper-ts/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angle943/react-minesweeper-ts/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angle943/react-minesweeper-ts/HEAD/public/logo512.png -------------------------------------------------------------------------------- /minesweeper-blueprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angle943/react-minesweeper-ts/HEAD/minesweeper-blueprint.png -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const MAX_ROWS = 9; 2 | export const MAX_COLS = 9; 3 | export const NO_OF_BOMBS = 10; 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Minesweeper 💣 😵 2 | 3 | React Minesweeper made using React, Typescript, and SASS! 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/minesweeper-ts.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | You need to enable JavaScript to run this app. 31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------