最近學習Vue和webpack,恰好搞個小遊戲練練手。
2048遊戲規則:css
每次能夠選擇上下左右其中一個方向去滑動,每滑動一次,全部的數字方塊都會往滑動的方向靠攏外,系統也會在空白的地方亂數出現一個數字方塊,相同數字的方塊在靠攏、相撞時會相加。不斷的疊加最終拼湊出2048這個數字就算成功。html
固然有些細微的合併規則,好比:
當向左滑動時,某列2 2 2 2 合併成 4 4 0 0 而非 8 0 0 0
也就是說,同列的某個數字最多隻被合併一次。
在線挑戰一把?(瞎折騰一陣後發現移動端沒法正常顯示了,趕忙調試去)
http://www.ccc5.cc/2048/
移動端掃下面的二維碼便可(微信會轉碼,請點擊掃描後底部的‘訪問原網頁’)
vue
4x4的方格,用一個16個成員的數組表示。當某個方格沒有數字的時候用''表示;node
建立Vue的模板,綁定數據,處理數據與相關class的關係;webpack
把數組看成一個4x4的矩陣,專一數據方面的處理,Dom方面的就交給vue更新ios
<template> <div id="app"> <ul> <li class='box' v-for="num in nums" v-text="num" v-getclass="num" v-getposition="$index" track-by="$index"></li> </ul> <button @click="reset()">重置</button> </div> </template>
其中getclass
與getposition
爲自定義指令:git
getclass
根據當前框數字設置不一樣的classgithub
getposition
根據當前框的索引位置,設置css樣式的top
與left
web
初始化一個長度爲16的數組,而後隨機選兩個地方填入2或者4。算法
<script> export default { data () { return { nums : [] //記錄16個框格的數字 } }, ready: function () { localStorage['save'] ? this.nums = JSON.parse(localStorage['save']) : this.reset(); }, methods:{ /*在一個隨機的空白位添加2或4 機率9:1*/ randomAdd(){ let arr = this.shuffle(this.blankIndex()); //延時100毫秒添加 setTimeout(_=>{ this.nums.$set(arr.pop(),Math.random()>0.9 ? 4 : 2); },100); }, /*獲取當前空白框索引組成的數組*/ blankIndex(){ let arr = []; this.nums.forEach((i,j)=>{ i==='' && arr.push(j); }); return arr; }, /*打亂數組*/ shuffle(arr){ let l = arr.length,j; while(l--){ j = parseInt(Math.random()*l); [arr[l],arr[j]] = [arr[j],arr[l]] } return arr; }, //保存遊戲進度 save(){ localStorage['save'] = JSON.stringify(this.nums); }, //重置遊戲 reset(){ this.nums = Array(16).fill(''); let i =0; while(i++<2){ //隨機添加2個 this.randomAdd(); } } } } </script>
這裏有必要說明下,在segmentfault看到不少人洗牌算法習慣這麼寫:
var arr = arr.sort(_=> { return Math.random() - 0.5 });
可是通過不少人的測試,這樣洗牌實際上是不太亂的,具體參考
數組的徹底隨機排列:https://www.h5jun.com/post/ar...
如何測試洗牌程序:http://coolshell.cn/articles/...
繼續回到主題,數據初始化完成以後,添加兩個自定義指令,功能前面已經講過了,實現也很簡單,
方格里面數字不一樣,對應的class不同,我這裏定義的規則是
數字2對應.s2
數字4對應.s4
...
<script> directives:{ getclass(value) { let classes = this.el.classList; /* //移動端貌似對classList的forEach支持不完善 //我在ios上調試了好久才找到是這裏的緣由致使以前移動端無法正常運行 classes.forEach(_=>{ if(/^s\w+$/.test(_))classes.remove(_); });*/ Array.prototype.forEach.call(classes,_=>{ if(/^s\w+$/.test(_))classes.remove(_); }); value ? classes.add('s' + value) : classes.add('empty'); }, getposition(index){ this.el.style.left = index%4*25 + '%'; this.el.style.top = Math.floor(index/4)*25 + '%'; } } </script>
監聽4個方向鍵和移動端的滑動事件,在ready環節處理
ready: function () { document.addEventListener('keyup', this.keyDown); document.querySelector('#app ul').addEventListener('touchstart', this.touchStart); document.querySelector('#app ul').addEventListener('touchend', this.touchEnd); //document上獲取touchmove事件 若是是由.box觸發的 則禁止屏幕滾動 document.addEventListener('touchmove', e=>{ e.target.classList.contains('box') && e.preventDefault(); }); }, methods:{ touchStart(e){ //在start中記錄開始觸摸的點 this.start['x'] = e.changedTouches[0].pageX; this.start['y'] = e.changedTouches[0].pageY; }, touchEnd(e){ //handle... }, /* *方向鍵 事件處理 */ keyDown(e){ //handle... } }
咱們將nums這個數組想象成一個4x4的方陣。
2 2 2 2 x x x x x x x x x x x x
當向左
合併的時候,以第一列來講,從左至右:
inxde=0位置的2在合併時是不須要動的,index=1位置的2和index=0位置的2數值相同,合併成4,放在index=0的位置,第一列變成4 '' 2 2
index=2位置的2,向左挪到index=1空出的位置,變成4 2 '' 2
,同時,index=3位置的2一直向左運動,直到碰上index=1處的2,兩者在這輪都沒有過合併,因此這裏能夠合併爲4 4 '' ''
從這裏咱們能夠看出,向左
運動時,最左的位置,也就是index%4 === 0
的位置,是無需挪動的,其餘位置,如有數字,其左側有空位的話則向左挪動一位,其左側有個與其相同的數字時,且兩者在此輪中沒有合併過,則兩者合併,空出當前位:
2 2 2 2
=> 4 4 '' ''
4 2 2 2
=> 4 4 2 ''
'' 4 2 2
=> 4 4 '' ''
2 2 4 2
=> 4 4 2 ''
...
若是打算將向上,向右,向下都這麼處理一遍並不是不能夠,但比較容易出錯。咱們既然都將nums想象成了一個4x4的方陣,那麼何不將該方陣旋轉一下呢。
當向下
運動時,咱們先將nums利用算法將想象中的方陣順時針旋轉一次,而後能夠用向左
運動處理的方法合併、移動方格,完畢後再順時針旋轉3次,或者逆時針旋轉1次還原便可。
旋轉算法:
<script> methods{ //... //arr 須要旋轉的數組 //n 順時針選擇n次 T(arr,n){ n=n%4; if(n===0)return arr; var l = arr.length,d = Math.sqrt(l),tmp = []; for(var i=0;i<d;i+=1) for(var j=0;j<d;j+=1) tmp[d-i-1+j*d] = arr[i*d+j]; if(n>1)tmp=this.T(tmp,n-1); return tmp; } //... } </script>
有了這個函數,咱們就只要寫向左
運動的處理函數了
<script> move(){ let hasMove = false, //一次操做有移動方塊時才添加方塊 /* *記錄已經合併過一次的位置 避免重複合併 *如 2 2 4 4 在一次合併後應爲 4 8 0 0 而非8 4 0 0 */ hasCombin = {}; tmp.forEach((j,k)=>{ while(k%4 && j!==''){ if(tmp[k-1] === ''){ //當前位置的前一位置爲空,交換倆位置 tmp[k-1] = j; tmp[k] = ''; hasMove = true; if(hasCombin[k]){//當前位有過合併,隨着挪動,也要標記到前面去 hasCombin[k-1] = true; hasCombin[k] = false; } }else if(tmp[k-1] === j && !hasCombin[k] && !hasCombin[k-1]){ //當前位置與前一位置數字相同,合併到前一位置,而後清空當前位置 j *= 2; tmp[k-1] = j; tmp[k] = ''; hasMove = true; hasCombin[k-1] = true; //記錄合併位置 }else{ break; } k--; } }); } </script>
詳細的方向轉換及處理過程
<script> methods:{ /*把數組arr當成矩陣,順時針轉置n次*/ T(arr,n){ n=n%4; if(n===0)return arr; var l = arr.length,d = Math.sqrt(l),tmp = []; for(var i=0;i<d;i+=1) for(var j=0;j<d;j+=1) tmp[d-i-1+j*d] = arr[i*d+j]; if(n>1)tmp=this.T(tmp,n-1); return tmp; }, touchStart(e){ this.start['x'] = e.changedTouches[0].pageX; this.start['y'] = e.changedTouches[0].pageY; }, touchEnd(e){ let curPoint = e.changedTouches[0], x = curPoint.pageX - this.start.x, y = curPoint.pageY - this.start.y, xx = Math.abs(x), yy = Math.abs(y), i = 0; //移動範圍過小 不處理 if(xx < 50 && yy < 50)return; if( xx >= yy){ //橫向滑動 i = x < 0 ? 0 : 2; }else{//縱向滑動 i = y < 0 ? 3 : 1; } this.handle(i); }, /* *方向鍵 事件處理 *把上、右、下方向經過旋轉 變成左向操做 */ keyDown(e){ //左上右下 分別轉置0 3 2 1 次 const map = {37:0,38:3,39:2,40:1}; if(!(e.keyCode in map))return; this.handle(map[e.keyCode]); }, handle(i){ this.move(i); this.save(); }, /*移動滑塊 i:轉置次數 */ move(i){ let tmp = this.T(this.nums,i),//把任意方向鍵轉置,當成向左移動 hasMove = false, //一次操做有移動方塊時才添加方塊 /* *記錄已經合併過一次的位置 避免重複合併 *如 2 2 4 4 在一次合併後應爲 4 8 0 0 而非8 4 0 0 */ hasCombin = {}; tmp.forEach((j,k)=>{ while(k%4 && j!==''){ if(tmp[k-1] === ''){ //當前位置的前一位置爲空,交換倆位置 tmp[k-1] = j; tmp[k] = ''; hasMove = true; if(hasCombin[k]){ hasCombin[k-1] = true; hasCombin[k] = false; } }else if(tmp[k-1] === j && !hasCombin[k] && !hasCombin[k-1]){ //當前位置與前一位置數字相同,合併到前一位置,而後清空當前位置 j *= 2; tmp[k-1] = j; tmp[k] = ''; hasMove = true; hasCombin[k-1] = true; //記錄合併位置 }else{ break; } k--; } }); this.nums = this.T(tmp,4-i);//轉置回去,把數據還給this.nums hasMove && this.randomAdd();//有數字挪動才添加新數字 } } </script>
當全部的16個方格都已經被填滿,且橫向與縱向都沒法合併時,遊戲結束
isPass(){ let isOver=true,hasPass=false,tmp = this.T(this.nums,1); this.nums.forEach((i,j)=>{ if(this.nums[j-4] == i || this.nums[j+4] == i || tmp[j-4] == tmp[j] || tmp[j+4] == tmp[j]){ isOver = false; } if(i==2048){ hasPass = true; } }); if(!this.blankIndex().length){ isOver && alert('遊戲結束!'); }; }
。。。。。。。。
如何才能PASS?你有本事能夠玩到很恐怖的數字。具體能玩到多少須要用數學證實吧,已有知乎大神證實,估計在3884450左右。傳送門https://www.zhihu.com/questio...
奉上所有代碼app.vue
<template> <div id="app"> <ul> <li class='box' v-for="num in nums" v-text="num" v-getclass="num" v-getposition="$index" track-by="$index"></li> </ul> <button @click="reset()">重置</button> </div> </template> <script> export default { data () { return { start : {}, //記錄移動端觸摸起始點 nums : [] //記錄15個框格的數字 } }, ready: function () { document.addEventListener('keyup', this.keyDown); document.querySelector('#app ul').addEventListener('touchstart', this.touchStart); document.querySelector('#app ul').addEventListener('touchend', this.touchEnd); //document上獲取touchmove事件 若是是由.box觸發的 則禁止屏幕滾動 document.addEventListener('touchmove', e=>{ e.target.classList.contains('box') && e.preventDefault(); }); localStorage['save'] ? this.nums = JSON.parse(localStorage['save']) : this.reset(); }, directives:{ getclass(value) { let classes = this.el.classList; classes.forEach(_=>{ if(/^s\w+$/.test(_))classes.remove(_); }); value ? classes.add('s' + value) : classes.add('empty'); }, getposition(index){ this.el.style.left = index%4*25 + '%'; this.el.style.top = Math.floor(index/4)*25 + '%'; } }, methods:{ /*在一個隨機的空白位添加2或4 機率9:1*/ randomAdd(){ let arr = this.shuffle(this.blankIndex()); //延時100毫秒添加 setTimeout(_=>{ this.nums.$set(arr.pop(),Math.random()>0.9 ? 4 : 2); },100); }, /*獲取當前空白隔索引組成的數組*/ blankIndex(){ let arr = []; this.nums.forEach(function(i,j){ i==='' && arr.push(j); }); return arr; }, /*打亂數組*/ shuffle(arr){ let l = arr.length,j; while(l--){ j = parseInt(Math.random()*l); [arr[l],arr[j]] = [arr[j],arr[l]] } return arr; }, /*把數組arr當成矩陣,轉置n次*/ /* [1,2, 1次轉置變爲 [3,1, 3,4] 4,2] */ T(arr,n){ n=n%4; if(n===0)return arr; var l = arr.length,d = Math.sqrt(l),tmp = []; for(var i=0;i<d;i+=1) for(var j=0;j<d;j+=1) tmp[d-i-1+j*d] = arr[i*d+j]; if(n>1)tmp=this.T(tmp,n-1); return tmp; }, touchStart(e){ this.start['x'] = e.changedTouches[0].pageX; this.start['y'] = e.changedTouches[0].pageY; }, touchEnd(e){ let curPoint = e.changedTouches[0], x = curPoint.pageX - this.start.x, y = curPoint.pageY - this.start.y, xx = Math.abs(x), yy = Math.abs(y), i = 0; //移動範圍過小 不處理 if(xx < 50 && yy < 50)return; if( xx >= yy){ //橫向滑動 i = x < 0 ? 0 : 2; }else{//縱向滑動 i = y < 0 ? 3 : 1; } this.handle(i); }, /* *方向鍵 事件處理 *把上、右、下方向經過旋轉 變成左向操做 */ keyDown(e){ //左上右下 分別轉置0 3 2 1 次 const map = {37:0,38:3,39:2,40:1}; if(!(e.keyCode in map))return; this.handle(map[e.keyCode]); }, handle(i){ this.move(i); this.save(); this.isPass();//判斷是否過關 }, /*移動滑塊 i:轉置次數 */ move(i){ let tmp = this.T(this.nums,i),//把任意方向鍵轉置,當成向左移動 hasMove = false, //一次操做有移動方塊時才添加方塊 /* *記錄已經合併過一次的位置 避免重複合併 *如 2 2 4 4 在一次合併後應爲 4 8 0 0 而非8 4 0 0 */ hasCombin = {}; tmp.forEach((j,k)=>{ while(k%4 && j!==''){ if(tmp[k-1] === ''){ //當前位置的前一位置爲空,交換倆位置 tmp[k-1] = j; tmp[k] = ''; hasMove = true; if(hasCombin[k]){ hasCombin[k-1] = true; hasCombin[k] = false; } }else if(tmp[k-1] === j && !hasCombin[k] && !hasCombin[k-1]){ //當前位置與前一位置數字相同,合併到前一位置,而後清空當前位置 j *= 2; tmp[k-1] = j; tmp[k] = ''; hasMove = true; hasCombin[k-1] = true; //記錄合併位置 }else{ break; } k--; } }); this.nums = this.T(tmp,4-i);//轉置回去,把數據還給this.nums hasMove && this.randomAdd(); }, save(){ localStorage['save'] = JSON.stringify(this.nums); }, //重置遊戲 reset(){ this.nums = Array(16).fill(''); let i =0; while(i++<2){ //隨機添加2個 this.randomAdd(); } }, isPass(){ let isOver=true,hasPass=false,tmp = this.T(this.nums,1); this.nums.forEach((i,j)=>{ if(this.nums[j-4] == i || this.nums[j+4] == i || tmp[j-4] == tmp[j] || tmp[j+4] == tmp[j]){ isOver = false; } if(i==2048){ hasPass = true; } }); if(!this.blankIndex().length){ isOver && alert('遊戲結束!'); }; } } } </script> <style> @import url(http://fonts.useso.com/css?family=Inknut+Antiqua); @import url(./main.css); </style>
index.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta content="telephone=no,email=no" name="format-detection"> <meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>2048 Game</title> <style> html,body{ width:100%; } body{ display:flex; justify-content: center; align-items: center; overflow:hidden; } </style> </head> <body> <app></app> <script src="bundle.js"></script> </body> </html>
main.js
import Vue from 'vue' import App from './app.vue' new Vue({ el: 'body', components:{App} });
webpack配置
const webpack = require('webpack'); module.exports = { entry: { app:["./app/main.js",'webpack-hot-middleware/client','webpack/hot/dev-server'] }, output: { path: __dirname + "/public",//打包後的文件存放的地方 filename: "bundle.js",//打包後輸出文件的文件名 publicPath:'http://localhost:8080/' }, module: { loaders: [ {test: /\.css$/, loader: 'style!css'}, {test: /\.vue$/, loader: 'vue'}, {test: /\.js$/,exclude:/node_modules|vue\/dist|vue-router\/|vue-loader\/|vue-hot-reload-api\// ,loader: 'babel'} ] }, vue:{ loaders:{ js:'babel' } }, babel: { presets: ['es2015','stage-0'], plugins: ['transform-runtime'] }, plugins:[ new webpack.HotModuleReplacementPlugin() ], devServer: { contentBase: "./public",//本地服務器所加載的頁面所在的目錄 colors: true,//終端中輸出結果爲彩色 hot: true, inline: true//實時刷新 }, resolve: { extensions: ['', '.js', '.vue'] } }
css就不貼了,Github上託管了:https://github.com/Elity/2048...
好久沒寫這麼長的文章了,累死。花了整整倆小時!!!