Combat rework: packs, abilities, rest, health potions, group bosses
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
472609ac70
commit
57d11e30da
10 changed files with 1562 additions and 380 deletions
120
tools/balance.py
Normal file
120
tools/balance.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Balance report: an hour of fighting for each trade with its own weapon, at each tier.
|
||||
|
||||
python3 tools/balance.py [solo|party] [seeds] [levels, e.g. 22,52] [potions]
|
||||
|
||||
Each row is one encounter: wins and losses in the hour, XP an hour, food eaten an hour,
|
||||
and the share of a kill's time spent fighting (the rest is resting between fights).
|
||||
"""
|
||||
|
||||
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 game # noqa: E402
|
||||
|
||||
T0 = 1_800_000_000.0
|
||||
WEIGHT = {"miner": "heavy", "builder": "heavy", "homesteader": "light", "ranger": "medium"}
|
||||
# (combat level, the tier of gear worn, the encounters to try)
|
||||
CHECKPOINTS = [
|
||||
(10, 0, ["fight_wolves", "fight_goblins", "fight_scouts"]),
|
||||
(22, 1, ["fight_clay_golems", "fight_cave_trolls", "fight_restless_dead", "fight_harpies", "fight_ruins"]),
|
||||
(37, 2, ["fight_stone_guardians", "fight_lizardfolk", "fight_bog_wraiths", "fight_giant_spiders", "fight_barrow_king"]),
|
||||
(52, 3, ["fight_iron_golems", "fight_orcs", "fight_mine_ghosts", "fight_wyverns", "fight_green_dragon"]),
|
||||
(67, 4, ["fight_ice_elementals", "fight_frost_trolls", "fight_banshees", "fight_griffins", "fight_blue_dragon"]),
|
||||
(82, 5, ["fight_magma_constructs", "fight_dragonkin", "fight_shades", "fight_rocs", "fight_red_dragon"]),
|
||||
]
|
||||
FOODS = ["cooked_shrimp", "cooked_trout", "cooked_salmon", "smoked_eel", "cooked_lobster", "cooked_swordfish"]
|
||||
|
||||
|
||||
def gear(cls: str, tier: int) -> dict:
|
||||
"""The trade's own weapon and its usual armour weight, all of one tier (and a shield for a sword)."""
|
||||
style = game.CLASS_STYLE[cls]
|
||||
equip = {}
|
||||
for item, spec in game.ITEMS.items():
|
||||
if spec.get("tier") != tier or spec.get("req") != ["combat", game.TIER_LEVELS[tier]]:
|
||||
continue
|
||||
if spec.get("style") == style or (style == "sword" and spec.get("slot") == "shield") or \
|
||||
spec.get("weight") == WEIGHT[cls] and spec["slot"] in ("helmet", "body", "legs"):
|
||||
equip[spec["slot"]] = item
|
||||
return equip
|
||||
|
||||
|
||||
POTIONS = False
|
||||
|
||||
|
||||
def fighter(world: dict, uid: str, cls: str, level: int, tier: int) -> dict:
|
||||
player = game.new_player(uid, uid.title(), cls, T0)
|
||||
world["players"][uid] = player
|
||||
player["xp"]["combat"] = game.XP_TABLE[level]
|
||||
player["equip"].update(gear(cls, tier))
|
||||
player["hp"] = game.max_hp(player)
|
||||
player["bank"] = {FOODS[tier]: 50_000, "arrows": 10_000}
|
||||
if POTIONS:
|
||||
player["bank"][f"health_potion_{tier + 1}"] = 10_000
|
||||
return player
|
||||
|
||||
|
||||
def hour(classes: list, level: int, tier: int, fight: str, seed: int) -> dict:
|
||||
world = game.new_world(T0)
|
||||
players = [fighter(world, f"p{i}", cls, level, tier) for i, cls in enumerate(classes)]
|
||||
game.start_action(world, players[0], fight, T0)
|
||||
for other in players[1:]:
|
||||
game.join_fight(world, other, players[0]["id"], T0)
|
||||
rng = random.Random(seed)
|
||||
end = T0 + 3600
|
||||
for now in range(int(T0) + 60, int(end) + 1, 60):
|
||||
for player in players:
|
||||
player["last_active"] = now
|
||||
game.catch_up(world, now, rng)
|
||||
away = players[0].get("away") or game.empty_summary()
|
||||
eaten = sum(away["eaten"].values())
|
||||
return {"wins": away["wins"], "losses": away["losses"], "xp": away["xp"].get("combat", 0), "eaten": eaten,
|
||||
"stopped": away["stopped"]}
|
||||
|
||||
|
||||
def report(groups: dict, seeds: int, levels: set | None = None) -> None:
|
||||
print(f"{'who':24} {'lvl':>3} {'encounter':22} {'wins':>5} {'lost':>4} {'xp/h':>6} {'food/h':>6} {'stop':>4}")
|
||||
for name, classes in groups.items():
|
||||
for level, tier, fights in CHECKPOINTS:
|
||||
if levels and level not in levels:
|
||||
continue
|
||||
for fight in fights:
|
||||
rows = [hour(classes, level, tier, fight, seed) for seed in range(seeds)]
|
||||
mean = lambda key: statistics.mean(row[key] for row in rows)
|
||||
stops = sum(1 for row in rows if row["stopped"])
|
||||
label = game.ACTION_BY_ID[fight]["name"][:22]
|
||||
print(f"{name:24} {level:>3} {label:22} {mean('wins'):>5.0f} {mean('losses'):>4.1f} "
|
||||
f"{mean('xp'):>6.0f} {mean('eaten'):>6.0f} {stops:>4}")
|
||||
print()
|
||||
|
||||
|
||||
BOSSES = ["fight_ruins", "fight_barrow_king", "fight_green_dragon", "fight_blue_dragon", "fight_red_dragon"]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else "solo"
|
||||
seeds = int(sys.argv[2]) if len(sys.argv) > 2 else 3
|
||||
levels = {int(level) for level in sys.argv[3].split(",")} if len(sys.argv) > 3 and sys.argv[3] != "all" else None
|
||||
global POTIONS
|
||||
POTIONS = "potions" in sys.argv[4:]
|
||||
if mode == "bosses":
|
||||
global CHECKPOINTS
|
||||
CHECKPOINTS = [(level, tier, [f for f in fights if f in BOSSES]) for level, tier, fights in CHECKPOINTS]
|
||||
report({"miner+ranger": ["miner", "ranger"], "builder+homesteader": ["builder", "homesteader"],
|
||||
"homesteader+ranger": ["homesteader", "ranger"], "all four": ["builder", "homesteader", "miner", "ranger"]},
|
||||
seeds, levels)
|
||||
elif mode == "party":
|
||||
report({"builder+homesteader": ["builder", "homesteader"],
|
||||
"all four": ["builder", "homesteader", "miner", "ranger"]}, seeds, levels)
|
||||
else:
|
||||
report({cls: [cls] for cls in game.CLASS_STYLE}, seeds, levels)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue