├── 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 | 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 | 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 | minesweeper 6 | 7 | minesweeper 8 | 9 | minesweeper 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 | 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 |