最后的效果是这个样子的:
界面是简陋了一点,好歹是可以用的,要啥自行车。其实这个 demo 主要是为了实践几件事情:
1. 界面指示器
2. 动态数据绑定
3. 更复杂的事件
扫雷的布局里面只需要用上 repeat 指示器,表示元素的重复出现,比如说一个 9*9 的雷区,布局文件非常的简单:
<template>
<container>
<container repeat="{{row}}" style="flex-direction: row; flex: 1;">
<container repeat="{{col}}" style="flex: 1;">
<text>{{text}}</text>
</container>
</container>
</container>
</template>
这样的话我们用 script 里面的 data binding 就能把重复的元素布局好。例如:
<script>
module.exports = {
data: {
row: [
{ "col": [ {}, {} ] },
{ "col": [ {}, {} ] },
]
}
}
</script>
但是如果真的这么写的话,一个 9*9 的布局不知道要搞到什么时候,幸亏 data-binding 也是支持动态化的。所以在游戏开始后生成这个布局就好了。
restart: function(e) { // restart game
var row = [];
var count = 0;
this.board = this.max; // display remain mines
this.finished = false;
for (var i = 0; i < this.size; ++i) { // init data-binding
var col = { "col": [] };
for (var j = 0; j < this.size; ++j) {
var tid = i * this.size + j;
col["col"][j] = {
tid: "" + tid, // identifier
state: "normal", // state
value: 0, // is a mine or not
text: "", // text
around: 0 // total count around this tile
};
}
row[i] = col;
}
this.row = row; // will cause view tree rendering
this.plant(); // arrange mines
this.calculate(); // calculate around values
},
初始化的时候生成每个节点的值,是否是一个雷,计算周围雷的总数,state 表示当前的状态(正常、挖开、标记),同时用 tid 来标记一个块(tile identifier)。
随机的在雷区布雷,直到满足个数:
plant: function() { // arrange mines
var count = 0;
while (count < this.max) {
var x = this.random(0, this.size);
var y = this.random(0, this.size);
var tile = this.row[x].col[y];
if (tile.value == 0) {
++count;
tile.value = 1;
}
}
}
然后做一次计算,把每个块周围的雷总数计算得到,这里有一个点可以优化,就是当点击第一次之后才去做布雷的操作,这样可以防止用户第一次就挂了。(如果你对扫雷有点了解的话,会知道在 Windows 扫雷里面,是出现过第一次点可能会挂和第一次点一定不会挂这两种的,区别就在这里)
calculate: function() { // calculate values around tiles
for (var i = 0; i < this.size; ++i) {
for (var j = 0; j < this.size; ++j) {
var around = 0;
this.map(i, j, function(tile) {
around += tile.value;
});
this.row[i].col[j].around = around;
}
}
}
这个计算做完之后,通过 Weex 的 data-binding 就彻底反映到了 View 上面,每个块都有了数据。这里面有个 map 函数,是定义在 script 里面的一个用于枚举位于 (x, y) 的块周围八个点的一个函数:
map: function(x, y, callback) { // visit tiles around (x, y)
for (var i = 0; i < 8; ++i) {
var mx = x + this.vector[i][0];
var my = y + this.vector[i][1];
if (mx >= 0 && my >= 0 && mx < this.size && my < this.size) {
callback(this.row[mx].col[my]);
}
}
}
通过枚举把块 callback 回来,这个函数有多次用到。
onclick: function(event) { // onclick tile
if (this.unfinished()) {
var tile = this.tile(event);
if (tile.state == "normal") {
if (tile.value == 1) { // lose game
this.onfail();
} else { // open it
this.display(tile);
if (tile.around == 0) {
this.dfs(tile); // start dfs a tile
}
this.judge(); // game judgement
}
}
}
},
onlongpress: function(event) { // onlongpress tile
if (this.unfinished()) {
var tile = this.tile(event);
tile.state = tile.state == "flag" ? "normal" : "flag";
if (tile.state == "flag") {
--this.board;
tile.text = this.strings.flag; // flag
} else {
++this.board;
tile.text = "";
}
this.judge();
}
}
然后绑定 onclick 和 onlongpress 函数,实现单击挖雷,长按标雷的功能。这里面的 tile 函数是通过事件发生的 event 对象取到元素的一个方法,值得一提的是这里面我试过官网说的 e.target.id 方法,拿到的是 undefined,所以我才在这里用了 tid 来标记一个元素。
tile: function(event) { // return tile object with click event
var tid = event.target.attr["tid"];
var pos = this.position(tid);
return this.row[pos["x"]].col[pos["y"]];
}
玩过扫雷的都知道,当你挖开一个点,发现这个点周围一个雷都没有,那么程序会自动挖开这个点周围的八个点,同时这个行为会递归下去,直到一整片全部被挖开,在程序里面就是上面的 dfs 函数
dfs: function(tile) { // dfs a tile
var pos = this.position(tile.tid);
var context = this;
tile.state = "open";
this.map(pos["x"], pos["y"], function(node) {
if (node.around == 0 && node.state == "normal") { // dfs
context.dfs(node); // dfs recursively
} else {
context.display(node); // display tile
}
});
}
发现某个点为空之后进入 dfs,递归或者展示某个点。接下来就是对雷区局面的判定动作,分为 onfail 和 judge 两个部分。
judge: function() {
var count = 0;
this.foreach(function(tile) {
if (tile.state == "open" || tile.state == "flag") {
++count;
}
});
if (count == this.size * this.size) { // win
this.finished = true;
this.board = this.strings.win;
}
},
onfail: function() { // fail
this.board = this.strings.lose;
this.finished = true;
var mine = this.strings.mine;
this.foreach(function(tile) {
if (tile.value == 1) {
tile.text = mine;
}
});
}
当点开雷的时候直接进入 onfail,否则进入 judge,如果满足胜利条件则游戏也结束。Weex 的 data 模块里面可以定义一个 JSON 数据,除了做数据绑定,也可以方便的储存其他的数据。
data: {
size: 9,
max: 10,
board: 0,
row: [],
vector: [[-1, 0], [-1, -1], [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1]],
strings: {
mine: "",
flag: "",
win: "YOU WIN!",
lose: "YOU LOSE~"
},
finished: false
}
最后
Weex 提供的指示器和数据绑定是不错的东西,用它们可以完成更灵活的界面布局和数据展现。
尤其是数据绑定,他让数值的变化可以直接反馈到界面上,省去了一些繁杂的界面更新逻辑。
这也许是一个不太实用的 demo,但我觉得很有趣。下面是源码:
<template>
<container>
<text class="btn">{{board}}</text>
<container repeat="{{row}}" style="flex-direction: row; flex: 1;">
<container repeat="{{col}}" style="flex: 1;">
<text tid="{{tid}}" onclick="onclick" onlongpress="onlongpress" class="{{state}} tile" around="{{around}}">{{text}}</text>
</container>
</container>
<text onclick="restart" class="btn">START</text>
</container>
</template>
<script>
module.exports = {
data: {
size: 9,
max: 10,
board: 0,
row: [],
vector: [[-1, 0], [-1, -1], [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1]],
strings: {
mine: "",
flag: "",
win: "YOU WIN!",
lose: "YOU LOSE~"
},
finished: false
},
methods: {
map: function(x, y, callback) { // visit tiles around (x, y)
for (var i = 0; i < 8; ++i) {
var mx = x + this.vector[i][0];
var my = y + this.vector[i][1];
if (mx >= 0 && my >= 0 && mx < this.size && my < this.size) {
callback(this.row[mx].col[my]);
}
}
},
dfs: function(tile) { // dfs a tile
var pos = this.position(tile.tid);
var context = this;
tile.state = "open";
this.map(pos["x"], pos["y"], function(node) {
if (node.around == 0 && node.state == "normal") { // dfs
context.dfs(node); // dfs recursively
} else {
context.display(node); // display tile
}
});
},
random: function(min, max) { // generate random number between [min, max)
return parseInt(Math.random() * (max - min) + min);
},
plant: function() { // arrange mines
var count = 0;
while (count < this.max) {
var x = this.random(0, this.size);
var y = this.random(0, this.size);
var tile = this.row[x].col[y];
if (tile.value == 0) {
++count;
tile.value = 1;
}
}
},
calculate: function() { // calculate values around tiles
for (var i = 0; i < this.size; ++i) {
for (var j = 0; j < this.size; ++j) {
var around = 0;
this.map(i, j, function(tile) {
around += tile.value;
});
this.row[i].col[j].around = around;
}
}
},
restart: function(e) { // restart game
var row = [];
var count = 0;
this.board = this.max; // display remain mines
this.finished = false;
for (var i = 0; i < this.size; ++i) { // init data-binding
var col = { "col": [] };
for (var j = 0; j < this.size; ++j) {
var tid = i * this.size + j;
col["col"][j] = {
tid: "" + tid,
state: "normal",
value: 0,
text: "",
around: 0
};
}
row[i] = col;
}
this.row = row; // will cause view tree rendering
this.plant(); // arrange mines
this.calculate(); // calculate around values
},
unfinished: function() { // check game status
var finished = this.finished;
if (this.finished) { // restart if finished
this.restart();
}
return !finished;
},
position: function(tid) { // return (x, y) with tile id
var row = parseInt(tid / this.size);
var col = tid % this.size;
return { x: row, y: col };
},
display: function(tile) {
tile.state = "open";
tile.text = (tile.around == 0) ? "" : tile.around;
},
tile: function(event) { // return tile object with click event
var tid = event.target.attr["tid"];
var pos = this.position(tid);
return this.row[pos["x"]].col[pos["y"]];
},
onclick: function(event) { // onclick tile
if (this.unfinished()) {
var tile = this.tile(event);
if (tile.state == "normal") {
if (tile.value == 1) { // lose game
this.onfail();
} else { // open it
this.display(tile);
if (tile.around == 0) {
this.dfs(tile); // start dfs a tile
}
this.judge(); // game judgement
}
}
}
},
onlongpress: function(event) { // onlongpress tile
if (this.unfinished()) {
var tile = this.tile(event);
tile.state = tile.state == "flag" ? "normal" : "flag";
if (tile.state == "flag") {
--this.board;
tile.text = this.strings.flag; // flag
} else {
++this.board;
tile.text = "";
}
this.judge();
}
},
foreach: function(callback) { // enumerate all tiles
for (var i = 0; i < this.size; ++i) {
for (var j = 0; j < this.size; ++j) {
callback(this.row[i].col[j]);
}
}
},
judge: function() {
var count = 0;
this.foreach(function(tile) {
if (tile.state == "open" || tile.state == "flag") {
++count;
}
});
if (count == this.size * this.size) { // win
this.finished = true;
this.board = this.strings.win;
}
},
onfail: function() { // fail
this.board = this.strings.lose;
this.finished = true;
var mine = this.strings.mine;
this.foreach(function(tile) {
if (tile.value == 1) {
tile.text = mine;
}
});
}
}
}
</script>
<style>
.btn {
margin: 2;
background-color: #e74c3c;
color: #ffffff;
text-align: center;
flex: 1;
font-size: 66;
height: 80;
}
.normal {
background-color: #95a5a6;
}
.open {
background-color: #34495e;
color: #ffffff;
}
.flag {
background-color: #95a5a6;
}
.tile {
margin: 2;
font-size: 66;
height: 80;
padding-top: 0;
text-align: center;
}
</style>