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

首页 编程分享 JQUERY丨JS丨VUE 正文

奈德丽 转载 编程分享 2025-04-22 20:03:38

简介 探索全栈开发的奇妙旅程!本文带你从零开始,打造一款具有AI对战功能的五子棋游戏。前端Vue的优雅交互,后端Node的稳健支撑,以及Socket.io实现的实时对战,每一步都充满挑战与惊喜。无论你是编程


大家好,我是奈德丽。想起来闲着无聊想下一把五子棋,但是打开小程序,五子棋里面很多广告,而且棋子和棋盘风格太古板,不是我想要的五子棋,索性自己写了一个,一起来看看吧,说不定你也喜欢这个五子棋。

项目演示

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

问题先答:

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

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

项目git地址:(五子棋git地址)

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';

export function useChessboard(socket, active, disabled, resetGame) {
  // 棋盘状态定义
  const rows = ref(10);
  const cols = ref(10);
  
  // 初始化棋盘
  const boxMap = new Map();
  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;
  }
  
  // 落子逻辑
  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}`, {
        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';

export function 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];
    }
    
    return null;
  }
  
  // 寻找获胜位置
  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}`, { 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 };
          }
        }
      }
    }
    
    return null;
  }
  
  // AI下棋逻辑
  function aiMove() {
    if (!isAIMode.value || active.value !== 'blackPlayer') return;
    
    // 延迟一下,模拟AI思考时间
    setTimeout(() => {
      let move;
      
      // 根据难度选择不同的策略
      if (aiDifficulty.value === 'easy') {
        move = findRandomMove();
      } else if (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";

export default {
  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 })
export class 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 >= 5) return true;

  // 检查垂直方向
  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) return true;

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

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

  return false;
}

这个算法的优化点在于:

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

2. 实时通信的状态同步

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

  1. 服务器作为权威源:所有状态变更必须经过服务器验证
  2. 广播机制:服务器接收到一个客户端的落子后,广播给所有客户端
  3. 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)

未来计划

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

  1. 功能扩展

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

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

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

总结

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

恩恩……懦夫的味道

转载链接:https://juejin.cn/post/7494657593361842214


Tags:


本篇评论 —— 揽流光,涤眉霜,清露烈酒一口话苍茫。


    声明:参照站内规则,不文明言论将会删除,谢谢合作。


      最新评论




ABOUT ME

Blogger:袅袅牧童 | Arkin

Ido:PHP攻城狮

WeChat:nnmutong

Email:nnmutong@icloud.com

标签云