Last active
July 31, 2025 03:36
-
-
Save Ian-07/f1ed977d2e1e1ad3d865cf56e42db5aa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# solver for the game Word Play: https://store.steampowered.com/app/3586660/Word_Play/ | |
# i don't have access to the full wordlist that the game uses by default (and it changes frequently as players can petition for new words to be included), but it's a modified version of https://github.com/wordnik/wordlist/blob/main/wordlist-20210729.txt, which should work in the vast majority of cases | |
# please use this responsibly! also, there may be additional modifiers i am not aware of | |
# also, due to the number of edge cases involving different types of special tiles/modifiers, this code may contain bugs | |
from copy import copy | |
import math | |
import re | |
while True: | |
try: | |
filename = input("Enter wordlist filename: ") | |
file = open(filename, "r") | |
break | |
except FileNotFoundError: | |
print(f"File {filename} not found. Make sure you have placed it in this directory, and make sure you include the file extension (e.g. 'wordlist-20210729.txt').") | |
print(f"Reading {filename}...") | |
trie = {} | |
nodes = {"": trie} | |
for line in file: | |
word = line[:-1].replace('"', '').upper() | |
if word.isalpha() and len(word) >= 1: | |
current_node = trie | |
for i in range(len(word)): | |
if word[i] not in current_node: | |
current_node[word[i]] = {} | |
current_node = current_node[word[i]] | |
nodes[word[:i+1]] = current_node | |
current_node[""] = True | |
print("Done.") | |
tile_values = { | |
"A": 1, | |
"B": 3, | |
"C": 3, | |
"D": 2, | |
"E": 1, | |
"F": 4, | |
"G": 2, | |
"H": 4, | |
"I": 1, | |
"J": 8, | |
"K": 5, | |
"L": 1, | |
"M": 3, | |
"N": 1, | |
"O": 1, | |
"P": 3, | |
"Q": 10, | |
"R": 1, | |
"S": 1, | |
"T": 1, | |
"U": 1, | |
"V": 4, | |
"W": 4, | |
"X": 8, | |
"Y": 4, | |
"Z": 10, | |
"ING": 8, | |
"QU": 10, | |
"ERS": 8, | |
"!": 0, | |
"*": 0, | |
"+": 0, | |
"=": 0, | |
">V": 1, | |
">C": 1, | |
">E": 3, | |
">G": 1 | |
} | |
strategic_bonuses = [0, 0, 0, 0, 5, 5, 5, 10, 10, 15, 15, 20, 20, 20, 25, 25, 25, 30, 40, 50] | |
casual_bonuses = [0, 0, 0, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80] | |
print("Defaulting to strategic scoring mode. Use 'mode' command to change this.") | |
bonuses = strategic_bonuses | |
grid = [] | |
def canonicalize_tile(tile): | |
try: | |
match = re.match(r'(\+|[A-Za-z*!=>]+)((?:\+?\d+|\.|\$|#|\^|@)*)', tile.upper()) | |
letter = match.group(1) | |
modifiers = re.findall(r'(\+?\d+|\.|\$|#|\^|@)', match.group(2)) | |
score = tile_values[letter] | |
special = "" | |
for modifier in modifiers: | |
if modifier.isnumeric(): | |
score = int(modifier) | |
elif modifier[0] == "+" and modifier[1:].isnumeric(): | |
score += int(modifier[1:]) | |
elif modifier in ".$#^@": | |
special = modifier | |
return letter + (str(score) if score != tile_values[letter] else "") + special | |
except AttributeError: | |
print(f"Invalid tile '{tile}'.") | |
except KeyError: | |
print(f"Unknown letter '{letter}'.") | |
def append_if_not_none(l, item): | |
if item is not None: | |
l.append(item) | |
def tiles_to_dict(tiles): | |
dict = {} | |
for tile in tiles: | |
if tile not in dict: | |
dict[tile] = 0 | |
dict[tile] += 1 | |
return dict | |
def check(word, partial=False, can_ignore_re=True): | |
if "+" in word: | |
word_split = word.split("+") | |
return len(word_split) == 2 and check(word_split[0]) and (check(word_split[1]) or (partial and check(word_split[1], True))) | |
return (word.upper() in nodes and (partial or "" in nodes[word.upper()])) or (start_with_re and can_ignore_re and word.upper()[:2] == "RE" and check(word[2:], partial, can_ignore_re=False)) | |
def get_potential_words_child(potential_words, letter, previous_tile_letter=None): | |
potential_words_child = [] | |
for potential_word in potential_words: | |
match letter: | |
case "*": | |
branches_to_check = list("abcdefghijklmnopqrstuvwxyz") | |
case "!": | |
branches_to_check = [""] | |
case "=" if previous_tile_letter is not None: | |
branches_to_check = [previous_tile_letter.lower()] | |
case ">V": | |
branches_to_check = list(f"aeiou{"y" if y_is_vowel else ""}") | |
case ">C": | |
branches_to_check = list(f"dlnrst{"z" if s_z_interchangeable else ""}") | |
case ">E": | |
branches_to_check = ["e"] | |
case ">G": | |
letters_in_grid = set() | |
for tile in grid: | |
grid_tile_letter = parse_tile(tile)[0] | |
if grid_tile_letter.isalpha(): | |
letters_in_grid.add(grid_tile_letter.lower()) | |
if s_z_interchangeable and grid_tile_letter in "SZ": | |
letters_in_grid.add("s") | |
letters_in_grid.add("z") | |
branches_to_check = sorted(list(letters_in_grid)) | |
case "S" if s_z_interchangeable: | |
branches_to_check = list("Sz") | |
case "Z" if s_z_interchangeable: | |
branches_to_check = list("sZ") | |
case _: | |
branches_to_check = [letter] | |
for branch in branches_to_check: | |
if check(potential_word + branch, True): | |
potential_words_child.append(potential_word + branch) | |
return potential_words_child | |
def parse_tile(tile): | |
match = re.match(r'(\+|[A-Za-z*!=>]+)(\d*)([.$#^@]?)', tile) | |
letter = match.group(1) | |
tile_score = int(match.group(2)) if match.group(2) != "" else (tile_values[letter] if letter in tile_values else 0) | |
special = match.group(3) | |
return letter, tile_score, special | |
def generate_plays(tiles, grid_dict, potential_words, required_conditions): | |
global valid_plays | |
if len(tiles) >= 4 and len(list(filter(lambda x: check(x), potential_words))) >= 1: | |
play_stats = score_play(tiles) | |
if play_stats[0] >= min_score and all([n in play_stats[2] for n in required_conditions]): | |
valid_plays[" ".join(tiles)] = play_stats | |
for tile in grid_dict: | |
tiles_copy = copy(tiles) | |
tiles_copy.append(tile) | |
grid_dict_copy = copy(grid_dict) | |
grid_dict_copy[tile] -= 1 | |
if grid_dict_copy[tile] == 0: | |
del grid_dict_copy[tile] | |
letter = parse_tile(tile)[0] | |
previous_tile_letter = parse_tile(tiles[-1])[0] if len(tiles) >= 1 else None | |
potential_words_child = get_potential_words_child(potential_words, letter, previous_tile_letter) | |
if len(potential_words_child) >= 1 and (len(tiles) == 0 or tiles[-1][0] != "!" or letter == "!"): | |
generate_plays(tiles_copy, grid_dict_copy, potential_words_child, required_conditions) | |
def leave(tiles, tiles_played): | |
tiles_dict = tiles_to_dict(tiles) | |
for tile in tiles_played: | |
if tile in tiles_dict: | |
tiles_dict[tile] -= 1 | |
if tiles_dict[tile] == 0: | |
del tiles_dict[tile] | |
leave_list = [] | |
for tile in tiles_dict: | |
for i in range(tiles_dict[tile]): | |
leave_list.append(tile) | |
return sorted(leave_list) | |
def is_vowel(str): | |
return str[0].upper() in "AEIOU" + ("Y" if y_is_vowel else "") or str in [">V", ">E"] | |
def is_consonant(str): | |
return not is_vowel(str) and str[0] not in ["!", "*", "+", "=", ">G"] | |
def modifier_to_str(mod): | |
return " ".join([str(j) for j in mod]) if mod[0] in valid_misc_mods else (("" if mod[0] is None else (" ".join([str(j) for j in mod[0]]) + " ")) + ("" if mod[2] is None else ("if " if mod[1] else "unless ") + " ".join([str(j) for j in mod[2]]))) | |
def score_play(tiles): | |
play_leave = leave(grid, tiles) | |
parsed_tiles = [parse_tile(tile) for tile in tiles] | |
potential_words = [""] | |
for i in range(len(tiles)): | |
potential_words = get_potential_words_child(potential_words, parsed_tiles[i][0], parsed_tiles[i - 1][0]) | |
try: | |
chosen_word = list(filter(lambda x: check(x), potential_words))[0] | |
except IndexError: | |
chosen_word = None | |
conditions = [] | |
for i in range(len(mods)): | |
mod = mods[i] | |
try: | |
if len(mod) >= 3 and type(mod[2]) is tuple: | |
match mod[2][0]: | |
case "consecutiveconsonants": | |
conditions.append(any([is_consonant(chosen_word[i]) and is_consonant(chosen_word[i+1]) for i in range(len(chosen_word)-1)])) | |
case "consecutivevowels": | |
conditions.append(any([is_vowel(chosen_word[i]) and is_vowel(chosen_word[i+1]) for i in range(len(chosen_word)-1)])) | |
case "contains": | |
conditions.append(mod[2][1].upper() in chosen_word.upper()) | |
case "containsmultiple": | |
conditions.append(chosen_word.upper().count(mod[2][1].upper()) >= mod[2][2]) | |
case "containsnum": | |
nums = ["ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "TEN", "ELEVEN", "TWELVE", "THIRTEEN", "FOURTEEN", "FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN", "NINETEEN", "TWENTY"] | |
conditions.append(any([num in chosen_word.upper() for num in nums])) | |
case "endswith": | |
s = mod[2][1].upper() | |
conditions.append(chosen_word.upper()[-len(s):] == s) | |
case "everyletterunique": | |
conditions.append(len(set(chosen_word.upper())) == len(chosen_word.upper())) | |
case "firstandlastsame": | |
conditions.append(chosen_word.upper()[0] == chosen_word.upper()[-1]) | |
case "firstandlastvowels": | |
conditions.append(is_vowel(chosen_word[0]) and is_vowel(chosen_word[-1])) | |
case "includestile": | |
conditions.append(mod[2][1].upper() in tiles) | |
case "isvowel": | |
conditions.append(is_vowel(parsed_tiles[mod[2][1]-1][0])) | |
case "lasttileisvowel": | |
conditions.append(is_vowel(parsed_tiles[-1][0])) | |
case "letterpair": | |
conditions.append(any([chosen_word.upper()[i] == chosen_word.upper()[i+1] for i in range(len(chosen_word)-1)])) | |
case "maxtiles": | |
conditions.append(len(tiles) <= mod[2][1]) | |
case "minspecialsleft": | |
conditions.append(len(list(filter(lambda x: x[2] != "", [parse_tile(tile) for tile in play_leave]))) >= mod[2][1]) | |
case "mindifferentspecials": | |
distinct_specials = set() | |
for parsed_tile in parsed_tiles: | |
if parsed_tile[2] != "": | |
distinct_specials.add(parsed_tile[2]) | |
conditions.append(len(distinct_specials) >= mod[2][1]) | |
case "mindifferentvowels": | |
distinct_vowels = set() | |
for c in chosen_word: | |
if is_vowel(c): | |
distinct_vowels.add(c.upper()) | |
conditions.append(len(distinct_vowels) >= mod[2][1]) | |
case "mintiles": | |
conditions.append(len(tiles) >= mod[2][1]) | |
case "morevowelsthanconsonants": | |
conditions.append(len(list(filter(lambda x: is_vowel(x), chosen_word))) > len(list(filter(lambda x: is_consonant(x), chosen_word)))) | |
case "numtiles": | |
conditions.append(len(tiles) == mod[2][1]) | |
case "startswith": | |
s = mod[2][1].upper() | |
conditions.append(chosen_word.upper()[:len(s)] == s) | |
case _: | |
conditions.append(None) | |
else: | |
conditions.append(None) | |
except IndexError: | |
print(f"WARNING: modifier #{i+1} ('{modifier_to_str(mod)}') was ignored because it contained insufficiently many parameters. Try 'removemod {i+1}' and readding it with the correct parameters.") | |
except TypeError: | |
print(f"WARNING: modifier #{i+1} ('{modifier_to_str(mod)}') was ignored because it contained the incorrect data type for one of its parameters. Try 'removemod {i+1}' and readding it with the correct parameters.") | |
golden_mult = 0 | |
word_score = 0 | |
emeralds = [] | |
word_mults = [] | |
bonus_score = 0 | |
bonus_mults = [] | |
final_mults = [] | |
leave_size = len(list(filter(lambda x: x[0] != ">", play_leave))) | |
num_specials = 0 | |
parsed_consonant_values = [(math.inf if not is_consonant(parsed_tile[0]) else parsed_tile[1]) for parsed_tile in parsed_tiles] | |
lowest_consonant_value = min(parsed_consonant_values) | |
lowest_consonant_index = None if lowest_consonant_value == math.inf else parsed_consonant_values.index(lowest_consonant_value) | |
for i in range(len(tiles)): | |
(letter, tile_score, special) = parsed_tiles[i] | |
if letter == "!" and all(j[0] == "!" for j in tiles[i+1:]): | |
tile_score = leave_size | |
if letter == "=" and i >= 1: | |
tile_score = parse_tile(tiles[i-1])[1] | |
for j in range(len(mods)): | |
try: | |
if type(mods[j][0]) is tuple and conditions[j] == mods[j][1] and (mods[j][0][0] == "alltilesadd" or (mods[j][0][0] == "lasttileadd" and i == len(tiles)-1)): | |
tile_score += mods[j][0][1] | |
except IndexError: | |
print(f"WARNING: modifier #{j+1} ('{modifier_to_str(mod)}') was ignored because it contained insufficiently many parameters. Try 'removemod {j+1}' and readding it with the correct parameters.") | |
except TypeError: | |
print(f"WARNING: modifier #{j+1} ('{modifier_to_str(mod)}') was ignored because it contained the incorrect data type for one of its parameters. Try 'removemod {j+1}' and readding it with the correct parameters.") | |
if vowels_zero and is_vowel(letter): | |
tile_score = 0 | |
if (i >= 1 and letter == parsed_tiles[i-1][0]) or (i <= len(tiles)-2 and letter == parsed_tiles[i+1][0]): | |
tile_score *= adjacent_tile_same_letter_mult | |
if waltz and len(tiles) == 5 and (i == 0 or i == 4): | |
tile_score *= 3 | |
for j in range(len(mods)): | |
try: | |
if type(mods[j][0]) is tuple and conditions[j] == mods[j][1]: | |
if mods[j][0][0] == "tilemult" and i == mods[j][0][1]-1: | |
tile_score *= mods[j][0][2] | |
elif mods[j][0][0] == "lowestconsonantmult" and i == lowest_consonant_index: | |
tile_score *= mods[j][0][1] | |
except IndexError: | |
print(f"WARNING: modifier #{j+1} ('{modifier_to_str(mod)}') was ignored because it contained insufficiently many parameters. Try 'removemod {j+1}' and readding it with the correct parameters.") | |
except TypeError: | |
print(f"WARNING: modifier #{j+1} ('{modifier_to_str(mod)}') was ignored because it contained the incorrect data type for one of its parameters. Try 'removemod {j+1}' and readding it with the correct parameters.") | |
if special == "" and letter[0] not in "=>" and (golden[i] or (midas_touch and i >= 1 and parsed_tiles[i-1][2] == "$")): | |
special = "$" | |
match special: | |
case "." if i == len(tiles) - 1: | |
word_mults.append(dot_tile_mult) | |
case "$": | |
golden_mult += 1 | |
case "#": | |
emeralds.append(4*tile_score) | |
if special != "": | |
num_specials += 1 | |
word_score += tile_score | |
bonus_score += bonuses[i] * (2 if double_bonus else 1) | |
if odd10even5: | |
bonus_score += (10 if len(tiles) % 2 == 1 else -5) | |
if last_tile_bonus: | |
bonus_score += parsed_tiles[-1][1] | |
if special_tile_mult and num_specials >= 2: | |
final_mults.append(num_specials) | |
for i in range(len(mods)): | |
mod = mods[i] | |
try: | |
if type(mod[0]) is tuple and conditions[i] == mod[1]: | |
match mod[0][0]: | |
case "bonus": | |
bonus_score += mod[0][1] | |
case "bonusmult": | |
bonus_mults.append(mod[0][1]) | |
case "finalmult": | |
final_mults.append(mod[0][1]) | |
case "finalmultbynumberof": | |
final_mults.append(max(chosen_word.upper().count(mod[0][1].upper()), 1)) | |
case "wordmult": | |
word_mults.append(mod[0][1]) | |
except IndexError: | |
print(f"WARNING: modifier #{i+1} ('{modifier_to_str(mod)}') was ignored because it contained insufficiently many parameters. Try 'removemod {i+1}' and readding it with the correct parameters.") | |
except TypeError: | |
print(f"WARNING: modifier #{i+1} ('{modifier_to_str(mod)}') was ignored because it contained the incorrect data type for one of its parameters. Try 'removemod {i+1}' and readding it with the correct parameters.") | |
golden_mult = max(golden_mult, 1) | |
for word_mult in word_mults: | |
word_score = round(word_score * word_mult) | |
for i in range(len(emeralds)): | |
emeralds[i] = round(emeralds[i] * word_mult) | |
word_score *= golden_mult | |
for i in range(len(emeralds)): | |
emeralds[i] *= golden_mult | |
for bonus_mult in bonus_mults: | |
bonus_score = round(bonus_score * bonus_mult) | |
final_score = word_score + max(bonus_score, 0) # i don't actually know whether bonus points are allowed to be negative, but i'm assuming they probably aren't | |
for final_mult in final_mults: | |
final_score = round(final_score * final_mult) | |
for i in range(len(emeralds)): | |
emeralds[i] = round(emeralds[i] * final_mult) | |
if double_bonus and len(tiles) <= 5: | |
final_score = 0 | |
for i in range(len(emeralds)): | |
emeralds[i] = 0 | |
conditions_met = [] | |
for i in range(len(mods)): | |
mod = mods[i] | |
if type(mod[0]) is tuple and conditions[i] == mod[1] and mod[0][0] == "zero": | |
final_score = 0 | |
for i in range(len(emeralds)): | |
emeralds[i] = 0 | |
if len(mod) <= 1 or type(mod[1]) is not bool or conditions[i] == mod[1]: | |
conditions_met.append(i) | |
return final_score, emeralds, conditions_met, play_leave, chosen_word | |
def valuation(play_stats): | |
return play_stats[0] + sum(play_stats[1])/(2 if emerald_multiple and len(play_stats[1]) >= 2 else 4) | |
def display_plays(n): | |
global sorted_plays | |
table = [["#", "Score", "Challenges", "Word", "Play", "Leave"]] | |
max_column_lengths = [len(i) for i in table[0]] | |
for i in range(min(n, len(sorted_plays))): | |
(play, (min_score, emeralds, conditions_met, play_leave, chosen_word)) = sorted_plays[i] | |
next_row = [f"{str(i+1)}.", (str(min_score) + "".join([f" (+{emerald})" for emerald in emeralds])), ", ".join([f"#{n+1}" for n in list(filter(lambda x: mods[x][0] is None, conditions_met))]), (chosen_word if chosen_word is not None else "(no valid words found)"), play, " ".join(play_leave)] | |
table.append(next_row) | |
for j in range(len(next_row)): | |
max_column_lengths[j] = max(max_column_lengths[j], len(next_row[j])) | |
for row in table: | |
print(" ".join([row[i].ljust(max_column_lengths[i]) for i in range(len(row))])) | |
mods = [] | |
sorted_plays = {} | |
n = 15 | |
valid_misc_mods = {"adjacenttilesamelettermult", "dottilemult", "doublebonus", "emeraldmultiple", "golden", "lasttilebonus", "lockedfirsttile", "midastouch", "minscore", "odd10even5", "re", "specialtilemult", "szinterchangeable", "vowelszero", "waltz", "yisvowel"} | |
valid_effects = {"alltilesadd", "bonus", "bonusmult", "finalmult", "finalmultbynumberof", "lasttileadd", "lowestconsonantmult", "tilemult", "wordmult", "zero"} | |
valid_conditions = {"consecutiveconsonants", "consecutivevowels", "contains", "containsmultiple", "containsnum", "endswith", "everyletterunique", "firstandlastsame", "firstandlastvowels", "includestile", "isvowel", "lasttileisvowel", "letterpair", "maxtiles", "mindifferentspecials", "mindifferentvowels", "minspecialsleft", "mintiles", "morevowelsthanconsonants", "numtiles", "startswith"} | |
while True: | |
command = input("Enter command (type 'help' for help): ").split(" ") | |
try: | |
adjacent_tile_same_letter_mult = 1 | |
dot_tile_mult = 2 | |
double_bonus = False | |
emerald_multiple = False | |
golden = [False for i in range(20)] | |
last_tile_bonus = False | |
locked_first_tile = [] | |
midas_touch = False | |
min_score = 0 | |
odd10even5 = False | |
s_z_interchangeable = False | |
special_tile_mult = False | |
start_with_re = False | |
vowels_zero = False | |
waltz = False | |
y_is_vowel = False | |
for i in range(len(mods)): | |
mod = mods[i] | |
try: | |
if type(mod[0]) is str: | |
match mod[0]: | |
case "adjacenttilesamelettermult": | |
adjacent_tile_same_letter_mult = mod[1] | |
print(adjacent_tile_same_letter_mult) | |
case "dottilemult": | |
dot_tile_mult = mod[1] | |
case "doublebonus": | |
double_bonus = True | |
case "emeraldmultiple": | |
emerald_multiple = True | |
case "golden": | |
if 1 <= mod[1] <= len(golden): | |
golden[mod[1]-1] = True | |
case "lasttilebonus": | |
last_tile_bonus = True | |
case "lockedfirsttile": | |
locked_first_tile = [mod[1].upper()] | |
case "midastouch": | |
midas_touch = True | |
case "minscore": | |
min_score = mod[1] | |
case "odd10even5": | |
odd10even5 = True | |
case "re": | |
start_with_re = True | |
case "specialtilemult": | |
special_tile_mult = True | |
case "szinterchangeable": | |
s_z_interchangeable = True | |
case "vowelszero": | |
vowels_zero = True | |
case "waltz": | |
waltz = True | |
case "yisvowel": | |
y_is_vowel = True | |
except IndexError: | |
print(f"WARNING: modifier #{i+1} ('{modifier_to_str(mod)}') was ignored because it contained insufficiently many parameters. Try 'removemod {i+1}' and readding it with the correct parameters.") | |
except TypeError: | |
print(f"WARNING: modifier #{i+1} ('{modifier_to_str(mod)}') was ignored because it contained the incorrect data type for one of its parameters. Try 'removemod {i+1}' and readding it with the correct parameters.") | |
match command[0].lower(): | |
case "add": | |
for tile in command[1:]: | |
append_if_not_none(grid, canonicalize_tile(tile)) | |
grid = sorted(grid) | |
print(f"Set grid to {" ".join(grid)}") | |
case "check": | |
for word in command[1:]: | |
print(f"{word} is {"" if check(word) else "NOT "}valid") | |
case "expand": | |
try: | |
n = int(command[1]) if len(command) >= 2 else n*2 | |
except ValueError: | |
n = n*2 | |
display_plays(n) | |
case "gen": | |
try: | |
n = int(command[1]) if len(command) >= 2 else 15 | |
except ValueError: | |
n = 15 | |
required_conditions = sorted(set([int(n)-1 for n in list(filter(lambda x: x.isnumeric(), command[2:]))])) | |
valid_plays = {} | |
print(f"Generating and scoring valid plays{f" meeting condition{"s" if len(required_conditions) >= 2 else ""} {", ".join([f"#{n+1}" for n in required_conditions])}" if len(required_conditions) >= 1 else ""}...") | |
generate_plays(locked_first_tile, tiles_to_dict(grid), ["".join([parse_tile(tile)[0] for tile in locked_first_tile])], required_conditions) | |
print("Done.") | |
print("Sorting plays by score...") | |
sorted_plays = sorted(valid_plays.items(), key=lambda x: valuation(x[1]), reverse=True) | |
print("Done.") | |
display_plays(n) | |
case "grid": | |
grid = [] | |
for tile in command[1:]: | |
append_if_not_none(grid, canonicalize_tile(tile)) | |
grid = sorted(grid) | |
print(f"Set grid to {" ".join(grid)}") | |
case "help": | |
print('''COMMAND LIST: | |
add: | |
Adds tiles to the grid without resetting. | |
add [list of tiles]: Adds tiles based on a list separated by spaces; see notes below on tile notation. | |
check: | |
Checks if a word (or words) is valid: | |
check [string]: Checks all words if separated by plusses, and also accounts for "RE-" if the "any word can start with RE-" modifier is active. | |
expand: | |
Prints a list that was previously generated by the 'gen' command, but with a different numbe of top plays. | |
expand [number]: Lists the top [number] moves; defaults to twice the previous value. | |
gen: | |
Generates a list of the highest scoring plays. | |
gen [number]: Lists the top [number] moves; defaults to 15. | |
gen [number] [list of required challenges]: Lists the top [number] moves which meet all required challenges. | |
grid: | |
Sets the tile grid. | |
grid [list of tiles]: Sets the grid based on a list of tiles separated by spaces; see notes below on tile notation. | |
help: | |
Prints this list. | |
mod: | |
Adds a modifier; see notes below on effects and conditions. | |
mod [effect]: Applies an effect for all plays. | |
mod [effect] if [condition]: Applies an effect for plays that meet a given condition. | |
mod [effect] unless [condition]: Applies an effect for plays that do NOT meet a given condition. | |
mod if [condition]: Does not affect play score, but tracks which plays meet a given condition. | |
mod unless [condition]: Does not affect play score, but tracks which plays do NOT meet a given condition. | |
mod adjacenttilesamelettermult [num]: If two adjacent tiles are the same letter, multiply both of their tile scores by [num]. | |
mod dottilemult [num]: Changes dot tile multiplier (default: 2.0). | |
mod doublebonus: Doubles the Bonus Point rewards for spelling longer words; words five tiles or shorter score 0 points. | |
mod emeraldmultiple: If the play contains more than one emerald tile, each emerald is twice as likely to trigger. | |
mod golden [pos]: When submitting, convert the tile at position [pos] into a golden tile. | |
mod lasttilebonus: Add bonus points equal to the last tile's score. | |
mod lockedfirsttile [str]: The first tile slot has a locked [str] tile placed there. | |
mod midastouch: For standard tiles, if the tile to the left is a golden, this tile will also be considered a golden. Does not apply to special (emerald, golden, diamond, dotted, potion), glass, or mirror tiles. | |
mod minscore [num]: Only display plays scoring at least [num] points. | |
mod odd10even5: If the play has an odd number of tiles, add 10 bonus points; if the play has an even number of tiles, remove 5 bonus points (minimum 0). | |
mod re: Any word can have "RE-" added to it and still be a word. | |
mod specialtilemult: The final score is multiplied by number of special tiles in the submission. | |
mod szinterchangeable: S and Z are always interchangeable. | |
mod vowelszero: Vowel tiles always score zero points. | |
mod waltz: If the play has exactly 5 tiles, the first and last tiles score 3x. | |
mod yisvowel: Y is always considered a vowel. | |
mode: | |
Changes how bonus scores are applied. | |
mode casual: Use casual (quickplay) scoring. | |
mode strategic: Use strategic (easy, normal, hard, legendary, marathon, ultramarathon) scoring. | |
modlist: | |
Lists all active modifiers. | |
remove: | |
Removes tiles from the grid without resetting. | |
remove [list of tiles]: Removes tiles based on a list separated by spaces; see notes below on tile notation. | |
removemod: | |
Removes modifiers. | |
remove [list of nums]: Removes modifiers with these indices, as shown by the 'modlist' command. | |
remove all: Removes all modifiers. | |
score: | |
Calculates the score of a single play. | |
score [list of tiles]: Calculates a play based on a list of tiles separated by spaces; see notes below on tile notation. Prints the minimum score, the potential bonuses from emerald tiles, and the tiles left behind. | |
NOTES ON CONDITIONS: | |
consecutiveconsonants: word has two consonants next to each other | |
consecutivevowels: word has two vowels next to each other | |
contains [str]: word contains [str] as a substring | |
containsmultiple [str] [num]: word contains [num] instances of [str] | |
containsnum: word contains the name of any number from ONE to TWENTY as a substring | |
endswith [str]: word ends with [str] | |
everyletterunique: every letter in the word is unique | |
firstandlastsame: word has same first and last letters | |
firstandlastvowels: first and last letters of the word are both vowels | |
includestile [tile]: play includes highlighted tile | |
isvowel [pos]: tile in position [pos] is a vowel | |
lasttileisvowel: last tile (not letter) of play starts with a vowel | |
letterpair: word contains two of the same letter in a row | |
maxtiles [num]: play contains at most [num] tiles | |
mindifferentspecials [num]: play contains at least [num] different types of special tiles (golden, emerald, diamond, potion, dot) | |
mindifferentvowels [num]: word has at least [num] different vowels | |
minspecialsleft [num]: play leaves behind at least [num] special tiles | |
mintiles [num]: play contains at least [num] tiles | |
morevowelsthanconsonants: word has more vowels than consonants | |
numtiles [num]: play contains exactly [num] tiles | |
startswith [str]: word starts with [str] | |
NOTES ON EFFECTS: | |
alltilesadd [num]: add [num] points to values of all tiles | |
bonus [num]: add bonus points | |
bonusmult [num]: add multiplier to bonus score | |
finalmult [num]: add multiplier to final score | |
finalmultbynumberof [letter]: multiply final score by number of [letter] in the word | |
lasttileadd [num]: add [num] points to value of last tile | |
lowestconsonantmult [num]: multiply value of lowest scoring consonant in play by [num] | |
tilemult [pos] [num]: multiply score of tile in position [pos] by [num] | |
wordmult [num]: add multiplier to word score | |
zero: score zero | |
NOTES ON TILE NOTATION: | |
Each individual tile must first be specified by the character(s) that appear on it. This can be any single letter from A to Z, the multigraphs "ERS", "QU", and "ING", or the special tiles "*", "!", "+", and "=" (mirror tile; copies letter and value of tile to left). You can also use ">V" (pick a vowel), ">C" (pick a consonant), ">E" (shuffle for E), or ">G" (create glass duplicate of tile) to stand in for those respective upgrades, though these are imperfect (e.g. the scores may be different, and you may want to select different vowels/consonants in some scenarios). All other modifiers can be applied in any order, without spaces separating them (e.g. A10$ or A$10 for a 10-point golden A). | |
10: sets the score of a tile to 10 (can be set to any integer) | |
+10: adds 10 points to a tile's existing value (can be set to any integer) | |
.: dot tile (doubles word score if placed at end of play) | |
$: golden tile (word score is multiplied by number of golden tiles in play) | |
#: emerald tile (letter score has a 1 in 4 chance of being multiplied by 5) | |
^: diamond tile (add points over time; this effect is already covered by +10 above, so the only effect this has is that it interacts with the 'mindifferentspecials' condition) | |
@: potion tile (has no direct effect on scoring, so the only effect this has is that it interacts with the 'mindifferentspecials' condition)''') | |
case "mod": | |
params = [] | |
for param in command[1:]: | |
try: | |
params.append(int(param)) | |
except ValueError: | |
try: | |
params.append(float(param)) | |
except ValueError: | |
params.append(param.lower()) | |
if len(params) >= 1: | |
if params[0] in valid_misc_mods: | |
if "if" in params[:-1] or "unless" in params[:-1]: | |
print(f"Modifier '{params[0]}' does not support conditions.") | |
else: | |
to_append = tuple(params) | |
elif "if" in params[:-1]: | |
index = params.index("if") | |
effect = params[:index] | |
condition = params[index+1:] | |
to_append = (tuple(effect) if len(effect) >= 1 else None, True, tuple(condition)) | |
elif "unless" in params[:-1]: | |
index = params.index("unless") | |
effect = params[:index] | |
condition = params[index + 1:] | |
to_append = (tuple(effect) if len(effect) >= 1 else None, False, tuple(condition)) | |
else: | |
to_append = (tuple(params), None, None) | |
if type(to_append[0]) is tuple and to_append[0][0] not in valid_effects: | |
print(f"Invalid effect name '{to_append[0][0]}'.\nValid effect names are {", ".join([f"'{s}'" for s in sorted(valid_effects)])}.\nOther valid modifiers are {", ".join([f"'{s}'" for s in sorted(valid_misc_mods)])}.") | |
elif len(to_append) >= 3 and type(to_append[2]) is tuple and to_append[2][0] not in valid_conditions: | |
print(f"Invalid condition name '{to_append[2][0]}'.\nValid conditions are {", ".join([f"'{s}'" for s in sorted(valid_conditions)])}.") | |
else: | |
mods.append(to_append) | |
print(f"Added modifier '{" ".join(command[1:]).lower()}'.") | |
case "mode": | |
match command[1].lower(): | |
case "strategic" | "easy" | "normal" | "hard" | "legendary" | "marathon" | "ultramarathon": | |
bonuses = strategic_bonuses | |
case "casual" | "quickplay": | |
bonuses = casual_bonuses | |
case _: | |
print("Unknown value. Try setting the mode to either 'strategic' or 'casual'.") | |
case "modlist": | |
if len(mods) == 0: | |
print("No active modifiers.") | |
else: | |
for i in range(len(mods)): | |
mod = mods[i] | |
print(f"{i+1}. {modifier_to_str(mod)}") | |
case "remove": | |
for tile in command[1:]: | |
if tile.upper() in grid: | |
grid.remove(canonicalize_tile(tile)) | |
grid = sorted(grid) | |
print(f"Set grid to {" ".join(grid)}") | |
case "removemod": | |
indices_to_remove = set() | |
for param in command[1:]: | |
if param == "all": | |
mods = [] | |
print("Removed all modifiers.") | |
else: | |
try: | |
k = int(param)-1 | |
indices_to_remove.add(k) | |
except ValueError: | |
print(f"Invalid index '{param}'.") | |
if len(indices_to_remove) >= 1: | |
for k in sorted(list(indices_to_remove), reverse=True): | |
if len(mods) > k: | |
del mods[k] | |
print(f"Removed modifier{"s" if len(indices_to_remove) >= 2 else ""} {", ".join([f"#{k+1}" for k in sorted(list(indices_to_remove))])}.") | |
case "score": | |
(min_score, emeralds, conditions_met, play_leave, chosen_word) = score_play([canonicalize_tile(tile) for tile in command[1:]]) | |
print(f"Score: {min_score}{"".join([f" (+{emerald})" for emerald in emeralds])}") | |
print(f"Challenges met: {", ".join([f"#{n+1}" for n in list(filter(lambda x: mods[x][0] is None, conditions_met))])}") | |
print(f"Leave: {" ".join(play_leave)}") | |
print(f"Word: {chosen_word if chosen_word is not None else "(no valid words found)"}") | |
case _: | |
print(f"Unknown command '{command[0]}'.") | |
except IOError: | |
print(f"Not enough arguments provided for command '{command[0]}'.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment