1#![warn(clippy::pedantic)]
2#![cfg_attr(test, allow(clippy::unwrap_used))]
3
4mod rng_joiner;
5mod rng_syllable;
6mod rng_syllables;
7mod rng_weighted_rnd;
8
9#[macro_use]
10extern crate bitflags;
11extern crate log;
12
13#[derive(Debug, Eq, PartialEq)]
14pub enum RNGError {
15 GenerationError,
16 InvalidLanguageFile,
17 ParsingError,
18 ReadError,
19}
20
21use anyhow::Result;
22use rand::{
23 distr::{Distribution, StandardUniform},
24 prelude::*,
25};
26use rust_embed::RustEmbed;
27use std::fmt;
28use std::str::FromStr;
29use titlecase::titlecase;
30
31use crate::rng_syllable::{Classification, Syllable};
32use crate::rng_syllables::Syllables;
33use crate::rng_weighted_rnd::{NORMAL_WEIGHT, SHORT_WEIGHT};
34
35#[allow(clippy::upper_case_acronyms)]
50#[derive(Clone, Debug, PartialEq)]
51pub struct RNG {
52 pub name: String,
53 pub prefixes: Syllables,
54 pub centers: Syllables,
55 pub suffixes: Syllables,
56 pub bad_syllables: Vec<String>,
57}
58
59impl RNG {
60 pub fn new(language: &Language) -> Result<RNG, RNG> {
67 let Ok(rng) = RNG::process(language) else {
68 return Err(RNG::empty(language.to_string()));
69 };
70
71 if rng.is_valid() { Ok(rng) } else { Err(rng) }
72 }
73
74 pub fn new_from_file(filename: String) -> Result<RNG, RNGError> {
78 match std::fs::read_to_string(filename.clone()) {
79 Ok(f) => match std::str::from_utf8(f.as_ref()) {
80 Ok(s) => Ok(RNG::classify(s, filename)),
81 Err(_) => Err(RNGError::InvalidLanguageFile),
82 },
83 Err(_) => Err(RNGError::InvalidLanguageFile),
84 }
85 }
86
87 #[must_use]
92 pub fn random() -> RNG {
93 let my_dialect_type: Language = rand::random();
94 RNG::process(&my_dialect_type).expect("embedded language files are always present")
95 }
96
97 fn process(language: &Language) -> Result<RNG, RNGError> {
98 let txt =
99 Asset::get(language.get_filename().as_str()).ok_or(RNGError::InvalidLanguageFile)?;
100 let lines =
101 std::str::from_utf8(txt.data.as_ref()).map_err(|_| RNGError::InvalidLanguageFile)?;
102 Ok(RNG::classify(lines, language.to_string()))
103 }
104
105 fn classify(lines: &str, name: String) -> RNG {
106 let mut rng = RNG::empty(name);
107
108 for line in lines.lines() {
109 if let Ok(sy) = Syllable::from_str(line) {
110 match sy.classification {
111 Classification::Prefix => rng.prefixes.add(sy),
112 Classification::Center => rng.centers.add(sy),
113 Classification::Suffix => rng.suffixes.add(sy),
114 }
115 } else {
116 rng.bad_syllables.push(line.to_string());
117 }
118 }
119 rng
120 }
121
122 #[must_use]
123 pub fn empty(name: String) -> RNG {
124 RNG {
125 name,
126 prefixes: Syllables::new(),
127 centers: Syllables::new(),
128 suffixes: Syllables::new(),
129 bad_syllables: Vec::new(),
130 }
131 }
132
133 #[must_use]
134 pub fn is_empty(&self) -> bool {
135 self.name.is_empty()
136 && self.prefixes.is_empty()
137 && self.centers.is_empty()
138 && self.suffixes.is_empty()
139 && self.bad_syllables.is_empty()
140 }
141
142 #[must_use]
143 pub fn is_valid(&self) -> bool {
144 !self.name.is_empty()
145 && !self.prefixes.is_empty()
146 && !self.centers.is_empty()
147 && !self.suffixes.is_empty()
148 && self.bad_syllables.is_empty()
149 }
150
151 #[must_use]
152 pub fn generate_name(&self) -> String {
153 self.generate_name_by_count(NORMAL_WEIGHT.random())
154 }
155
156 #[must_use]
159 pub fn generate_names(&self, number: usize, is_short: bool) -> Vec<String> {
160 let mut v: Vec<String> = Vec::new();
161
162 for _ in 0..number {
163 if is_short {
164 v.push(self.generate_short());
165 } else {
166 v.push(self.generate_name());
167 }
168 }
169
170 v
171 }
172
173 #[must_use]
174 pub fn generate_names_string(&self, n: usize, is_short: bool) -> String {
175 self.generate_names(n, is_short).join(" ")
176 }
177
178 #[must_use]
179 pub fn generate_short(&self) -> String {
180 self.generate_name_by_count(SHORT_WEIGHT.random())
181 }
182
183 #[must_use]
184 pub fn generate_name_by_count(&self, count: u8) -> String {
185 let name = self.generate_syllables_by_count(count).collapse();
186 titlecase(name.as_str())
187 }
188
189 #[must_use]
190 pub fn generate_syllables(&self) -> Syllables {
191 self.generate_syllables_by_count(NORMAL_WEIGHT.random())
192 }
193
194 #[must_use]
199 pub fn generate_syllables_by_count(&self, mut syllable_count: u8) -> Syllables {
200 let mut syllables = Syllables::new();
201 let mut last = self
202 .prefixes
203 .get_random()
204 .expect("language has at least one prefix syllable")
205 .clone();
206 syllables.add(last.clone());
207
208 while syllable_count > 2 {
209 let center_syllables = self.centers.filter_from(last.jnext);
210 last = center_syllables
211 .get_random()
212 .expect("language has a center syllable compatible with the previous one")
213 .clone();
214 syllables.add(last.clone());
215 syllable_count -= 1;
216 }
217
218 let last_syllables = self.suffixes.filter_from(last.jnext);
219
220 syllables.add(
221 last_syllables
222 .get_random()
223 .expect("language has a suffix syllable compatible with the previous one")
224 .clone(),
225 );
226
227 syllables
228 }
229
230 #[must_use]
231 pub fn syllables(&self) -> Syllables {
232 let v = [
233 self.prefixes.all().clone(),
234 self.centers.all().clone(),
235 self.suffixes.all().clone(),
236 ]
237 .concat();
238 Syllables::new_from_vector(v)
239 }
240}
241
242impl TryFrom<&Language> for RNG {
243 type Error = RNGError;
244
245 fn try_from(language: &Language) -> Result<Self, Self::Error> {
246 RNG::process(language)
247 }
248}
249
250#[derive(RustEmbed)]
251#[folder = "src/languages/"]
252struct Asset;
253
254#[cfg(test)]
255#[allow(non_snake_case)]
256mod lib_tests {
257 use super::*;
258 use proptest::prelude::*;
259
260 #[test]
261 fn try_from() {
262 let rng = RNG::try_from(&Language::Fantasy).unwrap();
263
264 assert_eq!(rng.name, Language::Fantasy.to_string());
265 assert!(rng.bad_syllables.len() < 1);
266 assert!(rng.prefixes.len() > 0);
267 assert!(rng.centers.len() > 0);
268 assert!(rng.suffixes.len() > 0);
269 }
270
271 #[test]
272 fn try_from__demonic() {
273 let rng = RNG::new(&Language::Demonic).unwrap();
274
275 assert!(rng.bad_syllables.len() < 1);
276 assert!(rng.prefixes.len() > 0);
277 assert!(rng.centers.len() > 0);
278 assert!(rng.suffixes.len() > 0);
279 }
280
281 #[test]
282 fn try_from__goblin() {
283 let result = RNG::try_from(&Language::Goblin).unwrap();
284
285 assert_eq!(result.name, Language::Goblin.to_string());
286 assert!(result.bad_syllables.len() < 1);
287 assert!(result.prefixes.len() > 0);
288 assert!(result.centers.len() > 0);
289 assert!(result.suffixes.len() > 0);
290 }
291
292 #[test]
293 fn try_from__roman() {
294 let result = RNG::try_from(&Language::Roman).unwrap();
295
296 assert_eq!(result.name, Language::Roman.to_string());
297 assert!(result.bad_syllables.len() < 1);
298 assert!(result.prefixes.len() > 0);
299 assert!(result.centers.len() > 0);
300 assert!(result.suffixes.len() > 0);
301 }
302
303 #[test]
304 fn try_from__klingon() {
305 let result = RNG::try_from(&Language::Klingon).unwrap();
306
307 assert_eq!(result.name, Language::Klingon.to_string());
308 assert!(result.bad_syllables.len() < 1);
309 assert!(result.prefixes.len() > 0);
310 assert!(result.centers.len() > 0);
311 assert!(result.suffixes.len() > 0);
312 }
313
314 #[test]
315 fn try_from__german_curse() {
316 let result = RNG::try_from(&Language::GermanCurse).unwrap();
317
318 assert_eq!(result.name, Language::GermanCurse.to_string());
319 assert!(result.bad_syllables.len() < 1);
320 assert!(result.prefixes.len() > 0);
321 assert!(result.centers.len() > 0);
322 assert!(result.suffixes.len() > 0);
323 }
324
325 #[test]
326 fn try_from__fantasy_russian() {
327 let result = RNG::try_from(&Language::Фантазия).unwrap();
328
329 assert_eq!(result.name, Language::Фантазия.to_string());
330 assert!(result.bad_syllables.len() < 1);
331 assert!(result.prefixes.len() > 0);
332 assert!(result.centers.len() > 0);
333 assert!(result.suffixes.len() > 0);
334 }
335
336 #[test]
337 fn new_from_file() {
338 let filename = "src/languages/Test-micro.txt";
339
340 let rng = RNG::new_from_file(filename.to_string());
341 let result = rng.as_ref().unwrap();
342
343 assert!(!rng.is_err());
344 assert_eq!(result.name, filename.to_string());
345 assert_eq!(result.bad_syllables.len(), 0);
346 assert_eq!(result.prefixes.len(), 1);
347 assert_eq!(result.centers.len(), 1);
348 assert_eq!(result.suffixes.len(), 1);
349 }
350
351 #[test]
352 fn new_from_file__russian_goblin() {
353 let filename = "src/languages/Гоблин.txt";
354
355 let rng = RNG::new_from_file(filename.to_string());
356 let result = rng.as_ref().unwrap();
357
358 assert!(!rng.is_err());
359 assert_eq!(result.name, filename.to_string());
360 assert_eq!(result.bad_syllables.len(), 0);
361 assert_eq!(result.prefixes.len(), 19);
362 assert_eq!(result.centers.len(), 13);
363 assert_eq!(result.suffixes.len(), 16);
364 }
365
366 #[test]
367 fn new_from_file__with_error() {
368 let filename = "src/languages/none.txt";
369
370 let result = RNG::new_from_file(filename.to_string());
371
372 assert!(result.is_err());
373 }
374
375 #[test]
376 fn new_from_file__russian_fantasy() {
377 let filename = "src/languages/Фантазия.txt";
378
379 let rng = RNG::new_from_file(filename.to_string());
380 let result = rng.as_ref().unwrap();
381
382 assert!(!rng.is_err());
383 assert_eq!(result.name, filename.to_string());
384 assert_eq!(result.bad_syllables.len(), 0);
385 assert_eq!(result.prefixes.len(), 180);
386 assert_eq!(result.centers.len(), 157);
387 assert_eq!(result.suffixes.len(), 19);
388 }
389
390 #[test]
391 fn process_file__with_error() {
392 let filename = "src/languages/none.txt";
393
394 let result = RNG::new_from_file(filename.to_string());
395
396 assert!(result.is_err());
397 }
398
399 #[test]
400 fn classify() {
401 let raw = "-ваа +c\n-боо +c\n-гар\n-бар\n-дар\n-жар\n-вар\n-кра\n-гра\n-дра\n-зра\n-гоб\n-доб\n-роб\n-фоб\n-зоб\n-раг\n-наг\n-даг\nбра\nга\nда\nдо\nго\nзе\nша\nназ\nзуб\nзу\nна\nгор\nбу +c\n+быр\n+гыр\n+д";
402 let filename = "src/languages/goblinRU.txt".to_string();
403
404 let classified = RNG::classify(raw, filename.clone());
405
406 assert_eq!(classified.name, filename);
407 assert_eq!(classified.bad_syllables.len(), 0);
408 assert_eq!(classified.prefixes.len(), 19);
409 assert_eq!(classified.centers.len(), 13);
410 assert_eq!(classified.suffixes.len(), 3);
411 }
412
413 #[test]
414 fn classify__fantasy_russian() {
415 let raw = "-а +c\n-аб\n-ак\n-ац\n-ад\n-аф\n-ам\n-ан\n-ап\n-ар\n-ас\n-ат\n-ав\n-аз\n-аэль\n-аэл\n-ао\n-аэр\n-аш\n-арш +v";
416 let filename = "src/languages/goblinRU.txt".to_string();
417
418 let classified = RNG::classify(raw, filename.clone());
419
420 assert_eq!(classified.name, filename);
421 assert_eq!(classified.bad_syllables.len(), 0);
422 }
426
427 fn create_min() -> RNG {
428 RNG {
429 name: "Min".to_string(),
430 prefixes: Syllables::new_from_array(&["a"]),
431 centers: Syllables::new_from_array(&["b"]),
432 suffixes: Syllables::new_from_array(&["c"]),
433 bad_syllables: vec![],
434 }
435 }
436
437 #[test]
438 fn generate_name() {
439 let min = create_min();
440
441 let chain: Vec<String> = (1..10).map(|_| min.generate_name()).collect();
442
443 chain
444 .iter()
445 .for_each(|name| assert!(name.as_str().starts_with("A")));
446 chain
447 .iter()
448 .for_each(|name| assert!(name.as_str().ends_with("c")));
449 }
450
451 #[test]
452 fn generate_names() {
453 let rng = RNG::try_from(&Language::Roman).unwrap();
454
455 let names = rng.generate_names(5, true);
456
457 assert_eq!(names.len(), 5);
458 }
459
460 #[test]
461 fn generate_names_string() {
462 let rng = RNG::try_from(&Language::Demonic).unwrap();
463
464 let names = rng.generate_names_string(12, true);
465
466 assert_eq!(names.split_whitespace().count(), 12);
467 }
468
469 #[test]
470 fn generate_short() {
471 let min = create_min();
472
473 let chain: Vec<String> = (1..10).map(|_| min.generate_short()).collect();
474
475 chain
476 .iter()
477 .for_each(|name| assert!(name.as_str().starts_with("A")));
478 chain
479 .iter()
480 .for_each(|name| assert!(name.as_str().ends_with("c")));
481 }
482
483 #[test]
484 fn generate_name_by_count() {
485 let min = create_min();
486
487 let chain: Vec<String> = (1..10)
488 .map(|_| min.generate_name_by_count(NORMAL_WEIGHT.random()))
489 .collect();
490
491 chain
492 .iter()
493 .for_each(|name| assert!(name.as_str().starts_with("A")));
494 chain
495 .iter()
496 .for_each(|name| assert!(name.as_str().ends_with("c")));
497 }
498
499 #[test]
500 fn generate_syllables() {
501 let rng = RNG::try_from(&Language::Elven).unwrap();
502 let non: Vec<u8> = vec![0, 1, 6, 7, 8];
503
504 let chain: Vec<Syllables> = (1..10).map(|_| rng.generate_syllables()).collect();
505
506 chain
507 .iter()
508 .for_each(|i| assert!(NORMAL_WEIGHT.counts.contains(&(i.len() as u8))));
509 chain
510 .iter()
511 .for_each(|i| assert!(!non.contains(&(i.len() as u8))));
512 }
513
514 #[test]
515 fn is_empty() {
516 assert!(RNG::empty("".to_string()).is_empty());
517 assert!(!create_min().is_empty());
518 }
519
520 #[test]
521 fn is_valid() {
522 let min = create_min();
523 assert!(min.is_valid())
524 }
525
526 #[test]
527 fn is_valid__not() {
528 let bad = RNG {
529 name: "bad".to_string(),
530 prefixes: Syllables::new(),
531 centers: Syllables::new(),
532 suffixes: Syllables::new(),
533 bad_syllables: vec!["#$@!".to_string()],
534 };
535 assert!(!bad.is_valid())
536 }
537
538 #[test]
539 fn generate_syllables_by_count__elven_two() {
540 general_generate_dialects_asserts(Language::Elven, 2);
541 }
542
543 #[test]
544 fn generate_syllables_by_count__elven_three() {
545 general_generate_dialects_asserts(Language::Elven, 3);
546 }
547
548 #[test]
549 fn generate_syllables_by_count__elven_four() {
550 general_generate_dialects_asserts(Language::Elven, 4);
551 }
552
553 #[test]
554 fn generate_syllables_by_count__elven_five() {
555 general_generate_dialects_asserts(Language::Elven, 5);
556 }
557
558 #[test]
559 fn generate_syllables_by_count__fantasy_two() {
560 general_generate_dialects_asserts(Language::Fantasy, 2);
561 }
562
563 #[test]
564 fn generate_syllables_by_count__fantasy_three() {
565 general_generate_dialects_asserts(Language::Fantasy, 3);
566 }
567
568 #[test]
569 fn generate_syllables_by_count__fantasy_four() {
570 general_generate_dialects_asserts(Language::Fantasy, 4);
571 }
572
573 #[test]
574 fn generate_syllables_by_count__fantasy_five() {
575 general_generate_dialects_asserts(Language::Fantasy, 5);
576 }
577
578 #[test]
579 fn generate_syllables_by_count__goblin_two() {
580 general_generate_dialects_asserts(Language::Goblin, 2);
581 }
582
583 #[test]
584 fn generate_syllables_by_count__goblin_three() {
585 general_generate_dialects_asserts(Language::Goblin, 3);
586 }
587
588 #[test]
589 fn generate_syllables_by_count__goblin_four() {
590 general_generate_dialects_asserts(Language::Goblin, 4);
591 }
592
593 #[test]
594 fn generate_syllables_by_count__goblin_five() {
595 general_generate_dialects_asserts(Language::Goblin, 5);
596 }
597
598 #[test]
599 fn generate_syllables_by_count__roman_two() {
600 general_generate_dialects_asserts(Language::Roman, 2);
601 }
602
603 #[test]
604 fn generate_syllables_by_count__roman_three() {
605 general_generate_dialects_asserts(Language::Roman, 3);
606 }
607
608 #[test]
609 fn generate_syllables_by_count__roman_four() {
610 general_generate_dialects_asserts(Language::Roman, 4);
611 }
612
613 #[test]
614 fn generate_syllables_by_count__roman_five() {
615 general_generate_dialects_asserts(Language::Roman, 5);
616 }
617
618 fn general_generate_dialects_asserts(language: Language, count: usize) {
620 for _ in 0..9 {
621 let rng = RNG::try_from(&language).unwrap();
622
623 let name = rng.generate_syllables_by_count(count as u8);
624
625 general_generate_syllables_asserts(&rng, &name);
626 assert_eq!(name.len(), count);
627 }
628 }
629
630 fn general_generate_syllables_asserts(rng: &RNG, syllables: &Syllables) {
631 assert!(rng.prefixes.contains(syllables.first().unwrap()));
632 assert!(!rng.centers.contains(syllables.first().unwrap()));
633 assert!(!rng.suffixes.contains(syllables.first().unwrap()));
634
635 let count = syllables.len();
636 let mut guard = 1;
637 while guard < count - 1 {
638 guard += 1;
639 assert!(!rng.prefixes.contains(syllables.get(guard - 1).unwrap()));
640 assert!(rng.centers.contains(syllables.get(guard - 1).unwrap()));
641 assert!(!rng.suffixes.contains(syllables.get(guard - 1).unwrap()));
642 }
643
644 assert!(!rng.prefixes.contains(syllables.last().unwrap()));
645 assert!(!rng.centers.contains(syllables.last().unwrap()));
646 assert!(rng.suffixes.contains(syllables.last().unwrap()));
647 }
648 proptest! {
651 #[test]
652 fn test_gen_rnd_syllable_count(_ in 0..100i32) {
653 let count = NORMAL_WEIGHT.random();
654 assert!((count < 6) && (count > 1), "count of {} should be less than 6 and greater than 1", count);
655 }
656 }
657}
658
659#[derive(Clone, Debug, PartialEq)]
662pub enum Language {
663 Curse,
664 Demonic,
665 Elven,
666 Эльфийский,
667 Fantasy,
668 Фантазия,
669 GermanCurse,
670 Goblin,
671 Гоблин,
672 Klingon,
673 Roman,
674 Римский,
675}
676
677impl fmt::Display for Language {
678 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
679 match self {
680 Language::GermanCurse => write!(f, "German-curse"),
683 _ => write!(f, "{self:?}"),
684 }
685 }
686}
687
688impl Distribution<Language> for StandardUniform {
689 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Language {
690 match rng.random_range(0..5) {
691 0 => Language::Demonic,
692 1 => Language::Elven,
693 2 => Language::Fantasy,
694 3 => Language::Goblin,
695 _ => Language::Roman,
696 }
697 }
698}
699
700impl Language {
701 #[must_use]
702 pub fn get_filename(&self) -> String {
703 format!("{self}.txt")
704 }
705
706 #[must_use]
707 pub fn get_path(&self) -> String {
708 format!("./src/languages/{}", self.get_filename())
709 }
710}
711
712#[derive(Debug, Clone, PartialEq)]
714pub struct BadLanguage;
715
716impl fmt::Display for BadLanguage {
717 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
718 write!(f, "Invalid Language")
719 }
720}
721
722#[cfg(test)]
723#[allow(non_snake_case)]
724mod test_language {
725 use super::*;
726
727 #[test]
728 fn to_filename() {
729 assert_eq!(String::from("Elven.txt"), Language::Elven.get_filename());
730 }
731
732 #[test]
733 fn to_filename__german_curse() {
734 assert_eq!(
735 String::from("German-curse.txt"),
736 Language::GermanCurse.get_filename()
737 );
738 }
739
740 #[test]
741 fn to_string() {
742 assert_eq!(String::from("Elven"), Language::Elven.to_string());
743 }
744
745 #[test]
746 fn get_path() {
747 assert_eq!(
748 "./src/languages/Fantasy.txt".to_string(),
749 Language::Fantasy.get_path()
750 );
751 }
752}
753