The character chooser: the main screen in an edit mode, where she taps the character in the side panel, steps through the characters with an arrow each side, steps through the voices with an arrow each side of the voice's name, and the bottom buttons are a check (save) and a cross (throw it away and leave).
What each input means is rules, not code. [domain::RULES] are written on the shared rete engine, so
the network can be drawn (docs/chooser-rules.svg, held to the network that runs by a test) and every
transition leaves a structured trace line. [Chooser] holds only the state the rules act on and performs the
effects they decide: which character and voice are shown, which are saved. Nothing is saved until the check,
and the check saves exactly the shown character and its voice.
The shell is thin: it sends an [Input], draws the [View] it gets back, and does the [ShellEffect]s in
order (speak an [Utterance], keep a [ShellEffect::Save], leave).
13#![forbid(unsafe_code)]
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};
A line to speak, and the voice to speak it in.
The words are fixed (they depend on the profile and the voice's name only), so the speech cache may keep
them: say it with fixed set. Debug prints lengths only, because the words hold the child's name.
The chooser opened: show the main screen in its edit mode.
47 Enter,
A character was shown: say hello in its saved voice. Nothing is saved.
49 PreviewCharacter(Utterance),
A voice was shown: introduce it in that voice. Nothing is saved.
51 PreviewVoice(Utterance),
Keep this character and this voice for it (None: the base voice). Exactly these two, nothing else.
53 Save { character: CharacterId, voice: Option<VoiceId> },
The change was thrown away.
55 Discard,
What to draw.
The voice shown; None is the household's base voice.
67 pub voice: Option<VoiceId>,
The name under the character: the shown voice's, as the service names it, if it is listed.
What one input came to.
The trace of the step, one JSON line per event: rule names, fact names, counts. Never the child's name or what was said. The same lines are logged at info level.
82 pub trace: Vec<String>,
The chooser's state, and what acts on it.
The household's base voice, which shows when a character has none of its own.
101impl Chooser {
A chooser over what the household has saved and the voices the service lists (empty if it could not be 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 }
What is saved now: after a check, the shown character and voice.
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 }
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 }
The voice list as ids, when there is one.
The voice character is heard in when it changes: its saved one, resolved (never another's).
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}
The host the rules run against: supplies the facts the shell's state knows, and does each effect.
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;