1//! The character chooser: the main screen in an edit mode, where she taps the character in the side panel, steps 2//! through the characters with an arrow each side, steps through the voices with an arrow each side of the 3//! voice's name, and the bottom buttons are a check (save) and a cross (throw it away and leave). 4//! 5//! **What each input means is rules, not code.** [`domain::RULES`] are written on the shared `rete` engine, so 6//! the network can be drawn (`docs/chooser-rules.svg`, held to the network that runs by a test) and every 7//! transition leaves a structured trace line. [`Chooser`] holds only the state the rules act on and performs the 8//! effects they decide: which character and voice are shown, which are saved. Nothing is saved until the check, 9//! and the check saves exactly the shown character and its voice. 10//! 11//! The shell is thin: it sends an [`Input`], draws the [`View`] it gets back, and does the [`ShellEffect`]s in 12//! order (speak an [`Utterance`], keep a [`ShellEffect::Save`], leave). 13#![forbid(unsafe_code)] 14 15pub mod domain; 16 17use std::collections::BTreeMap; 18 19use ::log::{debug, info, warn}; 20use rete::{Host, Known, Network, Outcome, Then}; 21use whiskers_core::wire::VoiceEntry; 22use whiskers_core::{Audience, CharacterId, Look, VoiceId, VoiceToUse, hi_line, voice_intro_line}; 23 24pub use domain::{ChooserDomain, Effect, Fact, Input, Note, RULES, Step, Value, network}; 25 26/// A line to speak, and the voice to speak it in. 27/// 28/// The words are fixed (they depend on the profile and the voice's name only), so the speech cache may keep 29/// them: say it with `fixed` set. `Debug` prints lengths only, because the words hold the child's name. 30#[derive(Clone, PartialEq, Eq)] 31pub struct Utterance { 32 pub text: String, 33 /// The voice to speak in; `None` is the household's base voice. 34 pub voice: Option<VoiceId>, 35} 36 37impl std::fmt::Debug for Utterance { 38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 write!(f, "Utterance({} chars, {})", self.text.chars().count(), self.voice.as_ref().map_or("base voice".to_owned(), VoiceId::tag)) 40 } 41} 42 43/// What the shell does, in order. 44#[derive(Clone, Debug, PartialEq, Eq)] 45pub enum ShellEffect { 46 /// The chooser opened: show the main screen in its edit mode. 47 Enter, 48 /// A character was shown: say hello in its saved voice. Nothing is saved. 49 PreviewCharacter(Utterance), 50 /// A voice was shown: introduce it in that voice. Nothing is saved. 51 PreviewVoice(Utterance), 52 /// Keep this character and this voice for it (`None`: the base voice). Exactly these two, nothing else. 53 Save { character: CharacterId, voice: Option<VoiceId> }, 54 /// The change was thrown away. 55 Discard, 56 /// The chooser closed: back to the normal screen. 57 Leave, 58} 59 60/// What to draw. 61#[derive(Clone, Debug, PartialEq, Eq)] 62pub struct View { 63 pub editing: bool, 64 pub character: CharacterId, 65 pub look: Look, 66 /// The voice shown; `None` is the household's base voice. 67 pub voice: Option<VoiceId>, 68 /// The name under the character: the shown voice's, as the service names it, if it is listed. 69 pub voice_name: Option<String>, 70 pub unsaved: bool, 71 /// Whether the voice arrows have somewhere to go. 72 pub voices_to_step: bool, 73} 74 75/// What one input came to. 76#[derive(Clone, Debug, PartialEq, Eq)] 77pub struct Output { 78 pub view: View, 79 pub effects: Vec<ShellEffect>, 80 /// The trace of the step, one JSON line per event: rule names, fact names, counts. Never the child's name or 81 /// what was said. The same lines are logged at info level. 82 pub trace: Vec<String>, 83 /// Why the input did nothing, when it did nothing. 84 pub notes: Vec<Note>, 85} 86 87/// The chooser's state, and what acts on it. 88pub struct Chooser { 89 audience: Audience, 90 network: Network<domain::ChooserDomain>, 91 voices: Vec<VoiceEntry>, 92 /// The household's base voice, which shows when a character has none of its own. 93 base: Option<VoiceId>, 94 saved_character: CharacterId, 95 saved_voices: BTreeMap<CharacterId, VoiceId>, 96 editing: bool, 97 character: CharacterId, 98 voice: Option<VoiceId>, 99} 100 101impl Chooser { 102 /// A chooser over what the household has saved and the voices the service lists (empty if it could not be 103 /// asked: then the voice arrows do nothing and she keeps the voice she has). 104 pub fn new( 105 audience: Audience, 106 saved_character: CharacterId, 107 saved_voices: BTreeMap<CharacterId, VoiceId>, 108 voices: Vec<VoiceEntry>, 109 base: Option<VoiceId>, 110 ) -> Self { 111 let voice = saved_voices.get(&saved_character).cloned(); 112 Self { audience, network: network(), voices, base, saved_character, saved_voices, editing: false, character: saved_character, voice } 113 } 114 115 /// What is saved now: after a check, the shown character and voice. 116 pub fn saved(&self) -> (CharacterId, &BTreeMap<CharacterId, VoiceId>) { 117 (self.saved_character, &self.saved_voices) 118 } 119 120 pub fn view(&self) -> View { 121 let shown = self.voice.as_ref().or(self.base.as_ref()); 122 View { 123 editing: self.editing, 124 character: self.character, 125 look: self.character.look(), 126 voice: self.voice.clone(), 127 voice_name: shown.and_then(|id| self.voices.iter().find(|v| &v.id == id)).map(|v| v.name.clone()), 128 unsaved: self.unsaved(), 129 voices_to_step: !self.voices.is_empty(), 130 } 131 } 132 133 fn unsaved(&self) -> bool { 134 self.character != self.saved_character || self.voice.as_ref() != self.saved_voices.get(&self.character) 135 } 136 137 /// Does what `input` means, by the rules, and says what to draw and do. 138 pub fn input(&mut self, input: Input) -> Output { 139 let mut host = Machine { state: self, input, effects: Vec::new() }; 140 let network = host.state.network.clone(); 141 let run = network.run(Known::default(), &mut host); 142 let effects = host.effects; 143 let notes: Vec<Note> = network 144 .held(&run.known) 145 .filter_map(|terminal| match terminal.then { 146 Then::Note(note) => Some(note), 147 _ => None, 148 }) 149 .collect(); 150 let trace: Vec<String> = run.trace.records().iter().filter_map(|r| serde_json::to_string(r).ok()).collect(); 151 for line in &trace { 152 info!("chooser {line}"); 153 } 154 if let Outcome::Stuck(facts) = &run.outcome { 155 warn!("chooser: the rules were stuck on {} fact(s); the input did nothing more", facts.len()); 156 } 157 let view = self.view(); 158 info!( 159 "chooser: input {input:?}: {} effect(s), {} note(s), character {}, voice {}, unsaved {}", 160 effects.len(), 161 notes.len(), 162 view.character, 163 view.voice.as_ref().map_or("base".to_owned(), VoiceId::tag), 164 view.unsaved 165 ); 166 Output { view, effects, trace, notes } 167 } 168 169 /// The voice list as ids, when there is one. 170 fn listed(&self) -> Option<Vec<VoiceId>> { 171 (!self.voices.is_empty()).then(|| self.voices.iter().map(|v| v.id.clone()).collect()) 172 } 173 174 /// The voice `character` is heard in when it changes: its saved one, resolved (never another's). 175 fn heard_in(&self, character: CharacterId) -> Option<VoiceId> { 176 match VoiceToUse::of(character, self.saved_voices.get(&character), self.listed().as_deref()) { 177 VoiceToUse::Own(id) => Some(id), 178 VoiceToUse::Base(_) => None, 179 } 180 } 181 182 fn step_voice(&mut self, step: Step) { 183 let n = self.voices.len(); 184 if n == 0 { 185 return; 186 } 187 let current = self.voice.as_ref().or(self.base.as_ref()); 188 let at = current.and_then(|id| self.voices.iter().position(|v| &v.id == id)); 189 let to = match (at, step) { 190 (Some(p), Step::Next) => (p + 1) % n, 191 (Some(p), Step::Previous) => (p + n - 1) % n, 192 (None, Step::Next) => 0, 193 (None, Step::Previous) => n - 1, 194 }; 195 self.voice = Some(self.voices[to].id.clone()); 196 } 197} 198 199/// The host the rules run against: supplies the facts the shell's state knows, and does each effect. 200struct Machine<'a> { 201 state: &'a mut Chooser, 202 input: Input, 203 effects: Vec<ShellEffect>, 204} 205 206fn yes_no(b: bool) -> Value { 207 if b { Value::Yes } else { Value::No } 208} 209 210impl Host<domain::ChooserDomain> for Machine<'_> { 211 fn ask(&mut self, facts: &[Fact]) -> Vec<(Fact, Value)> { 212 facts 213 .iter() 214 .filter_map(|fact| { 215 Some(( 216 *fact, 217 match fact { 218 Fact::Input => self.input.value(), 219 Fact::Editing => yes_no(self.state.editing), 220 Fact::Unsaved => yes_no(self.state.unsaved()), 221 Fact::Voices => yes_no(!self.state.voices.is_empty()), 222 _ => return None, 223 }, 224 )) 225 }) 226 .collect() 227 } 228 229 fn perform(&mut self, effect: Effect) -> Vec<(Fact, Value)> { 230 let s = &mut *self.state; 231 match effect { 232 Effect::Enter => { 233 s.editing = true; 234 s.character = s.saved_character; 235 s.voice = s.saved_voices.get(&s.character).cloned(); 236 self.effects.push(ShellEffect::Enter); 237 } 238 Effect::StepCharacter(step) => { 239 s.character = match step { 240 Step::Next => s.character.next(), 241 Step::Previous => s.character.previous(), 242 }; 243 // The new character shows the voice saved for it, not the one that was being previewed. 244 s.voice = s.saved_voices.get(&s.character).cloned(); 245 debug!("chooser: showing {}", s.character); 246 } 247 Effect::StepVoice(step) => s.step_voice(step), 248 Effect::SayHi => { 249 let text = hi_line(&s.audience); 250 let voice = s.heard_in(s.character); 251 self.effects.push(ShellEffect::PreviewCharacter(Utterance { text, voice })); 252 } 253 Effect::SayVoice => { 254 let name = s.view().voice_name; 255 let text = voice_intro_line(&s.audience, name.as_deref()); 256 self.effects.push(ShellEffect::PreviewVoice(Utterance { text, voice: s.voice.clone() })); 257 } 258 Effect::Save => { 259 s.saved_character = s.character; 260 match s.voice.clone() { 261 Some(v) => s.saved_voices.insert(s.character, v), 262 None => s.saved_voices.remove(&s.character), 263 }; 264 self.effects.push(ShellEffect::Save { character: s.character, voice: s.voice.clone() }); 265 } 266 Effect::Discard => { 267 s.character = s.saved_character; 268 s.voice = s.saved_voices.get(&s.character).cloned(); 269 self.effects.push(ShellEffect::Discard); 270 } 271 Effect::Leave => { 272 s.editing = false; 273 self.effects.push(ShellEffect::Leave); 274 } 275 } 276 vec![(<domain::ChooserDomain as rete::Domain>::teaches(effect), Value::Yes)] 277 } 278} 279 280#[cfg(test)] 281mod tests;