├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── demo ├── minesweeper-1.png ├── minesweeper-2.png ├── minesweeper-3.png └── minesweeper-4.png ├── package.json ├── public ├── favicon.ico ├── flag.svg └── index.html ├── src ├── App.vue ├── assets │ ├── logo.png │ └── logo.svg ├── components │ ├── GameOver.vue │ ├── GitHub.vue │ ├── Grid.vue │ └── Timer.vue ├── main.js ├── plugins │ └── vuetify.js └── store │ ├── index.js │ └── modules │ ├── game.js │ ├── grid.js │ └── timer.js ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"], 7 | parserOptions: { 8 | parser: "babel-eslint" 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ahmed Ashraf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minesweeper 2 | 3 | ## 📝 Table of Contents 4 | - [About](#about) 5 | - [Screenshots](#screenshots) 6 | - [Technology](#tech) 7 | - [Install](#Install) 8 | 9 | ## 🧐 About 10 | Minesweeper game built with Vue, Vuex, Vuetify, and SCSS. You can try it [here](https://aashrafh.github.io/minesweeper/). 11 | 12 | ## 🎥 Screenshots 13 |
14 |

15 | Image Demo
16 | Image Demo
17 | Image Demo
18 | Image Demo 19 |

20 |
21 | 22 | ## ⛏️ Built Using 23 | - [Vue](https://vuejs.org/) 24 | - [Vue CLI](https://cli.vuejs.org/) 25 | - [Vuex](https://vuex.vuejs.org/) 26 | - [Vuetify](https://vuetifyjs.com/) 27 | 28 | 29 | ## 🏁 Install 30 | ### Project setup 31 | ``` 32 | yarn install 33 | ``` 34 | 35 | ### Compiles and hot-reloads for development 36 | ``` 37 | yarn serve 38 | ``` 39 | 40 | ### Compiles and minifies for production 41 | ``` 42 | yarn build 43 | ``` 44 | 45 | ### Lints and fixes files 46 | ``` 47 | yarn lint 48 | ``` 49 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /demo/minesweeper-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aashrafh/minesweeper/a40fc45e72934f3cac3020a05339ec90d6be7219/demo/minesweeper-1.png -------------------------------------------------------------------------------- /demo/minesweeper-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aashrafh/minesweeper/a40fc45e72934f3cac3020a05339ec90d6be7219/demo/minesweeper-2.png -------------------------------------------------------------------------------- /demo/minesweeper-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aashrafh/minesweeper/a40fc45e72934f3cac3020a05339ec90d6be7219/demo/minesweeper-3.png -------------------------------------------------------------------------------- /demo/minesweeper-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aashrafh/minesweeper/a40fc45e72934f3cac3020a05339ec90d6be7219/demo/minesweeper-4.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minesweeper", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "vue": "^2.6.11", 13 | "vuetify": "^2.2.11", 14 | "vuex": "^3.4.0" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "~4.5.0", 18 | "@vue/cli-plugin-eslint": "~4.5.0", 19 | "@vue/cli-plugin-vuex": "~4.5.0", 20 | "@vue/cli-service": "~4.5.0", 21 | "@vue/eslint-config-prettier": "^6.0.0", 22 | "babel-eslint": "^10.1.0", 23 | "eslint": "^6.7.2", 24 | "eslint-plugin-prettier": "^3.1.3", 25 | "eslint-plugin-vue": "^6.2.2", 26 | "prettier": "^1.19.1", 27 | "sass": "^1.19.0", 28 | "sass-loader": "^8.0.0", 29 | "vue-cli-plugin-vuetify": "~2.0.7", 30 | "vue-template-compiler": "^2.6.11", 31 | "vuetify-loader": "^1.3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aashrafh/minesweeper/a40fc45e72934f3cac3020a05339ec90d6be7219/public/favicon.ico -------------------------------------------------------------------------------- /public/flag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Minesweeper 9 | 13 | 17 | 18 | 19 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | 35 | 36 | /** 37 | app background 38 | card theme 39 | on hover card 40 | */ -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aashrafh/minesweeper/a40fc45e72934f3cac3020a05339ec90d6be7219/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 -------------------------------------------------------------------------------- /src/components/GameOver.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/GitHub.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Grid.vue: -------------------------------------------------------------------------------- 1 | 37 | 67 | 68 | 111 | 112 | -------------------------------------------------------------------------------- /src/components/Timer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | 32 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import store from "./store/index"; 4 | import vuetify from "./plugins/vuetify"; 5 | 6 | Vue.config.productionTip = false; 7 | 8 | new Vue({ 9 | store, 10 | vuetify, 11 | render: h => h(App) 12 | }).$mount("#app"); 13 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuetify from "vuetify/lib"; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({}); 7 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import game from "./modules/game"; 4 | import grid from "./modules/grid"; 5 | import timer from "./modules/timer"; 6 | 7 | Vue.use(Vuex); 8 | 9 | const strictMode = process.env.NODE_ENV !== "production"; 10 | export default new Vuex.Store({ 11 | strict: strictMode, 12 | modules: { 13 | game, 14 | grid, 15 | timer 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /src/store/modules/game.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | won: false, 3 | lost: false 4 | }; 5 | 6 | const actions = { 7 | restartGame({ commit }) { 8 | commit("restartGame"); 9 | } 10 | }; 11 | 12 | const mutations = { 13 | restartGame(state) { 14 | state.won = false; 15 | state.lost = false; 16 | } 17 | }; 18 | 19 | const getters = { 20 | isWin(state) { 21 | return state.won; 22 | }, 23 | isLose(state) { 24 | return state.lost; 25 | } 26 | }; 27 | 28 | export default { 29 | namespaced: true, 30 | state, 31 | mutations, 32 | actions, 33 | getters 34 | }; 35 | -------------------------------------------------------------------------------- /src/store/modules/grid.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | cells: [0, 0, 0, 0, "X"], 3 | pattern: null, 4 | dim: 8, 5 | initTimer: null 6 | }; 7 | 8 | const actions = { 9 | async setPattern({ state, dispatch, commit }) { 10 | const pattern = []; 11 | const dim = state.dim; 12 | 13 | for (let row = 0; row < dim; row++) { 14 | const colPattern = await dispatch("setColPattern", { 15 | row, 16 | pattern 17 | }); 18 | 19 | pattern.push(colPattern); 20 | } 21 | 22 | commit("setPattern", pattern); 23 | }, 24 | 25 | async setColPattern({ state }, { row, pattern }) { 26 | const dim = state.dim; 27 | const colPattern = []; 28 | 29 | for (let col = 0; col < dim; col++) { 30 | const randomIdx = Math.floor(Math.random() * state.cells.length); 31 | let cell = state.cells[randomIdx]; 32 | 33 | let prevCell = colPattern[col - 1]; 34 | if (colPattern[col - 1]) { 35 | if (typeof cell === "string" && typeof prevCell.data === "number") 36 | colPattern[col - 1].data = prevCell.data + 1; 37 | if (typeof cell === "number" && typeof prevCell.data === "string") 38 | cell += 1; 39 | } 40 | 41 | if (row > 0) { 42 | const prevUpperLeft = pattern[row - 1][col - 1]; 43 | const prevUpperMid = pattern[row - 1][col]; 44 | const prevUpperRight = pattern[row - 1][col + 1]; 45 | 46 | if (prevUpperLeft) { 47 | if ( 48 | typeof cell === "string" && 49 | typeof prevUpperLeft.data === "number" 50 | ) 51 | pattern[row - 1][col - 1].data = prevUpperLeft.data + 1; 52 | 53 | if ( 54 | typeof cell === "number" && 55 | typeof prevUpperLeft.data === "string" 56 | ) 57 | cell += 1; 58 | } 59 | 60 | if (prevUpperMid) { 61 | if (typeof cell === "string" && typeof prevUpperMid.data === "number") 62 | pattern[row - 1][col].data = prevUpperMid.data + 1; 63 | 64 | if (typeof cell === "number" && typeof prevUpperMid.data === "string") 65 | cell += 1; 66 | } 67 | 68 | if (prevUpperRight) { 69 | if ( 70 | typeof cell === "string" && 71 | typeof prevUpperRight.data === "number" 72 | ) 73 | pattern[row - 1][col + 1].data = prevUpperRight.data + 1; 74 | 75 | if ( 76 | typeof cell === "number" && 77 | typeof prevUpperRight.data === "string" 78 | ) 79 | cell += 1; 80 | } 81 | } 82 | 83 | colPattern.push({ 84 | data: cell, 85 | display: false, 86 | flagged: false, 87 | bomb: typeof cell === "string" 88 | }); 89 | } 90 | return colPattern; 91 | }, 92 | openCell({ state, dispatch, commit, rootState }, { row, col }) { 93 | let pattern = state.pattern; 94 | if (!pattern[row] || !pattern[row][col]) return; 95 | if (pattern[row][col].flagged) return; 96 | if (!rootState.timer.initTimer) commit("openTimer", rootState); 97 | 98 | let cell = pattern[row][col]; 99 | if (cell.data === 0) { 100 | if (cell.display) return; 101 | dispatch("floodFill", { 102 | cell, 103 | row, 104 | col 105 | }); 106 | } 107 | 108 | if (cell.bomb) { 109 | commit("loseGame", { state, rootState }); 110 | } 111 | 112 | commit("openCell", cell); 113 | }, 114 | flagCell({ commit, state }, { row, col }) { 115 | let cell = state.pattern[row][col]; 116 | if (cell.display) return; 117 | commit("flagCell", { cell }); 118 | }, 119 | floodFill({ dispatch, commit }, { cell, row, col }) { 120 | commit("openCell", cell); 121 | dispatch("openCell", { row, col: col + 1 }); 122 | dispatch("openCell", { row, col: col - 1 }); 123 | dispatch("openCell", { row: row + 1, col }); 124 | dispatch("openCell", { row: row - 1, col }); 125 | }, 126 | checkWin({ commit, state, rootState }) { 127 | const pattern = state.pattern; 128 | const numbers = [].concat(...pattern).filter(cell => { 129 | return !cell.bomb && cell.data > 0; 130 | }); 131 | const openedCells = [].concat(...pattern).filter(cell => { 132 | return cell.data > 0 && cell.display; 133 | }); 134 | 135 | if (numbers.length === openedCells.length) commit("winGame", rootState); 136 | } 137 | }; 138 | 139 | const mutations = { 140 | setPattern(state, pattern) { 141 | state.pattern = pattern; 142 | }, 143 | openCell(_, cell) { 144 | cell.display = true; 145 | }, 146 | flagCell(_, { cell }) { 147 | cell.flagged = !cell.flagged; 148 | }, 149 | openTimer(_, rootState) { 150 | rootState.timer.initTimer = new Date().getTime(); 151 | }, 152 | winGame(_, rootState) { 153 | rootState.game.won = true; 154 | rootState.timer.stopTime = true; 155 | }, 156 | loseGame(_, { state, rootState }) { 157 | rootState.game.lost = true; 158 | state.pattern.map(cell => { 159 | cell.display = true; 160 | }); 161 | rootState.timer.stopTime = true; 162 | } 163 | }; 164 | 165 | const getters = { 166 | getPattern(state) { 167 | return state.pattern; 168 | }, 169 | getSize(state) { 170 | return state.dim; 171 | } 172 | }; 173 | export default { 174 | namespaced: true, 175 | state, 176 | mutations, 177 | actions, 178 | getters 179 | }; 180 | -------------------------------------------------------------------------------- /src/store/modules/timer.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | hrs: 0, 3 | mins: 0, 4 | seconds: 0, 5 | dividers: { 6 | day: 1000 * 60 * 60 * 24, 7 | hrs: 1000 * 60 * 60, 8 | mins: 1000 * 60, 9 | seconds: 1000 10 | }, 11 | stopTime: false, 12 | initTimer: 0 13 | }; 14 | 15 | const actions = { 16 | async setTimer({ dispatch, state }) { 17 | let time = setInterval(() => { 18 | if (state.initTimer) { 19 | const diff = Date.now() - state.initTimer; 20 | dispatch("interval", diff); 21 | } 22 | if (state.stopTime) clearInterval(time); 23 | }, 1000); 24 | return time; 25 | }, 26 | async interval({ dispatch }, diff) { 27 | const hrs = await dispatch("setHours", diff); 28 | const mins = await dispatch("setMinutes", diff); 29 | const seconds = await dispatch("setSeconds", diff); 30 | return (hrs > 0 ? hrs + ":" : "") + mins + ":" + seconds; 31 | }, 32 | setHours({ state, commit }, diff) { 33 | const hrs = Math.floor((diff % state.dividers.day) / state.dividers.hrs); 34 | commit("setHours", hrs); 35 | return hrs; 36 | }, 37 | setMinutes({ state, commit }, diff) { 38 | const mins = Math.floor((diff % state.dividers.hrs) / state.dividers.mins); 39 | commit("setMinutes", mins); 40 | return mins; 41 | }, 42 | setSeconds({ state, commit }, diff) { 43 | const seconds = Math.floor( 44 | (diff % state.dividers.mins) / state.dividers.seconds 45 | ); 46 | commit("setSeconds", seconds); 47 | return seconds; 48 | }, 49 | restartTime({ commit, dispatch }) { 50 | commit("restartTime"); 51 | dispatch("setTimer"); 52 | } 53 | }; 54 | 55 | const mutations = { 56 | setHours(state, hrs) { 57 | state.hrs = hrs; 58 | }, 59 | setMinutes(state, mins) { 60 | state.mins = mins; 61 | }, 62 | setSeconds(state, seconds) { 63 | state.seconds = seconds; 64 | }, 65 | restartTime(state) { 66 | state.stopTime = false; 67 | state.initTimer = 0; 68 | state.hrs = 0; 69 | state.min = 0; 70 | state.seconds = 0; 71 | } 72 | }; 73 | 74 | const getters = { 75 | getHours(state) { 76 | return state.hrs < 1 ? "" : state.hrs + ":"; 77 | }, 78 | getMinutes(state) { 79 | return (state.mins < 10 ? "0" : "") + state.mins + ":"; 80 | }, 81 | getSeconds(state) { 82 | return (state.seconds < 10 ? "0" : "") + state.seconds; 83 | }, 84 | getTime(_, getters) { 85 | return getters.getHours + getters.getMinutes + getters.getSeconds; 86 | } 87 | }; 88 | 89 | export default { 90 | namespaced: true, 91 | state, 92 | mutations, 93 | actions, 94 | getters 95 | }; 96 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: ["vuetify"] 3 | }; 4 | --------------------------------------------------------------------------------