也试着写游戏:四路顶

这是一篇旧文,其中的内容可能已经过时。

进入游戏

我学会的第一种棋,不是中国象棋也不是五子棋,而是家乡代代相传的土棋。它叫四路顶,听说在山东叫走四棋儿。

4×4 的棋盘,双方各执四子摆满相对的边;双方轮流走棋,每步一格。若一方通过走棋,摆成纵横相邻两个子,且一颗对方的子与这两个子相邻、这三个子的两端位置没有别的子,就把对方的那颗棋子吃掉了。

下面展示了空心子被吃的例子,方向键表示上次的移动。

┌─┬─↓─┐ ┌─↓─┬─┐
├─●─●─○ ├─●─●─○
├─┼─┼─┤ ├─●─┼─┤
└─┴─┴─┘ └─○─┴─┘

而下列情况均不构成吃棋:

┌─┬─↓─┐ ┌─↓─┬─┐ ┌─┬─↓─┐
●─●─●─○ ●─●─○─○ ●─●─○─┤
├─┼─┼─┤ ├─┼─┼─┤ ├─┼─┼─┤
└─┴─┴─┘ └─┴─┴─┘ └─┴─┴─┘

我开发了一个在线对战的版本,支持创建私人房间。服务端用 Node.js 和 socket.io,容器化后托管在 DaoCloud.io 上;客户端是用 canvas 绘制的。

在这个项目里,客户端只负责处理通信和绘图,逻辑处理均在服务端。尽管这个游戏谈不上复杂,但每次开发都会总结出些许经验:

  • socket.io 的数据传递方式是 JSON,而 undefined 和数组中的空位置都会在序列化过程中被转换为 null

  • 游戏大体开发完成时,我告知了一些友人,结果不幸被他们找到了注入点。谨记:客户端传来的所有数据都是不可信的。有个经典的例子licence plate camera sql injection

  • 适当把方法分离得细小,以便于子类的修改。我在高中时发明了一个变种,用六边形的棋盘,可以让三个人玩。这个类继承下来,只改了三个必改的方法:

    class LiuLuDing extends SiLuDing {
      constructor(room, emitter) {
        super(room, emitter);
      }
      initGrid() {
        // 设置棋盘
        this.grid = [
          [0, 0, 0, 0],
          [null, null, null, null, null],
          [null, null, null, null, null, null],
          [1, null, null, null, null, null, 2],
          [null, 1, null, null, null, null, 2],
          [null, null, 1, null, null, null, 2],
          [null, null, null, 1, null, null, 2],
        ];
      }
    
      check(i, j) {
        // 当玩家落子时,告诉程序要检查哪些方向
        this.checkByDirection(i, j, 'horizonal');
        this.checkByDirection(i, j, 'slash');
        this.checkByDirection(i, j, 'backslash');
      }
    
      getLineOfSeven(i, j, direction) {
        // 对于每个方向,拿出附近的七个位置的信息用来判断是否吃子
      }
    }
    
  • 对于这种在线多人应用,不能忽略内存回收的问题。JavaScript 的内存回收机制是这样的:在运行时,检测每个对象的被引用数,若这个数为 0,则表明这个对象已无法访问,垃圾回收器就会将其回收。所以当一个对象的生命周期结束后,记得手动清除对其所有的引用。

游戏源码 放在了 Github 上,感兴趣的同学可移步。之后还会加入一些特性,比如成就等等。说不定将来某天会登陆 steam 呢 😀