v0.5.59: item counter evidence for fears, catch up Web features to site version.
Ship OpenDota counter-stats reordering for feared items, finalize SITE_VERSION/docs for rankings/streamers/trends/matches/mechanics and draft archetypes, and ignore regenerable Web data caches. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+63
-2
@@ -20,6 +20,7 @@ from http_utils import http_json
|
||||
ITEMS_META = DATA / "items_meta.json"
|
||||
HERO_ABILITIES = DATA / "hero_abilities.json"
|
||||
HERO_FEAR_OVERRIDES = DATA / "hero_fear_overrides.json"
|
||||
ITEM_COUNTER_STATS = DATA / "item_counter_stats.json"
|
||||
OUT = DATA / "hero_item_fears.json"
|
||||
ABILITIES_URL = (
|
||||
"https://raw.githubusercontent.com/odota/dotaconstants/master/build/abilities.json"
|
||||
@@ -243,12 +244,51 @@ def load_hero_overrides() -> dict[str, dict]:
|
||||
return {str(k): v for k, v in heroes.items() if isinstance(v, dict)}
|
||||
|
||||
|
||||
def load_counter_stats() -> tuple[dict, dict[str, dict[str, dict]]]:
|
||||
"""Load optional adjusted OpenDota evidence indexed by hero and item."""
|
||||
if not ITEM_COUNTER_STATS.is_file():
|
||||
return {}, {}
|
||||
try:
|
||||
raw = load_json(ITEM_COUNTER_STATS)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}, {}
|
||||
indexed: dict[str, dict[str, dict]] = {}
|
||||
for hero_key, rows in (raw.get("by_hero") or {}).items():
|
||||
if not isinstance(rows, list):
|
||||
continue
|
||||
indexed[str(hero_key)] = {
|
||||
str(row["item"]): row
|
||||
for row in rows
|
||||
if isinstance(row, dict) and row.get("item")
|
||||
}
|
||||
return dict(raw.get("meta") or {}), indexed
|
||||
|
||||
|
||||
def evidence_adjustment(evidence: dict | None) -> int:
|
||||
"""Return a deliberately small corroboration bonus/penalty."""
|
||||
if not evidence:
|
||||
return 0
|
||||
games = int(evidence.get("games") or 0)
|
||||
if games < 100:
|
||||
return 0
|
||||
buy_lift_pp = float(evidence.get("buy_lift") or 0) * 100
|
||||
win_delta_pp = float(evidence.get("win_delta") or 0) * 100
|
||||
# Mixed signs are inconclusive. Requiring agreement avoids promoting generic
|
||||
# expensive winner items based on conditional win rate alone.
|
||||
if buy_lift_pp * win_delta_pp <= 0:
|
||||
return 0
|
||||
raw = 0.6 * buy_lift_pp + 0.4 * win_delta_pp
|
||||
reliability = min(1.0, games / 500)
|
||||
return round(max(-6.0, min(6.0, raw)) * reliability)
|
||||
|
||||
|
||||
def fears_for_hero(
|
||||
hero_key: str,
|
||||
summary: dict,
|
||||
by_tag: dict[str, list[dict]],
|
||||
items_by_key: dict[str, dict],
|
||||
overrides: dict[str, dict],
|
||||
counter_stats: dict[str, dict[str, dict]],
|
||||
top_n: int,
|
||||
) -> list[dict]:
|
||||
need = needed_tags(summary)
|
||||
@@ -304,12 +344,21 @@ def fears_for_hero(
|
||||
|
||||
ranked = []
|
||||
for row in acc.values():
|
||||
evidence = (counter_stats.get(hero_key) or {}).get(row["item"]["key"])
|
||||
sc, entry = score_item(
|
||||
row["item"],
|
||||
row["matched"],
|
||||
row["reasons"],
|
||||
bonus=int(row.get("bonus") or 0),
|
||||
bonus=int(row.get("bonus") or 0) + evidence_adjustment(evidence),
|
||||
)
|
||||
if evidence:
|
||||
entry["stats"] = {
|
||||
"games": int(evidence.get("games") or 0),
|
||||
"purchase_rate": float(evidence.get("buy_rate") or 0),
|
||||
"win_rate": float(evidence.get("win_rate") or 0),
|
||||
"purchase_lift": float(evidence.get("buy_lift") or 0),
|
||||
"win_delta": float(evidence.get("win_delta") or 0),
|
||||
}
|
||||
ranked.append((sc, entry))
|
||||
ranked.sort(key=lambda t: (-t[0], t[1]["item"]))
|
||||
|
||||
@@ -333,6 +382,7 @@ def main() -> None:
|
||||
|
||||
items_meta = load_json(ITEMS_META)
|
||||
abilities_db = load_json(HERO_ABILITIES)
|
||||
counter_meta, counter_stats = load_counter_stats()
|
||||
items = items_meta.get("items") or {}
|
||||
items_by_key = index_items_by_key(items)
|
||||
overrides = load_hero_overrides()
|
||||
@@ -350,7 +400,7 @@ def main() -> None:
|
||||
tags = list(h.get("tags") or [])
|
||||
summary = hero_summary(key, abilities_db, tags, unit_target_keys)
|
||||
by_hero[key] = fears_for_hero(
|
||||
key, summary, by_tag, items_by_key, overrides, args.top
|
||||
key, summary, by_tag, items_by_key, overrides, counter_stats, args.top
|
||||
)
|
||||
|
||||
items_out = {
|
||||
@@ -369,6 +419,17 @@ def main() -> None:
|
||||
"top_n": args.top,
|
||||
"heroes": len(by_hero),
|
||||
"overrides": str(HERO_FEAR_OVERRIDES.relative_to(DATA.parent)).replace("\\", "/"),
|
||||
"counter_stats": (
|
||||
{
|
||||
"path": str(ITEM_COUNTER_STATS.relative_to(DATA.parent)).replace("\\", "/"),
|
||||
"source": counter_meta.get("source"),
|
||||
"fetched_at": counter_meta.get("fetched_at"),
|
||||
"method": counter_meta.get("method"),
|
||||
"caveat": counter_meta.get("caveat"),
|
||||
}
|
||||
if counter_meta
|
||||
else None
|
||||
),
|
||||
},
|
||||
"by_hero": by_hero,
|
||||
"items": items_out,
|
||||
|
||||
Reference in New Issue
Block a user