4
\$\begingroup\$

What the code does short version: Given your hints for Wordle, narrows down the list of potential matches and returns them as a list.

Slightly longer version: It takes the positions and values of your green clues, positions and values of your yellow clues, and all your gray clues and creates a regular expression to filter the Wordle word bank (and then uses two control flows on top of that to further narrow it which is part of what I suspect could be done a lot cleaner and more efficiently)

Yesterday I wrote a quick and dirty Wordle cheat engine and a few parts of my code felt poorly written as I was writing them because I was kind of stuck on my first solution and couldn't figure out how to refactor them. I know using dictionaries instead of lists for the green and yellow letters feels needlessly complicated, but I already how to fix that and I'll clean it up when I make my refactor pass so the particulars I'm more concerned about are:

  • I couldn't figure out how to filter the matches list further without what feels like needlessly complicated control flows where I populate the matches2 and final_matches lists in the def statement for potential_answers(). It feels inefficient, the use of a generic flag variable feels like the wrong choice, and I don't like making 3 separate lists of increasingly narrow matches

  • Again, I used a generic flag variable in the local scope for the get_green() and get_yellow() functions and I think it needs a better variable name

But also if you have suggestions outside of those two points please feel free and let loose your fury upon me. I want to write cleaner, more readable code in general so any suggestions or code smells you notice are welcome.

"""
A program that takes your wordle clues and
gives you potential matches in the official
wordle answer bank
"""

import re


# Loads wordle answers and stores as list
wordle_answers = []
with open('WordleAnswers.txt', 'r') as f:
  for line in f:
    for word in line[1:-1].split(','):
      wordle_answers.append(word.strip('"'))
wordle_answers.sort()


def potential_answers(green: dict, yellow: dict, gray: str) -> list:
  """
  Takes 2 dicts, green, yellow, and a string gray
  example expected format:
  green = {1:'a', 2:'', 3:'', 4:'', 5:'e'}
  yellow = {1:'', 2:'ok', 3:'', 4: '', 5:''}
  gray = 'brlm'
  """
  search_sequence = ['.' for i in range(5)]  # stores a wildcard in every position to start
  # iterate over green dict and if the value isn't empty replace the character
  # at the matching index in search_sequence
  for key, val in green.items():
    if val:
      search_sequence[key-1] = val
  # create an empty string to store letters we know are in the word but don't know where
  # then we iterate over yellow dict and create a regex NOT term for each
  # non-empty index and append the relavent terms to the in_word variable
  # this way we know to look for words that have a letter but not at the Xth index
  in_word = ''
  for key, val in yellow.items():
    in_word += val
    if val and search_sequence[key-1] == '.':
      search_sequence[key-1] = '[^' + val + ']'
  # converts search_sequence list into a string then into a raw string and slices
  # off the single quotes introduced through the repr function
  search_sequence = repr(''.join(search_sequence))[1:-1]
  r = re.compile(search_sequence)
  # filters based on green known letters and which letters aren't at an index
  # but doesn't enforce the presence of yellow letters
  matches = list(filter(r.match, wordle_answers))
  # iterate through each word in matches, then iterates through every letter
  # we know must be in the word, if all letters are in the word we append the
  # word to our matches2 list
  matches2 = []
  for word in matches:
    flag = True
    for letter in in_word:
      if letter not in word:
        flag = False
    if flag:
      matches2.append(word)
  # iterate through every word in our matches2 list and then iterate through
  # every letter we know NOT to be in the word. If it contains none of the gray
  # letters we append our word to our final output list
  final_matches = []
  for word in matches2:
    flag = True
    for letter in gray:
      if letter in word:
        flag = False
    if flag:
      final_matches.append(word)
  print('Your potential matches are:\n')
  return final_matches


def get_green() -> dict:
  """
  Gets and validates user input for green letters and formats for feeding into
  the function potential_answers()
  """
  green = {1:'', 2:'', 3:'', 4:'', 5:''}  # initialize green dict
  print("Let's get your green letters:")
  print("-----------------------------")
  for i in range(1,6):
    flag = 0  # helps us validate user, keeping us in the while loop if not y/n
    while flag != 'y' and flag != 'n':
      flag = input(f'Do you know what letter is in spot {i}? (y/n)\n')
      if flag != 'y' and flag !='n':
        print('Please only type y or n right now\n')
    if flag == 'y':
      letter = '0'
      while not letter.isalpha() or len(letter) != 1:
        letter = input(f'What letter do you have for spot {i}?\n')
        if not letter.isalpha() or len(letter) != 1:
          print('Please only type a single letter right now')
      # store standardized user input in the correct index of green dict
      green[i] = letter.lower()
    else:
      continue
  return green


def get_yellow() -> dict:
  """
  Gets and validates user input for yellow letters and formats for passing into
  function potential_answers()
  """
  yellow = {1:'', 2:'', 3:'', 4:'', 5:''}
  print("Let's get your yellow letters:")
  print("-----------------------------")
  for i in range(1,6):
    flag = 0 # again helps us validate user input
    while flag != 'y' and flag != 'n':
      flag = input(f'Do you know a yellow letter for spot {i}? (y/n)\n')
      if flag != 'y' and flag !='n':
        print('Please only type y or n right now\n')
    if flag == 'y':
      letter = '0'
      while not letter.isalpha():
        letter = input(f'What yellow letters do you know for spot {i}?\n(Type all that apply)\n')
        if not letter.isalpha():
          print('Please only type a letter right now')
      # stores standardized user input in correct index of yellow dict
      yellow[i] = letter.lower()
    else:
      continue
  return yellow


def get_gray() -> str:
  """
  Gets and validates user input for gray letters and formats for passing into
  function potential_answers
  """
  gray = ''
  print(f"Let's get your gray letters:")
  print('-----------------------------')
  while not gray and not gray.isalpha():
    gray = input(f'What are your gray letters?\n(Type all that apply)\n')
    if not gray.isalpha():
      print('Please only type letters')
  return gray.lower()


if __name__ == '__main__':
  green = get_green()
  yellow = get_yellow()
  gray = get_gray()

  potential_answers(green, yellow, gray)

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

Return Values

Your get_green() and get_yellow() methods returns a dict. Since your dictionary keys are just the letter indices (1 through 5), each could simply return a list[str] with 5 elements.

Zero is not a letter

letter = '0' This is very hacky. I understand you want to start off with a string that isn't .isalpha(), so your while not letter.isalpha() loop will execute as least once. Better would be letter = '' as ''.isalpha() also returns False.

Usability

If your Wordle guess happens to hit all 5 letters, you can enter in 1 green letter and the 4 yellow letters, but then the program asks for gray letters and you don't have any, but it won't accept that, because ''.isalpha() is False! You have to make-up a gray letter, despite not actually having an actual gray letter.

There is no point asking about a yellow letter in position 3 if you have a green letter in position 3! Pass green into the get_yellow() function, so you can skip meaningless data entry.

Singular -vs- Plural

In get_yellow(), you ask for yellow letters (plural), but store the answer in letter (singular). Rename this.

Unnecessary Continue

    for ...:
        if ...:
            ...
        else:
            continue

There is no reason for the else: continue. These are unnecessary statements.

PEP-8

The Style Guide for Python Code specifies commas should be followed by a space (eg, range(1, 6)), and colons in dictionary keys should be followed by a space (eg, {1: '', 2: '', 3: '', 4: '', 5: ''}.

Standard indentation is 4 spaces, not 2.

Potential Answers

search_sequence could be initialized with search_sequence = ['0'] * 5. Better, if green was a list[str], you could use:

    search_sequence = [letter if letter else '.' for letter in green]

Why are you using repr and stripping off quotes?

    search_sequence = repr(''.join(search_sequence))[1:-1]

The result of ''.join() is already the required string:

    search_sequence = ''.join(search_sequence))

The result of filter is a generator. You don't need to realize it as a list, wasting memory. Simply use it in the for loop:

    matches = filter(r.match, wordle_answers)
    for word in matches:
        ...

or even:

    for word in filter(r.match, wordle_answers):
        ...

Use all() when you want to ensure all of an iterable collection of tests passes.

        flag = True
        for letter in in_word:
            if letter not in word:
                flag = False

becomes:

        flag = all(letter in word for letter in in_word)

This means match2 could be simply:

    match2 = [word for word in filter(r.match, wordle_answers)
              if all(letter in word for letter in in_word)]

Even better: turn in_word into (or build it as) a set, and leave match2 as another generator:

    in_word = set(in_word)
    match2 = (word for word in filter(r.match, wordle_answers) if set(word) >= in_word)

Similarly, turn gray into a set. If set(gray) & set(word) is not empty, then word contains a letter in gray, and can be excluded.

    in_word = set(in_word)
    final_matches = [word for word in match2 if gray.isdisjoint(word)]
\$\endgroup\$
3
\$\begingroup\$

Your script accepts a collection of gray letters from the user and treats them directly as "the letters which do not exist in the target word", but in Wordle this is not always true. In the screenshot of the example game below where the target word is "abbey":

  • the first guess "algae" has its second letter "A" marked as gray because "abbey" only has one "A"
  • similarly, the second guess "keeps" has its second letter "E" marked as gray because "abbey" only has one "E"

So if a user were to directly input the letters "A" and "E" as gray letters, in this case your script would end up filtering out the target word from the list of candidates.

The fix would be to remove any letters from gray that show up in green or yellow, call this new collection of letters something different, e.g. letters_not_in_target — to avoid mutating the original gray, which is generally good form anyway, and to give it a more correct name — and use letters_not_in_target to do your final filter pass.

Example Wordle game where the target word is "abbey"

Image source: Wordle Same Letter Twice Rules Explained: How Does it Work?

\$\endgroup\$
4
  • \$\begingroup\$ Oh I didn't actually know about that, that's a useful edge case thanks \$\endgroup\$
    – Candice
    Feb 3, 2022 at 15:47
  • \$\begingroup\$ There is another interpretation of "gray letters." Below the game grid is the letter buttons which also turn green, yellow, and dark gray. A green letter here means a place where the letter goes has been found, even if multiple of that letter exists and not all have been located. A yellow letter is one that is in the solution but never placed in the right spot. A dark gray letter is not in the solution. In the above grid, the E would turn yellow after the first guess & stay yellow on the second guess despite the second E shown gray in the game grid. \$\endgroup\$
    – AJNeufeld
    Feb 4, 2022 at 6:32
  • \$\begingroup\$ @AJNeufeld that's true, the keyboard below the six rows does highlight the letter keys with either green or dark gray to indicate existence/non-existence in the word. I don't think the keyboard lights up with yellow though, since that only makes sense in the context of the puzzle slots. \$\endgroup\$
    – Setris
    Feb 4, 2022 at 6:35
  • \$\begingroup\$ @AJNeufeld I see what you mean now, I do see the keys light up yellow as well in phases before the puzzle is completely solved (though the user probably wouldn't be entering in yellow letters from the keyboard but from the main puzzle UI itself) \$\endgroup\$
    – Setris
    Feb 4, 2022 at 6:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.