//! 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, steps: Vec, } #[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, 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) -> 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 { 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(()) }