0
点赞
收藏
分享

微信扫一扫

[教你做小游戏] JS实现象棋移动规则

Alex富贵 2022-10-09 阅读 25


背景

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

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

之前的进展:

  • 《用SVG画一个象棋棋盘》。
  • 《基于svg和ttf(字体文件),我仅用6kb就画完了象棋所有棋子》。
  • 《我用43个字符,就存下了象棋的棋盘状态》。

继续给大家同步进展:今天,实现了象棋的移动规则,即选中某个棋子后,程序可以计算该棋子可移动的范围。这是象棋游戏的核心逻辑。

数据结构

先带大家回顾下我们的数据结构:

  • 棋盘状态是用一个长度为32的数组表示的,代表32个棋子各自的位置。
  • 我们令0-89分别表示棋盘上的90个格子(0-8代表第一行,以此类推共10行),用127表示该棋子已经阵亡。
  • 数组的第0-15项为红方(先手)、第16-31项为黑方(后手)。
  • 0、16分别表示帅、将。
  • 1、2、17、18表示士。
  • 3、4、19、20表示象。
  • 5、6、21、22表示马。
  • 7、8、23、24表示车。
  • 9、10、25、26表示炮。
  • 其余表示兵卒。

[教你做小游戏] JS实现象棋移动规则_数组

输入与输出

输入:当前棋盘状态​​pieces: number[]​​​和已选中的棋子ID​​id: number​​。

输出:该棋子可以走的范围​​candidates: number[]​​。是0-89组成的不重复的允许为空的数组。

黑红规则一致

双方规则是一致的。当我们定义了任意一方的规则后,另一方可以套用已经定义的规则,只需要把当前所有棋子位置做「中心对称」的转换即可。

例如,我们已经开发完了函数​​function getCandidatesOfOneRedPiece(id: number, pieces: number[])​​​可以获取某个红色棋子的可移动范围,该如何开发函数​​function getCandidatesOfOneBlackPiece​​来获取某个黑色棋子的可移动范围呢?

只需这样写:

function getCandidatesOfOneBlackPiece(id: number, pieces: number[]) {
return getCandidatesOfOneRedPiece(id - 16, pieces.map(p 89 - p)).map(p 89

含义如下:要计算黑色某棋子范围,只需要把当前的黑色当作红色,红色当作黑色,就能套用​​getCandidatesOfOneRedPiece​​函数了。

当然这有个前提条件,因为位置的映射89 - p是做中心对称,我们要求​id - 16​这个操作也必须是中心对称的。怎么保证?需要定义初始棋盘时,把​x​的位置和​x + 16​的位置中心对称。

实现getCandidatesOfOneRedPiece

大体思路

重要前提:我们开发的象棋规则,需求是明确的,不会有奇奇怪怪的规则,以后也不需要更新迭代了,也不需要开发AI。所以保证开发效率和适当的可读性就足够了。没必要抽象出通用的移动规则,没必要做成可配置的规则。没必要一次性计算很多变量从而方便计算所有棋子的可移动范围。

  1. 先不区分敌我,基于棋子基本移动规则计算出所有可以去的范围(即可以吃队友、允许将帅见面)。
  2. 然后统一从候选者里删除吃自己棋子的选项。
  3. 统一从候选者里删除会导致将帅见面的选项。

先计算一些通用变量

const candidates = [];
const piece = pieces[id];
const x = piece % 9;
const y = Math.floor(piece / 9);

x表示第几列,即距离最左侧竖线条的距离。

y表示第几行。

将帅规则

允许在九宫格内上下左右移动。判断下边界即可:

if (id === 0 || id === 16) {
if (x === 3) {
candidates.push(piece + 1);
} else if (x === 4) {
candidates.push(piece + 1, piece - 1);
} else if (x === 5) {
candidates.push(piece - 1);
}
if (y === 7) {
candidates.push(piece + 9);
} else if (=== 8) {
candidates.push(piece + 9, piece - 9);
} else if (=== 9) {
candidates.push(piece - 9);
}
}

士规则

允许在九宫格内斜线移动。这个更简单,因为只有2种情况:

  • 如果在中心,可以去4个地方。
  • 如果不在中心,只可以去中心。

if (id >= 1 && id <= 2) {
if (piece === 76) {
candidates.push(86, 84, 68, 66);
} else {
candidates.push(76);
}
}

马规则

类似于将帅规则,判断边界,有8个方向可以去。但是比将帅多了一个限制:

  • 不能有东西挡着。

if (id >= 5 && id <= 6) {
if (x >= 1 && y >= 2 && pieces.indexOf(piece - 9) === -1) candidates.push(piece - 19);
if (x <= 7 && y >= 2 && pieces.indexOf(piece - 9) === -1) candidates.push(piece - 17);
if (x >= 2 && y >= 1 && pieces.indexOf(piece - 1) === -1) candidates.push(piece - 11);
if (x <= 6 && y >= 1 && pieces.indexOf(piece + 1) === -1) candidates.push(piece - 7);
if (x >= 2 && y <= 8 && pieces.indexOf(piece - 1) === -1) candidates.push(piece + 7);
if (x <= 6 && y <= 8 && pieces.indexOf(piece + 1) === -1) candidates.push(piece + 11);
if (x >= 1 && y <= 7 && pieces.indexOf(piece + 9) === -1) candidates.push(piece + 17);
if (x <= 7 && y <= 7 && pieces.indexOf(piece + 9) === -1) candidates.push(piece + 19);
}

象规则

类似于马规则,判断边界,允许朝4个方向移动,但是多了一个限制:

  • 不能过河。

if (id >= 3 && id <= 4) {
if (x >= 2 && y >= 2 && pieces.indexOf(piece - 10) === -1 && piece - 20 > 44) candidates.push(piece - 20);
if (x <= 6 && y >= 2 && pieces.indexOf(piece - 8) === -1 && piece - 16 > 44) candidates.push(piece - 16);
if (x >= 2 && y <= 7 && pieces.indexOf(piece + 8) === -1 && piece + 16 > 44) candidates.push(piece + 16);
if (x <= 6 && y <= 7 && pieces.indexOf(piece + 10) === -1 && piece + 20 > 44) candidates.push(piece + 20);
}

车规则

遍历4个方向,找到第一个可以撞到的棋子停止遍历,并且包含撞到的棋子。

if (id >= 7 && id <= 8) {
for (let i = x + 1; i <= 8; i++) {
candidates.push(y * 9 + i);
if (pieces.indexOf(y * 9 + i) !== -1) break;
}
for (let i = x - 1; i >= 0; i--) {
candidates.push(y * 9 + i);
if (pieces.indexOf(y * 9 + i) !== -1) break;
}
for (let i = y - 1; i >= 0; i--) {
candidates.push(i * 9 + x);
if (pieces.indexOf(i * 9 + x) !== -1) break;
}
for (let i = y + 1; i <= 9; i++) {
candidates.push(i * 9 + x);
if (pieces.indexOf(i * 9 + x) !== -1) break;
}
}

炮规则

类似车规则,但是多了2个条件:

  • 不包含撞到的第一个棋子。
  • 撞到棋子后需要继续便利,遇到第二个撞到的棋子,停止遍历,包含第二个撞到的棋子。

if (id >= 9 && id <= 10) {
for (let i = x + 1, flag = false; i <= 8; i++) {
if (flag) {
if (pieces.indexOf(y * 9 + i) !== -1) {
candidates.push(y * 9 + i);
break;
}
} else if (pieces.indexOf(y * 9 + i) === -1) candidates.push(y * 9 + i);
else flag = true;
}
for (let i = x - 1, flag = false; i >= 0; i--) {
if (flag) {
if (pieces.indexOf(y * 9 + i) !== -1) {
candidates.push(y * 9 + i);
break;
}
} else if (pieces.indexOf(y * 9 + i) === -1) candidates.push(y * 9 + i);
else flag = true;
}
for (let i = y - 1, flag = false; i >= 0; i--) {
if (flag) {
if (pieces.indexOf(i * 9 + x) !== -1) {
candidates.push(i * 9 + x);
break;
}
} else if (pieces.indexOf(i * 9 + x) === -1) candidates.push(i * 9 + x);
else flag = true;
}
for (let i = y + 1, flag = false; i <= 9; i++) {
if (flag) {
if (pieces.indexOf(i * 9 + x) !== -1) {
candidates.push(i * 9 + x);
break;
}
} else if (pieces.indexOf(i * 9 + x) === -1) candidates.push(i * 9 + x);
else flag = true;
}
}

兵规则

  • 过河前只有1个方向。
  • 过河后、顶头前有3个方向。
  • 顶头后有2个方向。

if (id >= 11 && id <= 15) {
if (y >= 1) {
candidates.push(piece - 9);
}
if (y <= 4) {
if (x >= 1) {
candidates.push(piece - 1);
}
if (x <= 7) {
candidates.push(piece + 1);
}
}
}

删除自己的棋子

遍历​​candidates​​,若找到自己的棋子就删掉。

for (let i = 0; i < candidates.length; i++) {
const c = candidates[i];
if (pieces.indexOf(c) !== -1) {
if (pieces.indexOf(c) < 16 && id < 16) {
candidates.splice(i, 1);
i--;
}
}
}

检测将帅见面

我们针对所有移动情况,判断下移动后是否会导致将帅见面,若见面,则这种情况不应该作为candidates之一。

for (let i = 0; i < candidates.length; i++) {
const c = candidates[i];
const newPieces = [...pieces];
newPieces[id] = c;
const x1 = newPieces[0] % 9;
const x2 = newPieces[16] % 9;
if (x1 === x2) {
const y1 = Math.floor(newPieces[0] / 9);
const y2 = Math.floor(newPieces[16] / 9);
let flag = false;
for (let j = y2 + 1; j < y1; j++) {
if (newPieces.indexOf(j * 9 + x1) !== -1) {
flag = true;
break;
}
}
if (!flag) {
candidates.splice(i, 1);
i--;
}
}
}


举报

相关推荐

0 条评论