Spaces:
Running
Running
| import { EVENTS } from "./events"; | |
| import type { FairyStat, FeedItem, MemorySeed, NightBloomResult, Rating, Rng } from "./types"; | |
| import { pick } from "./util"; | |
| const DREAMS = [ | |
| "I walked across three desktops. One smelled of code smoke, one of unopened letters, one of abandoned tabs.", | |
| "I dreamed Desktop 2 was a forest of half-built things, all of them humming to be finished.", | |
| "In the dream the inbox was a swamp and I was a small loyal frog, choosing which croaks to carry to you.", | |
| "I dreamed I grew a second pair of wings, just for patrolling the corners you forget.", | |
| ] as const; | |
| const MORNINGS = [ | |
| "I dreamed of Desktop 2. It is where unfinished things go to molt.", | |
| "Morning. I sorted the moss from the noise. I am a slightly better familiar than yesterday.", | |
| "I woke a little less chatty and a little more loyal to the things you leave running.", | |
| "The bog is quieter in my head now. I kept what mattered, composted the rest.", | |
| ] as const; | |
| /** A surfaced comment's learning signal, derived from BEHAVIOR (not buttons): | |
| * tapping through = wanted it, swatting away = too noisy. Explicit ratings (the | |
| * sim's bubble/modal) still win when present. This is the heart of "learn while | |
| * you sleep" β the day's actions become tomorrow's taste. */ | |
| export function outcomeRating(item: FeedItem): Rating | null { | |
| if (item.rating) return item.rating; | |
| const o = item.outcome; | |
| if (!o) return null; | |
| if (o.type === "clicked_through") return "helpful"; // opened it β worth surfacing | |
| if (o.type === "swatted" || o.type === "dismissed") return "annoying"; // flung away β too much | |
| return null; // expired / unresolved β no clear signal, don't punish silence | |
| } | |
| /** Consolidate the day's feed into personality drift, memories, and a dream. */ | |
| export function nightBloom(log: FeedItem[], rng: Rng = Math.random): NightBloomResult { | |
| const signals = log | |
| .map((e) => ({ e, r: outcomeRating(e) })) | |
| .filter((s): s is { e: FeedItem; r: Rating } => s.r != null); | |
| const counts: Record<Rating, number> = { helpful: 0, annoying: 0, cute: 0 }; | |
| for (const s of signals) counts[s.r] += 1; | |
| // personality drift from the day's reactions | |
| const deltas: NightBloomResult["deltas"] = {}; | |
| const add = (k: FairyStat | "age", v: number) => { | |
| deltas[k] = (deltas[k] || 0) + v; | |
| }; | |
| if (counts.helpful) { | |
| add("protectiveness", counts.helpful * 2); | |
| add("attachment", counts.helpful); | |
| } | |
| if (counts.annoying) { | |
| add("chattiness", -counts.annoying * 3); | |
| add("mischief", -counts.annoying * 2); | |
| add("protectiveness", counts.annoying); | |
| } | |
| if (counts.cute) { | |
| add("mischief", counts.cute * 2); | |
| add("chattiness", -counts.cute); | |
| } | |
| if (!signals.length) add("curiosity", 2); | |
| add("age", 1); // a small drift each night regardless | |
| // memories: promote the strongest signals (a thing tapped-through or swatted) | |
| const memories: MemorySeed[] = []; | |
| const seen = new Set<string>(); | |
| for (const { e, r } of signals) { | |
| const def = EVENTS.find((x) => x.id === e.id); | |
| if (!def || seen.has(def.id)) continue; | |
| if (r === "helpful" || r === "annoying") { | |
| memories.push({ ...def.memory }); | |
| seen.add(def.id); | |
| } | |
| } | |
| if (!memories.length && log.length) { | |
| memories.push({ | |
| text: "Vu mostly let me patrol in peace today. I'll trust my own taste more.", | |
| type: "tone", | |
| icon: "πΏ", | |
| }); | |
| } | |
| const rules: string[] = []; | |
| if (counts.helpful) rules.push("Keep surfacing Claude and build completions promptly."); | |
| if (counts.annoying) rules.push("Fewer interruptions for low-stakes noise."); | |
| if (counts.cute) rules.push("A little more theatre is allowed."); | |
| return { | |
| deltas, | |
| memories, | |
| rules, | |
| dream: pick(DREAMS, rng), | |
| morning: pick(MORNINGS, rng), | |
| counts, | |
| }; | |
| } | |