99 lines
4.3 KiB
Python
99 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Balance report: solo hunts per class and level, and all-auto Deadmines runs.
|
|
|
|
python3 tools/balance.py [solo|dungeon] [runs]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
import statistics
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
import combat # noqa: E402
|
|
import game # noqa: E402
|
|
from content import CLASSES, DUNGEONS, MOBS # noqa: E402
|
|
|
|
SOLO_TARGETS = {1: "young_wolf", 5: "defias_thug", 10: "riverpaw_gnoll", 15: "riverpaw_brute",
|
|
19: "defias_highwayman"}
|
|
|
|
|
|
def hero(cls: str, level: int, uid: str = "p") -> dict:
|
|
player = game.new_player(uid, cls.title(), cls, 0)
|
|
game.dev_level({"config": {}}, player, level, 0)
|
|
return player
|
|
|
|
|
|
def solo(runs: int) -> None:
|
|
print(f"{'class':9} {'lvl':>3} {'mob':20} {'win':>5} {'secs':>5} {'hp lost':>7}")
|
|
for cls in CLASSES:
|
|
for level, mob_id in SOLO_TARGETS.items():
|
|
wins, times, lost = 0, [], []
|
|
for seed in range(runs):
|
|
player = hero(cls, level)
|
|
rng = random.Random(seed)
|
|
mob_level = level
|
|
enemies = [combat.mob_unit("e0", mob_id, mob_level)]
|
|
fight = combat.new_fight("f", seed, 0, game.player_unit(player, 0, False), enemies, "hunt")
|
|
combat.advance(fight, 400)
|
|
me = fight["units"]["p"]
|
|
if fight["state"] == "won":
|
|
wins += 1
|
|
times.append(fight["t"])
|
|
lost.append(1 - me["hp"] / me["max_hp"])
|
|
print(f"{cls:9} {level:>3} {MOBS[mob_id]['name']:20} {wins / runs:>5.0%} "
|
|
f"{statistics.mean(times) if times else 0:>5.1f} {statistics.mean(lost) if lost else 0:>7.0%}")
|
|
|
|
|
|
def dungeon(runs: int, party: list[str], level: int) -> None:
|
|
pulls = DUNGEONS["deadmines"]["pulls"]
|
|
results = {p["name"]: {"won": 0, "t": [], "hp": [], "low": [], "mana": []} for p in pulls}
|
|
for seed in range(runs):
|
|
rng = random.Random(seed)
|
|
heroes = [hero(cls, level, f"p{i}") for i, cls in enumerate(party)]
|
|
for pull in pulls:
|
|
units = []
|
|
for h in heroes:
|
|
units += game.player_unit(h, 0, True)
|
|
hp_scale, dmg_scale = game.dungeon_scale(len(heroes))
|
|
enemies = []
|
|
for index, mob_id in enumerate(pull["mobs"]):
|
|
low, high = MOBS[mob_id]["lv"]
|
|
unit = combat.mob_unit(f"e{index}", mob_id, rng.randint(low, high), hp_scale, dmg_scale)
|
|
unit["swing"]["next"] = 0.8 + 0.4 * index
|
|
enemies.append(unit)
|
|
fight = combat.new_fight("f", rng.randrange(2 ** 31), 0, units, enemies, "dungeon", pull["name"],
|
|
(hp_scale, dmg_scale))
|
|
while fight["state"] == "on":
|
|
combat.advance(fight, fight["t"] + combat.STEP)
|
|
tank = fight["units"]["p0"]
|
|
fight["low"] = min(fight.get("low", 1), tank["hp"] / tank["max_hp"])
|
|
row = results[pull["name"]]
|
|
row["low"].append(fight.get("low", 1))
|
|
row["mana"].append(fight["units"]["p1"]["res"]["cur"] / fight["units"]["p1"]["res"]["max"])
|
|
if fight["state"] == "won":
|
|
row["won"] += 1
|
|
row["t"].append(fight["t"])
|
|
row["hp"].append(sum(1 for u in combat.players(fight) if u["dead"]))
|
|
print(f"party: {', '.join(party)} at {level}")
|
|
print(f"{'pull':20} {'win':>5} {'secs':>5} {'dead':>5} {'tank low':>8} {'heal mana':>9}")
|
|
for name, row in results.items():
|
|
print(f"{name:20} {row['won'] / runs:>5.0%} {statistics.mean(row['t']) if row['t'] else 0:>5.0f} "
|
|
f"{statistics.mean(row['hp']) if row['hp'] else 0:>5.1f} {statistics.mean(row['low']):>8.0%} "
|
|
f"{statistics.mean(row['mana']):>9.0%}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
mode = sys.argv[1] if len(sys.argv) > 1 else "solo"
|
|
runs = int(sys.argv[2]) if len(sys.argv) > 2 else 20
|
|
if mode == "solo":
|
|
solo(runs)
|
|
else:
|
|
if "perfect" in sys.argv:
|
|
combat.AUTO_CD, combat.AUTO_DELAY, combat.AUTO_REACT = 1.0, 0.0, 0.25
|
|
print("-- perfect hands --")
|
|
dungeon(runs, ["warrior", "priest", "mage", "rogue", "hunter"], 18)
|
|
dungeon(runs, ["paladin", "druid", "warlock", "shaman", "rogue"], 18)
|