Skip to content

Instantly share code, notes, and snippets.

@RandomComputerUser
Created December 4, 2024 02:30
Show Gist options
  • Save RandomComputerUser/06f52f3f28d5175c38bae3442fbf19de to your computer and use it in GitHub Desktop.
Save RandomComputerUser/06f52f3f28d5175c38bae3442fbf19de to your computer and use it in GitHub Desktop.
"""See https://terraria.wiki.gg/wiki/Modifiers."""
from collections.abc import Iterable
from dataclasses import dataclass
import random
from typing import Final
REFORGE_COST_MULT: Final[float] = 5 / 3 # relative to sell price
@dataclass(frozen=True)
class Modifier:
name: str
value_increase_pct: float
affects_knockback: bool = False
@property
def value_mult(self) -> float:
return 1.0 + 0.01 * self.value_increase_pct
def parse_wiki_modifier_table(
table: str, knockback_col: int, value_col: int
) -> list[Modifier]:
lines = table.splitlines()
modifiers: list[Modifier] = []
for line in lines:
words = line.split()
if not words:
continue
value = float(words[value_col][:-1].replace("\u2212", "-"))
name = words[0]
knockback = words[knockback_col] != "-"
modifiers.append(Modifier(name, value, knockback))
return modifiers
ACCESSORY_MODIFIERS: Final[list[Modifier]] = [
Modifier("Hard", 10.25),
Modifier("Guarding", 21),
Modifier("Armored", 32.25),
Modifier("Warding", 44),
Modifier("Precise", 21),
Modifier("Lucky", 44),
Modifier("Jagged", 10.25),
Modifier("Spiked", 21),
Modifier("Angry", 32.25),
Modifier("Menacing", 44),
Modifier("Brisk", 10.25),
Modifier("Fleeting", 21),
Modifier("Hasty", 32.25),
Modifier("Quick", 44),
Modifier("Wild", 10.25),
Modifier("Rash", 21),
Modifier("Intrepid", 32.25),
Modifier("Violent", 44),
Modifier("Arcane", 32.25),
]
UNIVERSAL_MODIFIERS: Final[list[Modifier]] = parse_wiki_modifier_table(
"""
Keen - +3% - +1 +12.36%
Superior +10% +3% +10% +2 +64.51%
Forceful - - +15% +1 +32.25%
Broken −30% - −20% −2 −68.64%
Damaged −15% - - −1 −27.75%
Shoddy −10% - −15% −2 −41.48%
Hurtful +10% - - +1 +21%
Strong - - +15% +1 +32.25%
Unpleasant +5% - +15% +2 +45.81%
Weak - - −20% −2 −36%
Ruthless +18% - −10% +1 +12.78%
Godly +15% +5% +15% +2 +111.63%
Demonic +15% +5% - +2 +60.02%
Zealous - +5% - +1 +21%
""",
3,
5,
)
# swords, pickaxes, hammers, axes, hamaxes, magic, summon
COMMON_MODIFIERS: Final[list[Modifier]] = parse_wiki_modifier_table(
"""
Quick - +10% - - +1 +21%
Deadly +10% +10% - - +2 +46.41%
Agile - +10% +3% - +1 +35.96%
Nimble - +5% - - +1 +10.25%
Murderous +7% +6% +3% - +2 +44.54%
Slow - −15% - - −1 −27.75%
Sluggish - −20% - - −2 −36%
Lazy - −8% - - −1 −15.36%
Annoying −20% −15% - - −2 −53.76%
Nasty +5% +10% +2% −10% +1 +16.87%
""",
4,
6,
)
# ranged
# removed duplicate Deadly modifier, which is different for ranged weapons with knockback
COMMON_RANGED_MODIFIERS: Final[list[Modifier]] = parse_wiki_modifier_table(
"""
Quick - +10% - - +1 +21%
Agile - +10% +3% - +1 +35.96%
Nimble - +5% - - +1 +10.25%
Murderous +7% +6% +3% - +2 +44.54%
Slow - −15% - - −1 −27.75%
Sluggish - −20% - - −2 −36%
Lazy - −8% - - −1 −15.36%
Annoying −20% −15% - - −2 −53.76%
Nasty +5% +10% +2% −10% +1 +16.87%
""",
4,
6,
)
# swords, pickaxes, hammers, axes, hamaxes, whips
MELEE_MODIFIERS: Final[list[Modifier]] = parse_wiki_modifier_table(
"""
Large - - - +12% - +1 +25.44%
Massive - - - +18% - +1 +39.24%
Dangerous +5% - +2% +5% - +1 +31.47%
Savage +10% - - +10% +10% +2 +77.16%
Sharp +15% - - - - +1 +32.25%
Pointy +10% - - - - +1 +21%
Tiny - - - −18% - −1 −32.76%
Terrible −15% - - −13% −15% −2 −60.49%
Small - - - −10% - −1 −19%
Dull −15% - - - - −1 −27.75%
Unhappy - −10% - −10% −10% −2 −46.86%
Bulky +5% −15% - +10% +10% +1 +16.62%
Shameful −10% - - +10% −20% −2 −37.27%
Heavy - −10% - - +15% - +7.12%
Light - +15% - - −10% - +7.12%
Legendary +15% +10% +5% +10% +15% +2 +209.85%
""",
5,
7,
)
RANGED_MODIFIERS: Final[list[Modifier]] = parse_wiki_modifier_table(
"""
Sighted +10% - +3% - - +1 +35.96%
Rapid - +15% - +10% - +2 +60.02%
Hasty - +10% - +15% - +2 +60.02%
Intimidating - - - +5% +15% +2 +45.81%
Deadly +10% +5% +2% +5% +5% +2 +75.38%
Staunch +10% - - - +15% +2 +60.02%
Awful −15% - - −10% −10% −2 −52.6%
Lethargic - −15% - −10% - −2 −41.48%
Awkward - −10% - - −20% −2 −48.16%
Powerful +15% −10% +1% - - +1 +11.45%
Frenzying −15% +15% - - - - −4.45%
Unreal +15% +10% +5% +10% +15% +2 +209.85%
""",
5,
7,
)
MAGIC_MODIFIERS: Final[list[Modifier]] = parse_wiki_modifier_table(
"""
Mystic +10% - - −15% - +2 +60.02%
Adept - - - −15% - +1 +32.25%
Masterful +15% - - −15% +5% +2 +92.83%
Inept - - - +10% - −1 −19%
Ignorant −10% - - +20% - −2 −48.16%
Deranged −10% - - - −10% −1 −34.39%
Intense +10% - - +15% - −1 −12.58%
Taboo - +10% - +10% +10% +1 +18.59%
Celestial +10% −10% - −10% +10% +1 +43.5%
Furious +15% - - +20% +15% +1 +11.94%
Manic −10% +10% - −10% - +1 +18.59%
Mythical +15% +10% +5% −10% +15% +2 +209.85%
""",
5,
7,
)
def main() -> None:
display_cost("Accessories", True, ["Warding"], ACCESSORY_MODIFIERS)
display_cost("Accessories", True, ["Lucky"], ACCESSORY_MODIFIERS)
display_cost("Accessories", True, ["Menacing"], ACCESSORY_MODIFIERS)
display_cost("Accessories", True, ["Lucky", "Menacing"], ACCESSORY_MODIFIERS)
display_cost("Accessories", True, ["Quick"], ACCESSORY_MODIFIERS)
display_cost("Accessories", True, ["Violent"], ACCESSORY_MODIFIERS)
display_cost("Accessories", True, ["Lucky", "Menacing", "Violent"], ACCESSORY_MODIFIERS)
display_cost("Accessories", True, ["Arcane"], ACCESSORY_MODIFIERS)
# All melee weapons have knockback
display_cost(
"Swords",
True,
["Legendary"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MELEE_MODIFIERS,
)
# All pickaxes, hammers, axes, and hamaxes have knockback
display_cost(
"Pickaxes, Hammers, Axes, and Hamaxes",
True,
["Light"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MELEE_MODIFIERS,
)
display_cost(
"Pickaxes, Hammers, Axes, and Hamaxes",
True,
["Light", "Legendary", "Deadly", "Agile", "Quick", "Nasty"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MELEE_MODIFIERS,
)
display_cost(
"Other Melee Weapons",
True,
["Godly"],
UNIVERSAL_MODIFIERS,
)
display_cost(
"Other Melee Weapons",
True,
["Godly", "Demonic"],
UNIVERSAL_MODIFIERS,
)
display_cost(
"Other Melee Weapons",
True,
["Ruthless"],
UNIVERSAL_MODIFIERS,
)
display_cost(
"Other Melee Weapons",
True,
["Godly", "Demonic", "Ruthless"],
UNIVERSAL_MODIFIERS,
)
display_cost(
"Ranged Weapons",
True,
["Unreal"],
UNIVERSAL_MODIFIERS + COMMON_RANGED_MODIFIERS + RANGED_MODIFIERS,
)
display_cost(
"Ranged Weapons",
False,
["Demonic"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + RANGED_MODIFIERS,
)
display_cost(
"Ranged Weapons",
False,
["Deadly"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + RANGED_MODIFIERS,
)
display_cost(
"Ranged Weapons",
False,
["Demonic", "Deadly"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + RANGED_MODIFIERS,
)
display_cost(
"Magic Weapons",
True,
["Mythical"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MAGIC_MODIFIERS,
)
display_cost(
"Magic Weapons",
False,
["Demonic"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MAGIC_MODIFIERS,
)
display_cost(
"Magic Weapons",
False,
["Deadly"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MAGIC_MODIFIERS,
)
display_cost(
"Magic Weapons",
False,
["Demonic", "Deadly"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MAGIC_MODIFIERS,
)
display_cost(
"Magic Weapons",
False,
["Mystic"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MAGIC_MODIFIERS,
)
display_cost(
"Magic Weapons",
False,
["Demonic", "Deadly", "Mystic"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MAGIC_MODIFIERS,
)
display_cost(
"Summon Weapons",
True,
["Ruthless"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MAGIC_MODIFIERS,
)
display_cost(
"Summon Weapons",
True,
["Ruthless", "Mythical"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MAGIC_MODIFIERS,
)
display_cost(
"Summon Weapons - Blade Staff",
False,
["Demonic", "Deadly", "Mystic", "Hurtful"],
UNIVERSAL_MODIFIERS + COMMON_MODIFIERS + MAGIC_MODIFIERS,
)
# All whips have knockback
display_cost(
"Whips",
True,
["Legendary"],
UNIVERSAL_MODIFIERS + MELEE_MODIFIERS,
)
def display_cost(
item_type: str, knockback: bool, goal: list[str], modifiers: Iterable[Modifier]
) -> None:
expected_cost = calculate_expected_reforge_cost(modifiers, goal, knockback)
item_label = item_type
if not knockback:
item_label += ", no knockback"
goal_label: str
if len(goal) == 1:
goal_label = goal[0]
elif len(goal) == 2:
goal_label = f"{goal[0]} or {goal[1]}"
else:
goal_label = f"{', '.join(goal[:-1])}, or {goal[-1]}"
label = f"{item_label} ({goal_label}):"
print(
f"{label} {expected_cost:.2f}x base sell price + initial reforge cost"
)
def calculate_expected_reforge_cost(
modifiers: Iterable[Modifier],
desired_modifiers: Iterable[str],
has_knockback: bool,
) -> float:
if not has_knockback:
modifiers = (
modifier for modifier in modifiers if not modifier.affects_knockback
)
goal = set(desired_modifiers)
single_reforge_cost = 0.0
modifier_count = 0
goal_count = 0
for modifier in modifiers:
modifier_count += 1
if modifier.name in goal:
goal_count += 1
continue
single_reforge_cost += modifier.value_mult
if goal_count != len(goal):
raise ValueError("invalid desired modifier")
single_reforge_cost *= REFORGE_COST_MULT / modifier_count
reroll_probability = 1 - goal_count / modifier_count
return single_reforge_cost / (1 - reroll_probability)
def simulate_reforge_cost(
modifiers: Iterable[Modifier],
desired_modifiers: Iterable[str],
has_knockback: bool,
) -> float:
possible: list[Modifier]
if has_knockback:
possible = list(modifiers)
else:
possible = [
modifier for modifier in modifiers if not modifier.affects_knockback
]
goal = set(desired_modifiers)
cost = 0.0
modifier = ""
while True:
new_modifier = random.choice(possible)
modifier = new_modifier.name
if modifier in goal:
break
cost += new_modifier.value_mult
return REFORGE_COST_MULT * cost
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment