Something went wrong. Try again.
a tiny pet with an even tinier brain, living in your browser
Something went wrong. Try again.
6.7 kB · 187 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188//! JSON stdin/stdout adapter for contract checks of the native core build://! `{"cases": [...]}` renders prompts/lookups/wake chances for Python parity;//! `{"scenario": {...}}` replays engine steps for native/WebAssembly parity.use serde::Deserialize;use serde_json::{Value, json};use std::io::{self, Read};use tokenling_core::{ engine::{Command, Engine}, save, types::WorldState, world,};#[derive(Deserialize)]struct Case { st: WorldState, q: String,}#[derive(Deserialize)]#[serde(rename_all = "camelCase")]struct Scenario { save: String, t0: f64, zone: String, seed: String, rolls: Vec<f64>, steps: Vec<Step>,}#[derive(Deserialize)]#[serde(tag = "op", rename_all = "camelCase")]enum Step { Command { dt: f64, command: Command, }, Line { dt: f64, text: String, }, /// Finishes the accepted catch game with this score. Game { dt: f64, score: u32, }, Advance { dt: f64, returning: bool, }, Chat { dt: f64, kind: String, replies: Vec<(String, String)>, }, /// `date`: absent for today, "missed" for the oldest unwritten day. Diary { dt: f64, date: Option<String>, reply: String, }, Task { dt: f64, background: bool, }, Alerts { dt: f64, },}fn sorted<'a>(values: &[&'a str]) -> Vec<&'a str> { let mut values = values.to_vec(); values.sort(); values}fn cases(cases: Vec<Case>) -> Value { let rows:Vec<_>=cases.iter().map(|c|{ let s=&c.st; let wake:Vec<_>=["talk","touch","light"].iter().flat_map(|kind|(1..=6).map(move|n|world::wake_chance(s.stats.energy,kind,n))).collect(); json!({"chat":world::header("chat",s),"diary":world::header("diary",s),"clock":world::tool_result(s,"clock",""),"look":world::tool_result(s,"look",""),"recall":world::tool_result(s,"recall",&c.q),"wake":wake}) }).collect(); json!({"cases":rows,"lists":{"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,"EFFECT_TOOLS":sorted(world::EFFECT_TOOLS),"LOOKUP_TOOLS":sorted(world::LOOKUP_TOOLS),"ANSWER_TOOLS":sorted(world::ANSWER_TOOLS)}})}/// Every step's output and the whole engine afterwards. Random draws cycle/// through `rolls`, so the WebAssembly replay draws the same values.fn scenario(sc: Scenario) -> anyhow::Result<Value> { let state = save::parse_save(&sc.save, sc.t0, &sc.zone)?; let mut engine = Engine::new(state, sc.t0); let mut next = 0; let rolls = sc.rolls; let mut dice = move || { next += 1; rolls[(next - 1) % rolls.len()] }; let (seed, mut t) = (sc.seed, sc.t0); let mut out = vec![]; for step in sc.steps { let result = match step { Step::Command { dt, command } => { t += dt; match engine.command(command, t, &seed, &mut dice) { Ok(()) => Value::Null, Err(e) => json!({ "error": e.0 }), } } Step::Line { dt, text } => { t += dt; engine.commit_line(&text, t, &seed, &mut dice); Value::Null } Step::Game { dt, score } => { t += dt; let game_id = engine .game .as_ref() .map_or_else(String::new, |g| g.id.clone()); let command = Command::GameComplete { game_id, score }; match engine.command(command, t, &seed, &mut dice) { Ok(()) => Value::Null, Err(e) => json!({ "error": e.0 }), } } Step::Advance { dt, returning } => { t += dt; engine.advance(t, &seed, returning, &mut dice); Value::Null } Step::Chat { dt, kind, replies } => { t += dt; engine.begin_turn(t, &mut dice); let mut turn = engine.chat_turn(&kind, t, &seed); let mut requests = vec![]; for (text, reason) in replies { requests.push(turn.request()); if !turn.feed(&text, &reason) { break; } } let body = turn.body().unwrap_or_default().to_owned(); engine.commit_line(&body, t, &seed, &mut dice); json!({ "requests": requests, "body": body }) } Step::Diary { dt, date, reply } => { t += dt; let date = match date.as_deref() { Some("missed") => engine.state.history.keys().next().cloned(), Some(date) => Some(date.to_owned()), None => Some(engine.state.today.date.clone()), } .unwrap_or_default(); match engine.diary_turn(&date, t, &seed, false) { None => Value::Null, Some(turn) => { let page = turn.finish(&reply); let committed = page.is_some(); if let Some(page) = page { engine.commit_page(page, false); } json!({ "request": turn.request(), "committed": committed }) } } } Step::Task { dt, background } => { t += dt; json!(engine.choose_task(t, background)) } Step::Alerts { dt } => { t += dt; json!(tokenling_core::body::alert_plan( &engine.state, t, &mut dice )) } }; out.push(json!({ "out": result, "engine": engine })); } Ok(json!(out))}fn main() -> anyhow::Result<()> { let mut raw = String::new(); io::stdin().read_to_string(&mut raw)?; let mut input: Value = serde_json::from_str(&raw)?; let output = if let Some(sc) = input.get_mut("scenario") { scenario(serde_json::from_value(sc.take())?)? } else { cases(serde_json::from_value(input["cases"].take())?) }; println!("{output}"); Ok(())}