拼棋派对开发实战系列封面

五子棋看起来只需要点一下棋盘,但手机上的小棋格容易误触。《拼棋派对》把落子分为“选择空位”和“本人确认”两步:先看清位置,再提交棋子。确认后只检查最新棋子经过的四条线,就能判断是否五连。

这一篇同时记录实操发现的悔棋问题。正常落子与五连获胜已经在API26模拟器上验证;当前1.0.19的悔棋仍有顺序错误,后文给出复现与改进方案。通过这个案例,可以看清棋盘位置与操作历史为什么必须分开保存。

本文目录

15×15盘面与待确认位置

createEmptyGomoku 创建225个空字符串,A、B表示双方棋子。索引转换与象棋类似,只是每行15个位置:row = Math.floor(index / 15),col = index % 15。页面上的列字母和行号供玩家阅读,内部继续使用零起点索引。

页面至少需要区分盘面、当前玩家、选择位置、已落手数和胜者。gomokuSelectedIndex 不等于已经落下的棋子,只有确认后才写入 gomokuCells。把这两个状态混在一起,撤销选择就会变成删除正式棋子,计分和胜负也可能被提前触发。

selectGomokuCell 会拒绝已占用位置和结束后的操作;选中空点时记录索引,并更新“已选,玩家A确认”提示。再次选其他空点可以调整落点,玩家不需要先悔棋才能纠正尚未确认的位置。

本人确认后才提交落子

confirmGomokuForPlayer 检查调用方是不是当前玩家,也检查比赛是否结束。通过后才进入提交函数。手机竖屏显示当前方的“A确认”或“B确认”,其他布局可以把双方确认入口放在各自区域,但事件仍携带玩家身份。

下面摘录提交函数的关键状态更新:

const cells = this.gomokuCells.slice();
cells[this.gomokuSelectedIndex] = this.gomokuCurrent;
this.gomokuCells = cells;
this.gomokuMoves += 1;
if (isGomokuWin(cells, this.gomokuSelectedIndex, this.gomokuCurrent)) {
  const winner = this.gomokuCurrent === 'A' ? '玩家A' : '玩家B';
  this.gomokuWinner = winner;
  this.gomokuSelectedIndex = -1;
  return;
}
this.gomokuCurrent = this.gomokuCurrent === 'A' ? 'B' : 'A';
this.gomokuSelectedIndex = -1;

片段省略了获胜声音、提示与战绩写入。核心顺序是复制盘面、落子、累加手数、判胜、未胜时换人。赢棋后提前返回,避免胜者刚显示出来又被切回正常回合。

五子棋选择空点,玩家A确认后正式落子并切换玩家B

沿四个方向检查连续棋子

最新落子之外的盘面没有变化,因此新形成的五连必定经过这枚棋子。项目检查横线、竖线和两条斜线,每条线分别向正反两个方向计数。两个方向都包括起点,所以总数需要减一。

以横线为例,实际规则是:

countGomokuLine(cells, row, col, 0, 1, player) +
countGomokuLine(cells, row, col, 0, -1, player) - 1 >= 5

计数函数每次推进一格,遇到空点、对方棋子或边界立即停止。它使用行列边界,而不是仅检查数组索引是否落在0—224之间:第一行最后一格与第二行第一格虽然索引连续,却不是一条横线。

while (currentRow >= 0 && currentRow < 15 &&
       currentCol >= 0 && currentCol < 15) {
  const index = currentRow * 15 + currentCol;
  if (cells[index] !== player) {
    break;
  }
  count += 1;
  currentRow += rowStep;
  currentCol += colStep;
}

当前使用“连续五枚及以上获胜”的休闲规则,没有三三、四四和长连禁手。这与含禁手的专业规则不同。介绍玩法时应写清规则范围,不能只因棋盘是15×15就宣称符合所有竞赛规则。

九手五连的实际效果

在空盘按以下索引交替落子,每一次都先选择再确认:A为110、111、112、113、114;B为140、142、144、146。A的五枚位于同一行相邻位置,B的四枚分散,避免提前形成五连。

第八手后页面仍处于玩家A待落子状态;第九手确认后显示玩家A获胜,随后“再来一局”恢复第1手。图中可对照A的横向连线与结束面板。

双方交替落子,第九手玩家A形成五连获胜,再来一局

这里验证了横向五连、结束提示和重开。纵线与两条斜线使用相同计数函数,但本次截图没有逐条覆盖它们;补充规则测试时还应加入被空点截断、被对方截断和边界跨行的情况。

复现悔棋顺序错误

实操中发现,当前 undoGomoku 从数组尾部向前查找第一枚非空棋子,然后删除。这找到的是“位置索引最大的一枚”,不是“最后一步”。只沿索引递增的位置落子,会掩盖这个问题。

复现只需要两步:A先落200,即第14行第6列;B后落10,即第1行第11列。此时点击悔棋,预期删除最后落下的B棋子,保留A棋子。实际结果相反:A棋子消失,B棋子留下。动图先显示两枚棋子均在盘上,再显示悔棋后的错误盘面。

复现悔棋错误:A先落200、B后落10,悔棋错误删除A棋子

出错代码的核心是:

for (let index = cells.length - 1; index >= 0; index--) {
  if (cells[index].length > 0) {
    cells[index] = '';
    // 更新盘面、手数与行动方
    return;
  }
}

盘面数组只表达“哪里有谁的棋子”,没有保存落子时间。仅从最终位置无法还原操作先后,这个问题不能靠换一种数组遍历方向解决。

用落子历史正确回退

下面是建议加入的历史方案,尚未写入本轮主应用,也不把前面的错误截图称为修复后效果。定义一条记录保存index与player,确认落子时追加记录,悔棋时pop最后一条。

interface GomokuMove {
  index: number;
  player: string;
}

@State gomokuHistory: GomokuMove[] = [];

确认时,在清除选择和切换玩家之前记录本次落子:

this.gomokuHistory = [...this.gomokuHistory, {
  index: this.gomokuSelectedIndex,
  player: this.gomokuCurrent
}];

回退主体可以改成以下形式,外围继续保留当前“不允许结束后悔棋”的产品规则:

const history = this.gomokuHistory.slice();
if (history.length === 0) {
  return;
}
const last = history.pop() as GomokuMove;
const cells = this.gomokuCells.slice();
cells[last.index] = '';
this.gomokuCells = cells;
this.gomokuHistory = history;
this.gomokuCurrent = last.player;
this.gomokuSelectedIndex = -1;
this.gomokuMoves = Math.max(0, this.gomokuMoves - 1);

重开时还要清空history。如果对局持久化,需要盘面与历史一起保存、一起恢复,否则重新进入页面后又失去先后顺序。接入修改后,至少复测200→10、连续两次悔棋、无历史悔棋和重开后悔棋,再执行构建和模拟器验收。

这次五子棋实操得到两项清楚的结果:确认落子与横向五连已跑通;悔棋暴露出数据模型缺少时间顺序的问题。记录问题和修复路径,比把错误行为包装成完整功能更能帮助后续开发。

源码入口:GameLogic.ets 的 isGomokuWin、countGomokuLine;Index.ets 的 selectGomokuCell、confirmGomokuMove、undoGomoku。ArkTS状态管理参考HarmonyOS官方文档。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐