示例
在线体验地址五子棋,记得一定要再拉个人才能对战
本期难点
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);
}
});