From 08d5c7c7de8d0b0ddd0e7c4a5998a4f7ab6f5938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 11 Mar 2025 16:56:32 -0400 Subject: [PATCH] tweaks from setting up models --- libs/common/src/tools/achievements/data.ts | 8 +++ .../src/tools/achievements/event-processor.ts | 54 +++++++++---------- libs/common/src/tools/achievements/inputs.ts | 8 +-- libs/common/src/tools/achievements/meta.ts | 10 ++-- libs/common/src/tools/achievements/types.ts | 22 +++++--- 5 files changed, 57 insertions(+), 45 deletions(-) create mode 100644 libs/common/src/tools/achievements/data.ts diff --git a/libs/common/src/tools/achievements/data.ts b/libs/common/src/tools/achievements/data.ts new file mode 100644 index 00000000000..55ce734d7fe --- /dev/null +++ b/libs/common/src/tools/achievements/data.ts @@ -0,0 +1,8 @@ +export const Type = Object.freeze({ + TimeOfDayRange: "TimeOfDayRange", + DayOfWeek: "DayOfWeek", + DayOfYear: "DayOfYear", + Threshold: "Threshold", +}); + +export const EvaluatorTypes = Object.freeze(Object.keys(Type) as ReadonlyArray); diff --git a/libs/common/src/tools/achievements/event-processor.ts b/libs/common/src/tools/achievements/event-processor.ts index 8b488b9f629..4f3c4d7516a 100644 --- a/libs/common/src/tools/achievements/event-processor.ts +++ b/libs/common/src/tools/achievements/event-processor.ts @@ -7,42 +7,39 @@ import { achievementsLocal$ as achievementsLog$, userActionIn$, } from "./inputs"; -import { isProgress } from "./meta"; -import { AchievementFormat, AchievementWatch, Earned, Progress } from "./types"; +import { isEarnedEvent, isProgressEvent } from "./meta"; +import { AchievementEvent, AchievementValidator } from "./types"; // OPTIMIZATION: compute the list of active monitors from trigger criteria function active( - status$: Observable, -): OperatorFunction { + status$: Observable, +): OperatorFunction { return pipe( withLatestFrom(status$), map(([monitors, log]) => { // partition the log into progress and earned achievements - const progress: Progress[] = []; - const earned: Earned[] = []; - for (const l of log) { - if (isProgress(l.achievement)) { - progress.push(l.achievement); - } else { - earned.push(l.achievement); - } - } - - const progressByName = new Map(progress.map((a) => [a.name, a.value])); - const earnedByName = new Set(earned.map((e) => e.name)); + const progressByName = new Map( + log.filter(isProgressEvent).map((e) => [e.achievement.name, e.achievement.value]), + ); + const earnedByName = new Set( + log.filter((e) => isEarnedEvent(e)).map((e) => e.achievement.name), + ); // compute list of active achievements const active = monitors.filter((m) => { + // 🧠 the filters could be lifted into a function argument & delivered + // as a `Map bool> + if (m.trigger === "once") { // monitor disabled if already achieved return !earnedByName.has(m.achievement); } // monitor disabled if outside of threshold - const progress = progressByName.get(m.achievement) ?? 0; - if (m.trigger.high ?? Number.POSITIVE_INFINITY < progress) { + const progress = progressByName.get(m.progress) ?? 0; + if (progress > (m.trigger.high ?? Number.POSITIVE_INFINITY)) { return false; - } else if (m.trigger.low ?? 0 > progress) { + } else if (progress < (m.trigger.low ?? 0)) { return false; } @@ -57,27 +54,26 @@ function active( // the formal event processor function validate( - monitors$: Observable, - status$: Observable, -): OperatorFunction { + monitors$: Observable, + status$: Observable, +): OperatorFunction { return pipe( withLatestFrom(monitors$), map(([action, monitors]) => { - // narrow the list of all live monitors to just those that may - // change the log + // 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(status$), concatMap(([[action, monitors], status]) => { - const results: AchievementFormat[] = []; + const results: AchievementEvent[] = []; // process achievement monitors sequentially, accumulating result records for (const monitor of monitors) { - const statusEntry = status.find( - (s) => s.achievement.name === monitor.achievement && isProgress(s.achievement), - ); - results.push(...monitor.action(action, statusEntry)); + const statusEntry = status.find((s) => s.achievement.name === monitor.achievement); + const progress = isProgressEvent(statusEntry) ? statusEntry : undefined; + + results.push(...monitor.action(action, progress)); } // deliver results as a stream containing individual records to maintain diff --git a/libs/common/src/tools/achievements/inputs.ts b/libs/common/src/tools/achievements/inputs.ts index b1b970c48b0..a9f14b0f2c6 100644 --- a/libs/common/src/tools/achievements/inputs.ts +++ b/libs/common/src/tools/achievements/inputs.ts @@ -2,19 +2,19 @@ import { Subject } from "rxjs"; import { EventFormat } from "../log/ecs-format"; -import { Achievement, AchievementFormat, AchievementWatch } from "./types"; +import { Achievement, AchievementEvent, AchievementValidator } from "./types"; // sync data from the server (consumed by event store) -const replicationIn$ = new Subject(); +const replicationIn$ = new Subject(); // data incoming from the UI (consumed by validator) const userActionIn$ = new Subject(); // what to look for (consumed by validator) -const achievementMonitors$ = new Subject(); +const achievementMonitors$ = new Subject(); // data stored in local state (consumed by validator and achievement list) -const achievementsLocal$ = new Subject(); +const achievementsLocal$ = new Subject(); // metadata (consumed by achievement list) const achievementMetadata$ = new Subject(); diff --git a/libs/common/src/tools/achievements/meta.ts b/libs/common/src/tools/achievements/meta.ts index 54556be4cd0..c742d1c6177 100644 --- a/libs/common/src/tools/achievements/meta.ts +++ b/libs/common/src/tools/achievements/meta.ts @@ -1,11 +1,11 @@ -import { Earned, Progress } from "./types"; +import { AchievementEarnedEvent, AchievementProgressEvent } from "./types"; -function isProgress(achievement: any): achievement is Progress { +function isProgressEvent(achievement: any): achievement is AchievementProgressEvent { return achievement.type === "progress" && "value" in achievement; } -function isEarned(achievement: any): achievement is Earned { - return !isProgress(achievement); +function isEarnedEvent(achievement: any): achievement is AchievementEarnedEvent { + return !isProgressEvent(achievement); } -export { isProgress, isEarned }; +export { isProgressEvent, isEarnedEvent }; diff --git a/libs/common/src/tools/achievements/types.ts b/libs/common/src/tools/achievements/types.ts index 4981ad47e7d..d2482f05150 100644 --- a/libs/common/src/tools/achievements/types.ts +++ b/libs/common/src/tools/achievements/types.ts @@ -3,15 +3,23 @@ import { Tagged } from "type-fest/source/opaque"; import { EventFormat, ServiceFormat } from "../log/ecs-format"; -export type AchievementId = string & Tagged<"achievement">; +import { Type } from "./data"; -type Progress = { type: "progress"; name: AchievementId; value: number }; -type Earned = { type: "earned"; name: AchievementId }; -export type AchievementFormat = EventFormat & ServiceFormat & { achievement: Progress | Earned }; +export type EvaluatorType = keyof typeof Type; +export type AchievementId = string & Tagged<"achievement">; +export type AchievementProgressId = string & Tagged<"achievement-progress">; + +export type AchievementProgressEvent = EventFormat & + ServiceFormat & { achievement: { type: "progress"; name: AchievementProgressId; value: number } }; +export type AchievementEarnedEvent = EventFormat & + ServiceFormat & { achievement: { type: "earned"; name: AchievementId } }; +export type AchievementEvent = AchievementProgressEvent | AchievementEarnedEvent; // consumed by validator and achievement list (should this include a "toast-alerter"?) export type Achievement = { achievement: AchievementId; + progress: AchievementProgressId; + evaluator: EvaluatorType; // pre-filter that disables the rule if it's met trigger: "once" | RequireAtLeastOne<{ low: number; high: number }>; @@ -20,13 +28,13 @@ export type Achievement = { }; // consumed by validator -export type AchievementWatch = Achievement & { +export type AchievementValidator = Achievement & { // when the watch triggers on incoming user events filter: (item: EventFormat) => boolean; // what to do when an incoming event is triggered action: ( item: EventFormat, - progress?: AchievementFormat, - ) => [AchievementFormat] | [AchievementFormat, AchievementFormat]; + progress?: AchievementProgressEvent, + ) => [AchievementEvent] | [AchievementEvent, AchievementEvent]; };