Jerome
Jerome

Reputation: 874

Why is my Wordle solver failing when there are repeated characters?

For context Wordle is game where you have to decipher a 5 letter word in 6 or less guesses based on certain hints. The hints you get are as follows:

  1. If a character is coloured black there are no characters that match that character in the target word.
  2. If a character is coloured orange there is a character that matches that character in the target word but it is in a different position.
  3. If a character is coloured green the position and the character are a match.

I am making a wordle solver program that takes in an array of word attempts and eliminates them from a list of the possible words.

I feel that the best algorithm to solve this problem is a black list where a word that breaks one of the rules is eliminated from the array. But if there is a better alternative I am open to suggestion.


const text =
[
  [
    ["N","black"],["i","black"],["g","black"],
    ["h","black"],["t","green"]
  ],
  [
    ["b","black"],["e","black"],["l","orange"],
    ["o","orange"],["w","black"]
  ]
]

const words = "dozen,brave,apple,climb,outer,pitch,ruler,holds,fixed,costs,calls, ...etc"

const solver = (text: any) => {
    this.resultingWords = words.split(",").filter(word => {
      word = word.toUpperCase()
      for (var i = 0; i < text.length; i++) {
        for (var j = 0; j < 5; j++) {
          let currentText = text[i][j]
          currentText[0] = currentText[0].toUpperCase()
          if (currentText[0] == '') { continue }
          if (currentText[1] == "green" && (word[j] != currentText[0])) {
            return false
          }
          if (currentText[1] == "black" && word.includes(currentText[0])) {
            return false;
          }
          if (currentText[1] == "orange" &&
          (word[j] == currentText[0] || !word.includes(currentText[0]))) {
            return false
          }
        }
      }
      return true
    })
  }

The issue I am having is if a word has multiple of the same letter and one of them is a green or orange match but the other is black. I get no results because of the way I've written my algorithm.

What would be the way to correctly solve this problem?

Is a black list style of filtering the best solution? (as opposed to white list).

Upvotes: 3

Views: 830

Answers (2)

maraca
maraca

Reputation: 8773

You are building a list of candidates, what is a good start I think. It doesn't really matter whether you white- or blacklist the result is the list of candidates. The only concern is that you could get the solution faster or more reliably by guessing words that are not on the candidates list. Why? Because that way you can introduce more new letters at once to check if the word contains them. Maybe a mix between the two strategies is best, hard to tell without testing it first.

  • "green" is OK.
  • "black" needs to count the number of non-black appearances of the letter in the guess and all words that not contain that exact amount of that letter can be eliminated (and also those that have the letter at a black position).
  • "orange" is OK but can be improved: you can count the number of non-black appearances of the letter in the guess and eliminate all words that contain the letter fewer times (checking for minimal appearance and not just at least once) and also what you already have applies: the letter cannot be at an orange position.

There are a lot of ideas for improvements. First I would create the filter before going through the words. Using similar logic as above you would get a collection of four different rule types: A letter has to be or cannot be at a specific position or a letter has to appear exactly (possibly 0) or at least a specific number of times. Then you go through the words and filter using those rules. Otherwise some work might be done multiple times. It is easiest to create such a filter by collecting the same letters in the guess first. If there is an exact number of appearence rule then you can obviously drop a minimal number of appearance rule for the same letter.

To guess the word fast I would create an evaluation function to find the most promising next guess among the candidates. Possible values to score:

  • How many new letters are introduced (letters that were not guessed yet).
  • Also the probabilites of the new letters could be taken into account. E.g. how likely is it that a word contains a specific letter. Or even look at correlations between letters: like if I have a Q then I probably also have a U or if the last letter is D then the likelyhood of the 2nd last being E is very high.
  • You could even go through all the possible answers for every candidate and see which guess eliminates the most words on average or something similar. Although this probably takes too long unless somehow approximated.

Upvotes: 1

trincot
trincot

Reputation: 350841

You need to match pairs of letters (one from the guess, one from the secret word) and strike them out, so they don't serve for any other match.

You cannot conclude from a "black" that the corresponding letter is not occurring at all in the solution. When the same letter was used twice in the guess, one may get a match (orange or green), while the other may get black.

I would suggest to manage the number of occurrences in the targeted word. A black indication means you know the maximum number of occurrences of a certain letter (which may be 0). When you get a green/orange indication, you know about a minimum number of occurrences (possibly updating the maximum so it is not inconsistent).

I made a little implementation with the following characteristics:

  • A Game class manages the choice of the secret word; giving feedback on a guess; and giving an indication on whether the game is over (after 6 guesses or a correct guess)

  • A play function which chooses a word based on previous feedback from the Game instance, and submits it as a guess, and repeats the process until the game is over.

  • The play logic will always make a guess (randomly) that is in line with all the feedback received during the game. NB: This is not the optimal strategy. For instance, 4 letters out of 5 may be green, but then many attempts may be needed to find that remaining character.

  • The play logic builds a regular expression from all the feedback it got, and only filters words with that regex.

Here is the code:

class Game {
    #solution; // Private
    constructor(words) {
        this.#solution = words[Math.floor(Math.random() * words.length)];
        this.turnCount = 0;
        this.state = "playing";
    }
    guess(guessed) {
        if (guessed.length !== 5) throw "Invalid length";
        if (this.state !== "playing") throw "Game is already over";
        this.turnCount++;
        let attempt = [...guessed];
        let correct = [...this.#solution];
        attempt.forEach((ch, i) => {
            if (correct[i] === ch) correct[i] = attempt[i] = null;
        });
        this.state = !attempt.some(Boolean) ? "won" : this.turnCount >= 6 ? "lost" : "playing";
        return attempt.map((ch, i) => {
            if (ch == null) return [guessed[i], "green"];
            let j = correct.indexOf(ch);
            if (j >= 0) {
                correct[j] = null;
                return [ch, "orange"];
            }
            return [ch, "black"];
        });
    }
}

function play(words) {
    let game = new Game(words);
    let allowed = Array(5).fill("abcdefghijklmnopqrstuvwxyz");
    let letters = {}; // Letters with their minimum and maximum frequency
    while (game.state === "playing") {
        let regex = RegExp(Object.entries(letters).map(([ch, {min,max}]) =>
            max ? `(?=^[^${ch}]*(?:${ch}[^${ch}]*){${min},${max}}$)`
                : `(?!.*${ch})`
        ).join("") + allowed.map(s => "[" + s + "]").join(""), "i");
        words = words.filter(word => regex.test(word));
        // Pick a random word from the list of potential candidates
        let word = words[Math.floor(Math.random() * words.length)];
        let result = game.guess(word);
        console.log(words.length + " options. Guessing: " + word + " - feedback: " + result.map(([ch, color]) => color[0]).join(""));
        letters = {}; // This always starts from scratch
        result.forEach(([ch, color], i) => {
            if (letters[ch]) {
                letters[ch].max = color === "black" ? letters[ch].min : Math.max(++letters[ch].min, letters[ch].max);
            } else {
                letters[ch] = color === "black" ? { min: 0, max: 0 } : { min: 1, max: 5 };
            }
            allowed[i] = color === "green" ? ch : allowed[i].replace(ch, "");
        });
    }
    console.log("Game over. I " + game.state + " after " + game.turnCount + " attempts.");
}

const words = "dozen,brave,apple,climb,outer,pitch,ruler,holds,fixed,costs,calls"
    .match(/\w+/g);
play(words);

Upvotes: 1

Related Questions