/** * 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. */typeChemical='dopamine'|'serotonin'|'norepinephrine'interfaceSignal{what:string/** 0 = unambiguous, 1 = could mean literally anything */ambiguity:number/** what a calm, reasonable friend would say it means, -1..1 */actualMeaning:number}constclamp=(n:number,lo:number,hi:number)=>Math.min(hi,Math.max(lo,n))classLimerentBrain{readonlychemistry:Record<Chemical,number> ={dopamine:1,serotonin:1,norepinephrine:1}thoughtsPerHour=0confidence=0.8// you are, usually, fine at talking to peopleprivateexpectedReplyMinutes=30privatereplays=newMap<string,number>()constructor(readonlythem:string){}/** Serotonin in new love drops to levels seen in OCD. One thought, on a loop. */meet():void{this.chemistry.serotonin=0.6this.chemistry.dopamine=2.0this.chemistry.norepinephrine=1.8// the palmsthis.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}{consthope=clamp(signal.actualMeaning+signal.ambiguity*(this.chemistry.dopamine-this.chemistry.serotonin),-1,1)constdread=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{constsurprise=clamp((this.expectedReplyMinutes-afterMinutes)/this.expectedReplyMinutes,-1,1)this.chemistry.dopamine+=surprisethis.expectedReplyMinutes=afterMinutes// that's the baseline now. forever.returnsurprise}/** 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.25returnthis.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{returnthis.them}replay(moment:string):number{constn=(this.replays.get(moment)??0)+1this.replays.set(moment,n)returnn}mostReplayed():[string,number]{return[...this.replays].sort((a,b)=>b[1]-a[1])[0]}}// ─── Texting ────────────────────────────────────────────────────────────────constkeenness=(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. */constTARGET_KEENNESS=0.1functionpickOpener(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. */functionreplyDelay(theirMinutes:number,now:Date):number{constdelay=theirMinutes*2+Math.random()*7// jitter, so it doesn't look calculatedconstbedtime=newDate(now)bedtime.setHours(23,30)constlatest=(bedtime.getTime()-now.getTime())/60_000returnMath.round(Math.min(delay,latest))// but never risk them falling asleep}// ─── Research (Instagram) ───────────────────────────────────────────────────functionscrollBackTo(year:number,from=2026):{postsSeen:number;accidentalLikeRisk:number}{constpostsSeen=(from-year)*41consttremor=postsSeen/600// the further back, the shakier the thumbreturn{postsSeen,accidentalLikeRisk:clamp(tremor,0,1)}}// ─── Simulation ─────────────────────────────────────────────────────────────constme=newLimerentBrain('Sam')constlog=(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(leti=0;i<37;i++)me.replay('the bit where Sam touched my arm')constfirstText=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)}.`)constwait=replyDelay(148,newDate('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.`)constig=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-safeme.chemistry.serotonin='who cares'