From b874cfc101a2e6280dc8eaa79ea5cf5a4bccfe7d Mon Sep 17 00:00:00 2001 From: Jeeb Date: Wed, 16 Feb 2022 12:28:21 -0600 Subject: [PATCH] Refactor everything: - F STRINGS???????????? - Enforce relativity - Rename things for better - Separate the cheating from the game playing - Remove most hardcoded constants - Introduce new wordlist 'format' (lol python, who needs json) - Test things fairly extensively --- src/Wardle.py | 2 +- src/cheater.py | 120 ++++++++++++++++++++++++++++++++++++ src/{c.py => color.py} | 41 ++++++------ src/dummy.py | 15 +++++ src/{g.py => game.py} | 137 +++++++++++++++++++---------------------- src/ui.py | 55 ++++++++--------- src/w.py | 6 ++ src/w2.py | 6 ++ 8 files changed, 258 insertions(+), 124 deletions(-) create mode 100644 src/cheater.py rename src/{c.py => color.py} (62%) create mode 100644 src/dummy.py rename src/{g.py => game.py} (60%) diff --git a/src/Wardle.py b/src/Wardle.py index 2b0acde..444cac0 100644 --- a/src/Wardle.py +++ b/src/Wardle.py @@ -1,3 +1,3 @@ #!/usr/bin/python3 # lol -import g \ No newline at end of file +import game \ No newline at end of file diff --git a/src/cheater.py b/src/cheater.py new file mode 100644 index 0000000..2e33497 --- /dev/null +++ b/src/cheater.py @@ -0,0 +1,120 @@ +""" +Copyright(c) 2022 SoftFish + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +import w +import argparse +import color as c +from time import sleep +from datetime import date, timedelta +from signal import signal +from signal import SIGINT +from os import get_terminal_size + +# Provide a clean getaway + + +def end(): + exit() + +# Set up sigint handler + + +def handler(signum, frame): + if signum == SIGINT: + end() + + +signal(SIGINT, handler) + +# Set up argument parser +parser = argparse.ArgumentParser( + description='A Wordle cheating tool. You have been warned.' +) +parser.add_argument('day', nargs="?", type=int, help="Wordle day number") +parser.add_argument('-l', metavar="solution", help="look up the day of a given solution") +parser.add_argument('-d', metavar="day", type=int, help="look up the solution of a given day") +parser.add_argument('--word', metavar="word", help="Change the hint word") +parser.add_argument('--classic', action='store_true', help="use the classic Wordle word list") +# TODO: Implement hard mode properly (more than just a *) + +# Parse the args +args = parser.parse_args() + +# Handle the args +# Select the correct word list +if args.classic: + import w +else: + import w2 as w + +# Get today's day number +if args.day is not None: + # Ahoy-hoy, time traveller! + d = args.day +else: + # find num. days since Wordle launch + d = (date.today() - date(2021, 6, 19)).days + +# l is for "Find this word in the answers list" +if args.l: + l = args.l.lower() + # Look up the answer's index + if l in w.Answers and w.Answers.index(l) < d: + print("Wordle {}: {}".format(w.Answers.index(l), l)) + else: + print("{} is not a Wordle (yet?)".format(args.l)) + exit(0) + +# d is for "Find this day's answer" +if args.d != None: + # Look up the answer + if args.d < len(w.Answers): + print(w.Answers[args.d]) + else: + print("No Solution") + exit(0) + + +def wordbox(word): + + # Colors! + # gray bg yellow bg green bg white fg black bg + COLORS = [0x13a3a3c, 0x1b59f3b, 0x1538d4e, 0xd7dadc, 0x1121213] + + + output = "" + line = [0] * len(word) + guess = list(word) + solution = list(w.Answers[d]) + # Green pass + for i in range(len(word)): + if guess[i] == solution[i]: + # Mark the letter 'green' (2) + line[i] = 2 + # Remove letter from solution and guess + solution[i] = guess[i] = 0 + # Yellow pass + for i in range(len(word)): + if guess[i] and word[i] in solution: + # Mark the letter 'yellow' (1) + line[i] = 1 + # Remove letter from solution and guess + solution[solution.index(word[i])] = guess[i] = 0 + # Turn blocks into a string, and print it + # Move up to replace the input box + for i in range(len(word)): + output += f"{c.c24(COLORS[3])}{c.c24(COLORS[line[i]])} {word[i].upper()} {c.RESET}" + return output + +if not args.word: + print(f"Wordle {d}:") + print(f"Today is {date(2021, 6, 19) + timedelta(days=d)}") + args.word = "crane" +print(f"Hint: {wordbox(args.word)}") diff --git a/src/c.py b/src/color.py similarity index 62% rename from src/c.py rename to src/color.py index c4ae8b8..57a2a12 100644 --- a/src/c.py +++ b/src/color.py @@ -9,13 +9,12 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI """ # c.py: ANSI Escape Sequence colors -from math import floor # 'constants' -- feel free to change at runtime ;P RESET = "\033[0m" def esc(c): - return "\033[" + str(c) + "m" + return f"\033[{c}m"# + str(c) + "m" def clear(line=True): @@ -26,34 +25,34 @@ def clear(line=True): # ANSI Colors def c(c, bg=0, i=0): if(c < 8 and i < 2 and bg < 2): - return esc(str(c + 30 + i*60 + bg*10)) + return esc(f"{c + 30 + i*60 + bg*10}") return RESET -# RGB666 colors -def rgb215text(r, g, b, bg=0): - return esc(str(38 + 10*(bg&1)) + ";5;" + str(16 + 36*r + 6*g + b)) +# RGB6*6*6 colors +def rgb8(r, g, b, bg=0): + return esc(f"{38 + 10*(bg&1)};5;{16 + 36*r + 6*g + b}") + #return esc(str(38 + 10*(bg&1)) + ";5;" + str(16 + 36*r + 6*g + b)) -def rgb888text(r, g, b, bg=0): - sc=";" +def rgb24(r, g, b, bg=0): # Generate terminal escape string - return esc(str(38 + 10*(bg&1)) + ";2;"+str(r)+sc+str(g)+sc+str(b)) + return esc(f"{38 + 10*(bg & 1)};2;{str(r)};{str(g)};{str(b)}") # 24-bit color in the terminal! # 0x bg rr gg bb def c24(color): - bg = (color >> 24 & 0x01) - r = (color >> 16 & 0xff) - g = (color >> 8 & 0xff) - b = (color >> 0 & 0xff) + # Set up arguments for the color printer + args24 = {"r": (color >> 16 & 0xff), "g": (color >> 8 & 0xff), "b": (color >> 0 & 0xff), "bg":(color >> 24 & 0x01)} + args8 = dict(args24); args8["r"] //= 43; args8["g"] //= 43; args8["b"] //= 43 # Convert to 256-color as fallback, but append 24-bit color selector to end - return rgb215text(r//43, g//43, b//43, bg) + rgb888text(r, g, b, bg) + return f"{rgb8(**args8)}{rgb24(**args24)}" -def c8(color): - bg = color >> 12 & 0x01 - r = color >> 8 & 0x07 - g = color >> 4 & 0x07 - b = color >> 0 & 0x07 - print (r, g, b, bg) - return rgb215text(r if r < 6 else 5, g if g < 6 else 5, b if b < 6 else 5, bg) +# use c24 -> it will handle backwards-compat +#def c8(color): +# bg = color >> 12 & 0x01 +# r = color >> 8 & 0x07 +# g = color >> 4 & 0x07 +# b = color >> 0 & 0x07 +# print (r, g, b, bg) +# return rgb8(r if r < 6 else 5, g if g < 6 else 5, b if b < 6 else 5, bg) diff --git a/src/dummy.py b/src/dummy.py new file mode 100644 index 0000000..63f65aa --- /dev/null +++ b/src/dummy.py @@ -0,0 +1,15 @@ +# dummy wordlist +# by Jeeb +# Information about your version of Wordle +Metadata = { + "name": "Dummy", + "version": "dummy", + "guesses": 4, + "launch": (2222, 2, 16) +} +# 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"] +# Your custom list of answer-words goes here +Answers = ["a"] \ No newline at end of file diff --git a/src/g.py b/src/game.py similarity index 60% rename from src/g.py rename to src/game.py index d6e4da4..70a5c05 100644 --- a/src/g.py +++ b/src/game.py @@ -8,16 +8,14 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ - -import w -import c import ui +import color as c import argparse from time import sleep from datetime import date from signal import signal from signal import SIGINT -from os import get_terminal_size +from os import get_terminal_size, terminal_size # Provide a clean getaway def end(): @@ -32,17 +30,16 @@ signal(SIGINT, handler) # Set up argument parser parser = argparse.ArgumentParser( - description='A barebones CLI Wordle clone, using official Wardle solutions. Also a Wordle cheating tool. You have been warned.' + 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('-l', metavar="solution", help="look up the day of a given solution") -parser.add_argument('-d', metavar="day", type=int, help="look up the solution of a given day") -parser.add_argument('-g', metavar="guesses", type=int, help="number of guesses") +parser.add_argument('-g', metavar="guesses", type=int, help="number of guesses (limited by terminal size)") 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('--nocenter', action='store_true', help="disable centering the screen") +parser.add_argument('--center', action='store_true', help="center the screen") # TODO: Implement hard mode properly (more than just a *) # Parse the args @@ -50,51 +47,35 @@ args = parser.parse_args() # Handle the args # Select the correct word list -if args.classic: - import w -else: - import w2 as w +Words, Answers, data = [], [], {"name":"", "version":"", "guesses":-1, "launch":(1970,1,1)} +if not args.list: + args.list = "w2" +args.list = "w" if args.classic else args.list +# lol everything's a good py +exec(f"from {args.list} import Words, Answers, Metadata as data") # Get today's day number if args.day is not None: # Ahoy-hoy, time traveller! d = args.day else: # find num. days since Wordle launch - d = (date.today() - date(2021, 6, 19)).days -# l is for "Find this word in the answers list" -if args.l: - l = args.l.lower() - # Look up the answer's index - if l in w.Answers and w.Answers.index(l) < d: - print ("Wordle {}: {}".format(w.Answers.index(l), l)) - else: - print ("{} is not a Wordle (yet?)".format(args.l)) - exit(0) -# d is for "Find this day's answer" -if args.d != None: - # Look up the answer - if args.d < len(w.Answers): - print (w.Answers[args.d]) - else: - print ("No Solution") - exit(0) + d = (date.today() - date(*data["launch"])).days # g is for "maximum guesses" if args.g: - max_guesses = abs(args.g) + max_guesses = min(get_terminal_size().lines - 10, abs(args.g)) else: - max_guesses = 6 + max_guesses = data["guesses"] # Get today's word if args.word: solution = args.word # Indicate a non-standard word - d = args.word + d = "Custom" # If solution not in words list, enable nonsense - if solution not in w.Words + w.Answers: + if solution not in Words + Answers: args.nonsense = True else: - if d >= len(w.Answers): - d = len(w.Answers) - 1 - solution = w.Answers[d] + d = len(Answers) - 1 if d >= len(Answers) else 0 if d < 0 else d + solution = Answers[d] # End arg parsing @@ -102,9 +83,8 @@ else: # Good data structures to have # Box characters! -boxes = ["⬛", "🟨", "🟩"] -# Colors! -# gray bg yellow bg green bg white fg black bg +GRAY, YELLOW, GREEN, WHITE, BLACK = range(5) +boxes = ["⬛", "🟨", "🟩", " ", " "] colors= [0x13a3a3c, 0x1b59f3b, 0x1538d4e, 0xd7dadc, 0x1121213] # Guesses go here guesses = [] @@ -113,41 +93,40 @@ letters = [4] * 27 # Letter is in boxes def wordbox(word, showword = 0): - output = "" - line = [0] * len(word) + line = [GRAY] * len(word) guess = list(word) sol = list(solution) # Green pass for i in range(len(word)): if guess[i] == sol[i]: # Mark the letter 'green' (2) - line[i] = 2 - letters[letter_num(word[i])] = 2 + line[i] = GREEN + letters[letter_num(word[i])] = GREEN # Remove letter from solution and guess - sol[i] = guess[i] = 0 + sol[i] = guess[i] = GRAY # Yellow pass for i in range(len(word)): if guess[i] and word[i] in sol: # Mark the letter 'yellow' (1) - line[i] = 1 - letters[letter_num(word[i])] = 1 + line[i] = YELLOW + letters[letter_num(word[i])] = YELLOW # Remove letter from solution and guess - sol[sol.index(word[i])] = guess[i] = 0 + sol[sol.index(word[i])] = guess[i] = GRAY # Gray pass for i in range(len(word)): - if line[i] == 0: - letters[letter_num(word[i])] = 0 + if line[i] == GRAY: + letters[letter_num(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 += c.c(7,0,1) + (c.c(2,1) if line[i] == 2 else c.c(3,1) if line[i] == 1 else c.c(0,1,1)) + word[i].upper() + " " + c.reset - output += c.c24(colors[3]) + c.c24(colors[line[i]]) + " " + word[i].upper() + " " + c.RESET + 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 += c.c(7, 0, 1) + (c.c(2, 1) if i == 2 else c.c(3, 1) if i == 1 else c.c(0, 1, 1)) + boxes[i] + c.reset - output += c.c24(colors[3]) + c.c24(colors[i]) + boxes[i] + c.RESET + output += f"{c.c24(colors[WHITE])}{c.c24(colors[i])}{boxes[i]}{c.RESET}" return output def letter_num(char): @@ -163,33 +142,36 @@ def keeb(x, y): return s S = ["qwertyuiop", "asdfghjkl", "zxcvbnm"+u"\u23CE"] for i in range(len(S)): - ui.uprint(x-(3*len(S[i])//2)+7, y+i, colorify(S[i])) + ui.uprint(x-(3*len(S[i])//2), y+i, colorify(S[i])) # intro: Print the intro text def intro(): - title = "Wordle {}{}".format(d, "*" if args.hard else "") - spaces = ' ' * ((14 - len(title)) // 2) + title = f"{data['name']} {d}{'*' if args.hard else ''}" + center = ui.m(-len(title)//2,0) + #spaces = ' ' * ((14 - len(title)) // 2) # [ Wordle 100* ] - intro = c.RESET + spaces + title + intro = f"{c.RESET}{center}{title}" return intro def game(): - if args.nocenter: - x = ui.size[0]//2 - 8 - else: - x = get_terminal_size().columns//2 - 8 + x = ui.size[0]//2 ui.uprint(x, 0, intro()) - words = w.Words + w.Answers + words = Words + Answers guess = 0 + keeb(x, 3 + max_guesses) while guess < max_guesses: # lol i should probably use curses - keeb(x+1, 9) - user_input = ui.uinput(x, len(guesses) + 2, c.clear() + " ").lower() - if len(user_input) == 5 and (user_input in words or args.nonsense): + user_input = ui.uinput(x-(len(solution)//2), len(guesses) + 2, c.clear()).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): # Add guesses to the guessed list, and put the word in boxes guesses.append(user_input) - ui.uprint(x, len(guesses) + 2, wordbox(user_input, 1)) + 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 if (user_input == solution): win(True) @@ -201,18 +183,25 @@ def game(): 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, len(guesses) + 2, "not in word list.") + ui.uprint (x-(3*len(solution)*3//2), len(guesses) + 2, "not in word list.") sleep(1.5) # lose win(False) def win(won): - board = "Wordle {} {}/{}{}\n".format(d, len(guesses) if won else "X", max_guesses, "*" if args.hard else "") + 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: - board += "\n" + wordbox(gi) - - print(ui.m(0,ui.size[1]+1) + board) + share += f"\n{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="") -ui.init(30,12) +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 diff --git a/src/ui.py b/src/ui.py index ea0f61d..2002ac3 100644 --- a/src/ui.py +++ b/src/ui.py @@ -10,11 +10,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI # ui.py: ANSI Escape Sequence colors - - -from time import sleep -import sys, re -import c +import sys ANSI_S = "\033[s" ANSI_U = "\033[u" @@ -30,47 +26,50 @@ LEFT = WEST = 'D' size = [0,0] - -def init(width, height): +# init: Initialize the "screen" and ensure the appropriate area is on screen and cleared +def init(width, height, x=0, y=0): size[0], size[1] = width, height - print(c.c24(0x00f0d0) + '0123456789ABCDE' + c.RESET) - print((" "*width + "\n")*(height-1)) - print(m(0,-height-1) + ANSI_S, end='') + for i in range(y,y+height): + uprint(x,i," "*width) + uprint(x,height+y,f"{m(0, -height)}{ANSI_S}") clear() def clear(x = 0, y = 0, behavior = 0): - uprint(x, y, "\x1b[%dJ"%behavior) + uprint(x, y, f"\x1b[{behavior}J") + +# ANSI Escape Sequence Generators: +# unless otherwise specified, return strings containing ANSI escape characters # Move the cursor relatively on the screen def m(x = 0, y = 0): - # TODO: implement relative x/y movement - s = "{}{}".format( - m_dir(UP, abs(y)) if y < 0 else m_dir(DOWN, y) if y > 0 else "", - m_dir(LEFT, abs(x)) if x < 0 else m_dir(RIGHT, x) if x > 0 else "") + # move on the y axis, followed by the x axis + s = f"{m_dir(UP, abs(y)) if y < 0 else m_dir(DOWN, y) if y > 0 else ''}" + \ + f"{m_dir(LEFT, abs(x)) if x < 0 else m_dir(RIGHT, x) if x > 0 else ''}" return s -# Move the cursor relatively on the screen +# Move the cursor relatively on the screen in a given direction def m_dir(dir, steps=1): if ord('A') <= ord(dir) <= ord('D') : - return "\033[{}{}".format(steps, dir) + return f"\033[{steps}{dir}" return "" # Position the cursor absolutely on the screen (0-index) def m_abs(x, y): - return "\033[{};{}H".format(y+1, x+1) + return f"\033[{y+1};{x+1}H" -ui_input, ui_print = input, print - -def uprint(x, y, *args, end='', **kwargs): - ui_print(ANSI_S + m(x, y), end='') - ui_print(*args, ANSI_U, **kwargs, end='') - ui_print(ANSI_U, end=end) +# print/input wrappers +# uprint: call print at a given x,y position, relative to the cursor +def uprint(x, y, *args, **kwargs): + print(f"{ANSI_S}{m(x, y)}", end='') + print(*args, **kwargs, end='') + print(ANSI_U, end='') sys.stdout.flush() - +# uinput: call input at a given x,y position, relative to the cursor +# returns: string containing user input def uinput(x, y, *args, **kwargs): - ui_print(ANSI_S + m(x, y), end='') - r = ui_input(*args, **kwargs) - ui_print(ANSI_U, end='') + print(f"{ANSI_S}{m(x, y)}", end='') + r = input(*args, **kwargs) + print(ANSI_U, end='') sys.stdout.flush() return r diff --git a/src/w.py b/src/w.py index 1f82ec9..1ffaa93 100644 --- a/src/w.py +++ b/src/w.py @@ -4,6 +4,12 @@ # and has been included here for compatibility with the official Wordle client. # w.py: Wordle non-answers and answers +Metadata = { + "name": "Wordle Classic", + "version": "powerlanguage", + "guesses": 6, + "launch": (2021, 6, 19) +} # These are not answers Words = [ diff --git a/src/w2.py b/src/w2.py index b9fcd4b..70c7cf1 100644 --- a/src/w2.py +++ b/src/w2.py @@ -4,6 +4,12 @@ # and has been included here for compatibility with the official Wardle/NYT Wordle client. # w.py: Wordle non-answers and answers +Metadata = { + "name": "Wordle", + "version": "NYT", + "guesses": 6, + "launch": (2021, 6, 19) +} # These are not answers Words = [