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:
parent
b874cfc101
commit
86280a4b55
@ -1,3 +1,6 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
# lol
|
|
||||||
import game
|
import game
|
||||||
|
|
||||||
|
game.init()
|
||||||
|
game.game()
|
||||||
|
game.end()
|
10
src/dummy.py
10
src/dummy.py
@ -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"]
|
185
src/game.py
185
src/game.py
@ -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():
|
||||||
if (args.center):
|
h = 2 + max_guesses + 1 + (0 if args.nokeyboard else 4)
|
||||||
w, h, stty = max(30, 3*len(solution)), 7 + max_guesses, get_terminal_size()
|
if (args.center):
|
||||||
|
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()
|
|
2
src/w.py
2
src/w.py
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user