From 9f795bf41579880da01bc3a6e2e8be94cff6f2b8 Mon Sep 17 00:00:00 2001 From: zhl Date: Thu, 29 Apr 2021 18:39:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=82=E5=8A=A0=E5=A4=9A?= =?UTF-8?q?=E4=BA=BA=E6=AF=94=E8=B5=9B=E7=9A=84=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.server.ts | 2 + src/api/controllers/puzzle.controller.ts | 78 ++++++++++++++++++++++-- src/api/controllers/server.controller.ts | 5 ++ src/clock/Clock.ts | 53 ++++++++++++++++ src/clock/ClockTimer.ts | 50 +++++++++++++++ src/clock/Delayed.ts | 61 ++++++++++++++++++ src/clock/Schedule.ts | 56 +++++++++++++++++ src/common/RoomLockErr.ts | 6 ++ src/models/match/PuzzleSession.ts | 16 ++++- src/models/shop/ShopActivity.ts | 74 ++++++++++++++++++++++ src/services/RoomState.ts | 25 ++++++++ src/services/WsSvr.ts | 32 +++++++++- 12 files changed, 450 insertions(+), 8 deletions(-) create mode 100644 src/api/controllers/server.controller.ts create mode 100644 src/clock/Clock.ts create mode 100644 src/clock/ClockTimer.ts create mode 100644 src/clock/Delayed.ts create mode 100644 src/clock/Schedule.ts create mode 100644 src/common/RoomLockErr.ts create mode 100644 src/models/shop/ShopActivity.ts create mode 100644 src/services/RoomState.ts diff --git a/src/api.server.ts b/src/api.server.ts index ab0c451..bcbff10 100644 --- a/src/api.server.ts +++ b/src/api.server.ts @@ -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) diff --git a/src/api/controllers/puzzle.controller.ts b/src/api/controllers/puzzle.controller.ts index b62a76a..841582a 100644 --- a/src/api/controllers/puzzle.controller.ts +++ b/src/api/controllers/puzzle.controller.ts @@ -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>(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 } + } } diff --git a/src/api/controllers/server.controller.ts b/src/api/controllers/server.controller.ts new file mode 100644 index 0000000..46c474c --- /dev/null +++ b/src/api/controllers/server.controller.ts @@ -0,0 +1,5 @@ +import BaseController from '../../common/base.controller' + +class ServerController extends BaseController { + +} diff --git a/src/clock/Clock.ts b/src/clock/Clock.ts new file mode 100644 index 0000000..af4efb6 --- /dev/null +++ b/src/clock/Clock.ts @@ -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 + } + +} diff --git a/src/clock/ClockTimer.ts b/src/clock/ClockTimer.ts new file mode 100644 index 0000000..82dac84 --- /dev/null +++ b/src/clock/ClockTimer.ts @@ -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 = []; + } + +} diff --git a/src/clock/Delayed.ts b/src/clock/Delayed.ts new file mode 100644 index 0000000..b160934 --- /dev/null +++ b/src/clock/Delayed.ts @@ -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; + } + +} diff --git a/src/clock/Schedule.ts b/src/clock/Schedule.ts new file mode 100644 index 0000000..832969d --- /dev/null +++ b/src/clock/Schedule.ts @@ -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 = 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 + } +} diff --git a/src/common/RoomLockErr.ts b/src/common/RoomLockErr.ts new file mode 100644 index 0000000..fb66832 --- /dev/null +++ b/src/common/RoomLockErr.ts @@ -0,0 +1,6 @@ + +export class RoomLockErr extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/src/models/match/PuzzleSession.ts b/src/models/match/PuzzleSession.ts index 29d4167..aa969b3 100644 --- a/src/models/match/PuzzleSession.ts +++ b/src/models/match/PuzzleSession.ts @@ -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 diff --git a/src/models/shop/ShopActivity.ts b/src/models/shop/ShopActivity.ts new file mode 100644 index 0000000..1a5ba07 --- /dev/null +++ b/src/models/shop/ShopActivity.ts @@ -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 }) diff --git a/src/services/RoomState.ts b/src/services/RoomState.ts new file mode 100644 index 0000000..860b8ef --- /dev/null +++ b/src/services/RoomState.ts @@ -0,0 +1,25 @@ +import { singleton } from '../decorators/singleton' + +/** + * 每当有一个人加入一个店铺的游戏, 则设置一个值, 防止重复创建新的房间 + */ +@singleton +export class RoomState{ + roomSet: Set = 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) + } +} diff --git a/src/services/WsSvr.ts b/src/services/WsSvr.ts index 59279f4..33c1c3d 100644 --- a/src/services/WsSvr.ts +++ b/src/services/WsSvr.ts @@ -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>} + */ +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) +} +