[教你做小游戏] 用JS实现平均每步仅占10bit位的象棋历史记录保存方案(encode篇)

6,277 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

背景

兄弟们,之前我开发了支持联机对战的五子棋、斗地主、UNO。在大家的呼吁之下,我准备开发「象棋」啦!

😄 不出意外,国庆假期,联机象棋就能跟大家见面了!

之前的进展:

继续给大家同步进展:今天,开发实现了象棋历史记录的保存方案,尽可能降低了空间的占用。基于该方案可以实现悔棋操作。

前文回顾

image.png

上篇文章讲到了我设计的方案:每一步操作需要4+1+2=7至4+1+4+5=14个二进制位。

如何用代码实现呢?

其实就是这个问题:给定这样的数据结构:XQRecord[],如何转换为Uint8Array类型(encode)?如何逆转换(decode)?

type XQRecord = {
  id: number; // 玩家移动的棋子ID
  before: number; // 棋子移动前的坐标(0-89)
  eat: number | null;  // 吃了棋子的ID,或者null表示没吃
};

encode难点

难点在于:这些都是变长的二进制数据,该如何拼接呢?JS并没有提供直接操控Uin8Array每一位的API。

简单情况

我们先尝试拼接简单的情况:4个字节的二进制。

function encodeRecords(records: XQRecord[]) {
  const array: number[] = [];
  let offset = 0;
  let current = 0;
  for (let i = 0; i < records.length; i++) {
    const record = records[i];
    const moveId = record.id % 16;
    if (offset < 4) {
      current |= moveId << (4 - offset);
      offset += 4;
    } else if (offset === 4) {
      current |= moveId;
      array.push(current);
      offset = 0;
    } else {
      current |= moveId >> (offset - 4);
      array.push(current);
      current = (moveId << (12 - offset)) & 0xff;
      offset -= 4;
    }
  }
  if (current) array.push(current);
  return Uint8Array.from(array);
}

因为Uint8Array不支持push方法,所以我们先用number[]代替它,最后再通过Uint8Array.from转换为Uint8Array

我们先计算出moveId,是0-15的数字,对16取余数即可。

随后,拼接一个number[]。利用offset标记当前的uint8已经填充了多少位,用current表示当前的uint8的具体数值。每当一个uint8被构造完毕,就把这个uint8放入number[]里,并重置current和offset。

最终,我们就构造了一个符合规则的字节串。

复杂情况

上面只是以moveId当做例子,其实如果这样写代码,就太麻烦了!因为我们bits有1位的、2位的、3位的、4位的、5位的,按上述方法,就需要写5次。

所以我们封装一个通用函数,用于拼接字节串:

function concatBits(current: number, offset: number, bits: number, bitsLength: number) {
  let newCurrent = current;
  let newOffset = offset;
  const newUint8: number[] = [];
  if (offset + bitsLength < 8) {
    newCurrent |= bits << (8 - bitsLength - offset);
    newOffset += bitsLength;
  } else if (offset + bitsLength === 8) {
    newUint8.push(current | bits);
    newCurrent = 0;
    newOffset = 0;
  } else {
    newCurrent |= bits >> (offset - 8 + bitsLength);
    newUint8.push(newCurrent);
    newCurrent = (bits << (16 - offset - bitsLength)) & 0xff;
    newOffset = offset - 8 + bitsLength;
  }
  return [newCurrent, newOffset, newUint8];
}

这样,就可以完成本文的方案啦~

当然这还是有个限制:bitsLength必须小于等于8。如果超过8,可能一个bits要覆盖3个uint8,这种情况没考虑在内。

因为本文描述场景只涉及1-5,所以就按这种方式实现了,不会有问题~

如果你需要拓展,欢迎继续完善它!

写在最后

我是HullQin,独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:《教你做小游戏》《极致用户体验》