likes
comments
collection

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

作者站长头像
站长
· 阅读数 12

背景

兄弟们,之前我开发了支持联机对战的五子棋、斗地主、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表示炮。
  • 其余表示兵卒。

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

输入与输出

输入:当前棋盘状态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。所以保证开发效率和适当的可读性就足够了。没必要抽象出通用的移动规则,没必要做成可配置的规则。没必要一次性计算很多变量从而方便计算所有棋子的可移动范围。

  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--;
    }
  }
}

写在最后

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