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.
This commit is contained in:
John 2022-02-16 17:51:57 -06:00
parent b874cfc101
commit 86280a4b55
5 changed files with 149 additions and 67 deletions

View File

@ -1,3 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
# lol
import game import game
game.init()
game.game()
game.end()

View File

@ -5,11 +5,13 @@ Metadata = {
"name": "Dummy", "name": "Dummy",
"version": "dummy", "version": "dummy",
"guesses": 4, "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 # Your custom list of non-answer words goes here
Words = [ Words = ["b", "c", "d", "e", "f"]
"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 # Your custom list of answer-words goes here
Answers = ["a"] Answers = ["a"]

View File

@ -18,9 +18,9 @@ from signal import SIGINT
from os import get_terminal_size, terminal_size from os import get_terminal_size, terminal_size
# Provide a clean getaway # Provide a clean getaway
def end(): def end(*args, **kwargs):
ui.clear() ui.clear()
exit() exit(*args, **kwargs)
# Set up sigint handler # Set up sigint handler
def handler(signum, frame): 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.' 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('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('--word', metavar="solution", help="force a particular word")
parser.add_argument('--list', metavar="wordlist", help="use a custom wordlist") 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('--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('--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") parser.add_argument('--center', action='store_true', help="center the screen")
# TODO: Implement hard mode properly (more than just a *)
# Parse the args # Parse the args
args = parser.parse_args() args = parser.parse_args()
@ -48,6 +50,16 @@ args = parser.parse_args()
# Handle the args # Handle the args
# Select the correct word list # Select the correct word list
Words, Answers, data = [], [], {"name":"", "version":"", "guesses":-1, "launch":(1970,1,1)} 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: if not args.list:
args.list = "w2" args.list = "w2"
args.list = "w" if args.classic else args.list args.list = "w" if args.classic else args.list
@ -76,20 +88,32 @@ if args.word:
else: else:
d = len(Answers) - 1 if d >= len(Answers) else 0 if d < 0 else d d = len(Answers) - 1 if d >= len(Answers) else 0 if d < 0 else d
solution = Answers[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 # End arg parsing
# ! The game is below this point # ! The game is below this point
# Good data structures to have # Good data structures to have
# Load data
for key in default_data:
if key not in data:
data[key] = default_data[key]
# Box characters! # Box characters!
GRAY, YELLOW, GREEN, WHITE, BLACK = range(5) GRAY, YELLOW, GREEN, WHITE, BLACK = range(5)
boxes = ["", "🟨", "🟩", " ", " "] boxes = data["boxes"] if "boxes" in data else default_data["boxes"]
colors= [0x13a3a3c, 0x1b59f3b, 0x1538d4e, 0xd7dadc, 0x1121213] colors = data["colors"]
# Guesses go here # Guesses go here
guesses = [] guesses = []
letters = [4] * 27
# Letter is in boxes # Letter is in boxes
def wordbox(word, showword = 0): def wordbox(word, showword = 0):
@ -101,7 +125,8 @@ def wordbox(word, showword = 0):
if guess[i] == sol[i]: if guess[i] == sol[i]:
# Mark the letter 'green' (2) # Mark the letter 'green' (2)
line[i] = GREEN line[i] = GREEN
letters[letter_num(word[i])] = GREEN if word[i] in letters:
letters[word[i]] = GREEN
# Remove letter from solution and guess # Remove letter from solution and guess
sol[i] = guess[i] = GRAY sol[i] = guess[i] = GRAY
# Yellow pass # Yellow pass
@ -109,82 +134,135 @@ def wordbox(word, showword = 0):
if guess[i] and word[i] in sol: if guess[i] and word[i] in sol:
# Mark the letter 'yellow' (1) # Mark the letter 'yellow' (1)
line[i] = YELLOW 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 # Remove letter from solution and guess
sol[sol.index(word[i])] = guess[i] = GRAY sol[sol.index(word[i])] = guess[i] = GRAY
# Gray pass # Gray pass
for i in range(len(word)): for i in range(len(word)):
if line[i] == GRAY: if word[i] in letters and \
letters[letter_num(word[i])] = GRAY 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 # Turn blocks into a string, and print it
output = "" output = ""
if showword: if showword:
# Move up to replace the input box # Move up to replace the input box
output += ui.m(0,-1)
for i in range(len(word)): for i in range(len(word)):
output += f"{c.c24(colors[WHITE])}{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: else:
for i in line: for i in line:
output += f"{c.c24(colors[WHITE])}{c.c24(colors[i])}{boxes[i]}{c.RESET}" 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'): letters = {}
return ord(char.lower()) - ord('a') keyboard = data["keyboard"]
return 26
def init_keeb():
for s in keyboard:
for char in s:
letters[char] = BLACK
return
def keeb(x, y): def keeb(x, y):
if args.nokeyboard:
return
def colorify(string): def colorify(string):
s = "" s = ""
for char in string: 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 return s
S = ["qwertyuiop", "asdfghjkl", "zxcvbnm"+u"\u23CE"] for i in range(len(keyboard)):
for i in range(len(S)): ui.uprint(x-(3*len(keyboard[i])//2), y+i, colorify(keyboard[i]))
ui.uprint(x-(3*len(S[i])//2), y+i, colorify(S[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 # intro: Print the intro text
def intro(): def intro(x, y):
title = f"{data['name']} {d}{'*' if args.hard else ''}" # Assemble the text
center = ui.m(-len(title)//2,0) HARDCHARS = [" ", "~", "*", "+"]
#spaces = ' ' * ((14 - len(title)) // 2) title = f"{data['name']} {d}{HARDCHARS[args.hard]}"
# [ Wordle 100* ] # Center the text
intro = f"{c.RESET}{center}{title}" ui.uprint(x-(len(title)//2), y, f"{c.RESET}{title}")
return intro
def game(): def game():
thicc = {"top":0, "intro": 2, "field":max_guesses+1, "keeb": 0 if args.nokeyboard else 3}
x = ui.size[0]//2 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 words = Words + Answers
guess = 0 while len(guesses) < max_guesses:
keeb(x, 3 + max_guesses) # Calculate board's starting depth
while guess < max_guesses: health = 2 + len(guesses)
# lol i should probably use curses # lol i should probably use curses
user_input = ui.uinput(x-(len(solution)//2), len(guesses) + 2, c.clear()).lower() 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, # This provides some leniency for multiple-length word lists,
# by first checking if the input is the length of the solution, # by first checking if the input is the length of the solution,
# and discarding without penalty otherwise. # 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 # Add guesses to the guessed list, and put the word in boxes
guesses.append(user_input) guesses.append(user_input)
ui.uprint(x-(len(solution)*3//2), len(guesses) + 2, wordbox(user_input, 1)) ui.uprint(x-(len(solution)*3//2), health, wordbox(user_input, 1))
# Print the new keyboard # update the keeb
keeb(x+1, 3 + max_guesses) keeb(x, thicc["intro"] + thicc["field"])
# win condition
if (user_input == solution): if (user_input == solution):
# win
win(True) win(True)
return return
else:
guess += 1
else: else:
# Allow the user to make it stop # Allow the user to make it stop
if user_input == "exit": if user_input == "exit":
end() end()
# Alert the user that the word was not found in the list, and wait for them to read it # 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.") s = "Not in word list."
sleep(1.5) ui.uprint (x-(len(s)//2), health, s)
sleep(1)
# lose # lose
win(False) win(False)
@ -192,16 +270,15 @@ def win(won):
used_guesses = len(guesses) if won else "X" used_guesses = len(guesses) if won else "X"
share = f"{data['name']} {d} {used_guesses}/{max_guesses}{'*' if args.hard else ''}\n" share = f"{data['name']} {d} {used_guesses}/{max_guesses}{'*' if args.hard else ''}\n"
for gi in guesses: 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 # ui.move to the end of the screen, and print the sharable boxes
print(f"{ui.m(0, ui.size[1] + 1)}{share}", end="") print(f"{ui.m(0, ui.size[1] + 1)}{share}", end="")
def init():
h = 2 + max_guesses + 1 + (0 if args.nokeyboard else 4)
if (args.center): if (args.center):
w, h, stty = max(30, 3*len(solution)), 7 + max_guesses, get_terminal_size() w, stty = max(30, 3*len(solution)), get_terminal_size()
# You can init a "screen" 'anywhere'! # You can init a "screen" 'anywhere'!
ui.init(w, h, (stty.columns - w) // 2, 0) ui.init(w, h, (stty.columns - w) // 2, 0)
else: else:
ui.init(max(30, 3*len(solution)), 7 + max_guesses) ui.init(max(30, 3*len(solution)), h)
game()

View File

@ -8,7 +8,7 @@ Metadata = {
"name": "Wordle Classic", "name": "Wordle Classic",
"version": "powerlanguage", "version": "powerlanguage",
"guesses": 6, "guesses": 6,
"launch": (2021, 6, 19) "launch": (2021, 6, 19),
} }
# These are not answers # These are not answers

View File

@ -8,7 +8,7 @@ Metadata = {
"name": "Wordle", "name": "Wordle",
"version": "NYT", "version": "NYT",
"guesses": 6, "guesses": 6,
"launch": (2021, 6, 19) "launch": (2021, 6, 19),
} }
# These are not answers # These are not answers