示例
在线体验地址五子棋,记得一定要再拉个人才能对战
本期难点
1、完成了五子棋的布局,判断游戏结束
2、基本的在线对战
3、游戏配套im(这个im的实现,请移步在线im)
下期安排
1、每步的倒计时设置
2、黑白棋分配由玩家自定义
3、新增旁观者
4、悔棋
5、自行创建一盘棋局
感兴趣的,还请点个免费的收藏与关注,后续会一直跟进这个系列
前端部分五子棋布局与游戏输赢判断
<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区 --><Imref="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 = 0this.gobangList.forEach((item) => {item.forEach((itey) => {if (itey.step_number > 0) {result += 1}})})return result},getQi() {// 自己是黑棋还是白棋let { _id } = this.userdataif (_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 % 2if (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,} = thisif (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.configfor (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 = nulltry {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)) returnObject.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.datathis.gobangList = this.gobangList.map((aaa) => {aaa = aaa.map((item) => {let { x, y } = itemarr.find((itey) => {let { step_content, step_number, gobang_member_id } = iteyif (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 } = itemlet { step_content, step_number, gobang_member_id } = rowif (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) returnthis.first = false// 判断当前棋盘是否有五子连珠let { line } = this.configlet { gobangList: list } = thislet 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_contentbreak}}}// 判断纵向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_contentbreak}}}// 判断左斜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_contentbreak}}}// 判断右斜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_contentbreak}}}// 判断是否平局let flag = truefor (let i = 0; i < line; i++) {for (let j = 0; j < line; j++) {if (list[i][j].step_content == 0) {flag = falsebreak}}}// 如果是平局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);}
});