[教你做小游戏] 五子棋 判断 四四禁手 算法
背景
之前我开发过五子棋,输出了一些文章:
- 《用177行代码写个体验超好的五子棋》
- 《用86行代码写一个联机五子棋WebSocket后端》
- 《五子棋怎么存棋局信息?》
- 《五子棋怎么判断输赢?你能5分钟交出代码吗?》
- 《我做的联机五子棋是如何追求极致用户体验的?(上)》
- 《我做的联机五子棋是如何追求极致用户体验的?(下)》
- 《如何开发实现: 判断 五子棋 长连禁手》
之前的版本,五子棋不支持禁手。现在,我准备加一下禁手功能,还真没那么简单。
禁手介绍
四四禁手
这个判断相对简单,我们判断下棋后,这个棋子是否同时组成了1个以上的4连就可以了(包括活四、冲四)。
- 活四:就是有两个点可以成五的四。(见下方第一个图)
- 冲四:有且只有一个点可以形成连五的四。(见下方第二个图,中间是连冲,两边是跳冲)
开发
我们需要兼顾跳冲四的场景。
以竖方向为例子,我们需要判断竖直方向,紧挨着当前黑棋的黑棋子有几个。随后继续往上遍历,如果碰到白棋结束遍历,如果碰到空白棋子,则跳过它,看看后面还有多少连续的黑色棋子。之后再往下遍历,如果碰到白棋结束遍历,如果碰到空白棋子,则跳过它,看看后面还有多少连续的黑色棋子。这样得到3个变量:middle
before
after
。
一个特殊情况
如果middle
已经是4了,我们还需要存下相邻是否有白棋,如果两头都是白色,那么这不算活四冲四。例如,下图带标记的黑色棋子不是禁手:
如果middle
上方相邻是白棋,我们可以把before
标为-1,类似的,如果下方相邻白棋,我们可以把after
标记为-1。
另一个特殊情况
还有个特殊情况,下面带标记的黑色棋子,也不是禁手:
因为左侧的空白,黑色不能落子,那里是长连禁手。此时黑棋只形成了1个跳冲四。
如何解决呢?我们判断before
和middle
相加大于等于5,则表示会触发长连禁手,那么这里就不算一个活四冲四。
外层逻辑
我们需要遍历4个方向,包括竖直、水平、左斜、右斜。都是一样的算法。我们没必要写四遍,可以通过参数dx
dy
表明每次遍历时,x和y的变化量。通过参数传入。传4遍就可以。例如这样:
function judgeFourFourBan(pieces: number[]) {
const fourCount = getFourOnOneLine(pieces, 1, 0)
+ getFourOnOneLine(pieces, 0, 1)
+ getFourOnOneLine(pieces, -1, 1)
+ getFourOnOneLine(pieces, 1, 1);
return fourCount > 1;
}
这完全是四四禁手的定义:是否存在1个以上的活四冲四。
注:同一行可能有2个冲四,即getFourOnOneLine
返回值为0-2,也就是说,fourCount
最大值为8,下面的情况,fourCount
达到最大值,所以中间的黑棋就是禁手了。
当然,如果棋局变成这样,由于长连禁手存在,fourCount
应该是0,这时候中间的黑棋不是禁手:
是不是很神奇!我作为开发者,已经懵了!!!佩服五子棋禁手的博大精深!
内层逻辑
下面我们编写核心逻辑getFourOnOneLine
:
function getFourOnOneLine(pieces: number[], dx: number, dy: number) {
const piece = pieces[pieces.length - 1];
const [x, y] = getPieceXY(piece);
let before = 0;
let middle = 1;
let after = 0;
let flag = false;
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) before = -1;
break;
}
if (isBlack(i, j, pieces)) {
if (flag) before += 1;
else middle += 1;
} else {
if (flag) break;
flag = true;
}
}
flag = false;
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) after = -1;
break;
}
if (isBlack(i, j, pieces)) {
if (flag) after += 1;
else middle += 1;
} else {
if (flag) break;
flag = true;
}
}
if (middle === 4) return before === 0 || after === 0 ? 1 : 0;
return (before > 0 && before + middle === 4 ? 1 : 0) + (after > 0 && middle + after === 4 ? 1 : 0);
}
下面,解释一下if (middle === 4) return before === 0 || after === 0 ? 1 : 0;
。这句话不是凭空写出来的,是经过严格推理论证的:
middle | before | after | 是否活四冲四 |
---|---|---|---|
4 | 0 | 0 | 活四 |
4 | -1 | 0 | (白棋点)冲四 |
4 | 0 | -1 | 冲四(白棋点) |
4 | 正数 | 0 | (长连禁手点)冲四 |
4 | 0 | 正数 | 冲四(长连禁手点) |
4 | -1 | -1 | (白棋点)四(白棋点) |
4 | -1 | 正数 | (白棋点)四(长连禁手点) |
4 | 正数 | -1 | (长连禁手点)四(白棋点) |
4 | 正数 | 正数 | (长连禁手点)四(长连禁手点) |
可以看到,middle=4时,形成活四冲四的充要条件:before和after至少一个为0。
写在最后
我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,联系我,交个朋友),转发本文前需获得作者HullQin授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋、象棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:《教你做小游戏》、《极致用户体验》。
转载自:https://juejin.cn/post/7155511034483048456