diff --git a/libs/common/src/tools/achievements/achievement-processor.ts b/libs/common/src/tools/achievements/achievement-processor.ts index dde4a98463e..2be603c22f9 100644 --- a/libs/common/src/tools/achievements/achievement-processor.ts +++ b/libs/common/src/tools/achievements/achievement-processor.ts @@ -1,11 +1,5 @@ import { Observable, OperatorFunction, concatMap, from, map, pipe, withLatestFrom } from "rxjs"; -import { active } from "./achievement-manager"; -import { - achievementMonitors$, - achievementsLocal$ as achievementsLog$, - userActionIn$, -} from "./inputs"; import { AchievementEvent, AchievementId, @@ -14,68 +8,67 @@ import { MetricId, UserActionEvent, } from "./types"; -import { mapProgressByName } from "./util"; +import { mapProgressByName as toMetricMap } from "./util"; -// the formal event processor -function validate( +/** Monitors a user activity stream to recognize achievements + * @param validators$ validators track achievement progress and award achievements + * @param captured$ the set of previously emitted achievement events + */ +function achievements( validators$: Observable, captured$: Observable, ): OperatorFunction { return pipe( withLatestFrom(validators$), + // narrow the list of all live monitors to just those that may produce new logs map(([action, monitors]) => { - // narrow the list of all live monitors to just those that may produce new logs const triggered = monitors.filter((m) => m.filter(action)); return [action, triggered] as const; }), withLatestFrom(captured$), + // monitor achievements concatMap(([[action, validators], captured]) => { - const results: AchievementEvent[] = []; - const progress = mapProgressByName(captured); - const measurements = new Map(); + const achievements: AchievementEvent[] = []; + const metrics = toMetricMap(captured); + const progress = new Map(); // collect measurements for (const validator of validators) { - const measured = validator.measure(action, progress); - measurements.set(validator.achievement, measured); - results.push(...measured); + const measured = validator.measure?.(action, metrics) ?? []; + progress.set(validator.achievement, measured); + + achievements.push(...measured); } // update processor's internal progress values const distinct = new Map(); - const entries = [...measurements.entries()].flatMap(([a, ms]) => - ms.map((m) => [a, m] as const), - ); + const entries = [...progress.entries()].flatMap(([a, ms]) => ms.map((m) => [a, m] as const)); for (const [achievement, measured] of entries) { const key = measured.achievement.name; + + // prevent duplicate updates if (distinct.has(key)) { - const msg = `${achievement} failed to set set ${key} value already set by ${distinct.get(key)}`; + const msg = `${achievement} failed to set ${key}; value already set by ${distinct.get(key)}`; throw new Error(msg); } - distinct.set(key, achievement); - progress.set(measured.achievement.name, measured.achievement.value); + + metrics.set(measured.achievement.name, measured.achievement.value); } // detect earned achievements for (const validator of validators) { - const measured = measurements.get(validator.achievement) ?? []; - const earned = validator.earn(measured, progress); - results.push(...earned); + const events = progress.get(validator.achievement) ?? []; + const awarded = validator.award?.(events, metrics) ?? []; + + achievements.push(...awarded); } // deliver results as a stream containing individual records to maintain // the map/reduce model of the validator - return from(results); + return from(achievements); }), ); } -// monitors are lazy until their trigger condition is met -const liveMonitors$ = achievementMonitors$.pipe(active(achievementsLog$)); - -// pre-wired achievement stream; this is the prototype's host, and -// in the full version is wired by the application -const validatedAchievements$ = userActionIn$.pipe(validate(liveMonitors$, achievementsLog$)); - -export { validate, validatedAchievements$ }; +export { achievements as validate }; diff --git a/libs/common/src/tools/achievements/examples/example-validators.ts b/libs/common/src/tools/achievements/examples/example-validators.ts index 6c5b89702d0..89d1f571c5e 100644 --- a/libs/common/src/tools/achievements/examples/example-validators.ts +++ b/libs/common/src/tools/achievements/examples/example-validators.ts @@ -20,10 +20,7 @@ const TotallyAttachedValidator = { filter(item) { return item.tags?.includes("with-attachment") ?? false; }, - measure(item, progress) { - return []; - }, - earn(progress) { + award(progress) { return [earnedEvent(TotallyAttachedAchievement)]; }, } satisfies AchievementValidator; @@ -49,9 +46,6 @@ const ItemCreatedTracker = { const value = 1 + (progress.get(ItemCreatedProgress) ?? 0); return [progressEvent(ItemCreatedProgress, value)]; }, - earn(progress) { - return []; - }, } satisfies AchievementValidator; const ItemCreatedValidator = { @@ -67,7 +61,7 @@ const ItemCreatedValidator = { measure(item, progress) { return [progressEvent(ItemCreatedProgress)]; }, - earn(progress) { + award(progress) { return [earnedEvent(ItemCreatedAchievement)]; }, } satisfies AchievementValidator; @@ -86,7 +80,7 @@ const ThreeItemsCreatedValidator = { const value = 1 + (progress.get(ItemCreatedProgress) ?? 0); return [progressEvent(ItemCreatedProgress, value)]; }, - earn(_measured, progress) { + award(_measured, progress) { const value = progress.get(ItemCreatedProgress) ?? 0; return value >= 3 ? [earnedEvent(ItemCreatedAchievement)] : []; }, @@ -106,7 +100,7 @@ const FiveItemsCreatedValidator = { const value = 1 + (progress.get(ItemCreatedProgress) ?? 0); return [progressEvent(ItemCreatedProgress, value)]; }, - earn(_measured, progress) { + award(_measured, progress) { const value = progress.get(ItemCreatedProgress) ?? 0; return value >= 5 ? [earnedEvent(ItemCreatedAchievement)] : []; }, diff --git a/libs/common/src/tools/achievements/types.ts b/libs/common/src/tools/achievements/types.ts index 995026e47a1..e9cb59679cc 100644 --- a/libs/common/src/tools/achievements/types.ts +++ b/libs/common/src/tools/achievements/types.ts @@ -47,11 +47,11 @@ export type AchievementValidator = Achievement & { filter: (item: EventFormat) => boolean; // observe data from the event stream and produces measurements - measure: (item: EventFormat, progress: Map) => AchievementProgressEvent[]; + measure?: (item: EventFormat, metrics: Map) => AchievementProgressEvent[]; // monitors achievement progress and emits earned achievements - earn: ( - measured: AchievementProgressEvent[], - progress: Map, + award?: ( + events: AchievementProgressEvent[], + metrics: Map, ) => AchievementEarnedEvent[]; };