稀土掘金技术社区

程序员自己设计开发的五子棋,炫酷又不失可爱

图片(💰金石瓜分计划回归,戳上图了解详情🔍)
大家好,我是奈德丽。想起来闲着无聊想下一把五子棋,但是打开小程序,五子棋里面很多广告,而且棋子和棋盘风格太古板,不是我想要的五子棋,索性自己写了一个,一起来看看吧,说不定你也喜欢这个五子棋。

项目演示

游戏主界面采用了简约而现代的设计风格,以粉色为背景,特效炫酷,主打一个为了让女朋友喜欢,棋盘采用传统的木纹背景,整体视觉效果清爽舒适。

问题先答:

「问题1」:我怎么样才可以玩上这个五子棋?

目前项目没有上线,大家可以访问git,将项目运行在本地,然后通过url就可以访问了。

「项目git地址」:五子棋git地址

https://github.com/bitbitDown/wuziqi)

git clone之后,在本地即可运行前后端,默认访问地址是 localhost:8888, 如果需要局域网对战,那么需要开启后端服务。在工程主目录终端下执行命令:pnmp -F front-end dev,pnmp -F back-end nest

「问题2」:为什么使用粉色背景,炫酷的特效?

还不是为了能让兄弟们可以跟女朋友玩上这么可爱的五子棋。

「问题3」:这个五子棋有什么特殊的地方?

  • 相比平常玩过的五子棋,只具备黑白两种棋子,这个五子棋具有自定义棋子的功能,且可选择的棋子丰富。
  • 相比普通单调的游戏界面,这个五子棋具备精美UI,炫酷动效
  • 支持Ai对战、本地对战、局域网联机对战
image.png

对战界面中,棋子采用了经典的黑白配色,同时支持自定义棋子样式,增加游戏的趣味性。

image.png
image.png

AI设置界面允许玩家选择不同难度的AI对手,从简单到困难,满足不同水平玩家的需求

这是我在PC下的演示Demo,不小心被ai给绝杀了,是我太蠢了还是ai有点聪明呢?

Image

好了,看完演示,是不是让你心动了呢?别着急,一起来看看技术要点吧

技术栈概览

本项目采用现代全栈技术架构,实现了本地对战、AI智能对抗和实时联机对战等核心功能。

前端技术

  • 「Vue 3」:基于组合式API构建高性能响应式UI
  • 「Tailwind CSS」:采用原子化CSS方案,实现快速、一致的UI开发
  • 「Socket.io-client」:处理低延迟的实时双向通信
  • 「Vite」:利用原生ES模块提供极速的开发体验和高效的构建流程

后端技术

  • 「NestJS」:基于Node.js的企业级框架,提供模块化架构
  • 「WebSocket (Socket.io)」:实现服务器与客户端间的实时数据同步
  • 「TypeScript」:全栈类型安全,提高代码质量和开发效率

项目架构

为了开发方便,我在项目中采用了基于pnpm的Monorepo架构,这是一种很常见的代码架构,在vue和React源码中都用到这个架构,它的优点有很多,也很适合独立开发一个全栈项目

  • 统一的依赖管理 :所有子项目使用相同版本的依赖,减少了版本冲突的可能性,pnpm的依赖提升功能也节省了磁盘空间。
  • 原子提交 :可以在一次提交中同时更新前端和后端代码,确保相关功能的一致性。
  • 简化的工作流 :只需要克隆一个仓库就能开始开发,不需要在多个仓库之间切换。
wuziqi-fullstack/
├── back-end/         # NestJS后端服务
│   ├── src/
│   │   ├── app.controller.ts
│   │   ├── app.service.ts
│   │   ├── socketio/   # WebSocket通信模块
│   │   └── main.ts   # 应用入口
├── front-end/        # Vue 3前端应用
│   ├── src/
│   │   ├── components/  # UI组件
│   │   ├── composables/ # 可复用逻辑
│   │   ├── plugins/     # 插件
│   │   └── lang/        # 国际化
├── package.json      # 工作区配置
└── pnpm-workspace.yaml  # 工作区定义

核心技术实现

1. 棋盘组件的设计

我采用map来管理对战双方的棋子状态,这样方便改写和查找,map结构相比数组也更加清晰, 同时结合vue的compositionApi封装各个功能。

import { ref, computed } from'vue';

exportfunctionuseChessboard(socket, active, disabled, resetGame) {
// 棋盘状态定义
const rows = ref(10);
const cols = ref(10);

// 初始化棋盘
const boxMap = newMap();
let row = 1;
let col = 1;
while (row <= rows.value) {
while (col <= cols.value + 1) {
      boxMap.set(`row${row}col${col}`, { empty: true, belongsTo: null });
      col++;
    }
    row++;
    col = 1;
  }

// 落子逻辑
functionputDownPiece(row, col, event) {
const location = getCornerClicked(event.target, event.clientX, event.clientY);
//处理行列,由单元格行变为边框,每个单元格跨2行2列
if (["bottomLeft", "bottomRight"].includes(location)) {
      row += 1;
    }
if (["topRight", "bottomRight"].includes(location)) {
      col += 1;
    }
if (boxMap.get(`row${row}col${col}`)?.empty) {
      boxMap.set(`row${row}col${col}`, {
empty: false,
belongsTo: active.value,
        location,
      });
      emitChessboard({ row, col }, active.value);
if (validSuccess(row, col, active.value)) {
        alert(`${active.value} 获胜!`);
        resetGame();
      } else {
        active.value =
          active.value === "whitePlayer" ? "blackPlayer" : "whitePlayer";
      }
    }
  }

// 其他方法...

return {
    rows,
    cols,
    boxMap,
    isEmpty,
    belongsToWho,
    putDownPiece,
    validSuccess,
    getCellStyle,
    initLocaltion,
    emitChessboard,
    resetChessboard
  };
}

2. 多层次AI算法实现

AI对战是本游戏的核心特色,我实现了三种难度等级的AI算法,从简单的随机策略到复杂的防守进攻策略:「建议大家选择困难难度,因为简单模式玩起来实在是人机。」

import { ref, watch } from'vue';

exportfunctionuseAI(boxMap, active, rows, cols, validSuccess, emitChessboard, resetGame) {
const isAIMode = ref(false);
const aiDifficulty = ref('medium'); // 'easy', 'medium', 'hard'

// 随机找一个空位下棋(简单难度)
functionfindRandomMove() {
// 收集所有空位
const emptyPositions = [];
for (let r = 1; r <= rows.value; r++) {
for (let c = 1; c <= cols.value; c++) {
if (boxMap.get(`row${r}col${c}`)?.empty) {
          emptyPositions.push({ row: r, col: c });
        }
      }
    }

// 随机选一个
if (emptyPositions.length > 0) {
const randomIndex = Math.floor(Math.random() * emptyPositions.length);
return emptyPositions[randomIndex];
    }

returnnull;
  }

// 寻找获胜位置
functionfindWinningMove(player) {
// 检查所有空位
for (let r = 1; r <= rows.value; r++) {
for (let c = 1; c <= cols.value; c++) {
if (boxMap.get(`row${r}col${c}`)?.empty) {
// 临时放置棋子
          boxMap.set(`row${r}col${c}`, { empty: false, belongsTo: player });

// 检查是否获胜
const isWinning = validSuccess(r, c, player);

// 恢复空位
          boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });

if (isWinning) {
return { row: r, col: c };
          }
        }
      }
    }

returnnull;
  }

// AI下棋逻辑
functionaiMove() {
if (!isAIMode.value || active.value !== 'blackPlayer') return;

// 延迟一下,模拟AI思考时间
    setTimeout(() => {
let move;

// 根据难度选择不同的策略
if (aiDifficulty.value === 'easy') {
        move = findRandomMove();
      } elseif (aiDifficulty.value === 'medium') {
        move = findBetterMove();
      } else {
        move = findBestMove();
      }

if (move) {
// AI放置棋子
        boxMap.set(`row${move.row}col${move.col}`, {
empty: false,
belongsTo: 'blackPlayer'
        });

        emitChessboard({ row: move.row, col: move.col }, 'blackPlayer');

// 检查是否获胜
if (validSuccess(move.row, move.col, 'blackPlayer')) {
          setTimeout(() => {
            alert('AI获胜了!');
            resetGame();
          }, 100);
        } else {
// 切换到玩家回合
          active.value = 'whitePlayer';
        }
      }
    }, 800); // 思考时间800毫秒
  }

// 监听玩家回合变化,触发AI下棋
  watch(active, (newValue) => {
if (isAIMode.value && newValue === 'blackPlayer') {
      aiMove();
    }
  });

return {
    isAIMode,
    aiDifficulty,
    toggleAIMode,
    setAIDifficulty,
    aiMove
  };
}

这种分层设计使得AI算法既有一定的智能性,又不会因为过度计算而影响游戏性能。

3. Socket.io实时通信架构

为了实现局域网通信里我采用了Socket,用来保证双方棋子状态能够实时更新。

前端实现:

import { io } from"socket.io-client";

exportdefault {
install: (app, { connection, options }) => {
// 创建socket实例
const socket = io(connection, options);

// 将socket实例添加到全局属性
    app.config.globalProperties.$socket = socket;

// 通过provide/inject使组件能够访问socket
    app.provide('socket', socket);

// 添加全局事件监听
    socket.on('connect', () => {
console.log('Socket连接成功');
    });

    socket.on('disconnect', () => {
console.log('Socket连接断开');
    });
  }
}

后端实现:

import { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer } from'@nestjs/websockets';
import { SocketioService } from'./socketio.service';

@WebSocketGateway({ cors: true })
exportclass SocketioGateway {
@WebSocketServer() server;
constructor(private readonly socketioService: SocketioService) {}

// 接受来自客户端的棋盘状态并广播
@SubscribeMessage('chessboard')
  chessboard(@MessageBody() { location, belongsTo }: { location: object, belongsTo: string }, @ConnectedSocket() socket) {
const updatedChessboard = this.socketioService.chessboard(location, belongsTo);

// 广播给所有客户端
this.server.sockets.sockets.forEach((socket) => {
      socket.emit('currentChessboard', updatedChessboard)
    });

console.log('Sender socket ID:', socket.id);
return {
      event: 'currentChessboard',
      data: { ...updatedChessboard, socketId: socket.id}
    };
  }
}

技术难点与解决方案

1. 胜负判定算法

五子棋的胜负判定需要检查四个方向(横、竖、斜)是否有五子连珠,这是一个比较复杂的算法:

functionvalidSuccess(row, col, active) {
// 检查水平方向
let count = 1;
for (
let i = col - 1;
    i >= 0 && boxMap.get(`row${row}col${i}`)?.belongsTo === active;
    i--
  ) {
    count++;
  }
for (
let i = col + 1;
    i <= cols.value && boxMap.get(`row${row}col${i}`)?.belongsTo === active;
    i++
  ) {
    count++;
  }
if (count >= 5) returntrue;

// 检查垂直方向
  count = 1;
for (
let i = row - 1;
    i >= 0 && boxMap.get(`row${i}col${col}`)?.belongsTo === active;
    i--
  ) {
    count++;
  }
for (
let i = row + 1;
    i <= rows.value && boxMap.get(`row${i}col${col}`)?.belongsTo === active;
    i++
  ) {
    count++;
  }
if (count >= 5) returntrue;

// 检查斜线方向(左上到右下)
  count = 1;
for (
let i = 1;
    row - i >= 0 &&
    col - i >= 0 &&
    boxMap.get(`row${row - i}col${col - i}`)?.belongsTo === active;
    i++
  ) {
    count++;
  }
for (
let i = 1;
    row + i <= rows.value &&
    col + i <= cols.value &&
    boxMap.get(`row${row + i}col${col + i}`)?.belongsTo === active;
    i++
  ) {
    count++;
  }
if (count >= 5) returntrue;

// 检查斜线方向(右上到左下)
  count = 1;
for (
let i = 1;
    row - i >= 0 &&
    col + i <= cols.value &&
    boxMap.get(`row${row - i}col${col + i}`)?.belongsTo === active;
    i++
  ) {
    count++;
  }
for (
let i = 1;
    row + i <= rows.value &&
    col - i >= 0 &&
    boxMap.get(`row${row + i}col${col - i}`)?.belongsTo === active;
    i++
  ) {
    count++;
  }
if (count >= 5) returntrue;

returnfalse;
}

这个算法的优化点在于:

  1. 只检查最后落子的四个方向,而不是遍历整个棋盘
  2. 使用方向遍历时的提前中断,减少不必要的计算
  3. 分别检查四个方向(水平、垂直、两个对角线)的连子情况

2. 实时通信的状态同步

在联机对战中,保证所有客户端的棋盘状态一致是一个挑战。我采用了以下策略:

  1. 「服务器作为权威源」:所有状态变更必须经过服务器验证
  2. 「广播机制」:服务器接收到一个客户端的落子后,广播给所有客户端
  3. 「Socket ID标识」:使用Socket ID区分消息来源,避免重复处理
// 前端发送落子信息
functionemitChessboard(location, belongsTo) {
  socket.emit("chessboard", { location, belongsTo }, (data) => {
console.log("chessboard:", data);
  });
}

// 后端处理并广播
@SubscribeMessage('chessboard')
chessboard(@MessageBody() { location, belongsTo }: { location: object, belongsTo: string }, @ConnectedSocket() socket) {
const updatedChessboard = this.socketioService.chessboard(location, belongsTo);

// 广播给所有客户端
this.server.sockets.sockets.forEach((socket) => {
    socket.emit('currentChessboard', updatedChessboard)
  });

return {
event: 'currentChessboard',
data: { ...updatedChessboard, socketId: socket.id}
  };
}

3. 国际化支持

项目实现了基本的国际化支持,这个功能我是考虑到可能会有很多新人朋友看到,也是给他们多提供一个实战案例。

// 中文语言包
exportdefault {
'standaloneMode':'单机模式',
"lanMode":"局域网模式"
}

// 在main.js中注册
import i18n from'./lang/i18n.js'
app.use(i18n)

未来计划

虽然项目已经实现了基本功能,但还有很多可以改进和扩展的地方:

  1. 「功能扩展」

  • 游戏回放功能:记录每一步棋,支持回放和分析
  • 排行榜系统:引入积分机制,记录玩家战绩
  • 多人房间系统:支持创建多个游戏房间,观战功能等
  • 「技术优化」

    • 更智能的AI:引入更复杂的算法,如极小极大算法、Alpha-Beta剪枝等
    • 性能优化:优化渲染性能,减少不必要的重绘
    • 离线支持:添加PWA支持,实现离线游戏
  • 「用户体验提升」

    • 主题定制:允许用户自定义游戏主题和棋子样式
    • 音效系统:添加落子、胜利等音效,增强游戏体验
    • 移动端适配:优化移动端触摸操作,提供更好的移动体验

    总结

    以上使用vue和nestjs开发了五子棋,具备了基础功能的同时也满足了我的个性需求,比如选择棋子,炫酷动效。希望大家也能积级的去实现自己想要的东西,当然话说回来,这个项目可以尝试尝试,去自己实现一遍五子棋,说不定你就能找到进击全栈的灵感。

    关注更多AI编程资讯请去AI Coding专区:https://juejin.cn/aicoding