tayum 0.1.0

Generate random words based on a set of rules.
Documentation
use hashbrown::HashMap;
use rand::Rng;

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Hash)]
pub struct Character {
    pub char: char,
    pub category: Category,
    pub valid_behind: CharRule,
    pub push: CharPush,
    pub allowed_in_row: AllowInRow,
    pub weight: u8, // u8::MAX == NEVER REGENERATE | u8::MIN == ALWAYS REGENERATE
}

impl Character {
    pub fn new(
        character: char,
        category: Category,
        valid_behind: CharRule,
        push: CharPush,
        allowed_in_row: AllowInRow,
        weight: u8,
    ) -> Self {
        return Self {
            char: character,
            category,
            valid_behind,
            push,
            allowed_in_row,
            weight,
        };
    }
}

#[derive(Debug, Eq, PartialEq, Clone)]
pub struct CharMap {
    pub map: HashMap<char, Character>,
}

impl CharMap {
    pub fn default() -> Self {
        let mut map = HashMap::new();

        let list = vec![
            ('a', Category::Vowel, CharRule::Any, CharPush::None, AllowInRow::Single, u8::MAX - 3),
            ('e', Category::Vowel, CharRule::Category(Category::Consonant), CharPush::Single('a', Chance::BelowOrEquals(u8::MAX / 4)), AllowInRow::Double, u8::MAX),
            ('i', Category::Vowel, CharRule::Any, CharPush::Many(vec!['e', 'o', 'a'], Chance::BelowOrEquals(u8::MAX / 2), 1), AllowInRow::Single, u8::MAX - 9),
            ('o', Category::Vowel, CharRule::Category(Category::Consonant), CharPush::Many(vec!['u', 'i'], Chance::BelowOrEquals(u8::MAX / 3), 1), AllowInRow::Double, u8::MAX - 15),
            ('u', Category::Vowel, CharRule::Any, CharPush::Single('i', Chance::BelowOrEquals(u8::MAX / 4)), AllowInRow::Single, u8::MAX - 21),

            ('b', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 30),
            ('c', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::Single('h', Chance::Equals(u8::MAX / 2)), AllowInRow::Single, u8::MAX - 21),
            ('d', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 15),
            ('f', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 24),
            ('g', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 27),
            ('h', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Single, u8::MAX - 21),
            ('j', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 55),
            ('k', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Single, u8::MAX - 33),
            ('l', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 15),
            ('m', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 21),
            ('n', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 12),
            ('p', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 24),
            ('q', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::Single('u', Chance::Always), AllowInRow::Single, u8::MAX - 75),
            ('r', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 15),
            ('s', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 15),
            ('t', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 3),
            ('v', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Single, u8::MAX - 30),
            ('w', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Single, u8::MAX - 45),
            ('x', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Single, u8::MAX - 60),
            ('y', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Single, u8::MAX - 27),
            ('z', Category::Consonant, CharRule::Category(Category::Vowel), CharPush::None, AllowInRow::Double, u8::MAX - 75),
        ];

        for i in list {
            map.insert(i.0, Character::new(i.0, i.1, i.2, i.3, i.4, i.5));
        }

        return Self {
            map,
        };
    }
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash)]
pub enum Category {
    Consonant,
    Vowel,
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Hash)]
pub enum CharRule {
    Any,
    Category(Category),
    Single(char),
    Many(Vec<char>),
    Rules(Vec<CharRule>),
}

impl CharRule {
    pub fn hit(&self, character: &Character) -> bool {
        return match self {
            Self::Any => true,
            Self::Category(cat) => { *cat == character.category },
            Self::Single(c) => {*c == character.char},
            Self::Many(v) => { v.contains(&character.char) },
            Self::Rules(rules) => {
                let mut hits = false;

                for i in rules.iter() {
                    if i.hit(character) {
                        hits = true;
                        break;
                    }
                }

                hits
            },
        };
    }
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Hash)]
pub enum CharPush {
    None,
    Single(char, Chance),
    Many(Vec<char>, Chance, usize),
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash)]
pub enum Chance {
    Always,
    Equals(u8),
    BelowOrEquals(u8),
    AboveOrEquals(u8),
}

impl Chance {
    pub fn gen_num() -> u8 {
        return rand::thread_rng().gen_range(0..=u8::MAX);
    }

    pub fn hit(&self, check: u8) -> bool {
        return match self {
            Self::Always => true,
            Self::Equals(c) => { check == *c },
            Self::BelowOrEquals(c) => { check <= *c },
            Self::AboveOrEquals(c) => { check >= *c },
        };
    }
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash)]
pub enum AllowInRow {
    Single,
    Double,
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Hash)]
pub struct WordRules {
    max_vowels_in_row: u8,
}

impl WordRules {
    pub fn default() -> Self {
        return Self {
            max_vowels_in_row: 1,
        };
    }
}

pub fn generate_word(word_rules: &WordRules, char_map: &CharMap, keys: &[char], min_cycles: usize, max_cycles: usize) -> String {
    let cycles = rand::thread_rng().gen_range(min_cycles..=max_cycles);

    let mut word: Vec<char> = Vec::new();

    for _ in 0..cycles {
        let mut push_char: char = *choice(keys);

        if word.len() > 0 {
            let mut regenerate = true;

            while regenerate {
                regenerate = false;

                let last_char: char = word[word.len() - 1];
                let last_character = char_map.map.get(&last_char).unwrap();

                let character = char_map.map.get(&push_char).unwrap();

                if character.valid_behind.hit(last_character) == false {
                    regenerate = true;
                }

                if character.char == last_character.char {
                    if character.allowed_in_row == AllowInRow::Single {
                        regenerate = true;
                    }

                    else {
                        if word.len() > 1 {
                            if word[word.len() - 2] == character.char {
                                regenerate = true;
                            }
                        }
                    }
                }

                let check: u8 = rand::thread_rng().gen();
                if check > character.weight {
                    regenerate = true;
                }

                if character.category == Category::Vowel {
                    let mut in_row: u8 = 0;

                    let iter_num: u8;

                    if word.len() >= word_rules.max_vowels_in_row as usize {
                        iter_num = word_rules.max_vowels_in_row;
                    }

                    else {
                        iter_num = word.len() as u8;
                    }

                    for i in 1..=iter_num as usize {
                        if char_map.map.get(&word[word.len() - i]).unwrap().category == Category::Vowel {
                            in_row += 1;
                        }

                        else {
                            break;
                        }
                    }

                    if in_row >= word_rules.max_vowels_in_row {
                        regenerate = true;
                    }
                }

                if regenerate {
                    push_char = *choice(keys);
                }
            }
        }

        let character = char_map.map.get(&push_char).unwrap();

        let mut segment: Vec<char> = Vec::new();

        segment.push(push_char);

        match &character.push {
            CharPush::None => (),
            CharPush::Single(c, chance) => {
                let check = Chance::gen_num();

                if chance.hit(check) {
                    segment.push(*c);
                }
            },
            CharPush::Many(vc, chance, spin_cycles) => {
                for _ in 0..*spin_cycles {
                    let check = Chance::gen_num();

                    if chance.hit(check) {
                        segment.push(*choice(&vc));
                    }
                }
            },
        };

        for i in segment {
            word.push(i);
        }
    }

    return word.into_iter().map(|x| x.to_string()).collect::<Vec<String>>().concat();
}

fn choice<T>(array: &[T]) -> &T {
    return &array[rand::thread_rng().gen_range(0..array.len())];
}