Skip to content

Instantly share code, notes, and snippets.

@Ian-07
Last active July 31, 2025 03:36
Show Gist options
  • Save Ian-07/f1ed977d2e1e1ad3d865cf56e42db5aa to your computer and use it in GitHub Desktop.
Save Ian-07/f1ed977d2e1e1ad3d865cf56e42db5aa to your computer and use it in GitHub Desktop.
# 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