lib.rsannotatedlib.rssource281 lines · 11.1 KB · raw
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;