First slice: classes, combat, questing, Deadmines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
71dd99795a
17 changed files with 4522 additions and 0 deletions
99
tools/balance.py
Normal file
99
tools/balance.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue