Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203//! The pet's body: needs, poop, sickness, collapsing and waking, growth and//! weather. Choices stay in the brain; this is only physiology and time.use crate::{calendar, types::PetState, world};
pub const MIN: f64 = 60_000.0;pub const HOUR: f64 = 60.0 * MIN;pub const DAY: f64 = 24.0 * HOUR;/// Catch-up after an absence simulates at most this long.pub const MAX_CATCH_UP: f64 = 7.0 * DAY;
pub fn clamp(n: f64) -> f64 { n.clamp(0.0, 100.0)}pub fn local(state: &PetState, t: f64) -> calendar::Local { calendar::local(&state.hosting.time_zone, t)}pub fn date_key(state: &PetState, t: f64) -> String { local(state, t).date_key()}pub fn is_night(state: &PetState, t: f64) -> bool { world::part_of_day(local(state, t).hour) == "night"}pub fn day_num(state: &PetState, t: f64) -> i64 { state .pet .as_ref() .map_or(1, |p| ((t - p.hatched) / DAY).floor() as i64 + 1)}/// Growth stages and the age each starts at, oldest first.pub const STAGE_AGES: [(&str, f64); 4] = [ ("adult", 5.0 * DAY), ("teen", 2.0 * DAY), ("kid", 12.0 * HOUR), ("baby", 0.0),];pub fn stage_for(age: f64) -> &'static str { STAGE_AGES .iter() .find(|(_, start)| age >= *start) .map_or("baby", |(stage, _)| stage)}/// Weather is a hash of the date, day or night, and the deployment's seed.pub fn weather_at(state: &PetState, t: f64, seed: &str) -> String { let d = local(state, t); let night = world::part_of_day(d.hour) == "night"; let text = d.date_key() + if night { "n" } else { "d" } + seed; let mut hash = 2166136261u32; // Each code point contributes its first UTF-16 unit (the original JS hash). for c in text.chars() { let mut units = [0u16; 2]; hash = (hash ^ c.encode_utf16(&mut units)[0] as u32).wrapping_mul(16777619); } let snow = d.month0 < 2 || d.month0 == 11; let day = [ "sunny", "sunny", "cloudy", "cloudy", "rainy", "windy", "foggy", if snow { "snowy" } else { "sunny" }, ]; let nite = [ "clear", "starry", "starry", "cloudy", "rainy", "windy", "clear", if snow { "snowy" } else { "clear" }, ]; if night { nite[hash as usize % 8] } else { day[hash as usize % 8] } .into()}
/// Advances needs from `from` to `to`. `quiet` steps in 10-minute slices for/// catch-up and projections. Returns the body events that happened.pub fn simulate_body( state: &mut PetState, from: f64, to: f64, quiet: bool, random: &mut dyn FnMut() -> f64,) -> Vec<&'static str> { let mut t = from; let mut events = Vec::new(); while t < to { let dt = (to - t).min(if quiet { 10.0 * MIN } else { to - t }); let rate = if state.asleep { [-2.0, 0.0, 22.0, -0.5] } else { [-8.0, -6.0, -5.0, -3.0] }; let h = dt / HOUR; state.stats.tummy = clamp(state.stats.tummy + rate[0] * h); state.stats.fun = clamp(state.stats.fun + rate[1] * h * if state.sick { 2.0 } else { 1.0 }); state.stats.energy = clamp(state.stats.energy + rate[2] * h); state.stats.clean = clamp(state.stats.clean + rate[3] * h); t += dt; if state.pet.is_none() { continue; } if !state.asleep && t >= state.next_poop { if state.poop < 4 { state.poop += 1; state.stats.clean = clamp(state.stats.clean - 15.0); events.push("pooped"); } state.next_poop = t + (2.5 + random() * 2.5) * HOUR; } let grimy = state.poop >= 3 || state.stats.tummy < 5.0 || state.stats.clean < 5.0; if !grimy { state.grime_since = 0.0; } else if state.asleep { // Sleep pauses grime exposure. if state.grime_since != 0.0 { state.grime_since += dt; } } else if state.grime_since == 0.0 { state.grime_since = t; } if !state.asleep && !state.sick && state.grime_since != 0.0 && t - state.grime_since > HOUR { state.sick = true; events.push("sick"); } // Choosing to nap is the brain's call ([sleep]); collapsing is the // body's, but not within 5 minutes of being woken up. if !state.asleep && state.stats.energy < 6.0 && t - state.woke_at > 5.0 * MIN { state.asleep = true; events.push("fell asleep"); } let hr = local(state, t).hour; if state.asleep && (state.stats.energy >= 99.0 || (state.stats.energy > 70.0 && (7..11).contains(&hr) && !state.lights_off)) { state.asleep = false; state.lights_off = false; events.push("woke"); } if state.stats.tummy < 15.0 { events.push("hungry"); } if state.stats.clean < 20.0 { events.push("stinky"); } } events}
/// A body alert the browser may show while its page is closed.#[derive(Clone, Debug, serde::Serialize)]pub struct Alert { pub kind: &'static str, pub at: f64, pub stage: Option<&'static str>,}/// Projects a copy of the body a day ahead in 10-minute steps and returns up/// to four first occurrences: hungry, poop, sick and growing up.pub fn alert_plan(state: &PetState, t: f64, random: &mut dyn FnMut() -> f64) -> Vec<Alert> { let Some(pet) = &state.pet else { return vec![]; }; let mut state = state.clone(); let mut items: Vec<Alert> = vec![]; let add = |items: &mut Vec<Alert>, kind, at, stage| { if !items.iter().any(|i| i.kind == kind) { items.push(Alert { kind, at, stage }); } }; let (hungry0, sick0) = (state.stats.tummy < 20.0, state.sick); let (mut poop, mut stage) = (state.poop, pet.stage.clone()); let mut now = t; while now < t + DAY && items.len() < 4 { simulate_body(&mut state, now, now + 10.0 * MIN, true, random); let at = now + 10.0 * MIN; if !hungry0 && state.stats.tummy < 20.0 { add(&mut items, "hungry", at, None); } if state.poop > poop { add(&mut items, "poop", at, None); } if !sick0 && state.sick { add(&mut items, "sick", at, None); } let st = stage_for(at - pet.hatched); if st != stage { stage = st.into(); add(&mut items, "grew", at, Some(st)); } poop = state.poop; now += 10.0 * MIN; } items}