Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194import type { WorldState } from "../types.ts";import * as Wd from "../world.ts";import { resetDisturbances } from "./animation.ts";import { simulateBody } from "./body.ts";import { CFG, DAY, HOUR, MIN, STAGE_AGE } from "./config.ts";import { maybeIdle, pushLine, wantTurn } from "./conversation.ts";import { maybeDiary, onSleep } from "./diary.ts";import { save } from "./storage.ts";import { emit, store, toast, view } from "./store.ts";import { dateKey } from "./utils.ts";
function hashStr(s: string) { let h = 2166136261; for (const c of s) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0;}export function weatherAt(t: number) { const d = new Date(t), h = d.getHours(), night = Wd.partOfDay(h) === "night"; const r = hashStr(dateKey(t) + (night ? "n" : "d") + (CFG.weatherSeed || "")); const day = [ "sunny", "sunny", "cloudy", "cloudy", "rainy", "windy", "foggy", d.getMonth() < 2 || d.getMonth() == 11 ? "snowy" : "sunny", ]; const nite = [ "clear", "starry", "starry", "cloudy", "rainy", "windy", "clear", d.getMonth() < 2 || d.getMonth() == 11 ? "snowy" : "clear", ]; return (night ? nite : day)[r % 8];}export const age = () => (store.state.pet ? Date.now() - store.state.pet!.hatched : 0);export const dayNum = () => Math.floor(age() / DAY) + 1;export const stageFor = (ms: number) => STAGE_AGE.find(([, a]) => ms >= a)![0];
export function worldState(): WorldState { const d = new Date(); const last = store.state.diary.filter((e) => e.date !== store.state.today.date).at(-1); let yesterday = ""; if (last) { const first = last.text .split(/(?<=[.!?])\s/)[0] .replace(/\s+/g, " ") .slice(0, 140); yesterday = /^dear diary/i.test(first) ? first : `dear diary, ${first}`; } return { pet: store.state.pet!.name, owner: store.state.owner, stage: store.state.pet!.stage, day: dayNum(), traits: store.state.pet!.traits, hh: d.getHours(), mm: d.getMinutes(), weather: weatherAt(Date.now()), stats: store.state.stats, memories: store.state.memories, today: store.state.today.log, shelf: store.state.shelf, toy: store.state.pet!.toy, poop: store.state.poop, asleep: store.state.asleep, sick: store.state.sick, yesterday, };}
// ---------- day log ----------export function logToday(entry: string) { rollDay(); const log = store.state.today.log; const i = log.indexOf(entry); if (i >= 0) log.splice(i, 1); log.push(entry); if (log.length > 14) log.shift(); save(); emit();}// At midnight a day with no page yet waits in store.state.history; maybeDiary() has the// pet write it later. Days with fewer than two things in them get no page, and// only the newest week is kept.export function rollDay() { const k = dateKey(Date.now()); if (store.state.today.date === k) return; if ( store.state.today.log.length >= 2 && !store.state.diary.some((e) => e.date === store.state.today.date) ) { store.state.history[store.state.today.date] = { log: store.state.today.log, weather: store.state.today.weather || weatherAt(Date.now() - DAY), day: store.state.today.day || dayNum() - 1, }; } for (const old of Object.keys(store.state.history).sort().slice(0, -7)) delete store.state.history[old]; store.state.today = { date: k, log: [], day: dayNum(), weather: weatherAt(Date.now()) };}
// ---------- the body: stats, poop, sickness, exhaustion ----------export function simulate(from: number, to: number, quiet: boolean) { return simulateBody(store.state, from, to, quiet);}
export function catchUp() { const now = Date.now(); const away = now - store.state.lastTick; if (store.state.pet && away > 2 * MIN) { const evs = simulate(store.state.lastTick, Math.min(now, store.state.lastTick + 7 * DAY), true); lastSecond = now; // the next frame must not replay time already caught up if (evs.includes("pooped")) logToday("pooped"); if (evs.includes("hungry")) logToday("was very hungry"); if (evs.includes("stinky")) logToday("was stinky"); if (evs.includes("fell asleep") || evs.includes("woke")) logToday("napped"); if (away > 30 * MIN) { const span = away > 2 * DAY ? `${Math.floor(away / DAY)} days` : away > 2 * HOUR ? `${Math.floor(away / HOUR)} hours` : `${Math.floor(away / MIN)} minutes`; pushLine(Wd.events.back(store.state.owner, span)); logToday(`${store.state.owner} was away for a while`); wantTurn(); } if (evs.includes("woke") && !store.state.asleep) pushLine(Wd.events.woke(store.state.pet!.name)); // An offer left open when the page closed has gone stale. if (store.state.offer) store.state.offer = null; } store.state.lastTick = now; growUp(); save();}
export function growUp() { if (!store.state.pet) return; const st = stageFor(age()); if (st !== store.state.pet!.stage) { store.state.pet!.stage = st; pushLine(Wd.events.grew(store.state.pet!.name, st)); toast(`${store.state.pet!.name} grew into a ${st}!`); wantTurn(); }}
let lastSecond = Date.now();export function tick() { const now = Date.now(); if (now - lastSecond < 1000) return; const evs = simulate(lastSecond, now, false); lastSecond = now; store.state.lastTick = now; if (!store.state.pet) return; rollDay(); growUp(); if (evs.includes("pooped")) { pushLine(Wd.events.pooped(store.state.pet!.name)); logToday("pooped"); wantTurn(); } if (evs.includes("sick")) toast(`${store.state.pet!.name} is feeling sick. Medicine helps.`); if (evs.includes("fell asleep")) { logToday("napped"); onSleep(); } if (evs.includes("woke")) { resetDisturbances(); view.dream = null; pushLine(Wd.events.woke(store.state.pet!.name)); wantTurn(); } if (store.state.stats.tummy < 15 && !store.state.today.log.includes("was very hungry")) logToday("was very hungry"); if (store.state.stats.clean < 20 && !store.state.today.log.includes("was stinky")) logToday("was stinky"); maybeIdle(); maybeDiary(); emit(); save();}