大家好,我是奈德丽。想起来闲着无聊想下一把五子棋,但是打开小程序,五子棋里面很多广告,而且棋子和棋盘风格太古板,不是我想要的五子棋,索性自己写了一个,一起来看看吧,说不定你也喜欢这个五子棋。
项目演示
游戏主界面采用了简约而现代的设计风格,以粉色为背景,特效炫酷,主打一个为了让女朋友喜欢,棋盘采用传统的木纹背景,整体视觉效果清爽舒适。
问题先答:
问题1:我怎么样才可以玩上这个五子棋?
目前项目没有上线,大家可以访问git,将项目运行在本地,然后通过url就可以访问了。
项目git地址:(五子棋git地址)
git clone
之后,在本地即可运行前后端,默认访问地址是 localhost:8888,
如果需要局域网对战,那么需要开启后端服务。在工程主目录终端下执行命令:pnmp -F front-end dev
,pnmp -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;
}
这个算法的优化点在于:
- 只检查最后落子的四个方向,而不是遍历整个棋盘
- 使用方向遍历时的提前中断,减少不必要的计算
- 分别检查四个方向(水平、垂直、两个对角线)的连子情况
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开发了五子棋,具备了基础功能的同时也满足了我的个性需求,比如选择棋子,炫酷动效。希望大家也能积级的去实现自己想要的东西,当然话说回来,这个项目可以尝试尝试,去自己实现一遍五子棋,说不定你就能找到进击全栈的灵感。
恩恩……懦夫的味道