持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情。
背景
兄弟们,之前我开发了支持联机对战的五子棋、斗地主、UNO。在大家的呼吁之下,我准备开发「象棋」啦!
😄 不出意外,国庆假期,联机象棋就能跟大家见面了!
之前的进展:
- 《用SVG画一个象棋棋盘》。
- 《基于svg和ttf(字体文件),我仅用6kb就画完了象棋所有棋子》。
- 《我用43个字符,就存下了象棋的棋盘状态》。
- 《JS实现象棋移动规则》。
- 《一种记录象棋历史记录的方案:平均每步仅占10bit位》。
继续给大家同步进展:今天,开发实现了象棋历史记录的保存方案,尽可能降低了空间的占用。基于该方案可以实现悔棋操作。
前文回顾
上篇文章讲到了我设计的方案:每一步操作需要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个专栏里分享:《教你做小游戏》、《极致用户体验》。