gamecloud
Version:
game logic server over cloud
245 lines (215 loc) • 8.98 kB
JavaScript
let {RankType, IndexType} = require('../../core/CoreOfBase/enum')
/**
* 排序管理,标准流程如下:
* 1、创建 Ranking 对象时,从接口参数集 rankParams 中获取排行榜尺寸、重新排序时间间隔等一系列参数,基本配置为100条、每隔10秒进行刷新
* 2、数据库载入阶段,调用 Update(entity, true) 添加排行记录
* 3、数据库加载完毕,调用 Init() 进行排序
* 4、当有新的数据变化时,调用 UpdateRecord(info, type, false) 添加排行记录,并引发重新排序
* 5、系统监视器定期调用 RunMonitor 监测特殊事件,例如跨天时每日榜自动清空
*/
class Ranking
{
/**
* 获取和 $class 相关联的 Ranking 对象
* @return {Ranking}
*
* @note 静态函数中的this指向类(类似PHP静态函数中的self),而其实例中的this指针指向实例对象
*/
static muster($class){
if(!$class || !$class.rankParams){
throw new Error('class/Object using Ranking must have rankParams function');
}
if(!$class.$ranking){
$class.$ranking = new this($class.rankParams);
}
return $class.$ranking;
}
/**
* 构造函数
*/
constructor(params){
//region 接收接口参数,设置重要参数的缺省值
this.params = params;
this.params.rankNumber = this.params.rankNumber || 100;
//endregion
this.rankList = new Map; //分类存储排名记录
this.indexList = new Map; //排名记录的反向索引
//为各类排名创建初始数据结构
for(let rType of Object.values(RankType)){
this.rankList.set(rType, []);
this.indexList.set(rType, new Map);
}
//记录榜单更新时间(日),以提供日榜自动刷新功能
this.dailyDay = (new Date()).toDateString();
}
/**
* 添加新的记录,或者更新已有记录,并更新排名
*/
UpdateRecord(infoMgr, rType, init=false){
if(infoMgr.score <=0){
return;
}
let rankData = this.rankList.get(rType);
let indexData = this.indexList.get(rType);
if(!!init){
//流程1、系统启动、初次加载数据
if(infoMgr.score > 0){
rankData.push(infoMgr);
}
//此处提前返回,结束流程1,注意此时rankList未排序,indexList未填充,有赖于后续调用 Init 完成这些操作
return;
}
/*流程2、后续数据变化后的更新,分为如下步骤:
1、如果是已存在记录
1、分数有变化但排名无变化:更新分数后返回
2、排名有变化:删除原记录,准备在后续流程中重新插入
2、如果是新记录:等待后续流程中插入
3、后续插入流程:
1、如果小于最后一名,且队列未满,直接添加到队列末尾后返回,否则直接返回
2、如果大于第一名,直接添加到队列头部,继续后续流程
3、如果是其它情况,使用二分法确定插入点,插入记录,继续后续流程
4、保持队列尺寸,从尾部删除多余记录
5、刷新插入点以后的所有记录的rank字段
*/
let $ori = indexData.get(infoMgr.id);
if(!!$ori){//已经存在记录
if($ori.score == infoMgr.score){
return; //已经存在记录,且分数没有发生变化,不需要进一步的处理
}
else{
if($ori.rank==1 || $ori.score < rankData[$ori.rank-2].score){
//已经是第一名了,或者虽然分数发生变化,但并没有发生超越,只要记录下新的分数就可以返回了
$ori.score = infoMgr.score;
return; //提前返回
}
//否则的话,删除原有元素,准备重新插入
rankData.splice($ori.rank-1,1);
}
}
if (rankData.length == 0 || rankData[rankData.length - 1].score >= infoMgr.score) {//小于最后一名
if (rankData.length < this.params.rankNumber) {
rankData.push(infoMgr);
indexData.set(infoMgr.id, infoMgr)
infoMgr.rank = rankData.length;
}
return; //提前返回
}
let $more=0, $little=rankData.length-1; //二分法标记字段
if(rankData[0].score < infoMgr.score){//大于第一名
//在头部插入,然后继续后续流程
rankData.unshift(infoMgr);
indexData.set(infoMgr.id, infoMgr)
}
else{//二分法寻找合适的插值位置,前提条件:数组已按从大到小排序
while(!($little == $more+1 || $little == $more)){
let candidate = (($more + $little) / 2)|0; //二分法预期插入点
if(infoMgr.score > rankData[candidate].score){//对比物小于自身
$little = candidate;
}
else if(infoMgr.score < rankData[candidate].score){
$more = candidate;
}
else{
$more = $little = candidate;
}
}
rankData.splice($little,0,infoMgr);
indexData.set(infoMgr.id, infoMgr)
}
//将最后一名删除,以保持列表尺寸不变
while(rankData.length > this.params.rankNumber){
let rec = rankData.pop();
indexData.delete(rec.id);
}
//在排序过的榜单上,标注每个人的名次
for(let i = $more; i<rankData.length; i++){
rankData[i].rank = i+1;
}
// endregion
}
/**
* 更新排行榜数据
* @param {*} entity //实体类
*/
Update(entity, init=false) {
if(!entity){ return; }
for(let rType of Object.values(RankType)){
if(RankType.friend == rType){
continue;//好友帮单独添加条目
}
let infoMgr = {//用来排名的数据
id: entity.IndexOf(IndexType.Foreign),
name: entity.IndexOf(IndexType.Name),
score: entity.ScoreOf(rType)
};
this.UpdateRecord(infoMgr, rType, init);
}
}
/**
* 在系统初始化完成、载入所有条目后调用,初始化起始排名信息
*/
Init(rType){
if(typeof rType == 'undefined'){
this.rankList.forEach((list, rType) => {
this.$Init(list, rType);
});
}
else{
let it = this.rankList.get(rType);
if(!!it){
this.$Init(it, rType);
}
}
return this;
}
$Init(list, rType){
list.sort((a, b)=>{return b.score - a.score;}); //排序
if(list.length > this.params.rankNumber){ //保持队列尺寸
list.splice(this.params.rankNumber, list.length - this.params.rankNumber);
}
//在排序过的榜单上,标注每个人的名次
let idx = 0;
for(let item of list){
item.rank = ++idx;
this.indexList.get(rType).set(item.id, item);
}
}
/**
* 返回携带排名信息的记录
* @param {*} id 排名者索引
* @param {*} rType 排行榜类别
*/
result(id, rType = RankType.total){
let l = this.indexList.get(rType);
if(!!l && l.has(id)){
return l.get(id);
}
return {rank:0};
}
/**
* 返回指定类型的排行榜
* @param {*} rType 排行榜类别
* @return {Array}
*/
list(rType){
return this.rankList.get(rType);
}
}
/**
* 监视器接口:由通用Monitor自动调用,检测跨天事件,复位每日榜单
* @param {CoreOfBase} fo 核心对象
* @return {Boolean} false继续监控 true结束监控
*
* @note
* 声明原型扩展方法时,不要使用兰姆达表达式,而要采用传统的function语法,否则会导致this指针异常
*/
Ranking.prototype.RunMonitor = function(fo) {
let day = (new Date()).toDateString();
if(this.dailyDay != day){ //跨天
this.dailyDay = day;
this.rankList.set(RankType.daily, []);
this.indexList.set(RankType.daily, new Map);
}
return false;
}
exports = module.exports = Ranking