cu8-lucky-draw-pool-engine
Version:
Lucky draw pool engine for luck based games
204 lines (183 loc) • 6 kB
text/typescript
import dayjs, { OpUnitType, QUnitType } from 'dayjs';
import mongoose, { Connection } from 'mongoose';
import PoolSchema from './models/Pool.schema';
import { ERRORS, PoolConfig, PoolStatus, Reward } from './types';
import { randomInt } from './utils';
//@ts-ignore;
import bezier from 'cubic-bezier';
const init = async ({
mongoUrl,
debug,
cleanUpPools,
}: {
mongoUrl: string;
debug?: boolean;
cleanUpPools?: boolean;
}) => {
let db: Connection;
const RATE_DEMICAL_PLACES = 2;
const RATE_DEMICAL_PLACES_MULTIPLIER = Math.pow(RATE_DEMICAL_PLACES, 10);
try {
db = mongoose.createConnection(mongoUrl);
} catch (e) {
throw new Error(ERRORS.ERR_DB_CONN);
}
const Pool = db.model('Pool', PoolSchema, 'Pool');
if (cleanUpPools) {
await Pool.deleteMany({});
}
const listPools = async () => {
const pools = await Pool.find();
return pools;
};
const createPool = async ({
poolKey,
name,
startTime,
endTime,
period,
initialAmount,
config,
data,
}: {
poolKey: string;
name: string;
startTime: Date;
endTime: Date;
period?: QUnitType | OpUnitType;
initialAmount: number;
config?: PoolConfig;
data?: { [key: string]: any };
}) => {
const pool = await Pool.create({
poolKey,
name,
startTime,
endTime,
period,
initialAmount,
unclaimed: initialAmount,
status: PoolStatus.ACTIVE,
config,
data,
});
return pool;
};
const disablePool = async (poolKey: string) => {
await Pool.updateOne({ poolKey }, { $set: { status: PoolStatus.DISABLED, disabledAt: Date.now() } });
};
const enablePool = async (poolKey: string) => {
await Pool.updateOne({ poolKey }, { $set: { status: PoolStatus.ACTIVE } });
};
const draw = async ({
poolKeys,
accessRate,
timestamp,
}: {
poolKeys: string[];
accessRate?: number;
timestamp?: Date;
}) => {
const handlingData: { [key: string]: any } = {};
handlingData.accessRate = 100;
if (accessRate || accessRate === 0) {
handlingData.accessRate = accessRate;
}
handlingData.accessRollResult = randomInt(0, 100 * RATE_DEMICAL_PLACES_MULTIPLIER - 1);
handlingData.poolsAccessed =
handlingData.accessRollResult < handlingData.accessRate * RATE_DEMICAL_PLACES_MULTIPLIER;
if (!handlingData.poolsAccessed) {
if (debug) {
console.log(handlingData);
}
return;
}
const currentTime = dayjs(timestamp);
let reward: Reward | undefined = undefined;
const pools = await Pool.find({ poolKey: { $in: poolKeys }, status: PoolStatus.ACTIVE });
handlingData.pools = [];
for (const pool of pools) {
const handlingPoolData: { [key: string]: any } = { poolKey: pool.poolKey };
let startTime = dayjs(pool.startTime);
let endTime = dayjs(pool.endTime);
let maxRate = 100;
let minRate = 0;
if (pool.config?.maxRate !== undefined) {
maxRate = pool.config.maxRate;
}
if (pool.config?.minRate !== undefined) {
minRate = pool.config.minRate;
}
handlingPoolData.burntAmount = pool.initialAmount - pool.unclaimed;
handlingPoolData.rate = 0;
const unit: OpUnitType | QUnitType = (pool.period as any) || 'ms';
handlingPoolData.expectedBurnSpeed = endTime.diff(startTime, unit, true) / pool.initialAmount;
handlingPoolData.expectedBurnAmount =
(currentTime.diff(startTime, unit) + 1) / handlingPoolData.expectedBurnSpeed;
handlingPoolData.remaining = handlingPoolData.expectedBurnAmount - handlingPoolData.burntAmount;
// console.log('duration', endTime.diff(startTime, unit, true));
// console.log('burn speed', handlingPoolData.expectedBurnSpeed);
// console.log(currentTime.diff(startTime, unit));
// console.log('expected', handlingPoolData.expectedBurnAmount);
// console.log(handlingPoolData.remaining);
if (handlingPoolData.remaining <= 0) {
handlingData.pools.push(handlingPoolData);
continue;
}
handlingPoolData.controlPoints = pool.config?.controlPoints || [
[0, 0],
[1, 1],
];
let rateFormula = bezier(
handlingPoolData.controlPoints[0][0],
handlingPoolData.controlPoints[0][1],
handlingPoolData.controlPoints[1][0],
handlingPoolData.controlPoints[1][1],
1000 / 60 / handlingPoolData.expectedBurnSpeed / 4
);
const rate =
((maxRate - minRate) * rateFormula(handlingPoolData.expectedBurnAmount - handlingPoolData.burntAmount) +
minRate) *
RATE_DEMICAL_PLACES_MULTIPLIER;
handlingPoolData.rate = rate / RATE_DEMICAL_PLACES_MULTIPLIER;
handlingPoolData.rollResult = randomInt(0, 100 * RATE_DEMICAL_PLACES_MULTIPLIER - 1);
handlingPoolData.poolWinned = handlingPoolData.rollResult < rate;
if (!handlingPoolData.poolWinned) {
handlingData.pools.push(handlingPoolData);
continue;
}
try {
const updatedPool = await Pool.findOneAndUpdate(
{
poolKey: pool.poolKey,
unclaimed: { $gte: 1, $eq: pool.unclaimed },
},
{ $inc: { unclaimed: -1 } },
{ new: true }
);
if (!updatedPool) {
handlingData.pools.push(handlingPoolData);
continue;
} else {
reward = {
poolName: updatedPool.name,
poolKey: updatedPool.poolKey,
poolData: updatedPool.data,
};
handlingData.pools.push(handlingPoolData);
break;
}
} catch (e: any) {
handlingPoolData.error = e.message;
handlingData.pools.push(handlingPoolData);
}
}
if (debug) {
console.log(JSON.stringify(handlingData, null, 2));
}
return reward;
};
return { listPools, createPool, enablePool, disablePool, draw };
};
export { init };
export default init;