Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153import test from "node:test";import assert from "node:assert/strict";import { readFileSync } from "node:fs";import * as C from "../web/core.ts";C.initCoreSync(readFileSync(new URL("../core/wasm/pkg/tokenling_core_bg.wasm", import.meta.url)));const MIN = 60e3, HOUR = 60 * MIN;const noon = new Date(2026, 9, 1, 12).getTime();function pet() { const state = C.freshSave(noon, "UTC"); state.owner = "Sam"; state.pet = { name: "Mochi", traits: ["sweet", "curious"], color: "cream", toy: "ball", hatched: noon, stage: "baby", }; state.nextPoop = noon + 10 * HOUR; return state;}test("awake and sleeping body rates remain unchanged", () => { const awake = pet(), sleeping = pet(); sleeping.asleep = true; sleeping.stats.energy = 40; assert.deepEqual(C.simulateBody(awake, noon, noon + HOUR, false).state.stats, { tummy: 62, fun: 64, energy: 75, clean: 87, }); assert.deepEqual(C.simulateBody(sleeping, noon, noon + HOUR, false).state.stats, { tummy: 68, fun: 70, energy: 62, clean: 89.5, });});test("sleep pauses grime exposure; awake exposure resumes it", () => { let state = pet(); state.poop = 3; state.grimeSince = noon - 45 * MIN; state.asleep = true; state.stats.energy = 10; state = C.simulateBody(state, noon, noon + 20 * MIN, true).state; assert.equal(state.sick, false); assert.equal(state.grimeSince, noon - 25 * MIN); state.asleep = false; assert.equal(C.simulateBody(state, noon + 20 * MIN, noon + 40 * MIN, true).state.sick, true);});test("exhaustion and recent wake protection are body rules", () => { let state = pet(); state.stats.energy = 5; state.wokeAt = noon; state = C.simulateBody(state, noon, noon + MIN, false).state; assert.equal(state.asleep, false); const { state: after, events } = C.simulateBody(state, noon + MIN, noon + 6 * MIN, false); assert.equal(after.asleep, true); assert.ok(events.includes("fell asleep"));});test("a rested pet wakes; the projection never changes its input", () => { const state = pet(); state.asleep = true; state.stats.energy = 98; const { state: after, events } = C.simulateBody(state, noon, noon + 10 * MIN, false); assert.ok(events.includes("woke")); assert.equal(after.asleep, false); assert.equal(state.asleep, true); assert.equal(state.stats.energy, 98);});
function engine(state = pet(), at = noon) { state.lastTick = at; return new C.Engine(state, at, "");}test("care offers are brain decisions; only one plain answer applies them", () => { const e = engine(); e.command({ type: "care", kind: "feed", item: "cookie" }, noon); assert.equal(e.state().chat.at(-1), "* Sam offered Mochi a cookie *"); assert.throws( () => e.command({ type: "care", kind: "bath", item: null }, noon), /still deciding about a cookie/, ); const tummy = e.state().stats.tummy; e.commitLine("[yes] [no] hmm", noon); assert.equal(e.state().offer, null, "an ambiguous answer counts as no"); assert.equal(e.state().stats.tummy, tummy); assert.ok(e.state().today.log.includes("said no to a cookie")); e.command({ type: "care", kind: "feed", item: "apple" }, noon); e.commitLine("yum [yes] [emote:happy]", noon); assert.equal(e.state().stats.tummy, tummy + 28); assert.deepEqual( e.takeEffects().map((x) => x.kind), ["eat", "emote"], ); assert.throws(() => e.command({ type: "care", kind: "clean", item: null }, noon), /All clean/);});test("a sleeping pet only dreams or answers an offer, and pats may wake it", () => { const state = pet(); state.asleep = true; state.stats.energy = 50; const e = engine(state); e.command({ type: "care", kind: "feed", item: "cookie" }, noon); e.commitLine("[dream:cookie] [no] [gift:flower]", noon); assert.equal(e.state().asleep, true); assert.deepEqual(e.state().shelf, []); assert.ok(e.state().today.log.includes("dreamed of a cookie")); e.setWakeRoll(() => 0.99); e.command({ type: "care", kind: "pat", item: null }, noon); assert.equal(e.state().asleep, true); e.setWakeRoll(() => 0); e.command({ type: "care", kind: "pat", item: null }, noon + 1000); assert.equal(e.state().asleep, false); assert.equal(e.state().chat.at(-2), "* Mochi woke up *");});test("turns: reactions first, lookups continue the line, stale replies are dropped", () => { const e = engine(); e.command({ type: "message", text: "what time is it?" }, noon); assert.deepEqual(e.chooseTask(noon, false), { type: "chat", kind: "react" }); e.beginTurn(noon); assert.equal(e.chooseTask(noon, false), null, "a hidden page does not idle"); const turn = e.chatTurn("react", noon); assert.equal(turn.request().prefix, "Mochi:"); assert.equal(turn.feed(" it is [clock]", "lookup"), true); assert.match(turn.request().prefix, /^Mochi: it is \[clock\]\{\d\d:\d\d \w+\}$/); assert.equal(turn.feed(" yay", "line"), false); e.commitLine(turn.body(), noon); assert.match(e.state().chat.at(-1), /^Mochi: it is \[clock\]\{.*\} yay$/); // A reply generated before the owner spoke again belongs to an older context. const seen = e.contextRevision; e.command({ type: "message", text: "hello?" }, noon + 1000); assert.notEqual(e.contextRevision, seen); assert.equal(e.pending, "react");});test("silent lines stay at two in a row", () => { const e = engine(); for (const line of ["[walk:left]", "[emote:spin]", "[walk:right]"]) e.commitLine(line, noon); assert.deepEqual( e.state().chat.slice(-2), ["Mochi: [emote:spin]", "Mochi: [walk:right]"].slice(-2), ); assert.equal(e.state().chat.filter((l) => l.startsWith("Mochi:")).length, 2);});test("coming back after a long absence is noticed once", () => { const e = engine(); e.advance(noon + 3 * HOUR, true); assert.equal(e.state().chat.at(-1), "* Sam came back after 3 hours *"); assert.ok(e.state().today.log.includes("Sam was away for a while")); assert.equal(e.pending, "react");});