增加参加多人比赛的接口

This commit is contained in:
zhl 2021-04-29 18:39:03 +08:00
parent 2f646863ae
commit 9f795bf415
12 changed files with 450 additions and 8 deletions

View File

@ -11,6 +11,7 @@ import {mongoose} from "@typegoose/typegoose";
import logger from 'logger/logger';
import config from 'config/config';
import { initData } from './common/GConfig'
import { Schedule } from './clock/Schedule'
const zReqParserPlugin = require('plugins/zReqParser');
@ -139,6 +140,7 @@ export class ApiServer {
self.setErrHandler();
self.setFormatSend();
initData()
new Schedule().start()
this.server.listen({port: config.api.port}, (err: any, address: any) => {
if (err) {
logger.log(err)

View File

@ -8,6 +8,12 @@ import {
import { ZError } from '../../common/ZError'
import { BaseConst } from '../../constants/BaseConst'
import { mission_vo } from '../../config/parsers/mission_vo'
import { beginGame, createRoom } from '../../services/WsSvr'
import { RoomState } from '../../services/RoomState'
import { retry } from '../../utils/promise.util'
import { RoomLockErr } from '../../common/RoomLockErr'
import { Schedule } from '../../clock/Schedule'
import { createDeflateRaw } from 'zlib'
const transformRecord = function (records: any[]) {
return records.map(o => {
@ -26,6 +32,7 @@ const transformRecord = function (records: any[]) {
}
})
}
class PuzzleController extends BaseController {
@role('anon')
@router('post /api/:accountid/puzzle/list')
@ -33,13 +40,14 @@ class PuzzleController extends BaseController {
let { shop, level, accountid } = req.params
level = +level || 1
const cfgs: mission_vo[] = Array.from(global.$cfg.get(BaseConst.MISSION).values())
const cfg = cfgs.find(o=>o.number == level) || cfgs[cfgs.length - 1]
const cfg = cfgs.find(o => o.number == level) || cfgs[cfgs.length - 1]
let count = cfg.beforehand_enemy || 10
let records = await Puzzle.randomQuestions({}, count)
let history = new PuzzleSession({ shop, level })
history.members.set(accountid, new PuzzleStatusClass())
history.questions = records.map(o => o._id)
history.expire = Date.now() + (cfg.time || 90) * 1000
history.type = 0
await history.save()
const results = transformRecord(records)
return {
@ -85,11 +93,11 @@ class PuzzleController extends BaseController {
statMap.answer.push(result)
statMap.questions.push(id)
if (result == 1) {
statMap.rightCount ++
statMap.comboCount ++
statMap.rightCount++
statMap.comboCount++
statMap.maxCombo = Math.max(statMap.maxCombo, statMap.comboCount)
} else {
statMap.errorCount ++
statMap.errorCount++
statMap.comboCount = 0
}
history.status = 1
@ -97,4 +105,66 @@ class PuzzleController extends BaseController {
await history.save()
return { result, answer: record.a1, stats: history.members }
}
@role('anon')
@router('post /api/:accountid/puzzle/match')
async joinMultipleGame(req, res) {
const { shop, accountid } = req.params
let data = { shop }
/**
* ,
* , ,
* , ,
* TODO::
*/
let activityId = '1111111'
let roomId = ''
let beginTime = 0
let result = new RoomState().isLock(shop)
try {
await retry<Promise<string>>(async () => {
if (result) {
throw new RoomLockErr('')
}
new RoomState().lock(shop)
let history = await PuzzleSession.findOne({ shop, status: 0, type: 1 })
if (history && !history.hasExpired()) {
new RoomState().unlock(shop)
beginTime = history.begin
return roomId = history.room
} else {
let rsp = await createRoom(data)
if (rsp.status != 200) {
new RoomState().unlock(shop)
throw new ZError(11, 'error create room')
}
roomId = rsp.data?.room?.roomId
history = new PuzzleSession({shop, status: 0, type: 1})
history.members.set(accountid, new PuzzleStatusClass())
history.room = roomId
//TODO: 根据配置赋值
const beginSecond = 20 * 1000
history.begin = Date.now() + beginSecond
beginTime = history.begin
history.expire = history.begin + (100 || 90) * 1000
history.type = 1
await history.save()
new Schedule().beginSchedule(beginSecond, async function () {
await beginGame(roomId, {})
history.status = 1
await history.save()
}, shop)
new RoomState().unlock(shop)
return roomId
}
}, 0, [RoomLockErr])
} catch (err) {
new RoomState().unlock(shop)
throw new ZError(12, 'error create room')
}
return { roomId, beginTime }
}
}

View File

@ -0,0 +1,5 @@
import BaseController from '../../common/base.controller'
class ServerController extends BaseController {
}

53
src/clock/Clock.ts Normal file
View File

@ -0,0 +1,53 @@
export class Clock {
public running: boolean = false;
public paused: boolean = false;
public deltaTime: number;
public currentTime: number;
public elapsedTime: number;
protected now: Function = (typeof(window) !== "undefined" && window.performance && window.performance.now && (window.performance.now).bind(window.performance)) || Date.now;
protected _interval;
constructor (useInterval: boolean = true) {
this.start(useInterval);
}
start (useInterval: boolean = true) {
this.deltaTime = 0;
this.currentTime = this.now();
this.elapsedTime = 0;
this.running = true;
if (useInterval) {
// auto set interval to 60 ticks per second
this._interval = setInterval(this.tick.bind(this), 1000 / 60);
}
}
stop () {
this.running = false;
if (this._interval) {
clearInterval(this._interval);
}
}
tick (newTime = this.now()) {
if (!this.paused) {
this.deltaTime = newTime - this.currentTime;
this.elapsedTime += this.deltaTime;
}
this.currentTime = newTime;
}
pause() {
this.paused = true
}
resume() {
this.paused = false
}
}

50
src/clock/ClockTimer.ts Normal file
View File

@ -0,0 +1,50 @@
import { Delayed, Type } from "./Delayed";
import { Clock } from './Clock'
export default class ClockTimer extends Clock {
delayed: Delayed[] = [];
constructor (autoStart: boolean = false) {
super(autoStart);
}
tick () {
super.tick();
if (this.paused) {
return
}
let delayedList = this.delayed;
let i = delayedList.length;
while (i--) {
const delayed = delayedList[i];
if (delayed.active) {
delayed.tick(this.deltaTime);
} else {
delayedList.splice(i, 1);
continue;
}
}
}
setInterval (handler: Function, time: number, ...args: any[]) {
let delayed = new Delayed(handler, args, time, Type.Interval);
this.delayed.push(delayed);
return delayed;
}
setTimeout (handler: Function, time: number, ...args: any[]) {
let delayed = new Delayed(handler, args, time, Type.Timeout);
this.delayed.push(delayed);
return delayed;
}
clear () {
let i = this.delayed.length;
while (i--) { this.delayed[i].clear(); }
this.delayed = [];
}
}

61
src/clock/Delayed.ts Normal file
View File

@ -0,0 +1,61 @@
export enum Type {
Interval,
Timeout
}
export class Delayed {
public active: boolean = true;
public paused: boolean = false;
public time: number;
public elapsedTime: number = 0;
protected handler: Function;
protected args: any;
protected type: number;
constructor (handler: Function, args: any, time: number, type: number) {
this.handler = handler;
this.args = args;
this.time = time;
this.type = type;
}
tick (deltaTime: number) {
if (this.paused) { return; }
this.elapsedTime += deltaTime;
if (this.elapsedTime >= this.time) {
this.execute();
}
}
execute () {
this.handler.apply(this, this.args);
if (this.type === Type.Timeout) {
this.active = false;
} else {
this.elapsedTime -= this.time;
}
}
reset () {
this.elapsedTime = 0;
}
pause () {
this.paused = true;
}
resume () {
this.paused = false;
}
clear () {
this.active = false;
}
}

56
src/clock/Schedule.ts Normal file
View File

@ -0,0 +1,56 @@
import { singleton } from '../decorators/singleton'
import Clock from './ClockTimer'
import { Delayed } from './Delayed'
/**
*
*/
@singleton
export class Schedule {
clock: Clock = new Clock()
gameClock: Map<string, Delayed> = new Map()
public start() {
this.clock.start()
}
beginSchedule(millisecond: number, handler: Function, name: string): void {
if (this.gameClock.has(name) && this.gameClock.get(name)?.active) {
console.log(`当前已存在进行中的clock: ${ name }`)
this.gameClock.get(name).clear()
this.gameClock.delete(name)
}
let timeOverFun = function () {
handler && handler()
}
this.gameClock.set(name, this.clock.setTimeout(timeOverFun, millisecond, name))
}
/**
*
*/
stopSchedule(name: string): number {
console.log(`manual stop schedule: ${ name }`)
if (!this.gameClock.has(name)) {
return -1
}
let clock = this.gameClock.get(name)
if (!clock.active) {
this.gameClock.delete(name)
return -1
}
let time = clock.elapsedTime
clock.clear()
this.gameClock.delete(name)
return time
}
/**
* active
* @param {string} name
* @return {boolean}
*/
scheduleActive(name: string): boolean {
return this.gameClock.has(name) && this.gameClock.get(name).active
}
}

View File

@ -0,0 +1,6 @@
export class RoomLockErr extends Error {
constructor(message: string) {
super(message);
}
}

View File

@ -62,6 +62,14 @@ export class PuzzleSessionClass extends BaseModule {
@prop()
public room: string
/**
*
* 0: 单人
* 1: 多人
* @type {number}
*/
@prop({default: 0})
public type: number
/**
*
@ -72,7 +80,13 @@ export class PuzzleSessionClass extends BaseModule {
*/
@prop({default: 0})
public status: number
/**
*
* ,
* @type {number}
*/
@prop()
public begin: number
@prop()
public expire: number

View File

@ -0,0 +1,74 @@
import { dbconn } from '../../decorators/dbconn'
import {
getModelForClass,
modelOptions,
mongoose,
prop
} from '@typegoose/typegoose'
import { BaseModule } from '../Base'
import { noJson } from '../../decorators/nojson'
import { Severity } from '@typegoose/typegoose/lib/internal/constants'
@dbconn()
@modelOptions({schemaOptions: {collection: "shop_activity", timestamps: true}, options: {allowMixed: Severity.ALLOW}})
class ShopActivityClass extends BaseModule {
@prop()
public shop: string
@prop()
public name: string
/**
*
* @type {string[]}
*/
@prop()
public qtypes: string[]
@prop()
public beginTime: number
// TODO:
@prop({type: mongoose.Schema.Types.Mixed})
public rewardInfo: {};
/**
*
* @type {boolean}
*/
@noJson()
@prop({default: false})
public deleted: boolean
@noJson()
@prop()
public deleteTime: Date
/**
*
* @type {string}
*/
@prop()
public createdBy: string
public static parseQueryParam(params) {
let {key, timeBegin, timeEnd} = params
let opt: any = {deleted: false, show: true}
if (key) {
opt.name = {$regex: key, $options: 'i'}
}
if (timeBegin && !timeEnd) {
opt.createdAt = {$gte: timeBegin};
} else if (timeBegin && timeEnd) {
opt['$and'] = [{createdAt: {$gte: timeBegin}}, {createdAt: {$lte: timeEnd}}];
} else if (!timeBegin && timeEnd) {
opt.createdAt = {$lte: timeEnd};
}
let sort = {_id: -1}
return { opt, sort }
}
}
export const Shop = getModelForClass(ShopActivityClass, { existingConnection: ShopActivityClass.db })

25
src/services/RoomState.ts Normal file
View File

@ -0,0 +1,25 @@
import { singleton } from '../decorators/singleton'
/**
* , ,
*/
@singleton
export class RoomState{
roomSet: Set<string> = new Set()
public lock(shop: string) {
if (this.roomSet.has(shop)) {
return false
}
this.roomSet.add(shop)
return true
}
public isLock(shop: string) {
return this.roomSet.has(shop)
}
public unlock(shop: string) {
this.roomSet.delete(shop)
}
}

View File

@ -18,7 +18,7 @@ export async function sendMsg(roomId, clientId, type, msg) {
method: 'smsg',
args: JSON.stringify(args)
}
return axios.post(url, {params: data})
return axios.post(url, data)
.then(res => {
return res.data
})
@ -38,7 +38,7 @@ export async function broadcast(roomId, type, msg) {
type,
msg
}
return axios.post(url, {params: data})
return axios.post(url, data)
.then(res => {
return res.data
})
@ -58,9 +58,35 @@ export async function kickClient(roomId, clientId) {
method: '_forceClientDisconnect',
args: JSON.stringify(args)
}
return axios.post(url, {params: data})
return axios.post(url, data)
.then(res => {
return res.data
})
}
/**
*
* @param roomId
* @param params
* @return {Promise<AxiosResponse<any>>}
*/
export async function beginGame(roomId, params) {
console.log(`begin game: ${roomId}, ${params}`)
const url = `${apiBase}/room/call`
const args = [params]
const data = {
roomId,
method: 'beginGame',
args: JSON.stringify(args)
}
return axios.post(url, data)
.then(res => {
return res.data
})
}
export async function createRoom(data) {
const url = `${apiBase}/matchmake/joinOrCreate/puzzle_room`
return axios.post(url, data)
}