稀土掘金技术社区 04月25日 10:01
程序员自己设计开发的五子棋,炫酷又不失可爱
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

作者因对现有五子棋小程序不满,自行开发了一款五子棋。该项目具有简约现代的设计风格,多种对战模式,采用全栈技术架构,实现了多种核心功能,且存在一些技术难点与解决方案,并有未来的改进计划。

🎮游戏界面设计:简约现代,粉色背景,木纹棋盘,特效炫酷

🎳多种对战模式:支持AI对战、本地对战、局域网联机对战

💻技术栈:前端Vue 3、Tailwind CSS等,后端NestJS、WebSocket等

🧠AI算法:实现三种难度等级,分层设计保证智能性与性能

📱实时通信:采用Socket.io保证棋子状态实时更新

原创 奈德丽 2025-04-25 08:31 重庆

点击关注公众号,“技术干货” 及时达!

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

项目演示

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

问题先答:

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

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

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

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

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

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

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

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

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

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

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

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

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

技术栈概览

本项目采用现代全栈技术架构,实现了本地对战、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';

exportfunction useChessboard(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}`, { emptytruebelongsTonull });
      col++;
    }
    row++;
    col = 1;
  }

// 落子逻辑
function putDownPiece(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}`, {
        emptyfalse,
        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';

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

// 随机找一个空位下棋(简单难度)
function findRandomMove({
    // 收集所有空位
    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;
  }

// 寻找获胜位置
function findWinningMove(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}`, { emptyfalsebelongsTo: player });
          
          // 检查是否获胜
          const isWinning = validSuccess(r, c, player);
          
          // 恢复空位
          boxMap.set(`row${r}col${c}`, { emptytruebelongsTonull });
          
          if (isWinning) {
            return { row: r, col: c };
          }
        }
      }
    }
    
    returnnull;
  }

// AI下棋逻辑
function aiMove({
    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}`, {
          emptyfalse,
          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. 胜负判定算法

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

function validSuccess(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 >= 5returntrue;

// 检查垂直方向
  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 >= 5returntrue;

// 检查斜线方向(左上到右下)
  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 >= 5returntrue;

// 检查斜线方向(右上到左下)
  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 >= 5returntrue;

returnfalse;
}

这个算法的优化点在于:

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

2. 实时通信的状态同步

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

    「服务器作为权威源」:所有状态变更必须经过服务器验证
    「广播机制」:服务器接收到一个客户端的落子后,广播给所有客户端
    「Socket ID标识」:使用Socket ID区分消息来源,避免重复处理
// 前端发送落子信息
function emitChessboard(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. 国际化支持

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

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

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

未来计划

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

    「功能扩展」

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

「技术优化」

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

「用户体验提升」

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

总结

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

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

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

五子棋 全栈技术 对战模式 AI算法 实时通信
相关文章