123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- const { tunnel } = require('../qcloud')
- const { mysql } = require('../qcloud')
- const option = {
- MAX_SCORE_GAP: 10000,//匹配的玩家最大分差不能超过10000分
- MATCH_SPEED: 3000,//匹配的频率:每3秒遍历匹配一次
- QUESTION_NUMBER: 5,//答题数5个
- SEND_QUESTIONS_DELAY: 3500,//匹配完成后,间隔3.5S后开始向前端发题
- SEND_QUESTION_TIME: 16000,//发题的频率:每16秒发送一题
- PING_PONG_TIME: 6000,//PING-PONG响应的PING发送频率
- PING_PONG_OUT_TIME: 20000,//PING-PONG响应超时时间
- MAX_NUMBER_TUNNEL_RESEND: 3,//每个信道允许出现无效信道时重新发送的次数
- }
- const players = {} //用户信息存储对象:openId为key
- const rooms = {}//房间存储对象:房间名为key
- const fightingRecord = {}//每局比赛战绩存储对象:房间名为key,包含:openId_winner,openId_loser,score_winner,score_loser
- const match = {//匹配对象:包含数据和函数
- queueData: [],
- init() {
- let finished = true
- function match_succ(player1,player2){
- //创建房间
- match.createRoom(player1.openId, player2.openId)
- //队列中删除匹配好的2个玩家
- tools.deleteQueueOpenId(player1.openId)
- tools.deleteQueueOpenId(player2.openId)
- }
- const loopMatch = setInterval(() => {
- if (finished) {
- finished = false
- for (let index1 = 0; index1 < this.queueData.length; index1++) {
- let player1 = players[this.queueData[index1]]
- //排位赛匹配
- if (player1.friendsFightingRoom === undefined) {
- for (let index2 = index1; index2 < this.queueData.length; index2++) {
- let player2 = players[this.queueData[index2]]
- if (player2.friendsFightingRoom === undefined && player2.sortId === player1.sortId && Math.abs(player2.score - player1.score) < option.MAX_SCORE_GAP && player2.openId !== player1.openId) {
- match_succ(player1,player2)
- //结束该player1的匹配
- break
- }
- }
- }
- //好友匹配
- if (player1.friendsFightingRoom !== undefined && player1.friendsFightingRoom !== null) {
- for (let index2 = index1; index2 < this.queueData.length; index2++) {
- let player2 = players[this.queueData[index2]]
- if (player2.friendsFightingRoom !== undefined && player2.friendsFightingRoom !== null && player2.friendsFightingRoom === player1.friendsFightingRoom && player2.openId !== player1.openId) {
- this.match_succ(player1,player2)
- //结束该player1的匹配
- break
- }
- }
- }
- }
- finished = true
- }
- }, option.MATCH_SPEED)
- },
- createRoom(openId1, openId2) {
- let roomName = new Date().getTime().toString() + parseInt(Math.random() * 10000000)//创建时间+随机数
- rooms[roomName] = {
- roomName,
- player1: openId1,
- player2: openId2,
- library: null,
- responseNumber: 0,//收到的响应次数
- finished: false,//是否完成了答题
- }
- players[openId1].roomName = roomName
- players[openId2].roomName = roomName
- console.info('创建后的总房间和玩家为:', rooms, players)
- //library,默认包含5道题目
- mysql('question_detail').where((players[openId1].sortId == 1) ? {} : { sort_id: players[openId1].sortId }).select('*').orderByRaw('RAND()').limit(option.QUESTION_NUMBER).then(res => {
- rooms[roomName].library = res //将查询到的题目存到房间的题库library中
- //房间创建完成后通知前端匹配完成
- tools.broadcast([players[openId1].tunnelId, players[openId2].tunnelId], 'matchNotice', {
- 'player1': {
- openId: openId1,
- nickName: players[openId1].nickName,
- avatarUrl: players[openId1].avatarUrl,
- roomName,
- },
- 'player2': {
- openId: openId2,
- nickName: players[openId2].nickName,
- avatarUrl: players[openId2].avatarUrl,
- roomName,
- }
- })
- //向客户端发题
- tools.sendQuestionMain(roomName)
- },error=>{
- console.log(error)
- })
- },
- }
- match.init()
- const tools = {//工具对象,包含常用数据和函数
- data: {
- timerSendQuestion: [],//每个房间的发题定时器
- numberTunnelResend: [],//key为信道ID,记录每个信道重复发送的次数
- },
- //广播到指定信道
- broadcast(tunnelIdsArray, type, content) {
- tunnel.broadcast(tunnelIdsArray, type, content)
- .then(result => {
- const invalidTunnelIds = result.data && result.data.invalidTunnelIds || []
- if (invalidTunnelIds.length) {
- console.error('======检测到无效的信道IDs======', invalidTunnelIds)
- invalidTunnelIds.forEach(tunnelId => {
- let number = this.data.numberTunnelResend[tunnelId] ? this.data.numberTunnelResend[tunnelId] : 0
- if (number < option.MAX_NUMBER_TUNNEL_RESEND) {//当发送次数不大于规定次数时,可以进行重发
- let timer = setTimeout(() => {//1.5S后重发一次数据
- this.data.numberTunnelResend[tunnelId] = ++number
- tools.broadcast([tunnelId], type, content)
- clearTimeout(timer)
- }, 2000)
- } else {
- console.log('当前的numberTunnelResend长度为:', Object.keys(this.data.numberTunnelResend).length)
- if (Object.keys(this.data.numberTunnelResend).length > 20) { //当累积未清理掉的无效信道达到20个时,清空一次数组
- this.data.numberTunnelResend = []
- console.info('清空后的numberTunnelResend为', this.data.numberTunnelResend)
- } else {
- this.clsTimeout(tunnelId, this.data.numberTunnelResend)
- console.info('删除后的numberTunnelResend为', this.data.numberTunnelResend)
- }
- }
- })
- }
- }, error => {
- console.log(error)
- })
- },
- //关闭指定信道
- closeTunnel(tunnelId) {
- console.info('开始关闭信道:' + tunnelId)
- tunnel.closeTunnel(tunnelId)
- let openId = this.getPlayersOpenId(tunnelId)
- if (players[openId]) {
- if (players[openId].roomName) {
- if (rooms[players[openId].roomName]) {
- if (!rooms[players[openId].roomName].finished) {//如果用户存在房号,则说明是战斗状态断线,视为逃跑
- tools.runAway(openId)
- }
- }
- }
- if (players[openId]) {
- if (players[openId].roomName) {
- this.clsTimeout(players[openId].roomName, tools.data.timerSendQuestion)//清除发题定时器
- delete rooms[players[openId].roomName]//删除房间
- }
- this.deleteQueueOpenId(openId)//删除匹配队列中的openId
- delete players[openId]//删除玩家信息
- console.info('信道关闭后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
- }
- }
- },
- //根据信道ID获取openId
- getPlayersOpenId(tunnelId) {
- for (let index in players) {
- if (players[index].tunnelId === tunnelId) {
- return index
- }
- }
- return null
- },
- //删除匹配队列中的指定openId
- deleteQueueOpenId(openId) {
- let index = match.queueData.indexOf(openId)
- if (~index) {
- match.queueData.splice(index, 1)
- }
- },
- //清除数组中延时定时器
- clsTimeout(index, arr) {
- clearTimeout(arr[index])
- delete arr[index]
- },
- //主控发题函数
- sendQuestionMain(roomName) {
- let sendQuestionsDelay = setTimeout(() => {
- this.sendQuestion(roomName)
- clearTimeout(sendQuestionsDelay)
- }, option.SEND_QUESTIONS_DELAY)
- },
- //发题函数
- sendQuestion(roomName) {
- try {
- let openId1 = rooms[roomName].player1
- let openId2 = rooms[roomName].player2
- this.clsTimeout(roomName, this.data.timerSendQuestion)
- tools.broadcast([players[openId1].tunnelId, players[openId2].tunnelId], 'sendQuestion', {
- question: rooms[roomName].library[0] ? rooms[roomName].library[0] : {},
- //choice = []//[openID:'',userChoose: '',//用户选择了第几个答案 answerColor: '',//用户是否答对 scoreMyself: 0,//用户总得分]
- choicePlayer1: players[openId1].choice,
- choicePlayer2: players[openId2].choice
- })
- console.info('已经向客户端发送一题')
- this.data.timerSendQuestion[roomName] = setTimeout(() => {
- this.sendQuestion(roomName)
- }, option.SEND_QUESTION_TIME)
- //当发送完{}时,清除掉定时器
- if (rooms[roomName].library[0] ? false : true) {
- this.clsTimeout(roomName, this.data.timerSendQuestion)
- }
- rooms[roomName].library.shift() //发送一个,原始题库就删除一个
- rooms[roomName].responseNumber = 0 //初始化房间响应次数
- players[openId1].choice[1] = ''//初始化用户选择状态:第几个答案
- players[openId1].choice[2] = ''//初始化用户选择状态:是否答对
- players[openId2].choice[1] = ''//初始化用户选择状态:第几个答案
- players[openId2].choice[2] = ''//初始化用户选择状态:是否答对
- } catch (error) {
- console.error('错误:' + error)
- }
- },
- //更新得分
- updateScore(openId, fightingResult) {
- mysql('cSessionInfo').where({ open_id: openId }).select('score').then(res => {//获取原始得分
- let score = res[0].score
- if (fightingResult === 1) {
- score = score + 10
- } else if (fightingResult === 0) {
- score = score - 10
- if (score < 0) {
- score = 0
- }
- } else {
- return
- }
- mysql('cSessionInfo').where({ open_id: openId }).update('score', score).then(res => {
- console.info(openId + '得分已更新:' + score)
- }, error => {
- //201803010019:此处添加数据库操作失败报错
- console.log(error)
- })
- })
- },
- //逃跑处理函数:
- runAway(openId) {
- //获取逃跑者和胜利者的openId
- console.info('开始执行逃跑函数')
- let openIdFail = openId, openIdWin
- let room = rooms[players[openIdFail].roomName]
- if (openIdFail === room.player1) {
- openIdWin = room.player2
- } else {
- openIdWin = room.player1
- }
- //更新得分
- this.updateScore(openIdWin, 1)
- this.updateScore(openIdFail, 0)
- //存储比赛结果
- this.storeFightingRecord(openIdWin, 1, true)
- this.storeFightingRecord(openIdFail, 0, true)
- //通知赢家对方已逃跑
- if (players[openId]) {
- if (players[openId].roomName) {
- rooms[players[openId].roomName].finished = true//先改变房间状态,再关闭,避免被认为逃跑行为
- }
- }
- this.broadcast([players[openIdWin].tunnelId], 'runawayNotice', {
- message: '对手已逃跑'
- })
- //有时出现无效信道ID,导致onclose无法删除用户信息,这里手工补充删除
- delete players[openId]//删除玩家信息
- },
- //保存战绩函数:
- //fightingResult: 0:表示输,1:表示赢,2:表示平手
- //runAway:true/false
- async storeFightingRecord(openId, fightingResult, runAway = false) {
- let roomName = players[openId].roomName
- try {
- if (fightingRecord[roomName] ? false : true) {
- fightingRecord[roomName] = {
- openId_winner: '',
- openId_loser: '',
- score_winner: 0,
- score_loser: 0,
- }
- }
- let myRecord = fightingRecord[roomName]
- //获取双方比赛数据
- if (fightingResult == 0) {//0为输
- myRecord.openId_loser = openId
- myRecord.score_loser = players[openId].choice[3]
- } else {
- myRecord.openId_winner = openId
- myRecord.score_winner = players[openId].choice[3]
- }
- //当获取到双方的数据时,开始存储到数据库
- if (myRecord.openId_winner && myRecord.openId_loser) {
- let room_name = roomName,
- run_away = runAway,
- open_id_winner = myRecord.openId_winner,
- open_id_loser = myRecord.openId_loser,
- score_winner = myRecord.score_winner,
- score_loser = myRecord.score_loser
- delete fightingRecord[room_name]
- try{
- await mysql('fighting_record').insert({ id: null, room_name, run_away, open_id_winner, open_id_loser, score_winner, score_loser, time: null })
- }catch(error){
- console.log(error)
- }
- console.log('清空后的fightingRecord为', fightingRecord)
- }
- } catch (error) {
- console.log(error)
- }
- },
- }
- /**
- * 实现 onConnect 方法
- * 在客户端成功连接 WebSocket 信道服务之后会调用该方法,
- */
- function onConnect(tunnelId) {
- tools.broadcast([tunnelId], 'tunnelIdReplaced', {
- newTunnelId: tunnelId
- })
- //PING-PONG机制:发送PING
- clearTimeout(players[tools.getPlayersOpenId(tunnelId)].timer)
- tools.broadcast([tunnelId], 'PING', {})
- }
- /**
- * 实现 onClose 方法
- * 客户端关闭 WebSocket 信道或者被信道服务器判断为已断开后,
- * 会调用该方法,此时可以进行清理及通知操作
- */
- function onClose(tunnelId) {
- console.info('onClose监听到信道' + tunnelId + '关闭')
- tools.closeTunnel(tunnelId)
- }
- /**
- * 实现 onMessage 方法
- * 客户端推送消息到 WebSocket 信道服务器上后,会调用该方法,此时可以处理信道的消息。
- */
- function onMessage(tunnelId, type, content) {
- console.info('onMessage监听到新消息:', { tunnelId, type, content })
- // if (!(tunnelId in players)) {
- // tools.closeTunnel(tunnelId)
- // }
- switch (type) {
- case 'PONG': //PING-PONG机制:监听PONG
- if (tunnelId) {
- let openId = content.openId
- clearTimeout(players[openId].timer)//清除掉定时器
- let timer = setTimeout(() => {
- if (players[openId]) {
- //再次设置一个定时器
- players[openId].timer = setTimeout(() => {//ping-pong机制:监听客户端是否离线
- console.log('开始执行PING-PONG定时器函数')
- tools.closeTunnel(tunnelId)//如果离线,则清空用户信息
- }, option.PING_PONG_OUT_TIME)
- //再次发送一个PING
- tools.broadcast([tunnelId], 'PING', {})
- console.log(tunnelId + '发送一个PING')
- clearTimeout(timer)
- }
- }, option.PING_PONG_TIME)
- }
- break
- case 'updateMatchInfo':
- if (tunnelId) {
- let openId = content.openId
- players[openId].sortId = content.sortId
- players[openId].friendsFightingRoom = content.friendsFightingRoom
- console.info('更新用户匹配条件信息:', players[openId])
- }
- break
- case 'answer':
- if (tunnelId) {
- tools.broadcast([tunnelId], 'getAnswer', {})//通知前端,后台已收到选项
- console.info('收到了' + players[content.choice.openId].nickName + '(' + tunnelId + ')的信息')
- let roomName = content.roomName
- let openId = content.choice.openId
- rooms[roomName].responseNumber = rooms[roomName].responseNumber + 1//房间获得了1次响应
- players[openId].choice[1] = content.choice.userChoose
- players[openId].choice[2] = content.choice.answerColor
- players[openId].choice[3] = content.choice.scoreMyself
- if (rooms[roomName].responseNumber === 2) {
- //当两位玩家都完成答题时,立刻向客户端发送下一题
- tools.sendQuestion(roomName)
- }
- }
- break
- case 'fightingResult': //用户答完题监听
- if (tunnelId) {
- console.info('进入fightingResult阶段')
- const fightingResult = content.fightingResult
- const openId = content.openId
- tools.updateScore(openId, fightingResult)//更新分数
- tools.storeFightingRecord(openId, fightingResult) //存储比赛详情数据
- if (rooms[players[openId].roomName]) {
- rooms[players[openId].roomName].finished = true//先改变房间状态,再关闭,避免被认为逃跑行为
- }
- tools.closeTunnel(tunnelId)//关闭信道连接
- console.info('战斗完成后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
- }
- break
- default:
- break
- }
- }
- module.exports = {
- get: async ctx => {//响应用户开始进行websocket连接,信道服务器连接成功后通知客户端
- //data:{tunnel:{tunnelId:xxx,connectUrl:xxx},userinfo:{openId:xxx.nickName:xxx,...}}
- let data = await tunnel.getTunnelUrl(ctx.req)//当用户发起信道请求的时候,会得到信道信息和用户信息
- let userinfo = data.userinfo
- let openId = userinfo.openId
- if (openId in players) {//如果已经存在openId,则说明只需更新信道ID
- players[openId].tunnelId = data.tunnel.tunnelId
- console.info('信道变化后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
- } else {
- let score = await mysql('cSessionInfo').where({ open_id: data.userinfo.openId }).select('score')//[{score:4324}]
- userinfo.score = score[0].score //number,在用户信息中加入得分
- userinfo.tunnelId = data.tunnel.tunnelId //在用户信息中加入tunnel_id属性
- userinfo.matchTime = new Date().getTime() //在用户信息中加入匹配的时间,如:1513670126897
- userinfo.roomName = null//在用户信息中加入当前战斗状态,默认为null,否则为对战房间号
- userinfo.friendsFightingRoom = null//初始值为null,匹配前会复制赋值为undefined或一个数字判断是排位赛还是好友匹配
- userinfo.sortId = null
- userinfo.choice = [openId, '', '', 0]//[openID:'',user_choose: '',//用户选择了第几个答案 answer_color: '',//用户是否答对 score_myself: '',//用户总得分]
- userinfo.timer = setTimeout(() => {//ping-pong机制:监听客户端是否离线
- //tools.closeTunnel(data.tunnel.tunnelId)//如果离线,则清空用户信息//暂时先取消此处定时器
- }, option.PING_PONG_OUT_TIME)
- players[openId] = userinfo //将此用户作为合法用户,并将openId和用户信息(不包含得分,状态等数据)关联起来
- console.info('新信道加入后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
- //在匹配队列中压入一个openId,外套一个队列限制函数
- match.queueData.push(openId)
- }
- ctx.state.data = data.tunnel //返回信道信息给用户
- },
- post: async ctx => {//用来处理信道传递过来的消息
- const packet = await tunnel.onTunnelMessage(ctx.request.body) //onTunnelMessage:当用户消息发送到信道上时,使用该函数处理信道的消息
- switch (packet.type) {
- case 'connect': //用户开始进行websocket连接,信道服务器连接成功后通知服务端
- onConnect(packet.tunnelId)
- break
- case 'message':
- onMessage(packet.tunnelId, packet.content.messageType, packet.content.messageContent)
- break
- case 'close':
- onClose(packet.tunnelId)
- break
- }
- }
- }
|