示例
在线体验地址五子棋,记得一定要再拉个人才能对战
本期难点
1、完成了五子棋的布局,判断游戏结束
2、基本的在线对战,掉线暂停对局,重连继续对局
3、游戏配套im(这个im的实现,请移步在线im)
后续安排
1、黑白棋分配由玩家自选黑白棋与定义下棋间隔,以及具体的倒计时实现
2、新增旁观者,可以观棋与发送im信息
3、悔棋
4、玩家可以主动让出棋手身份,其他观战者可以接力玩
5、接入ai对战
6、使用canvas绘制棋盘对战
感兴趣的铁子,还请点个免费的收藏与关注,后续会一直跟进这个系列
前端部分五子棋布局与游戏输赢判断
<template> <div class="flex-wrap flex-justify-between"> <div class="gobang-main"> <!-- {{ userdata._id }} {{ gameBase }} --> 步数: {{ currentStep }} 我方:{{ getQi }} <el-button v-if="isAdmin" @click="clearGame"> 清空游戏 </el-button> <!-- 下棋区 --> <game :list="gobangList" :config="config" @clickHandle="clickHandle"></game> <!-- <el-button v-if="isGameOver && isAdmin" @click="reloadGame"> 重新开始 </el-button> --> </div> <!-- im区 --> <Im ref="imRef" v-if="game_im_id" :style="{ width: '440px' }" :isShowLeft="false" :gobang_id="gobang_id" :game_im_id="game_im_id" @room_baseinfo="room_baseinfo" @get_game_content="get_game_content" @get_gb_info="get_gb_info" @del_gobang="del_gobang" ></Im> </div> </template> <script> import Im from '@/views/blog/im/index.vue' import { baseURL } from '@/plugins/config.js' import { get_gobang_list, post_gobang, get_gobang, del_gobang } from '@/api/data.js' import game from './game.vue' export default { components: { Im, game, }, data() { return { isGameOver: false, page: 'list', gameBase: { _id:'', status: '', max: 10, max_move_time: 0, all_time: 0, hei_user_id: '', bai_user_id: '', im_romm_id:'' }, gobangMembers: [], // 五子棋游戏成员列表 room_id: '', gobangList: [], config: { type: 1, // 1为白棋 2为黑棋 line: 15, // 棋盘线条数 width: 36, }, first: true, } }, computed: { ...Vuex.mapState(['userdata']), ...Vuex.mapGetters(['isAdmin']), gobang_id() { return this.gameBase._id }, game_im_id() { return this.gameBase.im_romm_id }, // 当前步数 currentStep() { let result = 0 this.gobangList.forEach((item) => { item.forEach((itey) => { if (itey.step_number > 0) { result += 1 } }) }) return result }, getQi() { // 自己是黑棋还是白棋 let { _id } = this.userdata if (_id == this.gameBase.bai_user_id) { return '白棋' } if (_id == this.gameBase.hei_user_id) { return '黑棋' } return '观众' }, step_content() { // 具体下的内容 1白字 2黑子 const obj = { 白棋: 1, 黑棋: 2, } return obj[this.getQi] || 0 }, }, created() { this.init() }, methods: { del_gobang() { // 清空数据 // this.gobangList = [] // this.isGameOver = false // this.gameBase = { // status: '', // max: 10, // max_move_time: 0, // all_time: 0, // hei_user_id: '', // bai_user_id: '', // } location.reload() }, clearGame() { this.$confirm('确定要清空游戏吗?') .then((res) => { this.$refs.imRef.send_msg({ room_id: this.room_id, specialType: 3, gobang_id: this.gobang_id, }) }) .catch(() => {}) }, // 黑棋先行,一人一只下 isShould() { let { currentStep, gobangList, step_content } = this // 黑棋个数 let heiNumber = 0 // 白棋个数 let baiNumber = 0 // 遍历棋盘 gobangList.forEach((aaa) => { aaa.forEach((item) => { if (item.step_content == 1) { baiNumber += 1 } if (item.step_content == 2) { heiNumber += 1 } }) }) // 判断现在下的步数的奇数还是偶数 let isOdd = currentStep % 2 if (step_content === 1) { // 白棋 return isOdd === 1 } if (step_content === 2) { // 黑棋 return isOdd === 0 } }, clickHandle({ x, y }) { let { step_content, room_id, gobang_id, userdata: { _id: author_id }, currentStep, isGameOver, } = this if (isGameOver) { return this.$message.warning('游戏已结束') } // 只有棋手才能下棋 if (![1, 2].includes(+step_content)) return // 判断是否该下子 if (!this.isShould()) { return this.$message.warning('请等待对方落子') } let obj = { room_id, specialType: 2, gobang_id, gobang_member_id: author_id, step_number: currentStep + 1, step_content, x, y, author_id, } this.$refs.imRef.send_msg(obj, () => {}) }, room_baseinfo({ gobangMembers, room_id }) { this.gobangMembers = Array.isArray(gobangMembers) ? gobangMembers : [] this.room_id = room_id || '' }, initConfigList() { // 生成一个二维数组 let { line } = this.config for (let i = 0; i < line; i++) { this.gobangList.push([]) for (let j = 0; j < line; j++) { this.gobangList[i].push({ x: i, y: j, step_content: 0, // 0: 空 1: 白 2: 黑 }) } } }, async init() { this.initConfigList() let res = null try { res = await get_gobang_list() } catch (err) { return } if (res.data || this.isArray(res.data.data)) { if (res.data.data.length === 0) { let res = await post_gobang({ game_name: '五子棋', }).catch(() => { return {} }) if (!res.data || !this.isObject(res.data.data)) return Object.assign(this.gameBase,res.data.data) } if (res.data.data.length) { Object.assign(this.gameBase,res.data.data[0]) } } // 获取现有的对局信息 get_gobang({ gobang_id: this.gobang_id }) .then((res) => { if (res.data && this.isArrayLength(res.data.data)) { let arr = res.data.data this.gobangList = this.gobangList.map((aaa) => { aaa = aaa.map((item) => { let { x, y } = item arr.find((itey) => { let { step_content, step_number, gobang_member_id } = itey if (itey.x == x + 1 && itey.y == y + 1 && step_content) { Object.assign(item, { step_content, step_number, gobang_member_id, }) return true } }) return item }) return aaa }) console.log(this.gobangList) } }) .catch(() => {}) }, get_game_content(row) { this.gobangList = this.gobangList.map((aaa) => { aaa = aaa.map((item) => { let { x, y } = item let { step_content, step_number, gobang_member_id } = row if (row.x == x + 1 && row.y == y + 1 && step_content) { Object.assign(item, { step_content, step_number, gobang_member_id, }) } return item }) return aaa }) this.checkWin() }, get_gb_info(row) { this.gameBase = Object.assign(this.gameBase, row) console.log('get_gb_info',this.gameBase) }, // 判断是否胜利 需要知道哪种棋子胜利 checkWin() { // if(this.first) return this.first = false // 判断当前棋盘是否有五子连珠 let { line } = this.config let { gobangList: list } = this let type = 0 // 0: 没有胜利 1: 白棋胜利 2: 黑棋胜利 3: 平局 // 判断横向 for (let i = 0; i < line; i++) { for (let j = 0; j < line - 4; j++) { if ( list[i][j].step_content !== 0 && list[i][j].step_content === list[i][j + 1].step_content && list[i][j].step_content === list[i][j + 2].step_content && list[i][j].step_content === list[i][j + 3].step_content && list[i][j].step_content === list[i][j + 4].step_content ) { type = list[i][j].step_content break } } } // 判断纵向 for (let i = 0; i < line; i++) { for (let j = 0; j < line - 4; j++) { if ( list[j][i].step_content !== 0 && list[j][i].step_content === list[j + 1][i].step_content && list[j][i].step_content === list[j + 2][i].step_content && list[j][i].step_content === list[j + 3][i].step_content && list[j][i].step_content === list[j + 4][i].step_content ) { type = list[j][i].step_content break } } } // 判断左斜 for (let i = 0; i < line - 4; i++) { for (let j = 0; j < line - 4; j++) { if ( list[i][j].step_content !== 0 && list[i][j].step_content === list[i + 1][j + 1].step_content && list[i][j].step_content === list[i + 2][j + 2].step_content && list[i][j].step_content === list[i + 3][j + 3].step_content && list[i][j].step_content === list[i + 4][j + 4].step_content ) { type = list[i][j].step_content break } } } // 判断右斜 for (let i = 0; i < line - 4; i++) { for (let j = 4; j < line; j++) { if ( list[i][j].step_content !== 0 && list[i][j].step_content === list[i + 1][j - 1].step_content && list[i][j].step_content === list[i + 2][j - 2].step_content && list[i][j].step_content === list[i + 3][j - 3].step_content && list[i][j].step_content === list[i + 4][j - 4].step_content ) { type = list[i][j].step_content break } } } // 判断是否平局 let flag = true for (let i = 0; i < line; i++) { for (let j = 0; j < line; j++) { if (list[i][j].step_content == 0) { flag = false break } } } // 如果是平局 if (flag) { type = 3 } const obj = { 1: '白棋胜利', 2: '黑棋胜利', 3: '平局', } if (obj[type]) { this.$message.success(obj[type]) this.isGameOver = true } }, }, } </script> <style lang="scss" scoped></style>
复制
express代码
Gobang 为五子棋基本设置表
GobangMember 五子棋对战与观战人表
GobangItem 每一步的对战信息表
const { io } = require("../../tool/socket.js"); const { AuthorInfo } = require("../../mod/author/author_info"); const { ImRoom } = require("../../mod/game/im_room.js"); const { ImRoomSys } = require("../../mod/game/im_room_sys.js"); const { ImRoomMember } = require("../../mod/game/im_room_member.js"); const { Game } = require("../../mod/game/game.js"); const { GameList } = require("../../mod/game/game_list.js"); const { Gobang } = require("../../mod/game/gobang.js"); const { GobangMember } = require("../../mod/game/gobang_member.js"); const { GobangItem } = require("../../mod/game/gobang_item.js"); let allSocket = {}; // 监听客户端的连接 io.on("connection", function (socket) { allSocket[socket.id] = socket; // 监听用户掉线 socket.on("disconnect", async () => { // 更新用户状态 let user = await ImRoomMember.findOneAndUpdate( { socket_id: socket.id }, { status: "2" } ); if (user) { delete allSocket[user.im_room_id]; // 这是触发的方法数组,默认只有im的人员信息变化 const funStatus = ["members_change"]; // 对于五子棋游戏相关退出房间操作 try { let res = await GobangMember.findOneAndUpdate( { socket_id: socket.id }, { status: "2" } ); // TODO: 这儿存在性能问题 if (res.n == 1) { funStatus.push("gobang_members_change"); } } catch (err) { console.log(err); } // 向房间的用户同步信息 sendMsgToRoom(user.im_room_id, null, funStatus); } }); // 监听加入房间 socket.on("join_room", async (data) => { if (!global.isObject(data)) { resMsg("加入房间参数错误", 400); return; } // game_id 是游戏id,只有游戏才需要传入 let { user_id, room_id, gobang_id } = data; if (!user_id) { resMsg("用户id不能为空", 400); return; } let user = await AuthorInfo.findOne({ _id: user_id }); if (!user) { resMsg("用户不存在", 400); return; } if (!room_id) { resMsg("房间id不能为空", 400); return; } let room = await ImRoom.findOne({ _id: room_id, status: "1" }); if (!room) { resMsg("房间不存在", 400); return; } let { max, status } = room; if (+status !== 1) { resMsg("房间未开放", 300); return; } // 查找所有加入该房间,并且状态为在线的用户 let members = await ImRoomMember.find({ im_room_id: room_id, status: 1, }).countDocuments(); if (members >= max) { resMsg("房间已满", 300); return; } // 查找用户是否已经加入过该房间 let oldUser = await ImRoomMember.findOne({ im_room_id: room_id, author_id: user_id, }); if (!oldUser) { let res = await new ImRoomMember({ im_room_id: room_id, author_id: user_id, author_type: 2, created_time: getCurrentTimer(), updated_time: getCurrentTimer(), status: 1, socket_id: socket.id, }).save(); if (!res) { resMsg("加入房间失败", 400); return; } } else { await ImRoomMember.updateOne( { im_room_id: room_id, author_id: user_id }, { socket_id: socket.id, status: 1 } ); } // 这是触发的方法数组,默认只有im的人员信息变化 const funStatus = ["members_change"]; // 对于五子棋游戏相关加入房间操作 if (gobang_id) { let game = await Gobang.findOne({ _id: gobang_id }); if (!game) { resMsg("游戏不存在", 400); return; } // 查找用户是否已经加入过该游戏 let oldUser = await GobangMember.findOne({ gobang_id, author_id: user_id, }); if (!oldUser) { let res = await new GobangMember({ gobang_id, author_id: user_id, created_time: getCurrentTimer(), updated_time: getCurrentTimer(), status: 1, socket_id: socket.id, user_type: "3", }).save(); if (!res) { resMsg("加入游戏失败", 400); return; } } else { try { await GobangMember.updateOne( { gobang_id, author_id: user_id }, { socket_id: socket.id, status: 1 } ); } catch (error) { console.log("err", err); } } // 查看是否需要更新游戏基本信息-黑棋与白棋 let gameInfo = await Gobang.findOne({ _id: gobang_id }); if (gameInfo) { let { bai_user_id, hei_user_id } = gameInfo; // 查看用户是否在线 let baiUser = await GobangMember.findOne({ author_id: bai_user_id, gobang_id, }); let heiUser = await GobangMember.findOne({ author_id: hei_user_id, gobang_id, }); console.log(111,heiUser,baiUser) if (!heiUser) { await Gobang.updateOne({ _id: gobang_id }, { hei_user_id: user_id }); } else if (!baiUser) { await Gobang.updateOne({ _id: gobang_id }, { bai_user_id: user_id }); } } funStatus.push("get_gb"); funStatus.push("gobang_members_change"); } // 房间信息改变,向房间内所有在线用户推送房间信息 sendMsgToRoom(room_id, null, funStatus, gobang_id); }); // 主动推出登录 socket.on("live_room", async (data) => { let { room_id, user_id, gobang_id } = data; // 更新用户状态 let user = await ImRoomMember.findOneAndUpdate( { im_room_id: room_id, author_id: user_id }, { status: "2" } ); if (user) { delete allSocket[user.socket_id]; // 这是触发的方法数组,默认只有im的人员信息变化 const funStatus = ["members_change"]; if (gobang_id) { // 对于五子棋游戏相关退出房间操作 try { await GobangMember.findOneAndUpdate( { gobang_id, author_id: user_id }, { status: "2" } ); } catch (err) { console.log(err); } funStatus.push("gobang_members_change"); } // 向房间的用户同步信息 sendMsgToRoom(room_id, null, funStatus, gobang_id); } }); // 发送消息 socket.on("send_msg", async (data) => { if (!global.isObject(data)) return; // time是时长 // specialType 默认1 im的发送消息;2 五子棋的发送下棋消息 3 五子棋发送清空数据消息 let { room_id, author_id, content, msg_type = "1", time = 0, poster = "", video_width = "", video_height = "", specialType = 1, gobang_id, gobang_member_id, step_number, step_content = 0, x, y, } = data; if (specialType == 3) { // 有关五子棋的消息 if (!gobang_id || !room_id) { resMsg("清空数据消息有字段缺失", 400, "err", socket); return; } let gobang = await Gobang.findOneAndDelete({ _id: gobang_id }); if (!gobang) { resMsg("删除失败", 400, "err", socket); return; } let { im_romm_id } = gobang; // 删除人员 await ImRoomMember.deleteMany({ gobang_id: gobang_id }); // 删除对局信息 await GobangItem.deleteMany({ gobang_id: gobang_id }); // 删除聊天室 await ImRoom.deleteOne({ _id: im_romm_id }); // 删除聊天记录 await ImRoomSys.deleteMany({ im_romm_id }); // 删除聊天成员信息 await ImRoomMember.deleteMany({ im_romm_id }); console.log(111) sendMsgToRoom(room_id, null, ["del_gobang"], gobang_id); return } if (specialType == 2) { // 有关五子棋的消息 console.log(data); if ( !room_id || !gobang_id || !gobang_member_id || !gobang_member_id || !step_number || !x || !y || !author_id ) { resMsg("下棋消息有字段缺失", 400, "err", socket); return; } if (![1, 2].includes(+step_content)) { resMsg("观众不能下棋", 400, "err", socket); return; } let oldGb = await GobangItem.findOne({ gobang_id, step_number, }); if (oldGb) { resMsg("您已经下过棋了", 400); return; } try { let newGb = await new GobangItem({ gobang_id, gobang_member_id: author_id, step_number, created_time: getCurrentTimer(), updated_time: getCurrentTimer(), step_content, x, y, }).save(); sendMsgToRoom(room_id, newGb, [], gobang_id); } catch (err) { console.log(err); resMsg("保存步数失败", 400); return; } return; } if (!content) { resMsg("请输入内容", 400); return; } // 判断用户是否存在 if (!author_id) { resMsg("用户id不能为空", 400); return; } let user = await AuthorInfo.findOne({ _id: author_id }); if (!user) { resMsg("用户id不能为空", 400); return; } // 判断房间是否存在 if (!room_id) { resMsg("房间id不能为空", 400); return; } let room = await ImRoom({ _id: room_id, status: "1" }); if (!room) { resMsg("房间未开放", 400); return; } if (!content) { resMsg("消息内容不能为空", 400); return; } // 保存消息 let params = { im_room_id: room_id, author_id: author_id, content: content, msg_type, created_time: getCurrentTimer(), updated_time: getCurrentTimer(), }; if (time) { params.time = time; } if (msg_type == 4) { if (poster) { params.poster = poster; } if (video_width) { params.video_width = video_width; } if (video_height) { params.video_height = video_height; } } let room_sys = await new ImRoomSys(params).save(); if (!room_sys) { resMsg("保存消息失败", 400); return; } // 找出对应的成员信息 let userinfo = await AuthorInfo.findOne( { _id: author_id }, { username: 1, header_img: 1, } ); if (!userinfo) { resMsg("用户信息不存在", 400); return; } room_sys.author_id = userinfo; sendMsgToRoom(room_id, room_sys); }); /** * 向一个房间内的所有在线用户推送房间的基本信息 * row 本次聊天信息 * name 触发事件的小别名 * **/ async function sendMsgToRoom(room_id, row = null, names = [], game_id = "") { if (!room_id) return; // 找出全部在线的成员 let members = await ImRoomMember.find( { im_room_id: room_id, status: 1, }, { socket_id: 1 } ); if (!members || members.length === 0) return; let sockets = members.map((item) => item.socket_id); // 找出房间信息 let room = (await ImRoom.findOne({ _id: room_id, status: "1" })) || {}; // 查找出当前房间的总消息数 let roomSysCount = await ImRoomSys.find({ im_room_id: room_id, }).countDocuments(); let res = { data: room, roomSysCount, msg: "房间信息已更新", }; if (names.length > 0) { // 去重 names = [...new Set(names)]; for (let i = 0; i < names.length; i++) { const item = names[i]; switch (item) { case "members_change": // 人员状态有变化 let roomMembers = await ImRoomMember.find( { im_room_id: room_id }, { author_id: 1, status: 1, room_username: 1 } ) .populate("author_id", "username header_img") .exec(); res.roomMembers = roomMembers; break; case "gobang_members_change": // 五子棋人员状态有变化 let gobangMembers = await GobangMember.find({ gobang_id: game_id }) .populate("author_id", "username header_img") .exec(); Object.assign(res, { gobangMembers, }); break; case "get_gb": // 获取游戏的基本信息 let gameInfo = await Gobang.findOne({ _id: game_id }); if (gameInfo) { res.gb_info = gameInfo; } break; case "del_gobang": res.is_del_gobang = true; break; } } } if (global.isObject(row)) { if (game_id) { // 五子棋消息 res.game_content = row; } else { // im有新消息 res.content = row; } } sockets.forEach((item) => { let socket = allSocket[item]; if (socket) { resMsg(res, 200, "room_baseinfo", socket); } }); } // 获取当前时间戳 function getCurrentTimer() { return Date.now(); } // 统一返回消息 function resMsg(msg, code = 400, name = "err", _socket) { let obj = { code, }; if (code === 200) { obj.msg = "操作成功"; obj.data = msg; } else { obj.msg = msg; } socket = _socket ? _socket : socket; socket.emit(name, obj); } });
复制