#!/usr/bin/env python3 """ Cost of complete builds: a limited character, its signature weapon, and duplicate copies (C1..C6 / E / M / S). python3 lab/builds.py lab/game_rules.json --out releases//data Character and weapon banners keep separate pity, so their pull counts add up. Copies are chased back to back on the character banner with pity and guarantee carrying over. """ import argparse import json import sys from pathlib import Path import numpy as np sys.path.insert(0, str(Path(__file__).resolve().parent)) from pull_sim import consolidated_rates # noqa: E402 from streaks import run as chase # noqa: E402 BUILDS = [("C0", 1, False), ("C0 + weapon", 1, True), ("C2", 3, False), ("C2 + weapon", 3, True), ("C6", 7, False), ("C6 + weapon", 7, True)] def main(): ap = argparse.ArgumentParser() ap.add_argument("rules") ap.add_argument("--players", type=int, default=500_000) ap.add_argument("--seed", type=int, default=20261021) ap.add_argument("--out", required=True) a = ap.parse_args() R = json.loads(Path(a.rules).read_text()) rng = np.random.default_rng(a.seed) out = Path(a.out) out.mkdir(parents=True, exist_ok=True) res = {"players": a.players, "seed": a.seed, "games": {}} for g, wr in R["weapons"].items(): cr = R["games"][g] usd = cr["usd_per_pull"]["value"] weapon, _, _ = chase(wr, a.players, 1, rng) copies = {n: chase(cr, a.players, n, rng)[0] for n in range(1, 8)} np.save(out / f"weapon_{g}.npy", weapon.astype(np.int16)) for n, arr in copies.items(): np.save(out / f"copies{n}_{g}.npy", arr.astype(np.int16)) builds = {} for label, n, with_w in BUILDS: t = copies[n] + (weapon if with_w else 0) builds[label] = {"p50": int(np.percentile(t, 50)), "p90": int(np.percentile(t, 90)), "max": int(t.max()), "p50_usd": round(float(np.percentile(t, 50)) * usd), "p90_usd": round(float(np.percentile(t, 90)) * usd)} ladder = {f"C{n - 1}": {"p50": int(np.percentile(copies[n], 50)), "p90": int(np.percentile(copies[n], 90)), "p50_usd": round(float(np.percentile(copies[n], 50)) * usd), "p90_usd": round(float(np.percentile(copies[n], 90)) * usd)} for n in range(1, 8)} res["games"][g] = {"name": cr["name"], "weapon_banner": wr["name"], "ladder": ladder, "weapon_model_check": consolidated_rates(wr), "weapon_official": {k: wr[k]["value"] for k in ("official_top_rarity_rate", "official_featured_rate") if k in wr}, "weapon_p50": int(np.percentile(weapon, 50)), "weapon_p90": int(np.percentile(weapon, 90)), "weapon_max": int(weapon.max()), "builds": builds} print(g, json.dumps(res["games"][g])) (out / "builds.json").write_text(json.dumps(res, indent=2)) if __name__ == "__main__": main()