使用HTML5 Canvas实现五子棋游戏(禁手)

612 阅读5分钟

在上一篇文章中,我们构建了一个基础的五子棋游戏,包括棋盘绘制、鼠标点击事件处理以及检查获胜条件的基本逻辑。现在,我们将增加对“禁手”的检测。

首先看一下禁手规则

禁手的定义
  • 对局中如果使用将被判负的行棋手段。
禁手的分类
  • 三三禁手(黑棋一子落下同时形成两个活三,此子必须为两个活三共同的构成子);
  • 四四禁手(黑棋一子落下同时形成两个或两个以上的冲四或活四);
  • 长连禁手(黑棋一子落下形成一个或一个以上的长连)。
构成禁手的基本子力要素
  • 活三(本方再走一着可以形成活四的三);
  • 活四(有两个点可以成五的四);
  • 冲四(只有一个点可以成五的四);
  • 长连(在棋盘上的阳线和阴线任意一条线上,形成的5个以上同色棋子不间隔的相连)。
有关禁手的规定
  • 黑方五连与禁手同时形成时,禁手失效,黑方胜。
下图是禁手的一些例子

43e6c73345e99950ad4b5fb4.webp

长连禁手规则检测

从规则上看长连禁手最容易判断,我们先来检查这个规则。从图中可以看出和落子点同一条线上相连的棋子大于5即为长连禁手。

// 检查长连禁手
function checkLongConnect(board, x, y, player, maxCount = 5) {
  let count = 0; // 连子计数

  function checkAndReset(reset = true) {
    if (count >= maxCount) return true
    reset && (count = 0);
  }

  // 向上检查
  for (let i = y - 1; i >= 0 && board[i][x] === player; i--) {
    count++;
  }

  // 向下检查
  for (let i = y + 1; i < boardSize && board[i][x] === player; i++) {
    count++;
  }

  if (checkAndReset()) return true;

  // 向左检查
  for (let i = x - 1; i >= 0 && board[y][i] === player; i--) {
    count++;
  }

  // // 向右检查
  for (let i = x + 1; i < boardSize && board[y][i] === player; i++) {
    count++;
  }

  if (checkAndReset()) return true;

  // 向左上检查
  for (let i = x - 1, j = y - 1; i >= 0 && j >= 0 && board[j][i] === player; i--, j--) {
    count++;
  }

  // 向右下检查
  for (let i = x + 1, j = y + 1; i < boardSize && j < boardSize && board[j][i] === player; i++, j++) {
    count++;
  }

  if (checkAndReset()) return true;

  // 向右上检查
  for (let i = x + 1, j = y - 1; i < boardSize && j >= 0 && board[j][i] === player; i++, j--) {
    count++;
  }

  // 向左下检查
  for (let i = x - 1, j = y + 1; i >= 0 && j < boardSize && board[j][i] === player; i--, j++) {
    count++;
  }

  if (checkAndReset(false)) return true;
}

三三禁手和四四禁手

三三禁手和四四禁手需要检测活三、活四和冲四,我们用变量startEndCount记录落子位置同一条线上的两端是否是空位。同时从图列可看出,活三在同一条线上可以出现一个跳空一子的情况,四四最多可以出现两个跳空一子的情况,用变量emptyCount来记录。

// 检查三三禁手、四四禁手
function checkThreeOrFour(board, x, y, player) {
  let count = 0; // 同一条线最多跳空一子计数
  let startEndCount = 0; // 头尾空位计数
  let emptyCount = 0; // 跳空计数
  let three = 0; // 三连计数
  let four = 0; // 四连计数
  let sideline = 'sideline'; // 边线
  
  // 向上检查
  let i = y - 1;
  while (i >= 0) {
    if (board[i][x] === player) {
      count++;
      i--;
    } else if (!board[i][x]) { // 空位
      if (i - 1 >= 0 && (!board[i - 1][x] || board[i - 1][x] === sideline)) { // 再下一个还是空位
        startEndCount++;
        break;
      } else {
        emptyCount++;
        i--;
        continue;
      }
    } else {
      break;
    }
  }

  // 向下检查
  for (let i = y + 1; i < boardSize && board[i][x] === player; i++) {
    count++;
  }
  
  let i = y + 1;
  while (i >= 0) {
    if (board[i][x] === player) {
      count++;
      i++;
    } else if (!board[i][x]) { // 空位
      if (i + 1 < boardSize && (!board[i + 1][x] || board[i + 1][x] === sideline)) { // 再下一个还是空位
        startEndCount++;
        break;
      } else {
        emptyCount++;
        i++;
        continue;
      }
    } else {
      break;
    }
  }
}

代码优化

写到这里我们发现代码中有很多重复,可以抽象到一个方法中。

// 检查三三禁手、四四、长连禁手
function checkThreeOrFour(board, x, y, player) {
  let count = 0; // 同一条线最多跳空一子计数
  let startEndCount = 0; // 头尾空位计数
  let emptyCount = 0; // 跳空计数
  let three = 0; // 三连计数
  let four = 0; // 四连计数
  let sideline = 'sideline'; // 边线
   
  // 检查活三、活四、冲四
  function traverse(currentPlayer, endPlayer, init, condition, next) {
    let { i, j } = init;

    while (condition(i, j)) {
      if (currentPlayer(i, j) === player) {
        count++;
        [i, j] = next(i, j);
      } else if (!currentPlayer(i, j)) { // 空位
        if (!endPlayer(i, j) || endPlayer(i, j) === sideline) { // 再下一个还是空位
          startEndCount++;
          break;
        } else {
          emptyCount++;
          [i, j] = next(i, j);
          continue;
        }
      } else {
        break;
      }
    }
  }
  
  // 判断是否形成禁手
  function check() {
    if (count >= 4 && emptyCount === 2) { // 单排活四或冲四
      return true;
    }

    if (count === 2 && startEndCount === 2 && emptyCount < 2) { // 活三禁手
      three++;
    }

    if (count === 3 && (startEndCount === 1 || startEndCount === 2) && emptyCount < 2) { // 四四禁手
      four++;
    }

    if (three >= 2 || four >= 2) return true;

    count = 0;
    startEndCount = 0;
    emptyCount = 0;
  }

  // 向上检查
  traverse(
    (i) => board[i][x],
    (i) => i - 1 >= 0 ? board[i - 1][x] : sideline,
    { i: y - 1 },
    (i) => i >= 0,
    (i) => [i - 1]
  );

  // 向下检查
  traverse(
    (i) => board[i][x], 
    (i) => i + 1 < boardSize ? board[i + 1][x] : sideline, 
    { i: y + 1 }, 
    (i) => i < boardSize, 
    (i) => [i + 1]
  );

  if (check()) return true;

  // 向左检查
  traverse(
    (i) => board[y][i], 
    (i) => i - 1 >= 0 ? board[i - 1][x] : sideline, 
    { i: x - 1 }, 
    (i) => i >= 0, 
    (i) => [i - 1]
  );

  // 向右检查
  traverse(
    (i) => board[y][i], 
    (i) => i + 1 < boardSize ? board[i + 1][x] : sideline, 
    { i: x + 1 }, 
    (i) => i < boardSize, 
    (i) => [i + 1]
  );

  if (check()) return true;

  // 向左上检查
  traverse(
    (i, j) => board[j][i], 
    (i, j) => i - 1 >= 0 && j - 1 >= 0 ? board[j - 1][i - 1] : sideline, 
    { i: x - 1, j: y - 1 }, 
    (i, j) => i >= 0 && j >= 0, 
    (i, j) => [i - 1, j - 1]
  );

  // 向右下检查
  traverse(
    (i, j) => board[j][i], 
    (i, j) => i + 1 < boardSize && j + 1 < boardSize ? board[j + 1][i + 1] : sideline, 
    { i: x + 1, j: y + 1 }, 
    (i, j) => i < boardSize && j < boardSize, 
    (i, j) => [i + 1, j + 1]
  );

  if (check()) return true;

  // 向右上检查
  traverse(
    (i, j) => board[j][i], 
    (i, j) => i + 1 < boardSize && j - 1 >= 0 ? board[j - 1][i + 1] : sideline, 
    { i: x + 1, j: y - 1 }, 
    (i, j) => i < boardSize && j >= 0, 
    (i, j) => [i + 1, j - 1]
  );

  // 向左下检查
  traverse(
    (i, j) => board[j][i], 
    (i, j) => i - 1 >= 0 && j + 1 < boardSize ? board[j + 1][i - 1] : sideline, 
    { i: x - 1, j: y + 1 }, 
    (i, j) => i >= 0 && j < boardSize, 
    (i, j) => [i - 1, j + 1]
  );

  if (check()) return true;
}

鼠标点击事件添加禁手判断

我们在这之前的鼠标点击事件中增加禁手判断代码。

function handleMouseClick(event) {
  // 之前的代码

  if (x >= 0 && y >= 0 && x < boardSize && y < boardSize && gameBoard[y][x] === null) {
    if (currentPlayer === 'black' && (checkLongConnect(gameBoard, x, y, currentPlayer) || checkThreeOrFour(gameBoard, x, y, currentPlayer))) {
      openDialog({ title: '哦豁!', text: '黑方禁手', btnShow: false });
      return;
    }
    // 之前的代码
  }
}

结语

通过增加禁手规则的检测,我们的五子棋游戏变得更加完善。玩家们现在不仅能享受到游戏的乐趣,还能在遵守规则的前提下进行对弈。

希望这篇文章能够帮助你进一步了解如何开发和完善一个五子棋游戏。如果你有任何建议或问题,欢迎在评论区交流。