媒介
近来进修Vue和webpack,恰好搞个小游戏练练手。
2048游戏划定规矩:
每次能够挑选上下摆布个中一个方向去滑动,每滑动一次,一切的数字方块都邑往滑动的方向挨近外,体系也会在空缺的处所乱数涌现一个数字方块,雷同数字的方块在挨近、相撞时会相加。不停的叠加终究拼凑出2048这个数字就算胜利。
固然有些纤细的兼并划定规矩,比方:
当向左滑动时,某列2 2 2 2 兼并成 4 4 0 0 而非 8 0 0 0
也就是说,同列的某个数字最多只被兼并一次。
在线应战一把?
(瞎折腾一阵后发明搬动端没法一般显现了,赶忙调试去)
http://www.ccc5.cc/2048/
搬动端扫下面的二维码即可(微信会转码,请点击扫描后底部的‘接见原网页’)
思绪
4×4的方格,用一个16个成员的数组示意。当某个方格没有数字的时刻用”示意;
建立Vue的模板,绑定数据,处置惩罚数据与相干class的关联;
把数组看成一个4×4的矩阵,专注数据方面的处置惩罚,Dom方面的就交给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>
个中getclass
与getposition
为自定义指令:
getclass
依据当前框数字设置差别的classgetposition
依据当前框的索引位置,设置css款式的top
与left
症结完成
初始化数据
初始化一个长度为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这个数组设想成一个4×4的方阵。
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设想成了一个4×4的方阵,那末何不将该方阵扭转一下呢。
当向下
活动时,我们先将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…
良久没写这么长的文章了,累死。花了整整俩小时!!!