lib.rsannotatedlib.rssource281 lines · 11.1 KB · raw

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.

30#[derive(Clone, PartialEq, Eq)]
31pub struct Utterance {
32    pub text: String,

The voice to speak in; None is the household's base voice.

34    pub voice: Option<VoiceId>,
35}
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}

What the shell does, in order.

44#[derive(Clone, Debug, PartialEq, Eq)]
45pub enum ShellEffect {

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,

The chooser closed: back to the normal screen.

57    Leave,
58}

What to draw.

61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct View {
63    pub editing: bool,
64    pub character: CharacterId,
65    pub look: Look,

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.

69    pub voice_name: Option<String>,
70    pub unsaved: bool,

Whether the voice arrows have somewhere to go.

72    pub voices_to_step: bool,
73}

What one input came to.

76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct Output {
78    pub view: View,
79    pub effects: Vec<ShellEffect>,

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>,

Why the input did nothing, when it did nothing.

84    pub notes: Vec<Note>,
85}

The chooser's state, and what acts on it.

88pub struct Chooser {
89    audience: Audience,
90    network: Network<domain::ChooserDomain>,
91    voices: Vec<VoiceEntry>,

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}
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.

116    pub fn saved(&self) -> (CharacterId, &BTreeMap<CharacterId, VoiceId>) {
117        (self.saved_character, &self.saved_voices)
118    }
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.

170    fn listed(&self) -> Option<Vec<VoiceId>> {
171        (!self.voices.is_empty()).then(|| self.voices.iter().map(|v| v.id.clone()).collect())
172    }

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    }
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.

200struct Machine<'a> {
201    state: &'a mut Chooser,
202    input: Input,
203    effects: Vec<ShellEffect>,
204}
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;