Weex 版扫雷游戏开发

简介: 扫雷是一个喜闻乐见的小游戏,今天在看 Weex 文档的过程中,无意中发现用 Weex 完全可以开发一个扫雷出来。当然这个扫雷和 Windows 那个有一点差距,不过麻雀虽小五脏俱全,随机布雷、自动挖雷、标雷那是必须有的。<br /><br />最后的效果是这个样子的:<br /><br /><img src="http://img4.tbcdn.cn/L1/461/1/e3e0deb067df8a
扫雷是一个喜闻乐见的小游戏,今天在看 Weex 文档的过程中,无意中发现用 Weex 完全可以开发一个扫雷出来。当然这个扫雷和 Windows 那个有一点差距,不过麻雀虽小五脏俱全,随机布雷、自动挖雷、标雷那是必须有的。

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



界面是简陋了一点,好歹是可以用的,要啥自行车。其实这个 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>
目录
相关文章
|
12月前
|
JSON 前端开发 数据库连接
【uniapp】实现买定离手小游戏
【uniapp】实现买定离手小游戏
【uniapp】实现买定离手小游戏
|
4月前
|
安全 Python Windows
游戏开发丨基于Tkinter的扫雷小游戏
游戏开发丨基于Tkinter的扫雷小游戏
67 3
|
12月前
|
存储 前端开发 JavaScript
【五子棋实战】第5章 开发五子棋前端页面
页面设计原则   1、可配置性。比如棋盘的大小可配置,棋盘边长可配置,黑白空期的值可配置;   2、响应式。各种屏幕大小下棋盘的布局要合理;   3、面向对象。棋子、棋盘的定义都用类来封装,代码要写的好看。
|
12月前
|
前端开发 Python
python开发五子棋小游戏如何实现
python开发五子棋小游戏如何 实现
90 0
游戏开发零基础入门教程(10):游戏积木之碰撞
顾名思义就是两个物体碰到一起。联系到我们的现实世界,你随手将一个东西扔出去,这个东西最终掉落在地上,就可以说这个东西与地面发生了碰撞。在游戏开发中,我们关注的“碰撞”重点在于发生的那一刻:是谁碰到了谁,碰撞的一方或者被撞的一方又该做出怎样的反应。
65 0
游戏开发零基础入门教程(10):游戏积木之碰撞
|
C语言
【C语言】项目实战——快速0基础上手五子棋游戏(内附源码)
【C语言】项目实战——快速0基础上手五子棋游戏(内附源码)
156 0
【游戏开发】游戏概念设计
游戏一直是人们追逐的梦想之一。如今,游戏一步一步地从以前的简单娱乐工具发展为现代娱乐、竞技甚至在某些方面能够模拟现实生活的现代工具。随着技术的不断进步,游戏的品质和数量也在不断提高。
|
开发工具
游戏开发零基础入门教程系列:人人都能做游戏
你正在阅读的是我原创的游戏开发零基础入门教程系列,面向的是没人任何游戏开发和编程经验,想要学习自己做游戏的普通人。这个系列教程已经帮助了很多人做出了自己人生的第一个游戏,希望它也能帮助到你。
364 0
游戏开发零基础入门教程(8):游戏积木之逻辑
“如果,那么”的逻辑非常的直白,如果玩家死了,那么游戏就应该结束。反之,如果玩家没有死,那么游戏就不应该结束。如果我干掉了一个敌人,那么我的得分就应该加一。如果敌人被子弹打中了,那么它就应该爆炸......
95 0
游戏开发零基础入门教程(11):游戏积木之动画
让我们直接从示例开始,上一节中我们让“战机”发射子弹,击中“敌机”,然后敌机直接隐藏。这个过程过于粗糙,在真实的游戏中,敌机被击中后往往会伴随着爆炸动画以及音效。
73 0