├── .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 | 
16 | 
17 | 
18 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------
/src/components/GameOver.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{message}}
6 | Play again?
7 |
8 |
9 | Yes
14 |
15 |
16 |
17 |
18 |
19 |
20 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/GitHub.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/Grid.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Minesweeper
5 | mdi-bomb
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 | mdi-bomb
21 | {{col.data}}
22 |
23 | mdi-asterisk
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
67 |
68 |
111 |
112 |
--------------------------------------------------------------------------------
/src/components/Timer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Time
5 |
6 |
7 | {{time}}
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------