likes
comments
collection
share

[教你做小游戏] 手写 五子棋 三三禁手 判断 有多难?还得递归!

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

背景

之前我开发过五子棋,输出了一些文章:

之前的版本,五子棋不支持禁手。现在,我准备加一下禁手功能,还真没那么简单。

之前带大家开发完了长连禁手和四四禁手,复杂度还算可以接受的。现在来到三三禁手,是真的难!

禁手介绍

[教你做小游戏] 手写 五子棋 三三禁手 判断 有多难?还得递归!

三三禁手有多难?

案例1:这是禁手

这是典型的三三禁手:

[教你做小游戏] 手写 五子棋 三三禁手 判断 有多难?还得递归!

案例2:这不是禁手

这个23不是禁手:

[教你做小游戏] 手写 五子棋 三三禁手 判断 有多难?还得递归!

你知道为什么吗?

因为7、5、23组成的三,不是活三,而是眠三。

【活三】定义:再下一子,可变活四。

【活四】定义:有2个地方,可变五连(意味着此时白棋若无四连,一定已经输了)。

我们分析此图,7、5、23是活三吗?

显然,23右侧是个黑的禁手,四四禁手,所以不能下。

此外,7左侧,也是黑的禁手,三三禁手,所以不能下。

所以7、5、23看起来是“活三”,其实只是个眠三。

这就导致,23不是禁手。

案例3:这是禁手

这个23是禁手:

[教你做小游戏] 手写 五子棋 三三禁手 判断 有多难?还得递归!

你知道跟案例2的差异吗?唯一的差异是22这手白子!正因为这手白子,7左侧不是黑的三三禁手了!

为什么呢?因为15、13和7左侧如果组成3连,这其实是眠3,所以7左侧不是三三禁手。

那么为什么15、13和7左侧是眠三呢?

  • 一方面,因为15左下角落子后,只是冲四不是活四。
  • 另一方面,7上方其实是黑的禁手,四四禁手(落子后导致左斜线有4、右斜线有4),所以7上方是不可以落黑子的。如图:

[教你做小游戏] 手写 五子棋 三三禁手 判断 有多难?还得递归!

开发思路

顶层逻辑

判断落子后有几个活三,如果超过1个,就认为是禁手。一共4个方向,我们都需要计算下:

function judgeThreeThreeBan(pieces: number[]) {
  const aliveThreeCount = getAliveThreeCountOnOneLine(pieces, 1, 0) + getAliveThreeCountOnOneLine(pieces, 0, 1)
    + getAliveThreeCountOnOneLine(pieces, -1, 1) + getAliveThreeCountOnOneLine(pieces, 1, 1);
  return aliveThreeCount > 1;
}

中层逻辑

经过严谨的遍历论证,我发现了一些事实:

  • 一个方向最多只能有1个活三,不可能一个方向有2个活三(一共有4个方向:竖直、水平、左斜、右斜)。
  • 活三的形态只有2种:空空⚫⚫⚫空空⚫空⚫⚫空。空表示非墙壁、非白棋、非黑棋,而是空格。
  • 第一个形态,注意空⚫⚫⚫空不一定是活三,如果想做活三,它还要求两侧不能都是白子or墙壁。如果只有一侧是白棋or墙壁,另一侧不是白棋墙壁,是可以的。
  • 第一个形态,若要做活三,要求紧中间的(即第二个空)不能都是禁手。其它2个空,允许是禁手。
  • 第二个形态空⚫空⚫⚫空,若要做活三,要求中间的不能是禁手。两侧的,允许是禁手。

如果不是这2个形态,那么一定不是活三。

如果落子后,形成了这2个形态之一,我们必须能够做出判断,是哪个形态,这个子是哪个位置。

之前我们判断四四禁手时,计算了3个变量middle before after。这次如果只算这3个是不够的。为了判断形态和当前棋子所处形态的位置,需要继续计算beforeBeforeafterAfrer

含义:

  • middle:刚落下的黑子,在这条线上,连续有几个。
  • before:若为-1,表明左侧紧挨着白棋或墙壁。否则,表明左侧空了一个空格后,有多少个连续的黑子。
  • beforeBefore:若为-1,表明连续before个黑子的左侧,紧挨着白棋或墙壁。否则,表示连续before个黑子左侧空了一个空格后,有多少个连续的黑子。
  • after:若为-1,表明右侧紧挨着白棋或墙壁。否则,表明右侧空了一个空格后,有多少个连续的黑子。
  • afterAfter:若为-1,表明连续after个黑子的右侧,紧挨着白棋或墙壁。否则,表示连续after个黑子右侧空了一个空格后,有多少个连续的黑子。

如果middle=3,表明形态1。如果middle=2或1,表明形态2。

按照上述分析,可以写出下面的代码:

(注意:因为有要求不是禁手,这涉及到调用函数judgeBan来判断,但是我们正在开发的逻辑属于judgeBan的一部分,所以这是一种递归逻辑。)

if (before === -1 || after === -1 || middle > 3) return 0;
if (middle === 3) {
  if (before !== 0 || after !== 0) return 0;
  if (beforeBefore === -1 && afterAfter === -1) return 0;
  if (beforeBefore === -1) {
    if (afterAfter > 0) return 0;
    return judgeBan([...pieces, -1, getXYPiece(x + dx * (4 - deltaMiddle), y + dy * (4 - deltaMiddle))]) ? 0 : 1;
  }
  if (afterAfter === -1) {
    if (beforeBefore > 0) return 0;
    return judgeBan([...pieces, -1, getXYPiece(x - dx * deltaMiddle, y - dy * deltaMiddle)]) ? 0 : 1;
  }
  if (beforeBefore > 0 && afterAfter > 0) return 0;
  const a = judgeBan([...pieces, -1, getXYPiece(x - dx * deltaMiddle, y - dy * deltaMiddle)]);
  const b = judgeBan([...pieces, -1, getXYPiece(x + dx * (4 - deltaMiddle), y + dy * (4 - deltaMiddle))]);
  return a && b ? 0 : 1;
}
if (middle === 2) {
  if (before === 1 && after === 0) {
    if (beforeBefore === -1) return 0;
    const newPieces = [...pieces, -1, getXYPiece(x - dx * deltaMiddle, y - dy * deltaMiddle)];
    return judgeBan(newPieces) ? 0 : 1;
  }
  if (before === 0 && after === 1) {
    if (afterAfter === -1) return 0;
    const newPieces = [...pieces, -1, getXYPiece(x + dx * (3 - deltaMiddle), y + dy * (3 - deltaMiddle))];
    return judgeBan(newPieces) ? 0 : 1;
  }
  return 0;
}
if (middle === 1) {
  if (before === 2 && after === 0) {
    if (beforeBefore === -1) return 0;
    const newPieces = [...pieces, -1, getXYPiece(x - dx, y - dy)];
    return judgeBan(newPieces) ? 0 : 1;
  }
  if (before === 0 && after === 2) {
    if (afterAfter === -1) return 0;
    const newPieces = [...pieces, -1, getXYPiece(x + dx, y + dy)];
    return judgeBan(newPieces) ? 0 : 1;
  }
}

底层逻辑

如何计算beforeBeforeafterAfrer

const piece = pieces[pieces.length - 1];
const [x, y] = getPieceXY(piece);
let beforeBefore = 0;
let before = 0;
let middle = 1;
let after = 0;
let afterAfter = 0;
let flag = 0;
for (let i = x - dx, j = y - dy; ; i -= dx, j -= dy) {
  if (!(i >= 0 && i <= 14 && j >= 0 && j <= 14) || isWhite(i, j, pieces)) {
    if (flag === 0) before = -1;
    if (flag === 1) beforeBefore = -1;
    break;
  }
  if (isBlack(i, j, pieces)) {
    if (flag === 1) before += 1;
    else if (flag === 2) beforeBefore += 1;
    else middle += 1;
  } else if (flag === 2) {
    break;
  } else {
    flag += 1;
  }
}
const deltaMiddle = middle;
flag = 0;
for (let i = x + dx, j = y + dy; ; i += dx, j += dy) {
  if (!(i >= 0 && i <= 14 && j >= 0 && j <= 14) || isWhite(i, j, pieces)) {
    if (flag === 0) after = -1;
    if (flag === 1) afterAfter = -1;
    break;
  }
  if (isBlack(i, j, pieces)) {
    if (flag === 1) after += 1;
    else if (flag === 2) afterAfter += 1;
    else middle += 1;
  } else if (flag === 2) {
    break;
  } else {
    flag += 1;
  }
}

中层逻辑+底层逻辑代码,就组成了我们的函数getAliveThreeCountOnOneLine

function getAliveThreeCountOnOneLine(pieces: number[], dx: number, dy: number) {
  // 底层逻辑
  // 中层逻辑
}

写在最后

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