/** * limerence.ts * A model of the first 72 hours after meeting someone you like. * Every mechanism below is from published research, which somehow makes it worse. */ type Chemical = 'dopamine' | 'serotonin' | 'norepinephrine' interface Signal { what: string /** 0 = unambiguous, 1 = could mean literally anything */ ambiguity: number /** what a calm, reasonable friend would say it means, -1..1 */ actualMeaning: number } const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n)) class LimerentBrain { readonly chemistry: Record = { dopamine: 1, serotonin: 1, norepinephrine: 1 } thoughtsPerHour = 0 confidence = 0.8 // you are, usually, fine at talking to people private expectedReplyMinutes = 30 private replays = new Map() constructor(readonly them: string) {} /** Serotonin in new love drops to levels seen in OCD. One thought, on a loop. */ meet(): void { this.chemistry.serotonin = 0.6 this.chemistry.dopamine = 2.0 this.chemistry.norepinephrine = 1.8 // the palms this.thoughtsPerHour = 12 } /** * "Acute sensitivity to any act that can be interpreted favourably" — and an * equal and opposite fear of rejection. (Tennov, 1979) * Ambiguity isn't noise. It's room for hope, and room for dread. You get both. */ decode(signal: Signal): { hope: number; dread: number } { const hope = clamp(signal.actualMeaning + signal.ambiguity * (this.chemistry.dopamine - this.chemistry.serotonin), -1, 1) const dread = clamp(signal.actualMeaning - signal.ambiguity * (1 - this.confidence) * 2, -1, 1) this.replay(signal.what) // Not knowing if they like you makes you think about them more, // which makes you like them more. (Whitchurch, Wilson & Gilbert, 2011) this.thoughtsPerHour += Math.round(signal.ambiguity * 10) return { hope, dread } // held simultaneously. this is normal. apparently. } /** * Dopamine fires on the gap between expected and actual. Surprise is the drug, * which is why a slow reply makes the next fast one feel incredible. */ receiveReply(afterMinutes: number): number { const surprise = clamp((this.expectedReplyMinutes - afterMinutes) / this.expectedReplyMinutes, -1, 1) this.chemistry.dopamine += surprise this.expectedReplyMinutes = afterMinutes // that's the baseline now. forever. return surprise } /** Crystallisation: you see the flaw. You keep it anyway. (Stendhal, via Tennov) */ crystallise(flaw: string): string { return `${flaw}, which is honestly kind of cute` } /** Normally confident people go suddenly shy around them. */ standNear(): string { this.confidence *= 0.25 return this.confidence < 0.3 ? 'said "you too" when they said happy birthday to someone else' : 'fine' } /** Arousal gets misattributed to whoever's nearby. (Dutton & Aron, 1974) */ explainHeartRate(_actualCause: 'the third espresso' | 'the wobbly bridge'): string { return this.them } replay(moment: string): number { const n = (this.replays.get(moment) ?? 0) + 1 this.replays.set(moment, n) return n } mostReplayed(): [string, number] { return [...this.replays].sort((a, b) => b[1] - a[1])[0] } } // ─── Texting ──────────────────────────────────────────────────────────────── const keenness = (text: string) => ((text.match(/y/g)?.length ?? 1) - 1) * 0.25 + // every extra y is a confession (text.match(/!/g)?.length ?? 0) * 0.2 + (text.includes(':)') ? 0.1 : 0) + (/^[A-Z]/.test(text) ? -0.3 : 0) // capital letter = your dentist /** Warm enough to be nice. Cold enough to look like you weren't waiting. */ const TARGET_KEENNESS = 0.1 function pickOpener(drafts: string[]): string { return [...drafts].sort( (a, b) => Math.abs(keenness(a) - TARGET_KEENNESS) - Math.abs(keenness(b) - TARGET_KEENNESS), )[0] } /** Nobody wrote these rules. Everyone follows them. */ function replyDelay(theirMinutes: number, now: Date): number { const delay = theirMinutes * 2 + Math.random() * 7 // jitter, so it doesn't look calculated const bedtime = new Date(now) bedtime.setHours(23, 30) const latest = (bedtime.getTime() - now.getTime()) / 60_000 return Math.round(Math.min(delay, latest)) // but never risk them falling asleep } // ─── Research (Instagram) ─────────────────────────────────────────────────── function scrollBackTo(year: number, from = 2026): { postsSeen: number; accidentalLikeRisk: number } { const postsSeen = (from - year) * 41 const tremor = postsSeen / 600 // the further back, the shakier the thumb return { postsSeen, accidentalLikeRisk: clamp(tremor, 0, 1) } } // ─── Simulation ───────────────────────────────────────────────────────────── const me = new LimerentBrain('Sam') const log = (when: string, what: string) => console.log(`${when.padEnd(14)} ${what}`) log('Sat 21:40', 'Sam laughs at the hummus joke.') me.meet() log('Sat 21:41', `Sam is standing next to you. Result: ${me.standNear()}.`) log('Sat 23:47', `Home. Coat still on. Heart rate explained by: ${me.explainHeartRate('the third espresso')}.`) for (let i = 0; i < 37; i++) me.replay('the bit where Sam touched my arm') const firstText = pickOpener(['Hey!', 'hey', 'heyyy', 'hey!', 'hey :)', 'hi', 'heyy!!']) log('Sun 11:02', `Seven drafts later, sends: "${firstText}"`) log('Sun 11:03', 'Phone unlocked 14 times. Read receipts: off. Regretting that.') log('Sun 13:30', `Reply after 148 min. Surprise: ${me.receiveReply(148).toFixed(2)}. Dopamine: ${me.chemistry.dopamine.toFixed(1)}.`) const wait = replyDelay(148, new Date('2026-11-08T13:30:00')) log('Sun 13:30', `Waits ${wait} min before replying, per the rules.`) log('Sun 22:15', `Reply after 4 min. Surprise: +${me.receiveReply(4).toFixed(2)}. Dopamine: ${me.chemistry.dopamine.toFixed(1)}. Slot machine pays out.`) const ig = scrollBackTo(2016) log('Mon 00:50', `${ig.postsSeen} posts deep (Crete, 2016). Accidental-like risk: ${(ig.accidentalLikeRisk * 100).toFixed(0)}%.`) log('Mon 00:51', `Finds: they still follow their ex. Filed as: "${me.crystallise('they still follow their ex')}".`) const { hope, dread } = me.decode({ what: 'haha yeah', ambiguity: 0.9, actualMeaning: 0.2 }) log('Mon 14:02', 'Sam: "haha yeah". Friends consulted: 3. Screenshots sent: 4.') log('Mon 14:02', `Read as: ${hope.toFixed(2)} (they love me) and ${dread.toFixed(2)} (they hate me). At the same time.`) const [moment, times] = me.mostReplayed() log('Mon 14:03', `Thoughts per hour: ${me.thoughtsPerHour}. Most replayed: "${moment}" (${times}x).`) log('Mon 14:06', 'Sam: "want to get a drink thursday?"') // @ts-expect-error feelings are not type-safe me.chemistry.serotonin = 'who cares'