[教你做小游戏] JS实现象棋移动规则
背景
兄弟们,之前我开发了支持联机对战的五子棋、斗地主、UNO。在大家的呼吁之下,我准备开发「象棋」啦!
😄 不出意外,国庆假期,联机象棋就能跟大家见面了!
之前的进展:
继续给大家同步进展:今天,实现了象棋的移动规则,即选中某个棋子后,程序可以计算该棋子可移动的范围。这是象棋游戏的核心逻辑。
数据结构
先带大家回顾下我们的数据结构:
- 棋盘状态是用一个长度为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表示炮。
- 其余表示兵卒。
输入与输出
输入:当前棋盘状态pieces: number[]
和已选中的棋子IDid: 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 - p);
}
含义如下:要计算黑色某棋子范围,只需要把当前的黑色当作红色,红色当作黑色,就能套用getCandidatesOfOneRedPiece
函数了。
当然这有个前提条件,因为位置的映射89 - p
是做中心对称,我们要求id - 16
这个操作也必须是中心对称的。怎么保证?需要定义初始棋盘时,把x
的位置和x + 16
的位置中心对称。
实现getCandidatesOfOneRedPiece
大体思路
重要前提:我们开发的象棋规则,需求是明确的,不会有奇奇怪怪的规则,以后也不需要更新迭代了,也不需要开发AI。所以保证开发效率和适当的可读性就足够了。没必要抽象出通用的移动规则,没必要做成可配置的规则。没必要一次性计算很多变量从而方便计算所有棋子的可移动范围。
- 先不区分敌我,基于棋子基本移动规则计算出所有可以去的范围(即可以吃队友、允许将帅见面)。
- 然后统一从候选者里删除吃自己棋子的选项。
- 统一从候选者里删除会导致将帅见面的选项。
先计算一些通用变量
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--;
}
}
}
写在最后
我是HullQin,独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:《教你做小游戏》、《极致用户体验》。
转载自:https://juejin.cn/post/7150687038910496781