Skip to content

Commit

Permalink
fix lotd bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
sspenst committed Mar 23, 2024
1 parent 3d989ca commit 1b3b981
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 114 deletions.
14 changes: 2 additions & 12 deletions components/level/game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -487,23 +487,13 @@ export default function Game({
}, [disableAutoUndo, disableCheckpoints, disablePlayAttempts, enableSessionCheckpoint, fetchPlayAttempt, game.displayName, isComplete, level._id, level.data, level.leastMoves, loadCheckpoint, onComplete, onMove, onNext, onPrev, onSolve, pro, saveCheckpoint, saveSessionToSessionStorage, trackStats]);

useEffect(() => {
if (disableCheckpoints || !pro || !checkpoints) {
if (disableCheckpoints || !pro || !checkpoints || !isComplete(gameState)) {
return;
}

const atEnd = isComplete(gameState);

const bestCheckpoint = checkpoints[BEST_CHECKPOINT_INDEX];

function newBest() {
if (!bestCheckpoint) {
return true;
}

return gameState.moves.length < bestCheckpoint.length;
}

if (atEnd && newBest()) {
if (!bestCheckpoint || gameState.moves.length < bestCheckpoint.length) {
saveCheckpoint(BEST_CHECKPOINT_INDEX);
}
}, [checkpoints, disableCheckpoints, enrichedLevel.userMoves, gameState, isComplete, pro, saveCheckpoint]);
Expand Down
2 changes: 1 addition & 1 deletion lib/withAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default function withAuth(

// eslint-disable-next-line @typescript-eslint/no-explicit-any
res.json = (data: any) => {
if (data && data.error) {
if (data && data.error && process.env.NODE_ENV !== 'test') {
if (!isLocal()) {
newrelic?.addCustomAttribute && newrelic.addCustomAttribute('jsonError', data.error);
} else {
Expand Down
178 changes: 94 additions & 84 deletions pages/api/level-of-day/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import KeyValue from '@root/models/db/keyValue';
import { Types } from 'mongoose';
import { NextApiResponse } from 'next';
import apiWrapper, { NextApiRequestWrapper } from '../../../helpers/apiWrapper';
import { enrichLevels, getEnrichLevelsPipelineSteps } from '../../../helpers/enrich';
import { getEnrichLevelsPipelineSteps } from '../../../helpers/enrich';
import { TimerUtil } from '../../../helpers/getTs';
import { logger } from '../../../helpers/logger';
import dbConnect from '../../../lib/dbConnect';
Expand All @@ -24,80 +24,26 @@ export function getLevelOfDayKVKey() {
return KV_LEVEL_OF_DAY_KEY_PREFIX + new Date(TimerUtil.getTs() * 1000).toISOString().slice(0, 10);
}

export async function getLevelOfDay(gameId: GameId, reqUser?: User | null) {
await dbConnect();
const key = getLevelOfDayKVKey();
const levelKV = await KeyValueModel.findOne({ key: key, gameId: gameId }).lean<KeyValue>();

if (levelKV) {
const levelAgg = await LevelModel.aggregate([
{
$match: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_id: new Types.ObjectId(levelKV.value as any),
gameId: gameId,
}
},
{
$lookup: {
from: UserModel.collection.name,
localField: 'userId',
foreignField: '_id',
as: 'userId',
pipeline: [
{
$project: {
...USER_DEFAULT_PROJECTION
}
}
]
}
},
{
$unwind: {
path: '$userId',
preserveNullAndEmptyArrays: true
}
},
{
$project: {
...LEVEL_DEFAULT_PROJECTION
}
},
...getEnrichLevelsPipelineSteps(reqUser, '_id', '')
]);

if (levelAgg && levelAgg.length > 0) {
cleanUser(levelAgg[0].userId);

return levelAgg[0] as EnrichedLevel;
} else {
logger.error(`Level of the day ${levelKV.value} not found. Could it have been deleted?`);

return null;
}
}

async function getNewLevelOfDay(key: string, gameId: GameId) {
const game = getGameFromId(gameId);
const difficultyEstimate = game.type === GameType.COMPLETE_AND_SHORTEST ? 'calc_difficulty_completion_estimate' : 'calc_difficulty_estimate';
const previouslySelected = await KeyValueModel.findOne({ key: KV_LEVEL_OF_DAY_LIST }).lean<KeyValue>();
// generate a new level based on criteria...
const previouslySelected = await KeyValueModel.findOne({ key: KV_LEVEL_OF_DAY_LIST, gameId: gameId }).lean<KeyValue>();

const MIN_STEPS = 12;
const MAX_STEPS = 100;
const MIN_REVIEWS = 3;
const MIN_LAPLACE = 0.66;

const levels = await LevelModel.find<Level>({
isDeleted: { $ne: true },
isDraft: false,
gameId: gameId,
leastMoves: {
// least moves between 10 and 100
$gte: MIN_STEPS,
$lte: MAX_STEPS,
},
[difficultyEstimate]: { $gte: 0, $exists: true },
calc_reviews_count: {
// at least 3 reviews
$gte: MIN_REVIEWS,
},
calc_reviews_score_laplace: {
Expand All @@ -106,16 +52,13 @@ export async function getLevelOfDay(gameId: GameId, reqUser?: User | null) {
_id: {
$nin: previouslySelected?.value || [],
},
}, '_id gameId name slug width height data leastMoves calc_difficulty_estimate calc_difficulty_completion_estimate', {
// sort by calculated difficulty estimate and then by id
}, '_id calc_difficulty_estimate calc_difficulty_completion_estimate', {
sort: {
[difficultyEstimate]: 1,
_id: 1,
},
}).lean<Level[]>();

let genLevel = levels[0];

const todaysDayOfWeek = new Date(TimerUtil.getTs() * 1000).getUTCDay();
const dayOfWeekDifficultyMap = [
40, // sunday
Expand All @@ -126,56 +69,61 @@ export async function getLevelOfDay(gameId: GameId, reqUser?: User | null) {
500, // friday
600, // saturday
];
let newLevelId: Types.ObjectId | null = null;

for (let i = 0; i < levels.length; i++) {
const level = levels[i];

if (level.calc_difficulty_estimate > dayOfWeekDifficultyMap[todaysDayOfWeek]) {
genLevel = levels[i];
if (level[difficultyEstimate] > dayOfWeekDifficultyMap[todaysDayOfWeek]) {
newLevelId = level._id;
break;
}
}

if (!genLevel) {
logger.warn('Could not generate a new level of the day as there are no candidates left to choose from');
logger.warn('Going to choose the last level published as the level of the day');
if (!newLevelId) {
logger.error(`Could not generate a new level of the day for ${gameId} as there are no candidates left to choose from`);

genLevel = await LevelModel.findOne<Level>({
// choose the last level published as the level of the day as a backup
const latestLevel = await LevelModel.findOne<Level>({
isDeleted: { $ne: true },
isDraft: false,
gameId: gameId,
}, '_id gameId name userId slug width height data leastMoves calc_difficulty_estimate calc_difficulty_completion_estimate', {
// sort by calculated difficulty estimate and then by id
_id: {
$nin: previouslySelected?.value || [],
},
}, '_id', {
sort: {
_id: -1,
},
}).lean<Level>() as Level;
}).lean<Level>();

if (!genLevel) {
logger.error('Could not even find a level to choose from for ' + gameId + '. This is a serious error');
if (!latestLevel) {
logger.error(`Could not find any level of the day for ${gameId}`);

return null;
}

newLevelId = latestLevel._id;
}

// Create a new mongodb transaction and update levels-of-the-day value and also add another key value for this level
const session = await KeyValueModel.startSession();

try {
await session.withTransaction(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(previouslySelected?.value as any)?.push(genLevel._id);
await KeyValueModel.updateOne({ key: 'level-of-day-list', gameId: gameId }, {
(previouslySelected?.value as any)?.push(newLevelId);

await KeyValueModel.updateOne({ key: KV_LEVEL_OF_DAY_LIST, gameId: gameId }, {
$set: {
gameId: genLevel.gameId,
value: previouslySelected?.value || [genLevel._id],
gameId: gameId,
value: previouslySelected?.value || [newLevelId],
}
}, { session: session, upsert: true });

await KeyValueModel.updateOne({ key: key, gameId: gameId }, {
$set: {
gameId: genLevel.gameId,
value: new Types.ObjectId(genLevel._id),
gameId: gameId,
value: newLevelId,
} }, { session: session, upsert: true });
});
session.endSession();
Expand All @@ -186,11 +134,73 @@ export async function getLevelOfDay(gameId: GameId, reqUser?: User | null) {
return null;
}

const enriched = await enrichLevels([genLevel], reqUser || null);
return newLevelId;
}

export async function getLevelOfDay(gameId: GameId, reqUser?: User | null) {
await dbConnect();
const key = getLevelOfDayKVKey();
const levelKV = await KeyValueModel.findOne({ key: key, gameId: gameId }).lean<KeyValue>();
let levelId: Types.ObjectId | null = null;

if (!levelKV) {
levelId = await getNewLevelOfDay(key, gameId);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
levelId = Types.ObjectId.isValid(levelKV.value as any as string) ? new Types.ObjectId(levelKV.value as any as string) : null;
}

if (!levelId) {
logger.error(`Level of the day ${levelId} not found. Could it have been deleted?`);

return null;
}

const levelAgg = await LevelModel.aggregate<EnrichedLevel>([
{
$match: {
_id: levelId,
gameId: gameId,
}
},
{
$lookup: {
from: UserModel.collection.name,
localField: 'userId',
foreignField: '_id',
as: 'userId',
pipeline: [
{
$project: {
...USER_DEFAULT_PROJECTION
}
}
]
}
},
{
$unwind: {
path: '$userId',
preserveNullAndEmptyArrays: true
}
},
{
$project: {
...LEVEL_DEFAULT_PROJECTION
}
},
...getEnrichLevelsPipelineSteps(reqUser, '_id', '')
]);

if (!levelAgg || levelAgg.length === 0) {
logger.error(`Level of the day ${levelId} not found`);

return null;
}

cleanUser(enriched[0].userId);
cleanUser(levelAgg[0].userId);

return enriched[0];
return levelAgg[0];
}

export default apiWrapper({
Expand Down
Loading

0 comments on commit 1b3b981

Please sign in to comment.