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:
voson
2026-07-29 02:11:49 +08:00
co-authored by Cursor
parent 37769580f5
commit 3ec8007077
72 changed files with 20669 additions and 2105 deletions
+63 -2
View File
@@ -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,