Rust woodle match character count

In my last post, there was a small bug that needs to be corrected. Until now the game has not counted the number of unique letters that match so let's implement this.

In order to do so, we need to add a new iterator that will keep track of each letter in the word chosen by the game.

Lets implement a new function to count the unique letters in a word. For this we will use a HashMap to store the character and its count. We will iterate though each character in the word and if the character already exists in the HashMap we will increment its could by one, if it doesn't exist we add it to the map.

First we have to add the HashMap package.

use std::collections::HashMap;

Next lets use the HashMap to count the unique characters.

fn count_unique_characters(word: &str) -> HashMap<String, usize> {
    let mut char_count: HashMap<String, usize> = HashMap::new();
    for char in word.chars() {
        if char_count.contains_key(&String::from(char)) {
            char_count.insert(String::from(char), char_count[&String::from(char)] + 1);
        } else {
            char_count.insert(String::from(char), 1);
        }
    }

    return char_count;
}

Now we have to modify the check_word_correct function to check the character count as well. For this we will begin by counting the unique characters.

let chosen_word_count = count_unique_characters(&word);

Then we will change the for loop to only check if a character is in the correct spot. The logic of this is to make sure we have counted correct placed characters before we start assessing if there are characters left over that are out of place or if the player has entered to many of a specific character.

let mut current_word_count = HashMap::new();
for (i, c) in chosen_word.chars().enumerate() {
  let mut map = CharacterMap {
    character: c,
    value: CharState::Wrong,
  };
  if does_character_exist(c, word) && is_position_correct(c, i, word) {
    map.value = CharState::Correct;
    if current_word_count.contains_key(&String::from(c)) {
      current_word_count
        .insert(String::from(c), current_word_count[&String::from(c)] + 1);
    } else {
      current_word_count.insert(String::from(c), 1 as usize);
    }
  }
  state.push(map);
}

First we create a new HashMap to keep a record of the character count for the word entered by the user. and then we loop though the characters in the word and check if the character exists and if its placed correctly, if so we set its map value to correct and we do as we did when counting unique characters, we increment the count of the character in the new HashMap.

We now have a view of the current characters, now we will do a similar iteration to tract the misplaced characters.

for (i, c) in chosen_word.chars().enumerate() {
    let char = &String::from(c) as &str;
    if does_character_exist(c, game_word) {
        let chosen_char = game_word_count.get(&String::from(c)).unwrap();

        let mut current_char_count: &usize = &0;
        if current_word_count.contains_key(char) {
            current_char_count = current_word_count.get(char).unwrap();
        }
        let mut map = state.get_mut(i).unwrap();
        if !is_position_correct(c, i, game_word) && current_char_count < chosen_char {
            map.value = CharState::Exists;
            if current_word_count.contains_key(&String::from(c)) {
                current_word_count
                    .insert(String::from(c), current_word_count[&String::from(c)] + 1);
            } else {
                current_word_count.insert(String::from(c), 1 as usize);
            }
        }
    }
}

This gives us a representation of comparison between the two words that we can use to display the correctness of the players input.

So the new implementation of check_word_correct will look like this:

fn check_word_correct(game_word: &str, chosen_word: &str) -> Vec<CharacterMap> {
    let game_word_count = count_unique_characters(&game_word);
    let mut state: Vec<CharacterMap> = Vec::new();
    let mut current_word_count: HashMap<String, usize> = HashMap::new();

    for (i, c) in chosen_word.chars().enumerate() {
        let mut map = CharacterMap {
            character: c,
            value: CharState::Wrong,
        };
        if does_character_exist(c, game_word) && is_position_correct(c, i, game_word) {
            map.value = CharState::Correct;
            if current_word_count.contains_key(&String::from(c)) {
                current_word_count
                    .insert(String::from(c), current_word_count[&String::from(c)] + 1);
            } else {
                current_word_count.insert(String::from(c), 1 as usize);
            }
        }
        state.push(map);
    }

    for (i, c) in chosen_word.chars().enumerate() {
        let char = &String::from(c) as &str;
        if does_character_exist(c, game_word) {
            let chosen_char = game_word_count.get(&String::from(c)).unwrap();

            let mut current_char_count: &usize = &0;
            if current_word_count.contains_key(char) {
                current_char_count = current_word_count.get(char).unwrap();
            }
            let mut map = state.get_mut(i).unwrap();
            if !is_position_correct(c, i, game_word) && current_char_count < chosen_char {
                map.value = CharState::Exists;
                if current_word_count.contains_key(&String::from(c)) {
                    current_word_count
                        .insert(String::from(c), current_word_count[&String::from(c)] + 1);
                } else {
                    current_word_count.insert(String::from(c), 1 as usize);
                }
            }
        }
    }
    return state;
}

You can see the changes here

I also tried to use an iterator for the implementation but I failed to get it to work if identical characters where not next to each others. I have added the solution below for anyone who is interested, and who might be able to fix it.

In order to use the iterator, we will add the use std::iter::Peekable; package which allows us to iterate through a list step by step to act on each item in the list. You can find a description of this package here.

We then have to write a struct to manage our counter. In this struct, is our iterator and the iter is an optional reference to the next item in the list which we can have a look at.

struct SequentialCount<I>
where
    I: Iterator,
{
    iter: Peekable<I>,
}

We can then create an implementation of the SequentialCount as an Iterator. In this implementation, we will create a new function that returns self which is the SequentialCount struct.

impl<I> SequentialCount<I>
where
    I: Iterator,
{
    fn new(iter: I) -> Self {
        SequentialCount {
            iter: iter.peekable(),
        }
    }
}

And we then create the implementation that uses the SequentialCount Iterator to loop through items and counts how many unique items exist in the provided list of items.

impl<I> Iterator for SequentialCount<I>
where
    I: Iterator,
    I::Item: Eq,
{
    type Item = (I::Item, usize);

    fn next(&mut self) -> Option<Self::Item> {
        // Check the next value in the inner iterator
        match self.iter.next() {
            // There is a value, so keep it
            Some(head) => {
                // We've seen one value so far
                let mut count = 1;
                // Check to see what the next value is without
                // actually advancing the inner iterator
                while self.iter.peek() == Some(&head) {
                    // It's the same value, so go ahead and consume it
                    self.iter.next();
                    count += 1;
                }
                // The next element doesn't match the current value
                // complete this iteration
                Some((head, count))
            }
            // The inner iterator is complete, so we are also complete
            None => None,
        }
    }
}

In this implementation, we loop through the list of items, and for each item, we check if any of the next items in the list match the current item and we increment the list count if it does. When we are done peaking at the following values we advance the iterator to the next value.

We can then use this iterator to create a function that uses the SequenceCount to map out a Vector of the characters and their count.

fn count_unique_characters(word: &str) -> HashMap<String, usize> {
    let mut char_count: HashMap<String, usize> = HashMap::new();
    for (char, count) in SequentialCount::new(word.chars()) {
        char_count.insert(String::from(char), count);
    }
    return char_count;
}

We can then match the current character and its count to the Vector of the chosen word with each other and take this into account when estimating if a Character is still valid.

But as I said, I could not figure out why it didn't work with characters that where not right next to each others. I will have to understand a bit more about now to debug Rust and how the iterator works.

Which brings me to the next topic I would like to cover in this series. I will properly be looking at either creating a WASM interface for the game or I might dive into debugging and how to get a proper workflow up and running instead of doing println's to investigate values.