A character: who Whiskers is to her. An id, a look, and the voice the character owns.
The characters are a closed list, data in this file: the drawn cats (the household's looks, which
until now were CatTheme: the grey cat, the ginger cat and the bunny) and a small set of friends from the pixel icon pack. A [CharacterId] can name
only a character of the list, so a document cannot point at one that does not exist. The voice is not in
this list: it is a household setting per character (see limits), because the household chooses it.
The moods of a pack character (listening, thinking, speaking) are the app's to draw; this module says only who there is and in what order the child steps through them.
11use std::fmt;
The household's cat look that a drawn character is.
One of the drawn characters (the two cats and the bunny), with every mood.
34 Cat(CatTheme),
Which character: one of the list, never anything else.
Why a text is not a character.
47impl fmt::Display for NotACharacter { 48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 49 f.write_str("not a character of the list") 50 } 51} 52 53impl std::error::Error for NotACharacter {} 54 55impl fmt::Debug for CharacterId { 56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 57 write!(f, "CharacterId({})", self.0) 58 } 59} 60 61impl fmt::Display for CharacterId { 62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 f.write_str(self.0) 64 } 65} 66 67impl CharacterId {
The character called id.
The character the cat look names: what a household that never chose one has.
The next character, after the last the first.
The previous character, before the first the last.
Every character, in the order the chooser steps through them: the drawn characters, then the friends.
109impl Serialize for CharacterId { 110 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { 111 s.serialize_str(self.0) 112 } 113} 114 115impl<'de> Deserialize<'de> for CharacterId { 116 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> { 117 let text = String::deserialize(d)?; 118 CharacterId::new(&text).map_err(|_| serde::de::Error::custom(format!("{NotACharacter}"))) 119 } 120}
Why a character speaks in the household's base voice.
No voice was chosen for the character.
126 NoneChosen,
One was chosen, and the account no longer lists it.
128 NotListed,
The voice a character is heard in: its own, or else the base voice, and which and why. Never another character's voice: the only two answers are this character's own voice and the household's base, so a missing voice cannot be filled by lending one that belongs to someone else.
142impl VoiceToUse {
The voice for character, given the voice the household chose for it (if any) and the voices the service lists (None while
the list could not be read: the voice is then used and the service has the last word, see
[VoiceToUse::refused]). Saying so goes in the log, with ids only: the character, and a tag of the voice.
146 pub fn of(character: CharacterId, chosen: Option<&VoiceId>, listed: Option<&[VoiceId]>) -> VoiceToUse { 147 let Some(own) = chosen else { 148 ::log::debug!("{character} has no voice of its own: the base voice"); 149 return VoiceToUse::Base(BaseBecause::NoneChosen); 150 }; 151 match listed { 152 Some(list) if !list.contains(own) => { 153 ::log::warn!("{character}'s voice {} is not listed by the service: speaking in the base voice", own.tag()); 154 VoiceToUse::Base(BaseBecause::NotListed) 155 } 156 _ => VoiceToUse::Own(own.clone()), 157 } 158 }
The service refused to speak in character's own voice: the base voice, and the log says so.
167#[cfg(test)] 168mod tests { 169 use super::*; 170 171 #[test] 172 fn every_pack_character_is_an_admitted_icon_and_no_id_repeats() { 173 for p in whiskers_art::PACK { 174 assert!(IconId::new(p.id).is_ok(), "{} is not an admitted icon", p.id); 175 } 176 let all: Vec<_> = ids().collect(); 177 let mut names: Vec<_> = all.iter().map(|c| c.as_str()).collect(); 178 names.sort_unstable(); 179 names.dedup(); 180 assert_eq!(names.len(), all.len(), "an id is one character"); 181 for c in &all { 182 let _ = c.look(); 183 } 184 } 185 186 #[test] 187 fn the_looks_are_the_three_drawn_characters_in_order_before_the_pack() { 188 assert_eq!(CharacterId::of_theme(CatTheme::Grey).look(), Look::Cat(CatTheme::Grey)); 189 assert_eq!(CharacterId::of_theme(CatTheme::Ginger).look(), Look::Cat(CatTheme::Ginger)); 190 assert_eq!(CharacterId::of_theme(CatTheme::Bunny).look(), Look::Cat(CatTheme::Bunny)); 191 let first: Vec<_> = ids().take(4).map(|c| c.as_str()).collect(); 192 assert_eq!(first, ["grey-cat", "ginger-cat", "bunny", "panda"]); 193 for theme in CatTheme::ALL { 194 assert!(matches!(CharacterId::of_theme(theme).look(), Look::Cat(t) if t == theme)); 195 } 196 assert_eq!(ids().next(), Some(CharacterId::of_theme(CatTheme::Grey)), "the household's default is first"); 197 } 198 199 #[test] 200 fn stepping_wraps_around_both_ways() { 201 let all: Vec<_> = ids().collect(); 202 assert_eq!(all.last().unwrap().next(), all[0]); 203 assert_eq!(all[0].previous(), *all.last().unwrap()); 204 for c in &all { 205 assert_eq!(c.next().previous(), *c); 206 assert_eq!(c.previous().next(), *c); 207 } 208 let mut c = all[0]; 209 for _ in 0..all.len() { 210 c = c.next(); 211 } 212 assert_eq!(c, all[0], "a full circle comes back"); 213 } 214 215 #[test] 216 fn a_document_cannot_name_a_character_that_is_not_there() { 217 assert!(serde_json::from_str::<CharacterId>("\"panda\"").is_ok()); 218 assert!(serde_json::from_str::<CharacterId>("\"beer\"").is_err()); 219 assert_eq!(serde_json::to_string(&CharacterId::new("fox").unwrap()).unwrap(), "\"fox\""); 220 assert!(CharacterId::new("").is_err()); 221 } 222 223 #[test] 224 fn a_character_without_its_voice_speaks_in_the_base_voice_and_never_in_another_characters() { 225 let (panda, fox) = (CharacterId::new("panda").unwrap(), CharacterId::new("fox").unwrap()); 226 let mut config = crate::limits::HouseholdConfig::default(); 227 config.character_voices.insert(panda, VoiceId::new("vp").unwrap()); 228 assert_eq!(VoiceToUse::of(panda, config.voice_of(panda), None), VoiceToUse::Own(VoiceId::new("vp").unwrap()), "list unknown: its own, the service decides"); 229 let listed = [VoiceId::new("vp").unwrap(), VoiceId::new("other").unwrap()]; 230 assert_eq!(VoiceToUse::of(panda, config.voice_of(panda), Some(&listed)), VoiceToUse::Own(VoiceId::new("vp").unwrap())); 231 assert_eq!(VoiceToUse::of(fox, config.voice_of(fox), Some(&listed)), VoiceToUse::Base(BaseBecause::NoneChosen), "fox is not lent panda's voice"); 232 assert_eq!(VoiceToUse::of(panda, config.voice_of(panda), Some(&[VoiceId::new("other").unwrap()])), VoiceToUse::Base(BaseBecause::NotListed)); 233 assert_eq!(VoiceToUse::refused(panda, &VoiceId::new("vp").unwrap()), VoiceToUse::Base(BaseBecause::Refused)); 234 } 235}