How to improve a checking algorithm for a copycat "WORDLE" game?

130 Views Asked by At

I'm working on a project to copycat a game called 'TERMO', a portuguese version of 'WORDLE'. The biggest problem I've encountered so far is outputting yellow letters(right letter but in a wrong position). Keeping it simple, I know this problem can be solved with another for, however it´s really bugging me if there is other options without overlaping for's.

If there aren't any solutions, I'll be glad to know.


selectedWord = "MOLDO" word = "OMODO"

What I was expecting my output to be:

O(yellow) M(yellow) O(normal) D(green) O(green)

What I'm getting as output:

O(yellow) M(yellow) O(yellow) D(green) O(green)

string checkWord(string word, string selectedWord){
    // 0 for green
    // 1 for yellow
    // 2 for normal

    // string assuming "normal" for all letters
    string result = {'2','2','2','2','2'};
    
    

    for(int i = 0; i < 5; ++i){
        char letter = word[i];

        //check if it's green
        if(letter == selectedWord[i]){
            result[i] = '0';
            continue;
        }

        //check if it's yellow
        bool willBeGreen = false;
        for(int k = 0; k < 5; ++k){
            //Check if will be green
            if(word[i] == selectedWord[k] && word[k] == letter){
                willBeGreen = true;
                break;
            }
            if(letter == selectedWord[k] && k != i && result[k] != '0'){
                result[i] = '1';
                // break;
            }
        }
        if(willBeGreen){
            result[i] = '2';
        }
    }


    return result;
    
}
2

There are 2 best solutions below

3
jwezorek On BEST ANSWER

EDIT: Actually my previous version of this was wrong...

You can't determine if a guessed letter is yellow without knowing which letters are going to be green; that is, you do need two passes. In the following I do the first pass via a range pipeline.

In the second pass, you need to keep track of the hidden letters that will not ultimately be green in the output and that have not been "used" yet by a yellow letter. In the following I use a multiset of letters to keep track. Output is like YY-GG for yellow, yellow, gray, green, green.

(this is C++23 because of the use of std::ranges::to<T> and zip but illustrates the idea)

#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <sstream>
#include <set>

namespace r = std::ranges;
namespace rv = std::ranges::views;

std::string score_wordle_guess(const std::string& guess, const std::string& hidden) {

    // "leftover_letters" are all the letters in the hidden word,
    // including duplications, minus those that will be green in 
    // the output...

    auto leftover_letters = rv::zip(guess, hidden) | rv::filter(
            [](auto pair) { return std::get<0>(pair) != std::get<1>(pair); }
        ) |  rv::transform(
            [](auto pair)->char {return std::get<1>(pair); }
        ) | r::to<std::multiset<char>>();

    std::stringstream scored_guess;
    for (auto [guess_char, hidden_char] : rv::zip(guess, hidden)) {
        char color = '-';
        if (guess_char == hidden_char) {
            color = 'G';
        } else if (leftover_letters.contains(guess_char)) {
            color = 'Y';
            leftover_letters.erase(leftover_letters.find(guess_char));
        }
        scored_guess << color;
    }
    return scored_guess.str();
}

void test(const std::string& guess, const std::string& hidden) {
    std::cout << "guess: " << guess << " + hidden: " << hidden
        << " => " << score_wordle_guess(guess, hidden) << "\n";
}

int main()
{
    test("omodo", "moldo"); // => "YY-GG"
    test("fffff", "staff"); // => "---GG"
    test("fffof", "staff"); // => "Y---G"
}
0
Yonatan Alon On

C++ is not a language I am fluent in so please forgive me for any syntax errors. The principles, however, should be the same.

First, let's define some constants:

enum Color {grey, yellow, green}
int word_length = 5;
map<char, int> wordBag = buildWordBag(string targetWord);

We then build a word bag once, when we generate the target word. For every letter in that word, we count how many times it appears in that word:

map<char, int> wordBag buildWordBag(string targetWord) {
    map<char, int> wordBag = new map<>();
    
    for (char letter : targetWord) {
        if (!wordBag.contains(letter)) {
            wordBag[letter] = 0;
        }
        
        wordBag[letter] = wordBag[letter] + 1;
    }
    
    return wordBag;
}

Every time the user guesses a word, we also input the word bag to the function:

string checkWord(string targetWord, map<char, int> wordBag, string inputWord) {
    Color[] guessResult = new Color[word_length]; 

    for (int index=0; index < word_length; index++) {
        char inputLetter = inputWord[index];
        char targetLetter = targetWord[index];
        
        if (inputLetter == targetLetter) {
            guessResult[index] = Color.green;
            wordBag[letter] = wordBag[letter] - 1;
        } else if (!wordBag.contains(inputLetter)) {
            guessResult[index] = Color.grey;
        } else if (wordBag[letter] <= 0) {
            guessResult[index] = Color.grey;
        } else {
            guessResult[index] = Color.yellow;
            wordBag[letter] = wordBag[letter] - 1;
        }
    }
    
    return guessResult;
}

Complexity:

  • buildWordBag() has a time-space complexity of O(n)
  • checkWord() has a time-space complexity of O(n)

Caveats:

  • checkWord() modifies wordBag in place. You might want to consider defining wordBag outside the function and referencing it, instead of providing it as an argument.