From 86280a4b55b1c1ecd8c60a9f6e4037ef562aada5 Mon Sep 17 00:00:00 2001 From: Jeeb Date: Wed, 16 Feb 2022 17:51:57 -0600 Subject: [PATCH] Implement hard mode, plus customization game: - New flag `--hard`: Select from 3 levels of difficulty: - Greenie: only check greens - Hard: classic Wordle* hard mode - Grayless: All hints are checked, even gray letters - New flag: `--nokeyboard` disables the keyboard - Improved customization: - Box characters, colors, and keyboard layouts moved into wordlists! - Support for non-English alpha characters to be determined. It should work though, I think. - Most things are no longer hardcoded! The main function is still a bit of a spaghetti mess right now, though. To be fixed! dummy: - New customization parameters: - `"boxes"`: Strings to be printed when assembling the share string (from least to most significant order) - `"colors"`: Text colors, to be printed on-screen. Order: Gray, Yellow, Green, Text Color, Keyboard background - `"keyboard"`: The keyboard layout. One string per line, of keyboard. --- src/Wardle.py | 7 +- src/dummy.py | 10 +-- src/game.py | 195 +++++++++++++++++++++++++++++++++++--------------- src/w.py | 2 +- src/w2.py | 2 +- 5 files changed, 149 insertions(+), 67 deletions(-) diff --git a/src/Wardle.py b/src/Wardle.py index 444cac0..4d1b40d 100644 --- a/src/Wardle.py +++ b/src/Wardle.py @@ -1,3 +1,6 @@ #!/usr/bin/python3 -# lol -import game \ No newline at end of file +import game + +game.init() +game.game() +game.end() \ No newline at end of file diff --git a/src/dummy.py b/src/dummy.py index 63f65aa..7925e32 100644 --- a/src/dummy.py +++ b/src/dummy.py @@ -5,11 +5,13 @@ Metadata = { "name": "Dummy", "version": "dummy", "guesses": 4, - "launch": (2222, 2, 16) + "launch": (2222, 2, 16), + "boxes": ["[]", "!!", "<3", " ", " "], + "colors": [0x1666666, 0x1bb9900, 0x1008800, 0xe8e8e8, 0x1222222], + "keyboard": ["aaaaaaaaaa"], # max 10 keys per row, please } # Your custom list of non-answer words goes here -Words = [ - "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", - "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] +Words = ["b", "c", "d", "e", "f"] + # Your custom list of answer-words goes here Answers = ["a"] \ No newline at end of file diff --git a/src/game.py b/src/game.py index 70a5c05..92f9e02 100644 --- a/src/game.py +++ b/src/game.py @@ -18,9 +18,9 @@ from signal import SIGINT from os import get_terminal_size, terminal_size # Provide a clean getaway -def end(): +def end(*args, **kwargs): ui.clear() - exit() + exit(*args, **kwargs) # Set up sigint handler def handler(signum, frame): @@ -33,14 +33,16 @@ parser = argparse.ArgumentParser( description='A barebones CLI Wordle clone, using official Wardle solutions. You have been warned.' ) parser.add_argument('day', nargs="?", type=int, help="Wordle day number") -parser.add_argument('-g', metavar="guesses", type=int, help="number of guesses (limited by terminal size)") +parser.add_argument('-g', metavar="guesses", type=int, help="number of guesses (bounded by terminal size)") +parser.add_argument('--hard', metavar="0-3", \ + nargs="?", const=2, type=int, help="0:normal 1:~greenie 2:*hard 3:+greyless") parser.add_argument('--word', metavar="solution", help="force a particular word") parser.add_argument('--list', metavar="wordlist", help="use a custom wordlist") -parser.add_argument('--classic', action='store_true', help="use the classic Wordle word list") -parser.add_argument('--nonsense', action='store_true', help="allow nonsensical guesses") -parser.add_argument('--hard', action='store_true', help="enable hard mode (wip)") -parser.add_argument('--center', action='store_true', help="center the screen") -# TODO: Implement hard mode properly (more than just a *) +parser.add_argument('--classic', action='store_true', help="use the classic Wordle word list") +#parser.add_argument('-c', action='store_true', help="enable colorblind mode") # TODO +parser.add_argument("--nokeyboard", action='store_true', help="disable the keyboard") +parser.add_argument('--nonsense', action='store_true', help="allow nonsensical guesses") +parser.add_argument('--center', action='store_true', help="center the screen") # Parse the args args = parser.parse_args() @@ -48,6 +50,16 @@ args = parser.parse_args() # Handle the args # Select the correct word list Words, Answers, data = [], [], {"name":"", "version":"", "guesses":-1, "launch":(1970,1,1)} +default_data = { + "name": "Wardle.py", + "version": "-1", + "guesses": 6, + "launch": (2022, 2, 13), + "boxes": ["⬛", "🟨", "🟩", " ", " "], + "colors": [0x13a3a3c, 0x1b59f3b, 0x1538d4e, 0xd7dadc, 0x1121213], + "keyboard": ["qwertyuiop", "asdfghjkl", "zxcvbnm"+u"\u23CE"] +} +data = dict(default_data) if not args.list: args.list = "w2" args.list = "w" if args.classic else args.list @@ -76,20 +88,32 @@ if args.word: else: d = len(Answers) - 1 if d >= len(Answers) else 0 if d < 0 else d solution = Answers[d] + +# Bound hardmode +if not args.hard: + args.hard = 0 +elif args.hard < 0: + args.hard = 0 +elif args.hard > 3: + args.hard = 3 # End arg parsing # ! The game is below this point # Good data structures to have +# Load data +for key in default_data: + if key not in data: + data[key] = default_data[key] + # Box characters! GRAY, YELLOW, GREEN, WHITE, BLACK = range(5) -boxes = ["⬛", "🟨", "🟩", " ", " "] -colors= [0x13a3a3c, 0x1b59f3b, 0x1538d4e, 0xd7dadc, 0x1121213] +boxes = data["boxes"] if "boxes" in data else default_data["boxes"] +colors = data["colors"] + # Guesses go here guesses = [] -letters = [4] * 27 - # Letter is in boxes def wordbox(word, showword = 0): @@ -101,7 +125,8 @@ def wordbox(word, showword = 0): if guess[i] == sol[i]: # Mark the letter 'green' (2) line[i] = GREEN - letters[letter_num(word[i])] = GREEN + if word[i] in letters: + letters[word[i]] = GREEN # Remove letter from solution and guess sol[i] = guess[i] = GRAY # Yellow pass @@ -109,82 +134,135 @@ def wordbox(word, showword = 0): if guess[i] and word[i] in sol: # Mark the letter 'yellow' (1) line[i] = YELLOW - letters[letter_num(word[i])] = YELLOW + if word[i] in letters: + l = letters[word[i]] + if l in (BLACK, GRAY): letters[word[i]] = YELLOW # Remove letter from solution and guess sol[sol.index(word[i])] = guess[i] = GRAY # Gray pass for i in range(len(word)): - if line[i] == GRAY: - letters[letter_num(word[i])] = GRAY + if word[i] in letters and \ + line[i] == GRAY and letters[word[i]] == BLACK: + if word[i] in letters: + letters[word[i]] = GRAY # Turn blocks into a string, and print it output = "" if showword: # Move up to replace the input box - output += ui.m(0,-1) for i in range(len(word)): output += f"{c.c24(colors[WHITE])}{c.c24(colors[line[i]])} {word[i].upper()} {c.RESET}" - #output += c.c24(colors[3]) + c.c24(colors[line[i]]) + " " + word[i].upper() + " " + c.RESET else: for i in line: output += f"{c.c24(colors[WHITE])}{c.c24(colors[i])}{boxes[i]}{c.RESET}" - return output + return f"{output}\n" -def letter_num(char): - if ord('a') <= ord(char.lower()) <= ord('z'): - return ord(char.lower()) - ord('a') - return 26 + +letters = {} +keyboard = data["keyboard"] + +def init_keeb(): + for s in keyboard: + for char in s: + letters[char] = BLACK + return def keeb(x, y): + if args.nokeyboard: + return def colorify(string): s = "" for char in string: - s += c.c24(colors[letters[letter_num(char)]]) + " " + char + " " + c.RESET + s += c.c24(colors[letters[char]]) + " " + char + " " + c.RESET return s - S = ["qwertyuiop", "asdfghjkl", "zxcvbnm"+u"\u23CE"] - for i in range(len(S)): - ui.uprint(x-(3*len(S[i])//2), y+i, colorify(S[i])) + for i in range(len(keyboard)): + ui.uprint(x-(3*len(keyboard[i])//2), y+i, colorify(keyboard[i])) + +ORDINAL = ["1st", "2nd", "3rd", "4th", "5th"] + +def hardmode(word, solution:str, level): + # If hard mode disabled, Continue + if not args.hard: + return + hints = { + GREEN: [], + YELLOW: [], + GRAY: [], + BLACK: [], + } + # For each letter, record which state it has + for letter in word: + if letter not in letters: + letters[letter] = BLACK + hints[letters[letter]].append(letter) + # Green pass + if level >= 1: + for l in hints[GREEN]: + i = solution.find(l) + if i >= 0 and word[i] is not l: + ORDINAL = ["1st", "2nd", "3rd", "4th", "5th"] + return f"{ORDINAL[i]} letter must be {l}" + # Yellow pass + if level >= 2: + for l in hints[YELLOW]: + if l not in word: + return f"Guess must contain {l}" + # Gray pass + if level >= 3: + for l in hints[GRAY]: + if l in word: + return f"Guess must not contain {l}" + return # intro: Print the intro text -def intro(): - title = f"{data['name']} {d}{'*' if args.hard else ''}" - center = ui.m(-len(title)//2,0) - #spaces = ' ' * ((14 - len(title)) // 2) - # [ Wordle 100* ] - intro = f"{c.RESET}{center}{title}" - return intro +def intro(x, y): + # Assemble the text + HARDCHARS = [" ", "~", "*", "+"] + title = f"{data['name']} {d}{HARDCHARS[args.hard]}" + # Center the text + ui.uprint(x-(len(title)//2), y, f"{c.RESET}{title}") def game(): + thicc = {"top":0, "intro": 2, "field":max_guesses+1, "keeb": 0 if args.nokeyboard else 3} x = ui.size[0]//2 - ui.uprint(x, 0, intro()) + # Print the title (2 rows) + intro(x,thicc["top"]) + # Field goes here (max_guesses+1) rows + # Print the keyboard + init_keeb() + keeb(x, thicc["intro"] + thicc["field"]) + words = Words + Answers - guess = 0 - keeb(x, 3 + max_guesses) - while guess < max_guesses: + while len(guesses) < max_guesses: + # Calculate board's starting depth + health = 2 + len(guesses) # lol i should probably use curses - user_input = ui.uinput(x-(len(solution)//2), len(guesses) + 2, c.clear()).lower() - # This provides some leniency for multiple-length-word lists, + user_input = ui.uinput(x-(len(solution)//2), health, f"{c.clear()}{c.c24(colors[WHITE])}").lower() + # This provides some leniency for multiple-length word lists, # by first checking if the input is the length of the solution, # and discarding without penalty otherwise. - if len(user_input) is len(solution) and (user_input in words or args.nonsense): + + if s := hardmode(user_input, solution, args.hard): + ui.uprint(x-(len(s)//2), health, s) + sleep(1) + elif len(user_input) is len(solution)and (user_input in words or args.nonsense): # Add guesses to the guessed list, and put the word in boxes guesses.append(user_input) - ui.uprint(x-(len(solution)*3//2), len(guesses) + 2, wordbox(user_input, 1)) - # Print the new keyboard - keeb(x+1, 3 + max_guesses) - # win condition + ui.uprint(x-(len(solution)*3//2), health, wordbox(user_input, 1)) + # update the keeb + keeb(x, thicc["intro"] + thicc["field"]) if (user_input == solution): + # win win(True) return - else: - guess += 1 else: # Allow the user to make it stop if user_input == "exit": end() # Alert the user that the word was not found in the list, and wait for them to read it - ui.uprint (x-(3*len(solution)*3//2), len(guesses) + 2, "not in word list.") - sleep(1.5) + s = "Not in word list." + ui.uprint (x-(len(s)//2), health, s) + sleep(1) # lose win(False) @@ -192,16 +270,15 @@ def win(won): used_guesses = len(guesses) if won else "X" share = f"{data['name']} {d} {used_guesses}/{max_guesses}{'*' if args.hard else ''}\n" for gi in guesses: - share += f"\n{wordbox(gi)}" + share += f"{wordbox(gi)}" # ui.move to the end of the screen, and print the sharable boxes print(f"{ui.m(0, ui.size[1] + 1)}{share}", end="") - -if (args.center): - w, h, stty = max(30, 3*len(solution)), 7 + max_guesses, get_terminal_size() - # You can init a "screen" 'anywhere'! - - ui.init(w, h, (stty.columns - w) // 2, 0) -else: - ui.init(max(30, 3*len(solution)), 7 + max_guesses) -game() \ No newline at end of file +def init(): + h = 2 + max_guesses + 1 + (0 if args.nokeyboard else 4) + if (args.center): + w, stty = max(30, 3*len(solution)), get_terminal_size() + # You can init a "screen" 'anywhere'! + ui.init(w, h, (stty.columns - w) // 2, 0) + else: + ui.init(max(30, 3*len(solution)), h) \ No newline at end of file diff --git a/src/w.py b/src/w.py index 1ffaa93..3b7e74f 100644 --- a/src/w.py +++ b/src/w.py @@ -8,7 +8,7 @@ Metadata = { "name": "Wordle Classic", "version": "powerlanguage", "guesses": 6, - "launch": (2021, 6, 19) + "launch": (2021, 6, 19), } # These are not answers diff --git a/src/w2.py b/src/w2.py index 70c7cf1..2a77f0f 100644 --- a/src/w2.py +++ b/src/w2.py @@ -8,7 +8,7 @@ Metadata = { "name": "Wordle", "version": "NYT", "guesses": 6, - "launch": (2021, 6, 19) + "launch": (2021, 6, 19), } # These are not answers