5
\$\begingroup\$

rules

UnWordle is a word guessing game in which it shows you a n*n color grid and a base word. The goal of the player is to fill words into the grid such that it follows these rules:

  • a green brick must contain a letter from the base word with correct positioning.
  • a yellow brick must contain a letter from the base word but in an incorrect position
  • a gray brick must NOT contain ANY letter from the base word
  • a level beneath another level must use all the information from the level above it:
    • so: if a yellow or green letter is used on an higher level they must be used in a lower level
    • if a gray letter is used on an higher level they must NOT be used on the lower level
    • a yellow letter can not be used on the same index multiple times
    • if it is known that there's only M amount of letter k, you can not use more than that amount in you building

example:

example starting position example solution
image on the left is the starting position.
image on the right is the winning position.
they follow all the rules mentioned above.

problem I wrote code to solve the game, and it can solve 80% of the puzzle with no sweat, but it gets really slow as the question becomes more difficult. I am not sure if it was due to the algorithm or the flow of the program. The code was provided below.

link to the dictionary file I use


unwordle.py

def help_() -> None:
    """message for help/-help flags."""

    s = """unwordle is a popular word game!
learn more about them here:https://unwordle.org/?daily=1
this program is a game solver.
first check out the unwordle game board.
then in command line type in `python unwordle.py `
if the first row is *green* *yellow* *gray* *gray* *gray*,
you would add `GYXXX` at the end of that command
you repeat this for the next two row.(all the hints)
and lastly typed in the answer word (letter only)
a sample output would look something like this:
`python unwordle.py YXXXX XYXYX XGYXG ralph\n`"""

    print(s)
    exit()
    return


def check_sys_arg(sys_argv: list) -> list:
    """
validating the input of the user.
<br>including checking if word is the right len
<br>and if contains disallowed characters.
  """

    
    ERROR_MSG_2 = ("include disallowed character in hint color:"+
"`G for green, Y for yellow, and X for gray`")
    ERROR_MSG_3 = "include disallowed character in answer word`"
    ERROR_MSG_4 = "inconsistent word length in input"

    ALLOWED_COLOR_S: set = set("GYX")
    ALLOWED_CHR_S: set = set("abcdefghijklmnopqrstuvwxyz")

    output = []

    wrd_ln = len(sys_argv[1])

    for x in range(1, len(sys_argv) - 1):
        x = sys_argv[x].upper()
        if set(x) - ALLOWED_COLOR_S:
            print("use `unwordle.py -help` to get help")
            raise Exception(ERROR_MSG_2)
        if len(x) != wrd_ln:
            print("use `unwordle.py -help` to get help")
            raise Exception(ERROR_MSG_4)
        output.append(x)

    x = sys_argv[-1].lower()
    if set(x) - ALLOWED_CHR_S:
        print("use `unwordle.py -help` to get help")
        raise Exception(ERROR_MSG_3)
    if len(x) != wrd_ln:
        print("use `unwordle.py -help` to get help")
        raise Exception(ERROR_MSG_4)
    output.append(x)

    return output


def import_dictionary(file: str = "dictionary.txt",
                      target_len_int: int = 5) -> list:
    """
reading and filtering words from txt files.
<br>file: the directory of which txt file is located
<br>target_len_int : target word length
  """
    output = []
    with open(file, "r+") as f:  #reading list of words
        temp = f.readlines()
        for word in temp:
            word = word.strip()
            if len(word) == target_len_int:
                output.append(word)
    return output


def color_word(answer_word: str, guess_word: str) -> str:
    """
take two words and color accordingly.
an example output would be "XXYYG"
  """
    color_l = ["X"] * len(guess_word)
    ans_wrd_l = list(answer_word)
    for counter, (chr_1, chr_2) in enumerate(zip(answer_word, guess_word)):
        if chr_1 == chr_2:
            color_l[counter] = "G"
            ans_wrd_l.remove(chr_1)
    for counter, chr_1 in enumerate(guess_word):
        if color_l[counter] == "G":
            continue
        if chr_1 in ans_wrd_l:
            color_l[counter] = "Y"
            ans_wrd_l.remove(chr_1)
    return "".join(color_l)


def find_candidate(wrd_l: list, final_ans: str, search: str) -> dict:
    """
loop through a word list and search for words that fit the criteria.
<br>the result to the function is a dict with:
<br>key being the word, val being a tuple of two set.:
<br>explain below:
<br>the first set contain sucess guesses(Y/G)
<br>the second set contain failed guesses:
<br>X or Y that is out of position
  """
    def multi_apperance(chr: str,
                        succes: set,
                        append_to: set,
                        break_: bool = True):
        for x in range(1, len(o) + 1):
            x = str(x) + chr
            if x not in success:
                append_to.add(x)
                if break_:
                    break

    output = {}
    for o in all_wrd_l:
        if color_word(final_ans, o) != search:
            continue

        fail = set()
        success = set()

        for counter, (chr, color) in enumerate(zip(o, search)):
            if color == "X":
                continue

            if chr in success:
                multi_apperance(chr, success, success)
            else:
                success.add(chr)

            if color == "Y":
                fail.add(counter * "X" + chr)

        for counter, (chr, color) in enumerate(zip(o, search)):
            if color != "X":
                continue
            if chr not in success:
                fail.add(chr)
            else:
                fail.add(counter * "X" + chr)
                multi_apperance(chr, success, fail, False)

        output[o] = (success, fail)
    return output


def main(cand_list: list, answer: str):
    """
collect a list of possible candidates and render result. always return 0.
<br>required args: 
<br>cand_list -> the candidate list. for reference only. 
<br>include a list of all possible candidate dict here.
<br>starting from clue 1 ...clue 3
<br> answer -> final answer
  """


  
    INSTRUCTION_MSG = """want a different answer?
enter 1 for a different answer or enter 0 to exit the program.
tip: you can enter any number and the word at that index will be changed
the left most word has a index of 1.
    """
    def askinput():
      print(INSTRUCTION_MSG)  
      while True:
        ask_input = input("your respond:")
        print()
        if not ask_input:
            ask_input = "1"
        ask_input = ask_input.strip()
        if set(ask_input) - set("1234567890"):
            print("invalid input, let's try again.")
        else:
            return ask_input
    def inner_main(depth: int,
                   cand_list: list,
                   answer: str,
                   display: list = [],
                   success_j: set = {},
                   fail_j: set = {}) -> 0:
        """The worker function for main."""

        i_cands = cand_list[depth]

        for cand in i_cands:
            success_i, fail_i = i_cands[cand]
            if display:
                if success_i - success_j:
                    continue
                if fail_i.intersection(fail_j):
                    continue
            display.insert(0, cand)

            if depth:
                _ = inner_main(depth - 1, cand_list, answer, display,
                               success_i, fail_i.union(fail_j))
            else:
                print("answer suggestion:")
                display.append(answer)
                print(display)

                _ = askinput()
              
                _ = int(_) - 1
                del display[-1]
            del display[0]
            if _:
                return _ - 1

        return 0

    inner_main(len(cand_list) - 1, cand_list, answer)
    return 0


if __name__ == "__main__":
    import sys
    print()  # print an extra line to avoid any confusion
    if len(sys.argv) == 1:
        print("use `unwordle.py -help` to get help")
        print()
        exit()
    help_flags = ["h", "help", "-help", "-h", "--help", "--h"]
    if sys.argv[1].lower() in help_flags:
        help_()
        print()
    if len(sys.argv) == 2:
        print("use `unwordle.py -help` to get help")
        print()
        exit()
    user_input_l: list = check_sys_arg(sys.argv)

    WRD_LN = len(user_input_l[0])
    ANSWER = user_input_l.pop()
    GUESSES = len(user_input_l)

    all_wrd_l: list = import_dictionary(target_len_int=WRD_LN)

    cand = []
    for clues in user_input_l:
        temp: dict = find_candidate(all_wrd_l, ANSWER, clues)
        cand.append(temp)

    main(cand, ANSWER)
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

Efficiency

It would be better to create a new input file instead of filtering out all the 5-letter words every time you run the code.

Also, the input file has many 5-letter combinations that, even if they are considered "words" by some loose definition, they are hardly common words:

abstr
accrd
accpt
ackgt

It would be better to use a smaller input file which only has common words. This may help your reported problem of your code being too slow in some cases.

Whitespace

pylint identified a few style issues. The code uses some inconsistent indentation, and some lines have trailing spaces, which can be annoying and can certainly be removed.

The black program can be used to automatically modify your code to fix these issues.

Naming

You have a variable named chr, which is also the name of a built-in Python function.
It would be better to change the name of the variable to something like char.

Some short variable names are not very meaningful. Consider changing them:

  • f to dictfile
  • s to help_string

You use the generic variable named output in a couple places. It would be more meaningful to name it words in the import_dictionary function, for example.

Command line

argparse is a standard alternative to parsing the command line yourself. It can generate a help message in a consistent way as well as providing a built-in -h option.

Documentation

It is great that you have the help documentation at the top of the file.

It would also be nice to mention that the code relies on the dictionary.txt input file as well.

Input checking

It is great that you check validity of data from the standard input. Consider the pyinputplus module as well.

Typo

The word sucess is misspelled in your docstring. It should be success.

Unused code

In the help_ function, the return statement is unreachable because of the preceding exit statement. return can be omitted.

\$\endgroup\$

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