#!/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)