Skip to content

Commit 46c795b

Browse files
SnoopLawgclaude
andcommitted
M7 B1: cb_comp_finder.py — survival-first comp enumerator (no external inputs)
From the game-truth synergy graph + owned roster, enumerates candidate comps per survival archetype (uk / bd / counter / protect / heal / revive): provider core + DPS fillers, scored by synergy-axis coverage only -- NO HellHades/DWJ in the path (the mission-compliant counterpart to cb_team_explorer). Survival is explicitly labeled UNVALIDATED: it generates candidates that HAVE the providers to attempt the pattern; trustworthy survive/wipe verdicts are Gate B (needs the hardened survival sim from Gate A). --validate runs cb_sim survival with a loud "optimistic until Gate A" warning. Roadmap: A3 + B1 marked done. Recorded a synergy-data gap: buff-extension (increase-buff-duration) isn't a tagged provides-axis, so BD+extension chains aren't yet findable -- add to m5_synergy_graph.py (no-key M5 follow-up). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 14197f0 commit 46c795b

2 files changed

Lines changed: 191 additions & 2 deletions

File tree

docs/roadmap.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,15 +220,15 @@ finder on top. **Nothing downstream ships prescriptive until Gate A passes.**
220220
|---|---|---|
221221
| A1 — Define survival accuracy bar | 🔴 | Separate from the damage ±5% bar: predicted death-turn within ±2-3 BT of real AND correct survive/wipe classification, across the whole fixture battery. |
222222
| A2 — Capture a DIVERSE fixture battery | 🔴 (key-gated) | Need real runs (build snapshot now auto-saved per run): MEN stable-day survive-T50 (HAVE none clean+recent); MEN Force wipe-T32 (✅ HAVE `20260624_152252`); ≥1 non-UK archetype (counter / ally-protect / heal-tank) the roster can field; a buff-extension-dependent comp. Use `cb_watcher.py` to grab wipe cases without burning keys. **Rate limiter: 2 keys/day + must own/gear the archetype.** |
223-
| A3 — Survival-diff harness | 🟡 | Productize the per-CB-turn sim-vs-real HP + coverage(UK/BD/shield/counter/protect) + death-turn diff we hand-rolled on the Force fixture`tools/cb_survival_diff.py`. |
223+
| A3 — Survival-diff harness | ✅ 2026-06-25 | `tools/cb_survival_diff.py` per-boss-turn sim-vs-real HP + UK/BD coverage + death-turn diff, with a survival verdict (death-turns within ±3 AND survive/wipe class match). Uses the fixture's `build_cb` snapshot for exact stats when present; maps sim-name↔real-type_id (truncation 6206→6200). Validated on the Force fixture: classifies Ninja/Venom deaths within tol, flags the Maneater/Demytha mismatch — i.e. it *measures* the gap A4/A5 must close. |
224224
| A4 — Un-stack survival compensating wrongs TOGETHER | 🔴 | Against the full battery (mission rule): promote game-truth buff cadence (`bugfix_buff_tick=False`) from finder-only to global + re-derive damage calibration + re-baseline the 2 locked damage tests once. |
225225
| A5 — Model missing survival mechanics (game-truth) | 🔴 | Ally-Protect redirect (currently OFF), counterattack damage magnitude+uptime (`CounterattackModifier −0.25` unwired), generalized buff-extension cadence, heal-vs-ramp sustain + heal caps. |
226226
| **GATE A** | 🔴 | Survival sim reproduces EVERY battery fixture within the bar. Until met, survival recs are sanity-check only. |
227227

228228
#### Phase B — Survival-first comp finder (depends on Gate A)
229229
| Sub-goal | Status | Notes |
230230
|---|---|---|
231-
| B1 — Coverage-chain enumerator | 🔴 | From the synergy graph + owned roster, enumerate comps that CAN form a survival pattern (UK-chain / BD+extension / counter-wall / ally-protect / heal-tank); tag archetype + providers. |
231+
| B1 — Coverage-chain enumerator | ✅ 2026-06-25 | `tools/cb_comp_finder.py` — from `data/m5_synergy.jsonl` + owned roster, enumerates candidate comps per archetype (uk / bd / counter / protect / heal / revive) with the provider core + DPS fillers, scored by game-truth synergy coverage (**no HellHades** in the path — unlike `cb_team_explorer`). Survival is explicitly labeled UNVALIDATED (it generates candidates; the verdict is Gate B). `--validate` runs cb_sim survival but warns it's optimistic until Gate A. **Synergy-data gap found**: buff-extension (Demytha-A2-style "increase buff duration") is NOT a tagged `provides` axis — add it to `m5_synergy_graph.py` so BD+extension chains are findable (no-key M5 follow-up). |
232232
| B2 — Validate per comp via hardened sim | 🔴 | Survive-to-50 per affinity via the finder's MC; **drop HellHades from the scoring path** (`cb_team_explorer` currently scores with `tierlist.json` — mission violation to fix). |
233233
| B3 — Auto-vs-manual classifier | 🔴 | Mark "automatic" only if the required skill order is preset-expressible; flag "manual" separately, never auto-recommended. |
234234
| B4 — Rank + output best runs | 🔴 | Output: archetype, providers, per-affinity survival turn, damage, auto/manual. DWJ tunes referenced ONLY as "we independently found this too" divergence flag. New tool `tools/cb_comp_finder.py`. |

tools/cb_comp_finder.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""Survival-first comp finder — M7 Phase B1 (coverage-chain enumerator).
2+
3+
Takes the game-truth synergy graph (`data/m5_synergy.jsonl`) + the owned roster
4+
and enumerates candidate team comps that CAN form a survive-to-T50 pattern,
5+
grouped by archetype:
6+
7+
- uk Unkillable chain (keep [Unkillable] up; [Block Debuffs] stops stun)
8+
- bd Block-Damage wall (block the boss hit outright)
9+
- counter Counterattack wall (+ sustain)
10+
- protect Ally-Protect wall (protector soaks; + sustain)
11+
- heal Heal-tank (Continuous Heal + Shield/Inc-DEF + bulk)
12+
- revive Revive-on-death last resort
13+
14+
This is the *generation* half of the finder. It deliberately does NOT claim a
15+
comp survives — that's M7 Gate B (validation through the HARDENED survival sim),
16+
which is blocked on Gate A (survival-model calibration vs a real fixture
17+
battery). Every comp here is a CANDIDATE: "has the providers to *attempt* the
18+
pattern." Use `--validate` to run each through cb_sim survival (clearly marked
19+
UNVALIDATED until Gate A — the sim currently over-predicts survival).
20+
21+
Mission-compliant: scoring uses game-truth synergy/provider coverage only.
22+
**No HellHades / DeadwoodJedi inputs** in the path (unlike `cb_team_explorer`).
23+
24+
CLI:
25+
python3 tools/cb_comp_finder.py # providers + candidates, all archetypes
26+
python3 tools/cb_comp_finder.py --archetype uk --top 10
27+
python3 tools/cb_comp_finder.py --archetype counter --validate --element force
28+
"""
29+
from __future__ import annotations
30+
31+
import argparse
32+
import itertools
33+
import json
34+
import sys
35+
from pathlib import Path
36+
37+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
38+
sys.path.insert(0, str(PROJECT_ROOT / "tools"))
39+
40+
SYNERGY = PROJECT_ROOT / "data" / "m5_synergy.jsonl"
41+
HEROES = PROJECT_ROOT / "heroes_all.json"
42+
43+
# archetype -> (required provider tags [any-of per group], label, needs-sustain)
44+
ARCHETYPES = {
45+
"uk": (["team_buff:Unkillable"], "Unkillable chain", True),
46+
"bd": (["team_buff:Block Damage"], "Block-Damage wall", True),
47+
"counter": (["team_buff:Counterattack"], "Counterattack wall", True),
48+
"protect": (["team_buff:Ally Protection"], "Ally-Protect wall", True),
49+
"heal": (["team_buff:Continuous Heal"], "Heal-tank", False),
50+
"revive": (["revive", "team_buff:Revive On Death", "team_buff:Revive on Death"], "Revive last-resort", True),
51+
}
52+
SUSTAIN_TAGS = ["team_buff:Continuous Heal", "team_buff:Shield", "team_buff:Increase DEF",
53+
"team_buff:Block Damage", "team_buff:Unkillable", "team_buff:Ally Protection"]
54+
55+
56+
def load_synergy():
57+
out = {}
58+
for line in open(SYNERGY, encoding="utf-8"):
59+
line = line.strip()
60+
if not line:
61+
continue
62+
d = json.loads(line)
63+
out[d["name"]] = d
64+
return out
65+
66+
67+
def load_owned(min_grade=6):
68+
"""Owned, comp-viable heroes (grade>=min_grade) by name. Falls back to
69+
live /all-heroes if the cache is missing."""
70+
heroes = None
71+
if HEROES.exists():
72+
try:
73+
heroes = json.loads(HEROES.read_text()).get("heroes", [])
74+
except Exception:
75+
heroes = None
76+
if heroes is None:
77+
import urllib.request
78+
with urllib.request.urlopen("http://localhost:6790/all-heroes?limit=20000", timeout=20) as r:
79+
heroes = json.loads(r.read()).get("heroes", [])
80+
names = set()
81+
for h in heroes:
82+
if (h.get("grade", 0) or 0) >= min_grade and h.get("name"):
83+
names.add(h["name"])
84+
return names
85+
86+
87+
def has_any(hero, tags):
88+
prov = hero.get("provides", []) if hero else []
89+
return any(t in prov for t in tags)
90+
91+
92+
def is_dps(hero):
93+
if not hero:
94+
return False
95+
if hero.get("synergy_role") in ("dot", "nuker", "dps"):
96+
return True
97+
return any(p.startswith("dot:") or p.startswith("debuff:Decrease DEF") for p in hero.get("provides", []))
98+
99+
100+
def find(owned_names, syn, archetype, top=8, max_comps=2000):
101+
tags, label, needs_sustain = ARCHETYPES[archetype]
102+
owned = [n for n in owned_names if n in syn]
103+
providers = [n for n in owned if has_any(syn[n], tags)]
104+
sustainers = [n for n in owned if has_any(syn[n], SUSTAIN_TAGS)]
105+
dps = [n for n in owned if is_dps(syn[n])]
106+
candidates = []
107+
# Core = 1 provider (+1 extra sustainer if the archetype needs it), then
108+
# fill the rest with DPS for damage + role diversity. Sampled/capped.
109+
seen = set()
110+
for prov in providers:
111+
pool_sustain = [s for s in sustainers if s != prov]
112+
# second sustain options (or skip if not needed)
113+
second_opts = pool_sustain[:6] if needs_sustain else [None]
114+
for s2 in second_opts:
115+
core = [prov] + ([s2] if s2 else [])
116+
fill_pool = [d for d in dps if d not in core][:10]
117+
need = 5 - len(core)
118+
for combo in itertools.combinations(fill_pool, need):
119+
team = tuple(sorted(core + list(combo)))
120+
if team in seen:
121+
continue
122+
seen.add(team)
123+
# provider coverage score (game-truth only): distinct survival
124+
# axes the team covers + dps count. NO external ratings.
125+
axes = set()
126+
for m in team:
127+
for p in syn[m].get("provides", []):
128+
if p in SUSTAIN_TAGS or p in tags:
129+
axes.add(p)
130+
n_dps = sum(1 for m in team if is_dps(syn[m]))
131+
score = len(axes) * 10 + n_dps
132+
candidates.append((score, team, sorted(axes), n_dps))
133+
if len(candidates) >= max_comps:
134+
break
135+
if len(candidates) >= max_comps:
136+
break
137+
if len(candidates) >= max_comps:
138+
break
139+
candidates.sort(reverse=True)
140+
return {"providers": providers, "label": label, "needs_sustain": needs_sustain,
141+
"candidates": candidates[:top]}
142+
143+
144+
def main(argv=None):
145+
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
146+
ap.add_argument("--archetype", choices=list(ARCHETYPES) + ["all"], default="all")
147+
ap.add_argument("--top", type=int, default=6)
148+
ap.add_argument("--min-grade", type=int, default=6)
149+
ap.add_argument("--validate", action="store_true",
150+
help="run each candidate through cb_sim survival (UNVALIDATED until M7 Gate A)")
151+
ap.add_argument("--element", default="force", choices=["magic", "force", "spirit", "void"])
152+
args = ap.parse_args(argv)
153+
154+
syn = load_synergy()
155+
owned = load_owned(args.min_grade)
156+
print(f"owned comp-viable heroes (grade>={args.min_grade}): {len(owned)} | synergy-graph heroes: {len(syn)}")
157+
archs = list(ARCHETYPES) if args.archetype == "all" else [args.archetype]
158+
159+
val = None
160+
if args.validate:
161+
from cb_survival_diff import run_sim, ELEMENT_MAP
162+
print("\n!!! --validate uses cb_sim survival, which OVER-PREDICTS until M7 Gate A. "
163+
"Treat survival turns as OPTIMISTIC, not trustworthy. !!!")
164+
165+
for a in archs:
166+
r = find(owned, syn, a, top=args.top)
167+
print(f"\n=== {a.upper()}{r['label']} ===")
168+
print(f" owned providers ({len(r['providers'])}): {', '.join(r['providers'][:20]) or '(none)'}")
169+
if not r["candidates"]:
170+
print(" (no candidate comps — missing a provider or DPS fillers)")
171+
continue
172+
print(f" candidate comps (game-truth synergy score; survival UNVALIDATED):")
173+
for score, team, axes, n_dps in r["candidates"]:
174+
line = f" [{score:>3}] {', '.join(team)} | dps={n_dps} axes={len(axes)}"
175+
if args.validate:
176+
try:
177+
sim = run_sim(list(team), ELEMENT_MAP[args.element])
178+
line += f" | sim~T{sim['cb_turns']}(optimistic)"
179+
except Exception as ex:
180+
line += f" | sim_err:{str(ex)[:30]}"
181+
print(line)
182+
print("\nNOTE: 'survival UNVALIDATED' — these are candidates that HAVE the providers "
183+
"to attempt the pattern. Ranking is game-truth synergy coverage only (no HH/DWJ). "
184+
"Trustworthy survive/wipe verdicts require M7 Gate A (hardened survival sim).")
185+
return 0
186+
187+
188+
if __name__ == "__main__":
189+
raise SystemExit(main())

0 commit comments

Comments
 (0)