├── src
├── scripts
│ ├── UIController.js
│ ├── index.js
│ └── Game.js
├── index.html
└── styles
│ └── index.scss
├── .travis.yml
├── .gitignore
├── .browserslistrc
├── .eslintrc
├── .dependabot
└── config.yml
├── .babelrc
├── README.md
├── webpack
├── webpack.config.dev.js
├── webpack.config.prod.js
└── webpack.common.js
└── package.json
/src/scripts/UIController.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "lts/*"
4 | - "node"
5 | script: npm run build
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # production
5 | build
6 |
7 | # misc
8 | .DS_Store
9 |
10 | npm-debug.log
11 | yarn-error.log
12 | yarn.lock
13 | .yarnclean
14 | .vscode
15 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | [production staging]
2 | >5%
3 | last 2 versions
4 | Firefox ESR
5 | not ie < 11
6 |
7 | [development]
8 | last 1 chrome version
9 | last 1 firefox version
10 | last 1 edge version
11 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true
5 | },
6 | "parserOptions": {
7 | "ecmaVersion": 6,
8 | "sourceType": "module"
9 | }
10 | // "rules": {
11 | // "semi": 2
12 | // }
13 | }
--------------------------------------------------------------------------------
/.dependabot/config.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | update_configs:
3 | - package_manager: "javascript"
4 | directory: "/"
5 | update_schedule: "weekly"
6 | automerged_updates:
7 | - match:
8 | dependency_type: "all"
9 | update_type: "semver:minor"
10 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "useBuiltIns": "usage",
7 | "corejs": "3.0.0"
8 | }
9 | ]
10 | ],
11 | "plugins": [
12 | "@babel/plugin-syntax-dynamic-import",
13 | "@babel/plugin-proposal-class-properties"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MineSweeper Game
2 |
3 | A web version of the old MineSweeper game.
4 |
5 | Check out the demo here 👉 [minesweeper.marc.dev](https://minesweeper.marc.dev)
6 |
7 | ### Installation
8 |
9 | ```
10 | npm install
11 | ```
12 |
13 | ### Start Dev Server
14 |
15 | ```
16 | npm start
17 | ```
18 |
19 | ### Build Prod Version
20 |
21 | ```
22 | npm run build
23 | ```
24 |
25 | ### Features:
26 |
27 | * Flag suspected mine
28 | * Recursively free up free space
29 | * Reveal a field
30 | * 5 levels
31 | * Customizable field size
--------------------------------------------------------------------------------
/webpack/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const Path = require('path');
2 | const Webpack = require('webpack');
3 | const merge = require('webpack-merge');
4 | const common = require('./webpack.common.js');
5 |
6 | module.exports = merge(common, {
7 | mode: 'development',
8 | devtool: 'cheap-eval-source-map',
9 | output: {
10 | chunkFilename: 'js/[name].chunk.js'
11 | },
12 | devServer: {
13 | inline: true
14 | },
15 | plugins: [
16 | new Webpack.DefinePlugin({
17 | 'process.env.NODE_ENV': JSON.stringify('development')
18 | })
19 | ],
20 | module: {
21 | rules: [
22 | {
23 | test: /\.js$/,
24 | include: Path.resolve(__dirname, '../src'),
25 | enforce: 'pre',
26 | loader: 'eslint-loader',
27 | options: {
28 | emitWarning: true,
29 | }
30 | },
31 | {
32 | test: /\.js$/,
33 | include: Path.resolve(__dirname, '../src'),
34 | loader: 'babel-loader'
35 | },
36 | {
37 | test: /\.s?css$/i,
38 | use: ['style-loader', 'css-loader?sourceMap=true', 'sass-loader']
39 | }
40 | ]
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/webpack/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const Webpack = require('webpack');
2 | const merge = require('webpack-merge');
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4 | const common = require('./webpack.common.js');
5 |
6 | module.exports = merge(common, {
7 | mode: 'production',
8 | devtool: 'source-map',
9 | stats: 'errors-only',
10 | bail: true,
11 | output: {
12 | filename: 'js/[name].[chunkhash:8].js',
13 | chunkFilename: 'js/[name].[chunkhash:8].chunk.js'
14 | },
15 | plugins: [
16 | new Webpack.DefinePlugin({
17 | 'process.env.NODE_ENV': JSON.stringify('production')
18 | }),
19 | new Webpack.optimize.ModuleConcatenationPlugin(),
20 | new MiniCssExtractPlugin({
21 | filename: 'bundle.css'
22 | })
23 | ],
24 | module: {
25 | rules: [
26 | {
27 | test: /\.js$/,
28 | exclude: /node_modules/,
29 | use: 'babel-loader'
30 | },
31 | {
32 | test: /\.s?css/i,
33 | use : [
34 | MiniCssExtractPlugin.loader,
35 | 'css-loader',
36 | 'sass-loader'
37 | ]
38 | }
39 | ]
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/webpack/webpack.common.js:
--------------------------------------------------------------------------------
1 | const Path = require('path');
2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
3 | const CopyWebpackPlugin = require('copy-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | module.exports = {
7 | entry: {
8 | app: Path.resolve(__dirname, '../src/scripts/index.js')
9 | },
10 | output: {
11 | path: Path.join(__dirname, '../build'),
12 | filename: 'js/[name].js'
13 | },
14 | optimization: {
15 | splitChunks: {
16 | chunks: 'all',
17 | name: false
18 | }
19 | },
20 | plugins: [
21 | new CleanWebpackPlugin(),
22 | new CopyWebpackPlugin([
23 | { from: Path.resolve(__dirname, '../public'), to: 'public' }
24 | ]),
25 | new HtmlWebpackPlugin({
26 | template: Path.resolve(__dirname, '../src/index.html')
27 | })
28 | ],
29 | resolve: {
30 | alias: {
31 | '~': Path.resolve(__dirname, '../src')
32 | }
33 | },
34 | module: {
35 | rules: [
36 | {
37 | test: /\.mjs$/,
38 | include: /node_modules/,
39 | type: 'javascript/auto'
40 | },
41 | {
42 | test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
43 | use: {
44 | loader: 'file-loader',
45 | options: {
46 | name: '[path][name].[ext]'
47 | }
48 | }
49 | },
50 | ]
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minesweeper",
3 | "version": "1.0.0",
4 | "description": "MineSweeper Game",
5 | "scripts": {
6 | "build": "cross-env NODE_ENV=production webpack --config webpack/webpack.config.prod.js --colors",
7 | "start": "webpack-dev-server --open --config webpack/webpack.config.dev.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/wbkd/webpack-starter.git"
12 | },
13 | "keywords": [
14 | "webpack",
15 | "startkit",
16 | "frontend",
17 | "es6",
18 | "javascript",
19 | "webdev"
20 | ],
21 | "author": "webkid.io",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/wbkd/webpack-starter/issues"
25 | },
26 | "devDependencies": {
27 | "@babel/core": "^7.8.4",
28 | "@babel/plugin-proposal-class-properties": "^7.8.3",
29 | "@babel/plugin-syntax-dynamic-import": "^7.8.3",
30 | "@babel/preset-env": "^7.8.4",
31 | "babel-loader": "^8.0.6",
32 | "clean-webpack-plugin": "^3.0.0",
33 | "copy-webpack-plugin": "^5.1.1",
34 | "cross-env": "^6.0.3",
35 | "css-loader": "^3.4.2",
36 | "eslint": "^6.8.0",
37 | "eslint-loader": "^3.0.3",
38 | "file-loader": "^4.3.0",
39 | "html-webpack-plugin": "^4.0.0-beta.11",
40 | "mini-css-extract-plugin": "^0.8.0",
41 | "node-sass": "^4.13.1",
42 | "sass-loader": "^8.0.2",
43 | "style-loader": "^1.1.3",
44 | "webpack": "^4.41.5",
45 | "webpack-cli": "^3.3.10",
46 | "webpack-dev-server": "^3.10.3",
47 | "webpack-merge": "^4.2.2"
48 | },
49 | "dependencies": {
50 | "@babel/polyfill": "^7.8.3",
51 | "@fortawesome/fontawesome-free": "^5.12.1",
52 | "canvas-confetti": "^1.0.3",
53 | "core-js": "^3.6.4"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Web-Based MineSweeper Game | Built by Marc Backes
7 |
8 |
9 |
31 |
32 |
33 |
34 |
35 |
Start game above 👆
36 |
37 |
Game Over 😞
38 | You won! 🎉
39 |
40 |
41 |
42 |
43 |
44 |
45 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
3 | color: #555;
4 | margin: 0;
5 | padding-top: 83px;
6 | }
7 |
8 | h2 {
9 | margin: 0.5rem;
10 | }
11 |
12 | .inputGroup {
13 | margin: 1rem 0;
14 | input {
15 | padding: 0.5rem;
16 | border-radius: 5px;
17 | border: solid 1px #ddd;
18 | outline: none;
19 | font-size: 1rem;
20 | width: 3rem;
21 | text-align: center;
22 | padding-left: 15px;
23 | }
24 | label {
25 | font-size: 1rem;
26 | }
27 | > * {
28 | margin: 0 0.5rem;
29 | }
30 | }
31 |
32 | .levels {
33 | line-height: 2.5rem;
34 |
35 | input[type='radio'] {
36 | display: none;
37 | + label {
38 | border-radius: 0.5rem;
39 | font-size: 1.5rem;
40 | padding: 0.5rem 0.75rem;
41 | cursor: pointer;
42 | &:hover {
43 | background: rgba(255, 255, 255, 0.1);
44 | }
45 | }
46 | &:checked + label {
47 | background: #ddd;
48 | }
49 | }
50 | }
51 |
52 | button {
53 | background: tomato;
54 | color: #fff;
55 | outline: none;
56 | border: none;
57 | border-radius: 5px;
58 | font-size: 1rem;
59 | padding: 1rem 2rem;
60 | cursor: pointer;
61 | }
62 |
63 | .controls {
64 | background: rgb(21, 32, 43);
65 | color: #fff;
66 | width: 100%;
67 | display: flex;
68 | align-items: center;
69 | position: fixed;
70 | top: 0;
71 | z-index: 1;
72 | > * {
73 | margin: 1rem;
74 | }
75 | }
76 |
77 | .wrapper {
78 | display: flex;
79 | justify-content: center;
80 | align-items: center;
81 | flex-direction: column;
82 | min-height: 100vh;
83 | width: 100%;
84 | position: absolute;
85 | top: 0;
86 | z-index: 0;
87 |
88 | .filler {
89 | width: 100%;
90 | height: 83px;
91 | }
92 |
93 | .game {
94 | flex: 1;
95 | display: flex;
96 | justify-content: center;
97 | align-items: center;
98 | text-align: center;
99 | &.over {
100 | .board {
101 | pointer-events: none;
102 | }
103 | h2.game-over {
104 | display: block;
105 | }
106 | }
107 |
108 | &.won {
109 | .board {
110 | pointer-events: none;
111 | }
112 | h2.game-won {
113 | display: block;
114 | }
115 | }
116 |
117 | h2 {
118 | &.game-over {
119 | color: tomato;
120 | display: none;
121 | }
122 | &.game-won {
123 | color: MediumSpringGreen;
124 | display: none;
125 | }
126 | }
127 | }
128 |
129 | .board {
130 | display: grid;
131 | grid-template-columns: repeat(5, 1fr);
132 | grid-gap: 5px;
133 | margin: 1rem;
134 |
135 | .field {
136 | width: 30px;
137 | height: 30px;
138 | border-radius: 5px;
139 | background: #ddd;
140 | cursor: pointer;
141 | display: flex;
142 | justify-content: center;
143 | align-items: center;
144 | &:hover {
145 | background: #ccc;
146 | }
147 |
148 | &.reveiled {
149 | background: #eee;
150 | }
151 |
152 | &.mine {
153 | background: tomato;
154 | color: #fff;
155 | }
156 |
157 | &.flagged {
158 | color: tomato;
159 | }
160 | }
161 | }
162 |
163 | .boardStatus {
164 | color: #888;
165 | border-radius: 5px;
166 | margin: 1rem;
167 | padding: 1rem;
168 | display: none;
169 | }
170 |
171 | footer {
172 | text-align: center;
173 | color: #aaa;
174 | a {
175 | text-decoration: none;
176 | color: #aaa;
177 | &:hover {
178 | color: tomato;
179 | }
180 | }
181 | }
182 | }
183 |
184 | @media (max-width: 600px) {
185 | .controls {
186 | overflow-y: scroll;
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/scripts/index.js:
--------------------------------------------------------------------------------
1 | import '../styles/index.scss'
2 | import '@fortawesome/fontawesome-free/js/all'
3 | import confetti from 'canvas-confetti'
4 |
5 | import { Game } from './Game'
6 |
7 | // Elements in the DOM that need to be accessed during the game
8 | const gameElement = document.getElementById('game')
9 | const boardElement = document.getElementById('board')
10 | const boardStatus = document.getElementById('boardStatus')
11 | const newGameButton = document.getElementById('newGameButton')
12 | const dimensionInput = document.getElementById('dimensionInput')
13 | const minesLeftHeader = document.getElementById('minesLeftHeader')
14 | const minesLeftText = document.getElementById('minesLeft')
15 | const gameStartHeader = document.getElementById('game-start')
16 |
17 | // The game variable. Here lies all the logic
18 | let game
19 |
20 | // Little bit self-promotion never hurt anyone 😉
21 | console.log('🔥 Build with love by Marc Backes 🔥')
22 | console.log(' 👉 Website: https://marc.dev')
23 | console.log(' 👉 Twitter: https://twitter.com/_marcba')
24 | console.log(' 👉 Codepen: https://codepen.io/_marcba')
25 | console.log(' 👉 Github: https://github.com/themarcba')
26 |
27 | // Reveal a field for the selected position
28 | const revealField = (x, y) => {
29 | game.reveal(x, y)
30 | if (game.isGameOver()) {
31 | gameOver()
32 | }
33 | }
34 |
35 | // Toggle the flag-status on a field for the selected position
36 | const toggleFlagField = (x, y) => {
37 | game.toggleFlag(x, y)
38 | updateUI()
39 | }
40 |
41 | // When the game is over (lost)
42 | const gameOver = () => {
43 | gameElement.classList.add('over')
44 | game.resolve()
45 | }
46 |
47 | // When the game has been won
48 | const gameWon = () => {
49 | gameElement.classList.add('won')
50 | // Throw some confetti! 🎉
51 | confetti({
52 | particleCount: 200,
53 | spread: 100
54 | })
55 | }
56 |
57 | // Create a new game
58 | const newGame = () => {
59 | // A 5x5 board is too small for the mine-placing rules in the Game class
60 | if (dimensionInput.value < 5) return alert('There must be at least a field dimension of 5')
61 |
62 | // Read desired board dimensions from the input field
63 | const dimensions = parseInt(dimensionInput.value)
64 |
65 | // Get the selected level from the emoji bar (radio buttons) (👶,😎,😐,🥵,😈)
66 | const level = document.querySelector('input[name="level"]:checked').value
67 |
68 | // Remove winning or losing state of the board
69 | gameElement.classList.remove('over')
70 | gameElement.classList.remove('won')
71 |
72 | // Create the Game object which contains all the logic
73 | game = new Game(dimensions, level)
74 |
75 | // Set CSS size for the board
76 | boardElement.style.gridTemplateColumns = `repeat(${dimensions}, 1fr)`
77 |
78 | // Empty board, because new elements will be created
79 | boardElement.innerHTML = ''
80 |
81 | // Create initial field elements
82 | game.fields.forEach((rowField, rowFieldIndex) => {
83 | // Rows
84 | rowField.forEach((columnField, columnFieldIndex) => {
85 | // Columns
86 |
87 | // Create & initialize a field element
88 | let newField = document.createElement('div')
89 | newField.id = `field_${rowFieldIndex}_${columnFieldIndex}`
90 | newField.className = 'field'
91 |
92 | // Left click event on field (reveal)
93 | newField.addEventListener('click', ev => {
94 | revealField(rowFieldIndex, columnFieldIndex)
95 | updateUI()
96 | })
97 |
98 | // Right click event on field (flag)
99 | newField.addEventListener('contextmenu', ev => {
100 | ev.preventDefault()
101 | toggleFlagField(rowFieldIndex, columnFieldIndex)
102 | updateUI()
103 | })
104 |
105 | // Append created field to board
106 | boardElement.appendChild(newField)
107 | })
108 | })
109 |
110 | // Update the interface!
111 | updateUI()
112 | }
113 |
114 | // Updates the interface
115 | // - Reads fields and sets CSS classes and content depending on field state
116 | // - Checks if game is won or lost to display the respective messages
117 | // - Hides "mines left" indicator when game is over
118 | const updateUI = () => {
119 | minesLeftText.innerText = game.getMineCount() - game.getFlaggedFields().length
120 | game.fields.forEach((rowField, rowFieldIndex) => {
121 | rowField.forEach((columnField, columnFieldIndex) => {
122 | const fieldElement = document.getElementById(`field_${rowFieldIndex}_${columnFieldIndex}`)
123 | const field = game.getField(rowFieldIndex, columnFieldIndex)
124 |
125 | if (field.isRevealed) {
126 | fieldElement.className = 'reveiled field'
127 | if (field.hasMine) {
128 | fieldElement.classList = 'mine field'
129 | fieldElement.innerHTML = ''
130 | } else if (field.hint) {
131 | fieldElement.innerHTML = field.hint
132 | } else {
133 | fieldElement.innerHTML = ''
134 | }
135 | } else if (field.isFlagged) {
136 | fieldElement.classList = 'flagged field'
137 | fieldElement.innerHTML = ''
138 | } else {
139 | fieldElement.innerHTML = ''
140 | }
141 | })
142 | })
143 |
144 | if (game.isGameWon()) {
145 | gameWon()
146 | }
147 |
148 | boardStatus.style.display = game.isGameCreated() ? 'block' : 'none'
149 | minesLeftHeader.style.display = game.isGameCreated() && !game.isGameWon() & !game.isGameOver() ? 'block' : 'none'
150 | gameStartHeader.style.display = game.isGameCreated() ? 'none' : 'block'
151 | }
152 |
153 | // Add event listeners
154 | const attachEvents = () => {
155 | // "New Game" Button
156 | newGameButton.addEventListener('click', newGame)
157 | }
158 |
159 | attachEvents()
160 |
--------------------------------------------------------------------------------
/src/scripts/Game.js:
--------------------------------------------------------------------------------
1 | // Fields that are around a specific field, defined by x, y
2 | const neighborFields = (x, y) => {
3 | return [
4 | [x - 1, y - 1],
5 | [x, y - 1],
6 | [x + 1, y - 1],
7 | [x - 1, y],
8 | [x + 1, y],
9 | [x - 1, y + 1],
10 | [x, y + 1],
11 | [x + 1, y + 1]
12 | ]
13 | }
14 |
15 | class Game {
16 | // Creates the game, but doesn't initialize the mines yet, because they are placed depending on the first click of the user
17 | constructor(dimensions = 10, difficulty = 1) {
18 | // In the worst case, there are mines everywhere, except the first clicked field and it's neighbor fields
19 | const maximumAllowedMines = dimensions * dimensions - 9
20 |
21 | // Calculate mine count depending on the number field dimensions and difficulty
22 | this.mineCount = Math.floor(
23 | dimensions * difficulty <= maximumAllowedMines ? dimensions * difficulty : maximumAllowedMines
24 | )
25 |
26 | this.dimensions = dimensions
27 | this.gameCreated = true
28 |
29 | // Initialize fields, fill them with empty objects
30 | this.fields = new Array(dimensions).fill({})
31 | window.__fields = this.fields
32 | this.fields.forEach((field, index) => {
33 | this.fields[index] = new Array(dimensions).fill({})
34 | })
35 | }
36 |
37 | // When the user makes their first click, the game starts
38 | // The positioning of the mines is determined based on this first clicked field position
39 | startGame(x, y) {
40 | this.startingPoint = [x, y]
41 | this.gameStarted = true
42 |
43 | // Set mines within the field
44 | for (let i = 0; i < this.mineCount; i++) {
45 | const [randomX, randomY] = this.getRandomFreeField()
46 | this.fields[randomX][randomY] = { hasMine: true }
47 | }
48 | }
49 |
50 | // Determines if a mine can be placed on the given position
51 | // Reasons for not being able to place a mine
52 | // - It's the first clicked field
53 | // - It's one of the neigboring fields of the first clicked field
54 | // - There is already a mine on that field
55 | canPlaceMine(x, y) {
56 | const [startX, startY] = this.startingPoint
57 |
58 | // Is it the first clicked field?
59 | const isStartingPoint = x === startX && y === startY
60 |
61 | // Is it a neighboring field?
62 | const isNeighboringField = neighborFields(startX, startY).some(([_x, _y]) => {
63 | return x == _x && y == _y
64 | })
65 |
66 | // Does it already have a mine?
67 | const hasAlreadyMine = this.fields[x][y].hasMine === true
68 |
69 | return !isNeighboringField && !hasAlreadyMine && !isStartingPoint
70 | }
71 |
72 | // Generate a random field. Generate random field positions until a valid field is found (determined by canPlaceMine)
73 | getRandomFreeField() {
74 | let randomX, randomY
75 | // Repeat until valid field
76 | do {
77 | randomX = Math.floor(Math.random() * this.dimensions)
78 | randomY = Math.floor(Math.random() * this.dimensions)
79 | } while (!this.canPlaceMine(randomX, randomY))
80 |
81 | return [randomX, randomY]
82 | }
83 |
84 | // Returns a list of flagged fields
85 | getFlaggedFields() {
86 | return this.fields.flat().filter(field => field.isFlagged)
87 | }
88 |
89 | // Returns a list of revealed fields
90 | getRevealedFields() {
91 | return this.fields.flat().filter(field => field.isRevealed)
92 | }
93 |
94 | // Returns the number of mines placed in the field
95 | getMineCount() {
96 | return this.mineCount
97 | }
98 |
99 | // Returns whether the game is won.
100 | // A game is won when both is true:
101 | // - All non-mined fields are revealed
102 | // - All mined fields are flagged
103 | isGameWon() {
104 | const nonMinedFields = this.fields.flat().filter(field => !field.hasMine)
105 | const minedFields = this.fields.flat().filter(field => field.hasMine)
106 |
107 | return nonMinedFields.every(field => field.isRevealed) && minedFields.every(field => field.isFlagged)
108 | }
109 |
110 | // Returns when the game is list
111 | // A game is lost when a mine has been revealed
112 | isGameOver() {
113 | return this.getRevealedFields().filter(field => field.hasMine).length > 0
114 | }
115 |
116 | // Returns if the game has been initialized
117 | isGameCreated() {
118 | return this.gameCreated
119 | }
120 |
121 | // Calculates how many neighbor fields have mines
122 | getHint(x, y) {
123 | const neighborsWithMine = neighborFields(x, y).reduce(
124 | (accumulator, [x, y]) => accumulator + (this.getField(x, y) && this.getField(x, y).hasMine ? 1 : 0),
125 | 0
126 | )
127 | return neighborsWithMine
128 | }
129 |
130 | // Reveals a field
131 | // - Sets the revealed status to the field (isRevealed)
132 | // - If the field is not neighboring a mine, recursively reveal the surrounding fields
133 | reveal(x, y) {
134 | // If the game has not already been started, it gets started (first click)
135 | if (!this.gameStarted) this.startGame(x, y)
136 |
137 | const hint = this.getHint(x, y)
138 | this.fields[x][y] = { ...this.fields[x][y], isRevealed: true, hint }
139 |
140 | // This is the stop-condition for the recursive call
141 | if (hint === 0) {
142 | // Check all neighboring fields
143 | neighborFields(x, y).forEach(([x, y]) => {
144 | const field = this.getField(x, y)
145 | // Reveal if the field exists, is not revealed yet and has no mine
146 | if (field !== null && !field.isRevealed && !field.hasMine) {
147 | this.reveal(x, y)
148 | }
149 | })
150 | }
151 | return this.fields[x][y]
152 | }
153 |
154 | // Resolve the entire game. All fields are being revealed
155 | resolve() {
156 | this.fields.forEach((rowField, rowFieldIndex) => {
157 | rowField.forEach((columnField, columnFieldIndex) => {
158 | const hint = this.getHint(columnFieldIndex, rowFieldIndex)
159 | this.fields[columnFieldIndex][rowFieldIndex] = {
160 | ...this.fields[columnFieldIndex][rowFieldIndex],
161 | isRevealed: true,
162 | hint
163 | }
164 | })
165 | })
166 | }
167 |
168 | // Flag a field. (Set the isFlagged property)
169 | flag(x, y) {
170 | if (!this.fields[x][y].isRevealed) {
171 | this.fields[x][y] = { ...this.fields[x][y], isFlagged: true }
172 | }
173 | return this.fields[x][y]
174 | }
175 |
176 | // Unflag a field. Opposite of flag()
177 | unflag(x, y) {
178 | this.fields[x][y] = { ...this.fields[x][y], isFlagged: false }
179 | return this.fields[x][y]
180 | }
181 |
182 | // Toggle the flagged-status of a field
183 | toggleFlag(x, y) {
184 | this.fields[x][y].isFlagged ? this.unflag(x, y) : this.flag(x, y)
185 | }
186 |
187 | // Return a field with the given position
188 | getField(x, y) {
189 | // Only return a field if it's in bounds
190 | if (x >= 0 && x < this.dimensions && y >= 0 && y < this.dimensions) {
191 | return this.fields[x][y]
192 | }
193 | // Return null if it doesn't exist (out of bounds)
194 | else {
195 | return null
196 | }
197 | }
198 | }
199 |
200 | export { Game }
201 |
--------------------------------------------------------------------------------