Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510//! JavaScript bindings of tokenling-core for the browser page and its brain//! worker. `web/core.ts` wraps these exports with types. No storage, network//! or model access happens here; the page passes in times and saves.use serde::{Serialize, de::DeserializeOwned};use serde_json::json;use tokenling_core::{ body, engine::{self, Command, Dice}, save, turn, types::{PetState, Stats, WorldState}, world,};use wasm_bindgen::prelude::*;
fn to_js<T: Serialize + ?Sized>(value: &T) -> Result<JsValue, JsError> { Ok(value.serialize(&serde_wasm_bindgen::Serializer::json_compatible())?)}fn from_js<T: DeserializeOwned>(value: JsValue) -> Result<T, JsError> { Ok(serde_wasm_bindgen::from_value(value)?)}fn sorted(values: &[&str]) -> Vec<String> { let mut values: Vec<_> = values.iter().map(|v| v.to_string()).collect(); values.sort(); values}
/// `Math.random`, or test overrides for every roll or only wake rolls.struct JsDice<'a> { rolls: Option<&'a js_sys::Function>, wake: Option<&'a js_sys::Function>,}fn call(f: Option<&js_sys::Function>) -> Option<f64> { f?.call0(&JsValue::NULL).ok()?.as_f64()}impl Dice for JsDice<'_> { fn roll(&mut self) -> f64 { call(self.rolls).unwrap_or_else(js_sys::Math::random) } fn wake(&mut self) -> f64 { call(self.wake).unwrap_or_else(|| self.roll()) }}
/// The closed word lists, as `train/world.py` names them.#[wasm_bindgen]pub fn lists() -> Result<JsValue, JsError> { to_js(&json!({ "STAGES": world::STAGES, "PERSONALITIES": world::PERSONALITIES, "FOODS": world::FOODS, "GIFTS": world::GIFTS, "DREAMS": world::DREAMS, "EMOTES": world::EMOTES, "WANTS": world::WANTS, "WALKS": world::WALKS, "DOODLES": world::DOODLES, "WEATHER_DAY": world::WEATHER_DAY, "WEATHER_NIGHT": world::WEATHER_NIGHT, "TOYS": world::TOYS, "COLORS": world::COLORS, "EFFECT_TOOLS": sorted(world::EFFECT_TOOLS), "LOOKUP_TOOLS": sorted(world::LOOKUP_TOOLS), "ANSWER_TOOLS": sorted(world::ANSWER_TOOLS), }))}#[wasm_bindgen]pub fn header(kind: &str, world_state: JsValue) -> Result<String, JsError> { if !["chat", "diary"].contains(&kind) { return Err(JsError::new("unknown prompt kind")); } Ok(world::header(kind, &from_js::<WorldState>(world_state)?))}#[wasm_bindgen(js_name = toolResult)]pub fn tool_result(world_state: JsValue, name: &str, arg: &str) -> Result<Option<String>, JsError> { Ok(world::tool_result(&from_js(world_state)?, name, arg))}#[wasm_bindgen]pub fn recall(memories: Vec<String>, query: &str) -> String { world::recall(&memories, query)}#[wasm_bindgen(js_name = partOfDay)]pub fn part_of_day(hour: u32) -> String { world::part_of_day(hour).into()}#[wasm_bindgen(js_name = levelWords)]pub fn level_words(stats: JsValue) -> Result<JsValue, JsError> { let [tummy, fun, energy, clean] = world::level_words(&from_js::<Stats>(stats)?); to_js(&json!({"tummy": tummy, "fun": fun, "energy": energy, "clean": clean}))}#[wasm_bindgen(js_name = wakeChance)]pub fn wake_chance(energy: f64, kind: &str, n: u32) -> f64 { world::wake_chance(energy, kind, n)}#[wasm_bindgen(js_name = dreamThing)]pub fn dream_thing(item: &str) -> String { world::dream_thing(item)}/// An app event line by name (`offerFood`, `pat`, `woke`...), for parity checks.#[wasm_bindgen]pub fn event(name: &str, owner: &str, pet: &str, arg: &str) -> Result<String, JsError> { use world::events as e; Ok(match name { "offerFood" => e::offer_food(owner, pet, arg), "offerBath" => e::offer_bath(owner, pet), "offerMedicine" => e::offer_medicine(owner, pet), "offerCatch" => e::offer_catch(owner, pet), "caught" => e::caught( owner, pet, arg.parse().map_err(|_| JsError::new("bad count"))?, ), "pat" => e::pat(owner, pet), "lights" => e::lights(owner, arg == "on"), "woke" => e::woke(pet), "pooped" => e::pooped(pet), "cleaned" => e::cleaned(owner), "back" => e::back(owner, arg), "grew" => e::grew(pet, arg), _ => return Err(JsError::new("unknown event")), })}#[wasm_bindgen]pub fn tags(text: &str) -> Result<JsValue, JsError> { to_js(&world::tags(text))}#[wasm_bindgen(js_name = cleanText)]pub fn clean_text(text: &str) -> String { world::clean_text(text)}#[wasm_bindgen]pub fn silent(text: &str) -> bool { world::silent(text)}#[wasm_bindgen(js_name = notCute)]pub fn not_cute(text: &str) -> bool { world::not_cute(text)}#[wasm_bindgen(js_name = cleanName)]pub fn clean_name(name: &str, max: usize) -> String { world::clean_name(name, max)}#[wasm_bindgen(js_name = cleanMessage)]pub fn clean_message(text: &str) -> String { world::clean_message(text)}/// The stop rule for generated text: `{reason, text}` or undefined.#[wasm_bindgen]pub fn stop(mode: &str, text: &str) -> Result<JsValue, JsError> { match world::stop(mode, text) { Some((reason, text)) => to_js(&json!({"reason": reason, "text": text})), None => Ok(JsValue::UNDEFINED), }}/// Fits a prompt into the context; `count` is the tokenizer's token count.#[wasm_bindgen]pub fn fit( header: &str, lines: Vec<String>, prefix: &str, max_new: usize, ctx: usize, count: &js_sys::Function,) -> Result<String, JsError> { let mut count = |text: &str| -> Result<usize, String> { count .call1(&JsValue::NULL, &text.into()) .map_err(|e| format!("{e:?}"))? .as_f64() .map(|n| n as usize) .ok_or_else(|| "the token count is not a number".into()) }; world::fit(header, &lines, prefix, max_new, ctx, &mut count).map_err(|e| JsError::new(&e))}#[wasm_bindgen(js_name = offerThing)]pub fn offer_thing(offer: JsValue) -> Result<String, JsError> { Ok(engine::offer_thing(&from_js(offer)?))}/// Growth stages with the age each starts at, oldest first.#[wasm_bindgen(js_name = stageAges)]pub fn stage_ages() -> Result<JsValue, JsError> { to_js(&body::STAGE_AGES)}#[wasm_bindgen(js_name = stageFor)]pub fn stage_for(age: f64) -> String { body::stage_for(age).into()}#[wasm_bindgen(js_name = freshSave)]pub fn fresh_save(now: f64, time_zone: &str) -> Result<JsValue, JsError> { to_js(&save::fresh(now, time_zone))}/// Reads a save: `{state}`, or `{error, newer}` for a save this page must keep.#[wasm_bindgen(js_name = parseSave)]pub fn parse_save(raw: &str, now: f64, time_zone: &str) -> Result<JsValue, JsError> { match save::parse_save(raw, now, time_zone) { Ok(state) => to_js(&json!({ "state": state })), Err(e) => to_js(&json!({ "error": e.message, "newer": e.newer })), }}#[wasm_bindgen(js_name = saveVersion)]pub fn save_version() -> u32 { save::VERSION}/// Runs the body on a copy of `state`; returns `{state, events}`.#[wasm_bindgen(js_name = simulateBody)]pub fn simulate_body(state: JsValue, from: f64, to: f64, quiet: bool) -> Result<JsValue, JsError> { let mut state: PetState = from_js(state)?; let events = body::simulate_body(&mut state, from, to, quiet, &mut js_sys::Math::random); to_js(&json!({ "state": state, "events": events }))}
/// A running pet. The browser keeps one for the save in this tab.#[wasm_bindgen]pub struct Engine { inner: engine::Engine, seed: String, overrides: Overrides, seen: u64,}#[derive(Default)]struct Overrides { rolls: Option<js_sys::Function>, wake: Option<js_sys::Function>,}impl Overrides { fn dice(&self) -> JsDice<'_> { JsDice { rolls: self.rolls.as_ref(), wake: self.wake.as_ref(), } }}#[wasm_bindgen]impl Engine { #[wasm_bindgen(constructor)] pub fn new(state: JsValue, now: f64, seed: String) -> Result<Engine, JsError> { let state: PetState = from_js(state)?; state.validate().map_err(|e| JsError::new(&e.0))?; Ok(Self { inner: engine::Engine::new(state, now), seed, overrides: Overrides::default(), seen: 0, }) } /// Replaces the save (an import, a transfer or a server snapshot). Turn /// requests, disturbances and an accepted game belong to the old copy. pub fn replace(&mut self, state: JsValue, now: f64) -> Result<(), JsError> { let state: PetState = from_js(state)?; state.validate().map_err(|e| JsError::new(&e.0))?; let seq = self.inner.effect_seq; let revision = self.inner.revision + 1; let context = self.inner.context_revision + 1; self.inner = engine::Engine::new(state, now); (self.inner.effect_seq, self.seen) = (seq, seq); self.inner.revision = revision; self.inner.context_revision = context; Ok(()) } /// Replaces the save but keeps the turn queue and effects (debug edits). pub fn edit(&mut self, state: JsValue) -> Result<(), JsError> { let state: PetState = from_js(state)?; state.validate().map_err(|e| JsError::new(&e.0))?; self.inner.state = state; self.inner.revision += 1; self.inner.context_revision += 1; Ok(()) } pub fn state(&self) -> Result<JsValue, JsError> { to_js(&self.inner.state) } #[wasm_bindgen(js_name = stateJson)] pub fn state_json(&self) -> Result<String, JsError> { Ok(serde_json::to_string(&self.inner.state)?) } /// The fields a body tick changes, for cheap refreshes between full reads. pub fn body(&self) -> Result<JsValue, JsError> { let s = &self.inner.state; to_js(&json!({ "stats": s.stats, "sick": s.sick, "poop": s.poop, "asleep": s.asleep, "lightsOff": s.lights_off, "grimeSince": s.grime_since, "nextPoop": s.next_poop, "lastTick": s.last_tick, "lastActive": s.last_active, "wokeAt": s.woke_at, })) } #[wasm_bindgen(getter)] pub fn revision(&self) -> f64 { self.inner.revision as f64 } #[wasm_bindgen(getter, js_name = contextRevision)] pub fn context_revision(&self) -> f64 { self.inner.context_revision as f64 } #[wasm_bindgen(getter)] pub fn pending(&self) -> Option<String> { self.inner.pending.clone() } #[wasm_bindgen(getter)] pub fn game(&self) -> Result<JsValue, JsError> { to_js(&self.inner.game) } /// Disturbances that still count at `now` (debug display of the next roll). #[wasm_bindgen(js_name = disturbancesAt)] pub fn disturbances_at(&self, now: f64) -> u32 { self.inner.disturbances_at(now) } #[wasm_bindgen(getter)] pub fn disturbances(&self) -> Result<JsValue, JsError> { to_js(&json!({"count": self.inner.disturbance_count, "last": self.inner.disturbance_last})) } /// Runs the body to `now`. Returns false when only `body()` fields changed. pub fn advance(&mut self, now: f64, returning: bool) -> bool { let before = ( self.inner.revision, self.inner.game.is_some(), self.inner.state.offer.is_some(), ); let mut dice = self.overrides.dice(); self.inner.advance(now, &self.seed, returning, &mut dice); self.inner.revision > before.0 + 1 || self.inner.game.is_some() != before.1 || self.inner.state.offer.is_some() != before.2 } /// Restarts the body clock, so time spent elsewhere is not replayed. #[wasm_bindgen(js_name = resetClock)] pub fn reset_clock(&mut self, now: f64) { self.inner.state.last_tick = now; self.inner.revision += 1; } /// An owner command, the same JSON the host accepts. Throws the refusal. pub fn command(&mut self, command: JsValue, now: f64) -> Result<(), JsError> { let command: Command = from_js(command)?; let mut dice = self.overrides.dice(); self.inner .command(command, now, &self.seed, &mut dice) .map_err(|e| JsError::new(&e.0)) } #[wasm_bindgen(js_name = layEgg)] pub fn lay_egg(&mut self, owner: &str, now: f64) -> Result<(), JsError> { self.inner .lay_egg(owner, now) .map_err(|e| JsError::new(&e.0)) } /// Device preferences in `settings` (not `chatter`, which is a command). #[wasm_bindgen(js_name = setPreference)] pub fn set_preference(&mut self, key: &str, value: JsValue) -> Result<(), JsError> { if key == "chatter" { return Err(JsError::new("chatter is a pet setting")); } let value: serde_json::Value = from_js(value)?; self.inner.state.settings.extra.insert(key.into(), value); self.inner.revision += 1; Ok(()) } /// Applies a line's tags as if the pet had said it (debug checks). #[wasm_bindgen(js_name = applyLine)] pub fn apply_line(&mut self, line: &str, now: f64) { let mut dice = self.overrides.dice(); self.inner.apply_line(line, now, &self.seed, &mut dice); } pub fn world(&self, now: f64) -> Result<JsValue, JsError> { if self.inner.state.pet.is_none() { return Ok(JsValue::NULL); } to_js(&self.inner.world(now, &self.seed)) } pub fn weather(&self, now: f64) -> String { body::weather_at(&self.inner.state, now, &self.seed) } #[wasm_bindgen(js_name = dayNum)] pub fn day_num(&self, now: f64) -> f64 { body::day_num(&self.inner.state, now) as f64 } #[wasm_bindgen(js_name = wantTurn)] pub fn want_turn(&mut self, kind: &str) { self.inner .want_turn(if kind == "idle" { "idle" } else { "react" }); } /// When the next idle turn may come. #[wasm_bindgen(js_name = idleAt)] pub fn idle_at(&mut self, at: f64) { self.inner.next_idle = at; } #[wasm_bindgen(js_name = chooseTask)] pub fn choose_task(&self, now: f64, background: bool) -> Result<JsValue, JsError> { to_js(&self.inner.choose_task(now, background)) } #[wasm_bindgen(js_name = beginTurn)] pub fn begin_turn(&mut self, now: f64) { let mut dice = self.overrides.dice(); self.inner.begin_turn(now, &mut dice); } #[wasm_bindgen(js_name = chatTurn)] pub fn chat_turn(&self, kind: &str, now: f64) -> Result<ChatTurn, JsError> { if self.inner.state.pet.is_none() { return Err(JsError::new("an egg has no turns")); } Ok(ChatTurn(self.inner.chat_turn(kind, now, &self.seed))) } #[wasm_bindgen(js_name = commitLine)] pub fn commit_line(&mut self, body: &str, now: f64) { let mut dice = self.overrides.dice(); self.inner.commit_line(body, now, &self.seed, &mut dice); } /// Answers the open offer when the brain said nothing usable. #[wasm_bindgen(js_name = declineOffer)] pub fn decline_offer(&mut self, now: f64) { let mut dice = self.overrides.dice(); self.inner.answer_offer(false, now, &self.seed, &mut dice); } #[wasm_bindgen(js_name = diaryTurn)] pub fn diary_turn(&self, date: &str, now: f64, replace: bool) -> Option<DiaryTurn> { self.inner .diary_turn(date, now, &self.seed, replace) .map(DiaryTurn) } /// Commits a diary page; false when the text was filtered (the day stays pending). #[wasm_bindgen(js_name = commitDiary)] pub fn commit_diary(&mut self, turn: &DiaryTurn, generated: &str) -> bool { match turn.0.finish(generated) { Some(page) => { self.inner.commit_page(page, turn.0.replace); true } None => false, } } /// Effects added since the last call, oldest first. #[wasm_bindgen(js_name = takeEffects)] pub fn take_effects(&mut self) -> Result<JsValue, JsError> { let seen = self.seen; let fresh: Vec<_> = self.inner.effects.iter().filter(|e| e.seq > seen).collect(); self.seen = self.inner.effect_seq; to_js(&fresh) } pub fn disturb(&mut self, kind: &str, now: f64) -> bool { let mut dice = self.overrides.dice(); self.inner.disturb(kind, now, &mut dice) } #[wasm_bindgen(js_name = resetDisturbances)] pub fn reset_disturbances(&mut self) { self.inner.reset_disturbances(); } /// Overrides wake rolls (browser checks); `null` restores `Math.random`. #[wasm_bindgen(js_name = setWakeRoll)] pub fn set_wake_roll(&mut self, roll: Option<js_sys::Function>) { self.overrides.wake = roll; } /// Overrides every random draw (replay tests); `null` restores `Math.random`. #[wasm_bindgen(js_name = setRolls)] pub fn set_rolls(&mut self, rolls: Option<js_sys::Function>) { self.overrides.rolls = rolls; } /// The whole engine as the host stores it, for cross-build checks. #[wasm_bindgen(js_name = engineJson)] pub fn engine_json(&self) -> Result<String, JsError> { Ok(serde_json::to_string(&self.inner)?) } /// Debug: makes the pet `age` milliseconds old. #[wasm_bindgen(js_name = setAge)] pub fn set_age(&mut self, age: f64, now: f64) { if let Some(pet) = self.inner.state.pet.as_mut() { pet.hatched = now - age; pet.stage = body::stage_for(age).into(); self.inner.revision += 1; } } /// Debug: runs the body without the day log or chat lines. pub fn simulate(&mut self, from: f64, to: f64, quiet: bool) -> Result<JsValue, JsError> { let mut dice = self.overrides.dice(); let events = body::simulate_body(&mut self.inner.state, from, to, quiet, &mut || dice.roll()); self.inner.revision += 1; to_js(&events) } #[wasm_bindgen(js_name = alertPlan)] pub fn alert_plan(&self, now: f64) -> Result<JsValue, JsError> { let mut dice = self.overrides.dice(); to_js(&body::alert_plan(&self.inner.state, now, &mut || { dice.roll() })) }}
/// One pet line in progress: request, feed each generated piece, read the body.#[wasm_bindgen]pub struct ChatTurn(turn::ChatTurn);#[wasm_bindgen]impl ChatTurn { #[wasm_bindgen(getter)] pub fn kind(&self) -> String { self.0.kind.clone() } pub fn request(&self) -> Result<JsValue, JsError> { to_js(&self.0.request()) } /// True when the line needs another generation. pub fn feed(&mut self, text: &str, reason: &str) -> bool { self.0.feed(text, reason) } pub fn text(&self) -> String { self.0.text().into() } pub fn body(&self) -> Option<String> { self.0.body().map(String::from) }}
#[wasm_bindgen]pub struct DiaryTurn(turn::DiaryTurn);#[wasm_bindgen]impl DiaryTurn { #[wasm_bindgen(getter)] pub fn date(&self) -> String { self.0.date.clone() } pub fn request(&self) -> Result<JsValue, JsError> { to_js(&self.0.request()) }}