自娱自乐–写个简易的五子棋游戏玩一玩

前言

最近一段时间,迷恋上了五子棋。下着下着,突然头脑中萌生了想开发一款简易五子棋游戏的想法。这个想法时强时弱,最近比较强。于是想趁着这股热乎劲还在,把自己的想法实现了。没开发之前,感觉困难重重,做出来之后,小有一番成就感。如果你也喜欢动手编码,做一些非必要而是出自内心热爱的事情,那么本文很适合你。现在我们进入正题,学习一下如何开发一个简易版的五子棋游戏。

效果展示

以终为始,先看看最终的效果。进入游戏界面,点击匹配玩家按钮,开始匹配玩家。匹配成功之后,会随机的分配先后手。视频中演示了两组玩家同时玩五子棋的情景,在玩的过程中,轮到哪一方落子时,会有头像转动提示,在棋盘上移动鼠标,鼠标指针也会在显示棋子还没有落下的实时位置。落子之后,棋盘局势会同步给双方,当某一方率先在任一方向实现五子连珠,获胜时,对局的双方都会受到某方获胜的消息通知。选择再玩一局时,会重新匹配玩家,上一局的对弈方会收到己方的下线通知。此外,可以看到两组对弈,互不干扰,支持多组玩家同时在线对弈。

自娱自乐--写个简易的五子棋游戏玩一玩

在实现这个简易的五子棋游戏之前,我们了解一下五子棋的规则。

五子棋的规则

五子棋是一项两人对弈的棋类游戏,主要规则如下:

  1. 棋盘与棋子

    • 棋盘通常为15×15的网格,也可以是更大的尺寸,棋盘上有纵横交错的线条。
    • 棋子有两种颜色,通常为黑色和白色,双方各执一色。
  2. 游戏流程

    • 游戏开始时棋盘为空,黑子先行,然后轮流下子。
    • 每回合玩家在棋盘的任意一个空格(交叉点)放下一颗棋子。
  3. 胜利条件

    • 任何一方在横、竖、斜线上连续排列五个同色棋子即可获胜,称为“五连”。
    • 对于正规比赛中的黑棋,除了普通的五连之外,还要遵守“禁手”规则。黑棋在某一回合中不能形成“双活三”、“双四”或“长连”(即一步形成超过五子连珠的情况),否则即使形成了五连也不能算赢,反而会被判负。白棋不受此限制。
  4. 开局规则

    • 正规比赛中,黑棋的第一手棋必须下在棋盘中心,也就是天元位置。
    • 开局前可能有猜先环节,由一方抓取一定数量的棋子,另一方猜奇偶数,猜对者执黑先行。
  5. 结束游戏

    当一方达成五连或者游戏进行到最后无法分出胜负时(比如棋盘满而无五连),游戏结束,五连的一方为胜者。

由于我们是开发简易版的,所以先不考虑禁手规则。 棋盘的大小,输赢判定就要依据上面的规则来编写。

编码实现

先使用pnpm create vite创建一个vite+vue3的空项目。然后安装npm项目依赖包,把空项目运行起来。

第一步 画出棋盘和棋子

如何画棋盘,棋盘和网格高度相仿,你应该想到用css的哪个布局属性画棋盘了,没错,就是grid,标准的可拍摆放棋子的棋盘大小是15*15, 因为要给棋盘加边框,所以我们需要绘制一个16*16的二维网格。用js生成一个16*16的一维数组,数组中每项值填充为空字符串, 用grid布局控制每行显示16格。

<style lang="less">
.chess-board {
  display: grid;
  grid-template-columns: repeat(16, 1fr);
  grid-gap: 1px;
  background-color: #d18b47;
  padding: 8px;
  width: fit-content;
  margin: 0 auto;
  .chess-grid {
    width: 30px;
    height: 30px;
    background-color: #f9d16b;
  }
}
</style>

<div  class="chess-board">
    <div v-for="(chess, index) in board" class="chess-grid" :key="index">
      <div class="chess-cell"></div>
    </div>
 </div>
 
 <script>
 const Board_Size = 16;
  // 初始化棋盘数组,没放置黑白棋子时值初始化为空字符串
  const board = ref(Array(Board_Size * Board_Size).fill(""));
 </script>

棋盘绘制好之后, 我们可以数一数,可以放置棋子的数量是不是15*15,笔者数了一下,够数,不多也不少。

自娱自乐--写个简易的五子棋游戏玩一玩

我们接着绘制棋子,给每个方格里面放置一个棋子。效果如下图所示,看了一下,好像哪里不对劲。正常棋子是应该摆放在方格的交叉点上,而不是方格里面。

.chess-grid {
    // ..
    display: flex;
    justify-content: center;
    align-items: center;
    .chess-cell {
      width: 20px;
      height: 20px;
      border-radius: 50%;
      background-color:black;
   }
}

自娱自乐--写个简易的五子棋游戏玩一玩

既然如此,调整一下棋子的位置

.chess-grid {
  // ...
  position: relative;
  .chess-cell {
      // ...
      position: absolute;
      right: -10px;
      bottom: -10px;
  }
}

自娱自乐--写个简易的五子棋游戏玩一玩

发现多出最后一列和最后一行,我们把多余的棋子隐藏掉,现在棋子被我们放置得井井有条。

 .chess-grid {
    // ...
    // 隐藏最后一列和最后一行超出棋盘边界的棋子
    &:nth-child(16n),
    &:nth-child(n + 240) {
      & > .chess-cell {
        display: none;
      }
    }
 }

自娱自乐--写个简易的五子棋游戏玩一玩

第二步 棋盘落子

落子效果是这样实现的:刚开始的时候,棋盘上的棋子未设置背景色,浏览器默认的背景颜色是透明背景,很自然地隐藏了棋子,当鼠标点击了棋盘上的棋子时,如果此时轮到先手下棋,显示先手执棋的颜色,如果是轮到后手下棋,显示后手执棋的颜色。可以通过给棋子动态的添加样式类名,控制落子点的棋子颜色。另外,根据五子棋的规则,棋子落子是有条件的:

  • 该处没有落过棋子
  • 且没有一方获胜
  • 且轮到己方下棋了

我们加一下落子时棋子的显示条件。玩家的角色和轮到哪一方下棋了是由服务器分配的,这个后面再讲。

<template>
  <div class="chess-board">
    <div v-for="(chess, index) in board" class="chess-grid" :key="index">
      <div @click="placeChess(index)" :class="['chess-cell', chess]"></div>
    </div>
  </div>
</template>

<script setup>
const FrontChessColor = "black";
const BackChessColor = "#52c41a";
// 给当前玩家分配的角色,初始为空,取值为'front'|'back'
const playerRole = ref("");
// 轮到落子的一方 取值为'front'|'back'
const activePlayer = ref("");
// 获胜方 取值为'front'|'back'
const winner = ref("");
// 放置棋子
const placeChess = (index) => {
  // 放置条件--正在下棋的过程中+该点位未落过棋子+轮到己方落子
  if (winner.value === "" && board.value[index] === "" && playerRole.value === activePlayer.value) {
    // 设置落子位置
    board.value[index] = activePlayer.value;
  }
};
</script>
<style>
  .chess-grid {
    // ...

    & > .chess-cell {
      // ...
      &.front {
        background-color: v-bind(FrontChessColor);
      }

      &.back {
        background-color: v-bind(BackChessColor);
      }
    }
  }
</style>

上面的代码虽然实现了落子的功能,可是交互体验不是很好,更好的交互体验是鼠标在棋盘上移动时,实时刷新落子点。就是下图这样的效果。

自娱自乐--写个简易的五子棋游戏玩一玩

现在我们实现一下这个效果, 定义一个跟随鼠标的dom元素#js-follow-mouse-chess,这个元素默认是隐藏的,只有给这个dom元素添加样式类show的时候,才让其显示出来。 让这个跟随鼠标的元素显示的条件是:

  • 双方正在对弈之中
  • 且轮到自己下棋了
  • 且鼠标坐标在棋盘范围之内

可以监听html根元素的鼠标移动事件,当进入到棋盘范围内时,在鼠标指针上粘附一个棋子形状,需要实时计算跟随鼠标棋子的位置,并向上,向左偏移10px, 使鼠标指针显示在跟随移动棋子的正中央,轮到哪一方下棋,将跟随鼠标移动的棋子的背景色设置成下棋方的棋子颜色。

鼠标是否进入到棋盘#js-chessboard范围的判断条件:我们看看下面的两幅图,温故一下event.clientXevent.clientY,以及getBoundingClientRect()方法返回对象的四个位置值left,right,top,bottom的含义。

自娱自乐--写个简易的五子棋游戏玩一玩

自娱自乐--写个简易的五子棋游戏玩一玩

看完这两幅dom的位置属性图,对于鼠标是否进入棋盘范围的判断,一目了然。很容易写出鼠标是否进入棋盘范围的判断条件是:event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom,这里的event表示html根元素的鼠标移动事件属性,rect表示棋盘元素#js-chessboard距离可视窗口的位置信息。对弈时棋子在棋盘范围内跟随鼠标移动的完整代码片段是:

<template>
  <div id="js-chessboard" class="chess-board">
    <div v-for="(chess, index) in board" class="chess-grid" :key="index">
      <div @click="placeChess(index)" :class="['chess-cell', chess]"></div>
    </div>
  </div>
  <!-- 跟随鼠标的棋子 -->
  <div id="js-follow-mouse-chess" class="follow-mouse"></div>
</template>

<script setup>
import { ref, onMounted } from "vue";
onMounted(()=>{
    setMouseFollowChess();
})

// 设置棋盘上鼠标移动时棋子跟随效果
const setMouseFollowChess = () => {
  const chessBoardArea = document.getElementById("js-chessboard");
  const rect = chessBoardArea.getBoundingClientRect();
  const followMouseChess = document.getElementById("js-follow-mouse-chess");
  // 添加鼠标移动事件监听器,限定在棋盘区域内
  document.addEventListener("mousemove", function (event) {
    // 还未分配角色 || 该对方走棋了 || 或者某方获胜了
    if (playerRole.value === "" || winner.value || playerRole.value !== activePlayer.value) return;
    // 检查鼠标是否在棋盘区域内
    if (
      event.clientX >= rect.left &&
      event.clientX <= rect.right &&
      event.clientY >= rect.top &&
      event.clientY <= rect.bottom
    ) {
      // 更新自定义形状的位置
      followMouseChess?.classList.add("show");
      followMouseChess.style.left = `${event.clientX - 10}px`; // 减去一半宽度以居中
      followMouseChess.style.top = `${event.clientY - 10}px`; // 减去一半高度以居中
      followMouseChess.style.backgroundColor = playerRole.value === "front" ? FrontChessColor : BackChessColor;
    } else {
      // 如果鼠标移出棋盘区域,隐藏跟随鼠标棋子
      followMouseChess?.classList.remove("show");
    }
  });
};
</script>

<style lang="less">
.follow-mouse {
  position: absolute;
  border-radius: 50%;
  width: 20px;
  height: 20px;
  /* 防止遮挡鼠标事件 */
  pointer-events: none;
  display: none;
  &.show {
    display: block;
  }
}
</style>

第三步 获胜检查

根据五子棋的获胜规则,任一方向出现同颜色的五子连珠,则判定这种颜色对应的某方获胜。对于这一部分逻辑,刚开始我以为每落一个棋子,要检查一下整个棋盘是否出现同颜色的五子连珠。细想了一下,计算量没有那么大。只要取出落子点所在行,所在列,左上+落子点+右下,右上+落子点+左下四个方向的元素,逐一判断这四个方向的数组中是否存在当前落子方连续的五个棋子颜色,就能判断出来当前落子方是否获胜。如下图所示:

自娱自乐--写个简易的五子棋游戏玩一玩

其实更精确的判断条件是判断每个方向上落子点前4后4范围内的元素,是否出现和当前落子方棋子颜色相同的连续5个元素。但这段逻辑写出来有些繁琐,而且可读性不是很好,所以现在的判断条件是检测落子点某个方向的数组从头到尾,是否出现五子连珠。

有了大体思路之后,我们具体实现一下。检查某个方向是否出现五子连珠的代码片段是:从数组的头部开始,5个5个的进行遍历,检测棋子的颜色是不是等于当前落子的颜色,如果出现连续5个相同的颜色,说明当前落子方获胜了。

const checkFiveChessConnected = (line, player) => {
  if (line.length < 5) return false;

  for (let i = 0; i < line.length - 4; i++) {
    // console.log("checkFiveChessConnected", line.slice(i, i + 5));
    if (line.slice(i, i + 5).every((cell) => cell === player)) {
      return true;
    }
  }
  return false;
};

检测正在下棋的一方是否获胜,需要获取到落子点横向,纵向,两条交叉线方向的落子。其中落子点横纵方向的落子值比较好获取,两条对角线方向的落子值获取,相对麻烦一些。这是横纵方向是否出现五子连珠的检测方法。

// 检测正在下棋的一方是否获胜
const checkWinner = (index) => {
  const player = board.value[index];
  // 计算所放棋子所在的行列索引
  const row = Math.floor(index / Board_Size);
  const col = index % Board_Size;

  // 检查落子点所在行是否存在五子连珠
  const rowLine = board.value.slice(row * Board_Size, row * Board_Size + Board_Size);

  if (checkFiveChessConnected(rowLine, player)) {
    winner.value = player;
    return;
  }

  // 检查落子点所在列是否存在五子连珠
  const colLine = [];
  for (let i = 0; i < Board_Size; i++) {
    colLine.push(board.value[i * Board_Size + col]);
  }

  if (checkFiveChessConnected(colLine, player)) {
    winner.value = player;
    return;
  }
  
 }

下面我们看看如何获取两条对角线方向的落子值。落子点两条对角线方向的落子值。实际上是由左上+落子点+右下,右上+落子点+右下两部分拼接起来的。 我们看看落子点左上,右下这条对角线上的棋子在棋盘数组中的索引值获取方法:

  • 落子点左上对角线棋子在棋盘数组中的索引值的计算方法是(不理解的话看看下面的图):
行号和列号必须大于等于0的条件下,行号递减,列号递减
索引值=递减之后的行号`*`每行的元素数量+递减之后的列号
  • 落子点右下对角线棋子在棋盘数组中的索引值的计算方法是:
行号和列号小于每行元素数量的条件下,行号递增,列号递增
索引值=递增之后的行号`*`每行的元素数量+递增之后的列号

自娱自乐--写个简易的五子棋游戏玩一玩

这两部分计算值再加上落子点的索引值就是落子点左上右下这条对角线上的棋子的索引值。理解了左上右下这条对角线棋子索引值的计算方法,右上左下那条对角线上棋子的索引值的计算也就相当容易。这部分功能的代码如下:


/**
 * 计算落子点对角线索引
 * @param {落子点的行号} row
 * @param {落子点的列号} col
 * @param {落子点在棋盘上的索引} placeIndex
 * @param {棋盘规格} boardSize
 */
function calcDiagonalIndex(row, col, placeIndex, boardSize) {
  /**
   * 计算某个基准点的左上,右上,左下,右下对角线上的落子点索引
   * @param {对角线计算基准点的行号} startRow
   * @param {对角线计算基准点的列号} startCol
   * @param {行的自增方向} directionRow
   * @param {列的自增方向} directionCol
   * @param {棋盘规格} boardSize
   */
  const getDiagIndex = (startRow, startCol, directionRow, directionCol, boardSize) => {
    const indexArr = [];
    let curRow = startRow + directionRow;
    let curCol = startCol + directionCol;

    while (curRow >= 0 && curCol >= 0 && curRow < boardSize && curCol < boardSize) {
      indexArr.push(curRow * boardSize + curCol);
      curRow += directionRow;
      curCol += directionCol;
    }

    return indexArr;
  };

  // 左上对角线落子点
  const leftTopDiag = getDiagIndex(row, col, -1, -1, boardSize);
  // 右下对角线落子点
  const rightBottomDiag = getDiagIndex(row, col, 1, 1, boardSize);

  // 右上对角线落子点
  const rightTopDiag = getDiagIndex(row, col, -1, 1, boardSize);

  // 左下对角线落子点
  const leftBottomDiag = getDiagIndex(row, col, 1, -1, boardSize);

  return {
    left: [...leftTopDiag.reverse(), placeIndex, ...rightBottomDiag],
    right: [...rightTopDiag.reverse(), placeIndex, ...leftBottomDiag],
  };
}

我们要在刚才检测某方获胜的函数checkWinner中加入两条对象线上是否存在五子连珠的情况,以及棋盘摆满时,还未出现五子连珠,判断哪方获胜的逻辑。五子棋检测是否获胜的功能就比较完善了。需要注意的是calcDiagonalIndex函数计算出来的是对角线上的棋子索引值,所以在传给检测函数的时候,要把其中的落子值取出之后再传递给checkFiveChessConnected函数。

const checkWinner = (index) => {
  // ...
  // 检查落子点所在对角线是否存在五子连珠
  const diagonalIndex = calcDiagonalIndex(row, col, index, Board_Size);
  // console.log(diagonalIndex);

  // 检查左上右下对角线是否存在五子连珠
  if (
    checkFiveChessConnected(
      diagonalIndex.left.map((itemIndex) => board.value[itemIndex]),
      player
    )
  ) {
    winner.value = player;
    return;
  }

  // console.log(index, diagonalIndex, player);

  // 检查右上左下对角线是否存在五子连珠
  if (
    checkFiveChessConnected(
      diagonalIndex.right.map((itemIndex) => board.value[itemIndex]),
      player
    )
  ) {
    winner.value = player;
    return;
  }

  // 平局检测-因为黑子先下有优势,如何棋子落满棋盘,仍是平局,判断白子胜
  if (board.value.includes("") === false) {
    winner.value = "back";
  }
};

第四步 客户端和服务端通信

前三步实现的功能,只能让我们在本地玩。要想联机玩,就必须实现客户端和服务端的通信功能。五子棋对弈场景理所当然的应该使用长链接,websocket是首选方案。我们先实现一下websocket客户端的功能。把websocket用到的功能如服务器地址配置,连接,收发消息,主动关闭方法进行封装。因为需要标识每一个玩家的身份,所以在服务器地址要添加一个查询参数uid

import { v4 as uuidv4 } from "uuid";
export default class myWebSocket {
  constructor(url) {
    // 指定ws的地址,并生成客户端id
    this.url = url || `ws://localhost:3000?uid=${uuidv4()}`;
    this.socket = null; // 实例化的ws对象
  }

  connect(cb) {
    // console.log({ url: this.url });
    this.socket = new WebSocket(this.url);

    // 监听连接是否正常打开
    this.socket.onopen = function (e) {
      console.log("连接成功");
    };
    // 监听错误
    this.socket.onerror = (e) => {
      console.error("连接错误", e);
      this.close();
    };
    // 接收服务端推送的消息
    this.socket.onmessage = (wsObj) => {
      cb?.(JSON.parse(wsObj.data));
    };
  }
  send(msg) {
    // 发消息
    this.socket.send(JSON.stringify(msg));
  }
  close() {
    // 使用close去关闭已经开启的WebSocket服务
    this.socket.close();
    this.socket = null; // 回归默认值
  }
}

再看看客户端连接服务器和处理服务端消息的功能实现。在页面上放置一个按钮,显示两个人的对弈状态。刚开始按钮名称显示的是匹配玩家。点击之后会客户端会向服务器发起连接,然后按钮文案更改为正在匹配中...

<template>
  <div class="player-box">
    <a-button type="primary" :loading="loading" @click="matchPlayer">{{ btnName }}</a-button>
 </div>
</template>

<script setup>
import { ref, computed, onBeforeMount, onBeforeUnmount } from "vue";
import { message } from "ant-design-vue";
import myWebSocket from "./websocket";

const loading = ref(false);
const btnName = ref("匹配玩家");

// 给当前玩家分配的角色,初始为空
const playerRole = ref("");
// 轮到落子的一方
const activePlayer = ref("");
// 获胜方
const winner = ref("");

// websocket实例
let myWs = {};
onBeforeUnmount(() => {
  myWs?.close?.();
});

// 匹配玩家
const matchPlayer = () => {
  // 正在匹配中 禁止点击
  if (loading.value) return;

  board.value = Array(Board_Size * Board_Size).fill("");
  winner.value = "";
  activePlayer.value = "";
  loading.value = true;
  playerRole.value = "";

  if (btnName.value === "匹配玩家") {
    connectServer(); // 发起连接
  } else if (btnName.value === "再玩一局") {
    myWs?.close?.(); // 关闭连接
    connectServer(); // 重新连接
  }

  btnName.value = "正在匹配中...";
};

// 创建连接,并处理服务器推送的消息
const connectServer = () => {
  myWs = new myWebSocket();
  myWs.connect(handleServerMsg);
};

</script>

服务端会对进入的玩家两两配对,匹配成功后,服务端会发送消息类型为role的消息,客户端收到这样的消息之后,就知道了自己的角色。游戏就正式开始了。此时按钮名称显示对战中..., 接着双方开始斗智,每走一步,一方面要把发送消息给服务端,服务端把棋局消息推送给对弈的双方。调用下面的方法给服务端推送棋盘消息。

   // 同步棋盘信息给服务器
    myWs.send({ type: "board", content: board.value, role: activePlayer.value });

另一方面,要进行获胜检测。总共有四种类型的消息,如下所示:

// 处理服务器推送的消息
const handleServerMsg = (data = {}) => {
  const action = {
    // 棋局
    board: () => {
      board.value = data.content;
      activePlayer.value = data.role;
      btnName.value = "对战中...";
    },
    // 匹配玩家
    role: () => {
      playerRole.value = data.role;
      activePlayer.value = "front";
      loading.value = false;
      setMouseFollowChess();
      btnName.value = "匹配成功";
    },
    // 获胜
    winner: () => {
      showWinnerMsg(data.winner);
      winner.value = data.winner;
      btnName.value = "再玩一局";
    },
    // 掉线
    disconnect: () => {
      message.error("对方掉线了");
      myWs?.close?.();
      btnName.value = "匹配玩家";
    },
  };

  action?.[data.type]?.();
};

当有一方获胜时,按钮名称会变成再玩一局, 当一方掉线时,按钮名称会变成匹配玩家,这两种情况都会重新向服务器发起连接,开始下一轮游戏。

接着实现一下服务端的功能,服务端的功能职责是匹配玩家,分配玩家角色,给对弈方同步棋盘信息,控制该哪一方下棋,获胜与对弈方掉线通知。

服务端需要监听connection的事件,在connection的事件中为每个客户端添加一个唯一标识,此外给每个连接的客户端添加messageclose事件,分别用于接收客户端的消息和监听客户端掉线。主体代码如下:

import qs from "qs";
import WebSocket, { WebSocketServer } from "ws";

const webSocket = new WebSocketServer({
  port: 3000,
});

webSocket.on("connection", function (ws, req) {
  // 解析客户端的uid
  const query = qs.parse(req.url.split("?")[1]);
  // 给每个客户端连接添加一个uid
  ws.uid = query.uid;
  console.log(`${ws.uid} 上线了`);

  // 监听客户端发送的消息
  ws.on("message", function (message) {
    console.log(`[SERVER] Received: ${message}`);
  });

  // 断开监听
  ws.on("close", (code, reason) => {
    console.log(`有一方掉线了:`);
  });
});

现在我们添加一下匹配玩家,分配角色的功能。每进来一个玩家,都是触发服务端的connection
事件,当玩家数量为偶数且大于等于2时, 取出连接队列中尾部新进入的两位玩家,随机分配先后手,并给每个玩家的连接对象上添加上对弈方的uid,后面推送棋盘局势的时候要用。然后分别给配对成功的玩家推送分配角色消息。

import qs from "qs";
import WebSocket, { WebSocketServer } from "ws";

const webSocket = new WebSocketServer({
  port: 3000,
});

webSocket.on("connection", function (ws, req) {
  // 解析客户端的uid
  const query = qs.parse(req.url.split("?")[1]);
  // 给每个客户端连接添加一个uid
  ws.uid = query.uid;
  console.log(`${ws.uid} 上线了`);
  // ...
  // 当有偶数个用户连接时,进行玩家匹配,随机决定先后手点击
  matchPlayers(Array.from(webSocket.clients.values()));

});

/**
 * 匹配五子棋玩家
 * @param {长链接实例} webSocket
 */
function matchPlayers(clients) {
  if (clients.length % 2 === 0 && clients.length >= 2) {
    // 先后手随机序号计算
    const frontPlayerIndex = Math.floor(Math.random() * 2);
    const backPlayerIndex = 1 - frontPlayerIndex;

    const matchPlayer = clients.slice(-2);

    // 给随机生成的先后手添加配对成员id
    matchPlayer[frontPlayerIndex].partnerUid = matchPlayer[backPlayerIndex].uid;
    matchPlayer[backPlayerIndex].partnerUid = matchPlayer[frontPlayerIndex].uid;

    console.log(`匹配成功的一对玩家ID:`);
    console.table([{ uid: matchPlayer[frontPlayerIndex].uid }, { uid: matchPlayer[backPlayerIndex].uid }]);
    // 给同组成员分别推送给自己随机设定的角色
    matchPlayer[frontPlayerIndex].send(
      JSON.stringify({
        type: "role",
        role: "front",
      })
    );

    matchPlayer[backPlayerIndex].send(
      JSON.stringify({
        type: "role",
        role: "back",
      })
    );
  }
}

接着实现棋盘信息同步的功能,如果有多组玩家同时在线玩游戏时,每组的棋盘信息不能串扰。这一点是通过过滤uid,只给对弈的一组的玩家uid推送消息做到的。另外如果是对弈消息类型, 每走一步,需要切换一下走棋的角色。

webSocket.on("connection", function (ws, req) {
  // ...

  // 监听客户端发送的消息
  ws.on("message", function (message) {

    // 不做这样的转换,会以二进制数据发送
    let msgData = JSON.parse(message);

    if (["board"].includes(msgData.type)) {
      // 轮哪一方下棋了的切换逻辑
      msgData.role = msgData.role === "front" ? "back" : "front";
    }

    // 给所有的客户端广播消息(发消息的客户端除外)
    webSocket.clients.forEach(function each(client) {
      // 只给同组成员推送棋局消息
      if ([ws.uid, ws.partnerUid].includes(client.uid) && client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify(msgData));
      }
    });
  });
});

因为是双人对局,所以一方掉线之后,另一方就无法再继续玩游戏了。这就需要添加掉线检测功能。通过监听连接对象的close事件,将掉线消息通知对弈方。因为在匹配玩家的时候,已经在连接对象上添加了对弈方的uid, 所以遍历客户端连接队列,就能找到对弈方的连接对象,让其推送消息给客户端。

webSocket.on("connection", function (ws, req) {
  // ...
  
  // 断开监听
  ws.on("close", (code, reason) => {
    console.log(`有一方掉线了:`);
    console.table({ 掉线方Id: ws.uid, 对手方Id: 2, 出错代码: code });
    // 通知五子棋对手方,己方掉线了
    broadcastToOthers(ws, "对手下线了");
  });
});

// 通知五子棋对手方,连接断开
function broadcastToOthers(closeWs, message) {
  webSocket.clients.forEach((client) => {
    if (client.uid === closeWs.partnerUid && client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify({ type: "disconnect", disconnect: message }));
    }
  });
}

最后

至此,前面演示的五子棋视频效果,除了轮到某一方下棋时,头像转动效果(这部分比较简单,限于篇幅,就不赘述)。简易版的五子棋程序就开发出来了。人不应总干一些被安排的事情,必须做的事情,偶尔也要干点自己感兴趣,能让自己真正感觉开心的事情。如果后续还有时间,打算再丰富一下情景交互。比如说加一些音效:开局的声音,落子的声音、获胜的掌声,对弈方掉线声音等;还有加一些表情(催促,赞美,嘚瑟,惊讶,抖机灵等),可供对弈者在下棋的过程中抒发自己的微妙情绪变化,还有加一个超时的倒计时动画提示,累计超时超过设定时间就算输。如果你能看到这里,说明你是真的对这个游戏感兴趣。本文开发的简易版五子棋程序已上传至码云,欢迎看到这里的读者点此下载,交流学习。

原文链接:https://juejin.cn/post/7343484473184895013 作者:去伪存真

(0)
上一篇 2024年3月8日 下午4:42
下一篇 2024年3月8日 下午4:54

相关推荐

发表回复

登录后才能评论