Weex 版扫雷游戏开发

扫雷是一个喜闻乐见的小游戏,今天在看 Weex 文档的过程中,无意中发现用 Weex 完全可以开发一个扫雷出来。当然这个扫雷和 Windows 那个有一点差距,不过麻雀虽小五脏俱全,随机布雷、自动挖雷、标雷那是必须有的。

最后的效果是这个样子的:

Weex 版扫雷游戏开发
Weex 版扫雷游戏开发
界面是简陋了一点,好歹是可以用的,要啥自行车。其实这个 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(); } }

然后绑定 onclickonlongpress 函数,实现单击挖雷,长按标雷的功能。这里面的 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>
上一篇:五款实用免费的Python机器学习集成开发环境(5 free Python IDE for Machine Learning)(图文详解)


下一篇:关于Juniper ScreenOS MIP/VIP地址说明