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:
		@@ -1,3 +1,6 @@
 | 
			
		||||
#!/usr/bin/python3
 | 
			
		||||
# lol
 | 
			
		||||
import game
 | 
			
		||||
 | 
			
		||||
game.init()
 | 
			
		||||
game.game()
 | 
			
		||||
game.end()
 | 
			
		||||
							
								
								
									
										10
									
								
								src/dummy.py
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								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"]
 | 
			
		||||
							
								
								
									
										185
									
								
								src/game.py
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								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('-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('--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 *)
 | 
			
		||||
 | 
			
		||||
# 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()
 | 
			
		||||
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)), 7 + max_guesses)
 | 
			
		||||
game()
 | 
			
		||||
   else:
 | 
			
		||||
      ui.init(max(30, 3*len(solution)), h)
 | 
			
		||||
							
								
								
									
										2
									
								
								src/w.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user