럿고의 개발 노트
[인프런] 웹 게임을 만들며 배우는 Vue 8. 지뢰찾기 본문
Vue.js Note/[인프런] 웹 게임을 만들며 배우는 Vue(동영상 강의)
[인프런] 웹 게임을 만들며 배우는 Vue 8. 지뢰찾기
KimSeYun 2019. 11. 19. 11:57[인프런] 웹 게임을 만들며 배우는 Vue
8. 지뢰찾기
8.1. 지뢰찾기 구조 만들기
8.2. 지뢰찾기 코드 부여하기
8.3. 데이터 가공해 화면 그리기
8.4. 타이머 켜고 끄기
8.5. 칸 클릭하기
8.6. 지뢰 밟기와 주변 지뢰 개수 찾기
8.7, 주변 칸 한 번에 열기
8.8. 승리 조건 체크하기와 마무리
- 예제(웹 게임)을 통해 Vue를 만나는 시간으로, 예제를 풀면서 주석으로 배운 내용과 알아야 할 내용을 최대한 적어 놓았습니다. 코드와 주석을 참고해주세요!. 노드와 웹팩을 사용하여 설정파일들이 존재합니다. 아래 깃허브를 참고해주세요.
[main.js]
1 2 3 4 | import Vue from 'vue'; import MineSweeper from './MineSweeper'; new Vue(MineSweeper).$mount('#root'); | cs |
[package.json]
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 | { "name": "mine-sweeper", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "bulid": "webpack --watch", "dev": "webpack-dev-server --hot" }, "author": "", "license": "ISC", "dependencies": { "vue": "^2.6.10", "vue-loader": "^15.7.2", "vue-template-compiler": "^2.6.10", "vuex": "^3.1.2", "webpack": "^4.41.2", "webpack-cli": "^3.3.10" }, "devDependencies": { "css-loader": "^3.2.0", "vue-style-loader": "^4.1.2", "webpack-dev-server": "^3.9.0" } } | cs |
[webpack.config.js]
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 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); const path = require('path'); // 경로 불러오는 것 module.exports = { // 노드에 모듈을 만듬, 이안에 웹팩 모든 설정을 넣으면 됨. mode:'development', // 배포할건지 개발한건지 devtool: 'eval', // 개발할때는 eval, 배포할때는 hid,den-source-map resolve: { extensions: ['.js', '.vue'], // 이걸 하면 import할때 확장자를 넣어줄 필요가 없다. }, entry: { // 스크립트를 모인것 중에서 대표적인 스크립트 app : path.join(__dirname, 'main.js'), // app은 하나로 합친 스크립트 이름 }, module: { // 웹팩의 핵심 // entry로 처리하다가 이상한거 나오면 loader를 실행 rules: [{ // 합칠때 어떻게 합칠지를 설정해주는 것 test: /\.vue$/, // .vue로 끝나는 파일은 vue-loader를 사용하겠다, 정규표현식 loader: 'vue-loader', // npm i vue-loader },{ test: /\.css$/, use: [ // loader와 user는 똑같은 기능 'vue-style-loader', 'css-loader', ] }], }, plugins: [ new VueLoaderPlugin(), ], output: { filename: '[name].js', // 출력할 파일 이름(최종결과) path: path.join(__dirname, 'dist'), // 폴더 경로 publicPath: '/dist', // webpack-dev-server 세팅시 필요 }, }; // entry, module, plugins, output이 주 설정 나머지는 부가적인 설정 | cs |
[store.js]
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 89 90 91 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export const START_GAME = 'START_GAME'; export const OPEN_CELL = 'OPEN_CELL'; export const CLICK_MINE = 'CLICK_MINE'; export const FLAG_CELL = 'FLAG_CELL'; export const QUESTION_CELL = 'QUESTION_CELL'; export const NORMALIZE_CELL = 'NORMALIZE_CELL'; export const INCREMENT_TIMER = 'INCREMENT_TIMER'; export const CODE = { MINE: -7, NORMAL: -1, QUESTION: -2, FLAG: -3, QUESTION_MINE: -4, FLAG_MINE: -5, CLICKED_MINE: -6, OPENED: 0, // 0 이상이면 다 opened }; // [ 빈칸은 -1, 오픈은 0이상 값들, 지뢰는 -7, 깃발은 -3 그것이 위의 CODE다 // ['-1', '-1', '-7], // ['-1', '-1', '-1], // ['-1', '-1', '-1], // ] const plantMine = (row, cell, mine) => { console.log(row, cell, mine); const candidate = Array(row * cell).fill().map((arr, i) => { return i; }); const shuffle = []; while (candidate.length > row * cell - mine) { const chosen = candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]; shuffle.push(chosen); } const data = []; for (let i = 0; i < row; i++) { const rowData = []; data.push(rowData); for (let j = 0; j < cell; j++) { rowData.push(CODE.NORMAL); } } for (let k = 0; k < shuffle.length; k++) { const ver = Math.floor(shuffle[k] / cell); const hor = shuffle[k] % cell; data[ver][hor] = CODE.MINE; } console.log(data); return data; }; export default new Vuex.Store({ // import store from './store'; state: { tableData: [], data: { row: 0, cell: 0, mine: 0, }, timer: 0, halted: true, // true : 중단, false : 시작 result: '', openedCount: 0, }, // vue의 data와 비슷 getters: { }, // vue의 computed와 비슷 mutations: { [START_GAME](state, { row, cell, mine }) { state.data = { // 데이터를 통으로 변경하는 것이라서 아래와 같은 상황보다는 자유롭다 row, cell, mine, }; //state.data[row] = row; -> 객체안에 속성을 이름으로 변경했을때 화면이 안변할수도 있음 //Vue.set(state.data, 'row', row); -> 위에 명령어를 이렇게 변경해줘야 함 state.tableData = plantMine(row, cell, mine); state.timer = 0; state.halted = false; state.openedCount = 0; state.result = ''; }, [OPEN_CELL](state, { row, cell }) { let openedCount = 0; const checked = []; function checkAround(row, cell) { // 주변 8칸 지뢰인지 검색 const checkRowOrCellIsUndefined = row < 0 || row >= state.tableData.length || cell < 0 || cell >= state.tableData[0].length; if (checkRowOrCellIsUndefined) { return; } if ([CODE.OPENED, CODE.FLAG, CODE.FLAG_MINE, CODE.QUESTION_MINE, CODE.QUESTION].includes(state.tableData[row][cell])) { return; } if (checked.includes(row + '/' + cell)) { return; } else { checked.push(row + '/' + cell); } let around = []; if (state.tableData[row - 1]) { around = around.concat([ state.tableData[row - 1][cell - 1], state.tableData[row - 1][cell], state.tableData[row - 1][cell + 1] ]); } around = around.concat([ state.tableData[row][cell - 1], state.tableData[row][cell + 1] ]); if (state.tableData[row + 1]) { around = around.concat([ state.tableData[row + 1][cell - 1], state.tableData[row + 1][cell], state.tableData[row + 1][cell + 1] ]); } const counted = around.filter(function(v) { return [CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v); }); if (counted.length === 0 && row > -1) { // 주변칸에 지뢰가 하나도 없으면 const near = []; if (row - 1 > -1) { near.push([row - 1, cell - 1]); near.push([row - 1, cell]); near.push([row - 1, cell + 1]); } near.push([row, cell - 1]); near.push([row, cell + 1]); if (row + 1 < state.tableData.length) { near.push([row + 1, cell - 1]); near.push([row + 1, cell]); near.push([row + 1, cell + 1]); } near.forEach((n) => { if (state.tableData[n[0]][n[1]] !== CODE.OPENED) { checkAround(n[0], n[1]); } }); } if (state.tableData[row][cell] === CODE.NORMAL) { openedCount += 1; } Vue.set(state.tableData[row], cell, counted.length); } checkAround(row, cell); let halted = false; let result = ''; if (state.data.row * state.data.cell - state.data.mine === state.openedCount + openedCount) { halted = true; result = `${state.timer}초만에 승리하셨습니다.`; } state.openedCount += openedCount; state.halted = halted; state.result = result; }, [CLICK_MINE](state, { row, cell }) { state.halted = true; Vue.set(state.tableData[row], cell, CODE.CLICKED_MINE); }, [FLAG_CELL](state, { row, cell }) { if (state.tableData[row][cell] === CODE.MINE) { Vue.set(state.tableData[row], cell, CODE.FLAG_MINE); } else { Vue.set(state.tableData[row], cell, CODE.FLAG); } }, [QUESTION_CELL](state, { row, cell }) { if (state.tableData[row][cell] === CODE.FLAG_MINE) { Vue.set(state.tableData[row], cell, CODE.QUESTION_MINE); } else { Vue.set(state.tableData[row], cell, CODE.QUESTION); } }, [NORMALIZE_CELL](state, { row, cell }) { if (state.tableData[row][cell] === CODE.QUESTION_MINE) { Vue.set(state.tableData[row], cell, CODE.MINE); } else { Vue.set(state.tableData[row], cell, CODE.NORMAL); } }, [INCREMENT_TIMER](state) { state.timer += 1; }, }, // state를 수정할 때 사용해요. 동기적으로 }); | cs |
[MineSweeper.html]
1 2 3 4 5 6 7 8 9 10 11 12 13 | <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>지뢰찾기</title> </head> <body> <div id="root"></div> <script src ="dist/app.js"></script> </body> </html> | cs |
[MineSweeper.vue]
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 | <template> <div> <mine-form></mine-form> <div>{{timer}}</div> <table-component></table-component> <div>{{result}}</div> </div> </template> <script> import { mapState } from 'vuex'; import store, { INCREMENT_TIMER } from './store'; import TableComponent from './TableComponent'; import MineForm from './MineForm'; let interval; // 메모리 누수 막기 위해 생성 export default { store, components: { TableComponent, MineForm, }, computed: { ...mapState(['timer', 'result', 'halted']), }, methods: { }, watch: { halted(value, oldValue){ if(value === false){ interval = setInterval(() => { this.$store.commit(INCREMENT_TIMER); }, 1000); }else{ // 게임중단 clearInterval(interval); } } }, }; </script> <style scoped> table{ border-collapse: collapse; } td{ border: 1px solid black; width: 40px; height: 40px; text-align: center; } </style> | cs |
[TableComponent.vue]
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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | <template> <table> <tr v-for="(rowData, rowIndex) in tableData" :key="rowIndex"> <td v-for="(cellData, cellIndex) in rowData" :key="cellIndex" :style="cellDataStyle(rowIndex, cellIndex)" @click="onClickTd(rowIndex, cellIndex)" @contextmenu.prevent="onRightClickTd(rowIndex, cellIndex)">{{cellDataText(rowIndex, cellIndex)}}</td> <!-- @contextmenu.prevent : 오른쪽마우스 클릭 막기 및 오른쪽 마우스 클릭시 실행될 메소드 설정 --> </tr> </table> </template> <script> import { mapState } from 'vuex'; import { CODE, OPEN_CELL, FLAG_CELL, QUESTION_CELL, NORMALIZE_CELL, CLICK_MINE } from './store'; export default { computed: { ...mapState(['tableData', 'halted']), cellDataStyle(state) { return (row, cell) => { switch (this.$store.state.tableData[row][cell]) { case CODE.NORMAL: case CODE.MINE: return { background: '#444', }; case CODE.CLICKED_MINE: case CODE.OPENED: return { background: 'white', }; case CODE.FLAG: case CODE.FLAG_MINE: return { background: 'red', }; case CODE.QUESTION: case CODE.QUESTION_MINE: return { background: 'yellow', }; default: return {}; } }; }, cellDataText() { return (row, cell) => { switch (this.$store.state.tableData[row][cell]) { case CODE.MINE: return 'X'; case CODE.NORMAL: return ''; case CODE.FLAG_MINE: case CODE.FLAG: return '!'; case CODE.QUESTION_MINE: case CODE.QUESTION: return '?'; case CODE.CLICKED_MINE: return '펑'; default: return this.$store.state.tableData[row][cell] || ''; } }; }, }, methods: { onClickTd(row, cell){ if(this.halted){ return; } switch (this.tableData[row][cell]) { case CODE.NORMAL: return this.$store.commit(OPEN_CELL, { row, cell }); case CODE.MINE: return this.$store.commit(CLICK_MINE, { row, cell }); default: return; } }, onRightClickTd(row, cell){ if(this.halted){ return; } switch(this.tableData[row][cell]){ case CODE.NORMAL: case CODE.MIME: this.$store.commit(FLAG_CELL, { row, cell }); return; case CODE.FLAG_MINE: case CODE.FLAG: this.$store.commit(QUESTION_CELL, { row, cell }); return; case CODE.QUESTION_MINE: case CODE.QUESTION: this.$store.commit(NORMALIZE_CELL, { row, cell }); return; default: return; } } }, }; </script> <style scoped> </style> | cs |
[MineForm.vue]
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 | <template> <div> <input type="number" placeholder="세로" :value="row" @change="onChangeRow" /> <input type="number" placeholder="가로" :value="cell" @change="onChangeCell" /> <input type="number" placeholder="지뢰" :value="mine" @change="onChangeMine" /> <button @click="onClickBtn">시작</button> </div> </template> <script> import { START_GAME } from './store'; //이 지역(컴포넌트)에서만 데이터를 사용하려면 굳이 vuex를 사용할 필요가 없다.(vuex코드만 길어질뿐...) export default { data() { return { row: 10, cell: 10, mine: 20, }; }, methods: { onChangeRow(e) { this.row = e.target.value; }, onChangeCell(e) { this.cell = e.target.value; }, onChangeMine(e) { this.mine = e.target.value; }, onClickBtn() { this.$store.commit(START_GAME, { row: this.row, cell: this.cell, mine: this.mine }); } }, } </script> | cs |
'Vue.js Note > [인프런] 웹 게임을 만들며 배우는 Vue(동영상 강의)' 카테고리의 다른 글
[인프런] 웹 게임을 만들며 배우는 Vue 후기 (0) | 2019.11.19 |
---|---|
[인프런] 웹 게임을 만들며 배우는 Vue 9. Vue Router (0) | 2019.11.19 |
[인프런] 웹 게임을 만들며 배우는 Vue 7. 틱택토 (0) | 2019.11.16 |
[인프런] 웹 게임을 만들며 배우는 Vue 6. 로또 추점기 (0) | 2019.11.14 |
[인프런] 웹 게임을 만들며 배우는 Vue 5. 가위바위보 (0) | 2019.11.14 |
Comments