[教你做小游戏] 手写 五子棋 三三禁手 判断 有多难?还得递归!
背景
之前我开发过五子棋,输出了一些文章:
- 《用177行代码写个体验超好的五子棋》
- 《用86行代码写一个联机五子棋WebSocket后端》
- 《五子棋怎么存棋局信息?》
- 《五子棋怎么判断输赢?你能5分钟交出代码吗?》
- 《我做的联机五子棋是如何追求极致用户体验的?(上)》
- 《我做的联机五子棋是如何追求极致用户体验的?(下)》
- 《如何开发实现: 判断 五子棋 长连禁手》
- 《如何开发实现: 判断 五子棋 四四禁手》
之前的版本,五子棋不支持禁手。现在,我准备加一下禁手功能,还真没那么简单。
之前带大家开发完了长连禁手和四四禁手,复杂度还算可以接受的。现在来到三三禁手,是真的难!
禁手介绍
三三禁手有多难?
案例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个是不够的。为了判断形态和当前棋子所处形态的位置,需要继续计算beforeBefore
和afterAfrer
。
含义:
- 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;
}
}
底层逻辑
如何计算beforeBefore
和afterAfrer
?
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个专栏里分享:《教你做小游戏》、《极致用户体验》。
转载自:https://juejin.cn/post/7155882522159611911