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 = { 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(); 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, }; }