Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
6.2 kB · 179 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180import { useEffect, useRef, useState } from "preact/hooks";import { brainHas } from "../app/brain.ts";import { attachCanvas } from "../app/canvas.ts";import { act } from "../app/care.ts";import { ACT_NEEDS, CFG } from "../app/config.ts";import { gameControl } from "../app/game.ts";import { dayNum } from "../app/simulation.ts";import { brain, store, view } from "../app/store.ts";import type { Action, Stat } from "../types.ts";import { FOODS, levelWords } from "../world.ts";import { Setup } from "./Setup.tsx";import { Icon } from "./shared.tsx";const actions: [Action, string, string][] = [ ["feed", "feed", "rice ball"], ["play", "play", "ball"], ["bath", "bath", "bubbles"], ["pat", "pat", "heart"], ["clean", "clean", "sparkle"], ["medicine", "medicine", "pill"], ["lights", "lights off", "bulb"],];export function Device() { const { state: s } = store; const canvas = useRef<HTMLCanvasElement>(null); const foods = useRef<HTMLDivElement>(null); const feed = useRef<HTMLButtonElement>(null); const [foodOpen, setFoodOpen] = useState(false); const off = !s.pet || !!view.game || brain.state !== "ready"; const game = !!view.game; useEffect(() => (canvas.current ? attachCanvas(canvas.current) : undefined), []); useEffect(() => { if (off) setFoodOpen(false); }, [off]); useEffect(() => { if (foodOpen) foods.current?.querySelector("button")?.focus(); }, [foodOpen]); useEffect(() => { if (game) canvas.current?.focus(); }, [game]); const words = levelWords(s.stats); const status = [ brain.state !== "ready" ? ["off", "error"].includes(brain.state) ? "dozing (no brain)" : "waking up" : s.asleep ? "asleep" : "", s.sick ? "sick" : "", s.poop ? `${s.poop} poop${s.poop > 1 ? "s" : ""}` : "", ].filter(Boolean); const label = s.pet ? `${s.pet.name} the ${s.pet.stage}: tummy ${words.tummy}, fun ${words.fun}, energy ${words.energy}, ${words.clean}${s.asleep ? ", asleep" : ""}${s.sick ? ", sick" : ""}` : "an egg, waiting to hatch"; return ( <section class="pet-device" aria-label="your pet"> <div class="pet-screen"> <canvas ref={canvas} id="pet-lcd" width={96} height={64} tabIndex={0} role="img" aria-label={label} style={{ touchAction: game ? "none" : "" }} /> <div id="game-pad" class="pet-pad" hidden={!game}> <Pad direction={-1} /> <span> hold to move <br /> catch stars, dodge ghosts! </span> <Pad direction={1} /> </div> </div> <p id="pet-status" class="pet-status" aria-live="polite"> {s.pet && `${s.pet.name} · ${s.pet.stage} · day ${dayNum()} · ${s.pet.traits.join(", ")}${status.length ? " · " + status.join(", ") : ""}`} </p> <div id="pet-meters" class="pet-meters" hidden={!s.pet}> {(Object.keys(s.stats) as Stat[]).map((k) => ( <div class="pet-meter" key={k}> <span class="pet-meter-label">{k}</span> <span class="pet-meter-pips" role="meter" aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(s.stats[k])} aria-label={`${k}: ${words[k]}`} > {[0, 1, 2, 3, 4].map((i) => ( <i key={i} class={s.stats[k] > i * 20 + 4 ? "on" : ""} /> ))} </span> <span class="pet-meter-word">{words[k]}</span> </div> ))} </div> <div id="pet-actions" class="pet-actions" aria-label="care" hidden={!s.pet}> {actions.map(([kind, label, icon]) => { const locked = !brainHas(ACT_NEEDS[kind]); return ( <button key={kind} ref={kind === "feed" ? feed : undefined} id={`act-${kind === "medicine" ? "meds" : kind}`} type="button" class={`pet-btn${locked ? " needs-brain" : ""}`} disabled={off || locked} data-needs={ACT_NEEDS[kind]} title={locked ? `needs the new brain (${CFG.modelId})` : undefined} aria-expanded={kind === "feed" ? foodOpen : undefined} aria-controls={kind === "feed" ? "food-menu" : undefined} onClick={() => (kind === "feed" ? setFoodOpen(!foodOpen) : act(kind))} > <Icon name={icon} /> <span>{kind === "lights" && s.lightsOff ? "lights on" : label}</span> </button> ); })} </div> <div id="food-menu" ref={foods} class="pet-actions pet-food" aria-label="snacks" hidden={!foodOpen || off} onKeyDown={(e) => { if (e.key === "Escape") { setFoodOpen(false); feed.current?.focus(); } }} > {FOODS.map((food) => ( <button key={food} type="button" class="pet-btn" onClick={() => { setFoodOpen(false); act("feed", food); }} > <Icon name={food} /> <span>{food}</span> </button> ))} </div> <Setup /> </section> );}function Pad({ direction }: { direction: number }) { const release = (e: PointerEvent) => gameControl(`pointer:${e.pointerId}`, 0); return ( <button type="button" id={direction < 0 ? "pad-left" : "pad-right"} class="pet-btn" aria-label={direction < 0 ? "move left" : "move right"} onPointerDown={(e) => { if (!view.game || e.button !== 0) return; e.preventDefault(); e.currentTarget.setPointerCapture(e.pointerId); gameControl(`pointer:${e.pointerId}`, direction); }} onPointerUp={release} onPointerCancel={release} onLostPointerCapture={release} onContextMenu={(e) => e.preventDefault()} > {direction < 0 ? "◀" : "▶"} </button> );}