v0.2.0: relations preview, item shop, abilities, overlay recommend, GSI enhancements
- Add relations/item/abilities preview (serve_relations.py + web/relations/) - Add fetch scripts: hero_items, item_shop, items_meta, hero_abilities, ability_videos, patches, stratz, matchups, portraits - Add overlay.py (role tags + Top-3 cyan marks), recommend.py - Add http_utils.py, loc_format.py, hero_tags.py, item_fears.py - GSI: full payload JSONL dump, foreground window detection - Drop real template library; CDN-only matching - Update docs: CHANGELOG 0.2.0, DESIGN config table, AGENTS module table - .gitignore: exclude large regenerable assets (icons/portraits/videos)
This commit is contained in:
+159
@@ -0,0 +1,159 @@
|
||||
"""Draft suggestions from qualitative hero relations.
|
||||
|
||||
Score candidates for the player's role-queue position using:
|
||||
+1 per enemy the candidate counters
|
||||
-1 per enemy that counters the candidate
|
||||
+1 per already-locked ally synergy
|
||||
|
||||
Data: data/relations.json (not OpenDota winrates).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from common import ROOT
|
||||
from grid import hero_table
|
||||
from relations import DEFAULT_RELATIONS, indexes, load_relations
|
||||
|
||||
DEFAULT_ROLE_TAGS = {
|
||||
"1": ["Carry"],
|
||||
"2": ["Carry", "Nuker", "Escape"],
|
||||
"3": ["Initiator", "Durable", "Carry"],
|
||||
"4": ["Support"],
|
||||
"5": ["Support"],
|
||||
}
|
||||
|
||||
|
||||
def _maps() -> tuple[dict[str, str], dict[str, list[str]]]:
|
||||
table = hero_table()
|
||||
names = {h["key"]: h["name_loc"] for h in table}
|
||||
roles = {h["key"]: list(h.get("roles") or []) for h in table}
|
||||
return names, roles
|
||||
|
||||
|
||||
def enemy_keys(confirmed: dict[int, str], self_team: str | None) -> list[str]:
|
||||
if self_team == "radiant":
|
||||
slots = range(6, 11)
|
||||
elif self_team == "dire":
|
||||
slots = range(1, 6)
|
||||
else:
|
||||
return []
|
||||
return [confirmed[s] for s in slots if confirmed.get(s)]
|
||||
|
||||
|
||||
def ally_keys(confirmed: dict[int, str], self_team: str | None, self_slot: int | None = None) -> list[str]:
|
||||
"""Teammates already locked (excludes your own slot)."""
|
||||
if self_team == "radiant":
|
||||
slots = range(1, 6)
|
||||
elif self_team == "dire":
|
||||
slots = range(6, 11)
|
||||
else:
|
||||
return []
|
||||
out = []
|
||||
for s in slots:
|
||||
if self_slot is not None and s == self_slot:
|
||||
continue
|
||||
if confirmed.get(s):
|
||||
out.append(confirmed[s])
|
||||
return out
|
||||
|
||||
|
||||
def candidates_for_position(
|
||||
position: int | None,
|
||||
*,
|
||||
roles_by_key: dict[str, list[str]],
|
||||
role_tags: dict | None = None,
|
||||
) -> list[str]:
|
||||
tags_map = role_tags or DEFAULT_ROLE_TAGS
|
||||
if position is None:
|
||||
return []
|
||||
wanted = set(tags_map.get(str(position)) or tags_map.get(position) or [])
|
||||
if not wanted:
|
||||
return []
|
||||
out = []
|
||||
for key, tags in roles_by_key.items():
|
||||
if wanted.intersection(tags):
|
||||
out.append(key)
|
||||
return out
|
||||
|
||||
|
||||
def suggest_top(
|
||||
*,
|
||||
position: int | None,
|
||||
enemies: list[str],
|
||||
allies: list[str] | None = None,
|
||||
exclude: set[str] | list[str],
|
||||
relations: dict | None = None,
|
||||
top_n: int = 3,
|
||||
role_tags: dict | None = None,
|
||||
**_ignored,
|
||||
) -> list[dict]:
|
||||
"""Return Top-N picks from qualitative counters / synergies.
|
||||
|
||||
Extra kwargs (matchups, synergies, min_games) accepted and ignored for
|
||||
backward compatibility with older call sites.
|
||||
"""
|
||||
if position is None:
|
||||
return []
|
||||
allies = list(allies or [])
|
||||
enemies = list(enemies or [])
|
||||
if not enemies and not allies:
|
||||
return []
|
||||
rel = relations if relations is not None else load_relations()
|
||||
if not rel.get("counters") and not rel.get("synergies"):
|
||||
return []
|
||||
|
||||
names, roles_by_key = _maps()
|
||||
counters_of, countered_by, synergies_of = indexes(rel)
|
||||
exclude_set = {e for e in exclude if e}
|
||||
|
||||
scored: list[tuple[int, str, dict]] = []
|
||||
for cand in candidates_for_position(position, roles_by_key=roles_by_key, role_tags=role_tags):
|
||||
if cand in exclude_set:
|
||||
continue
|
||||
beats = []
|
||||
beaten_by = []
|
||||
with_allies = []
|
||||
for ek in enemies:
|
||||
for edge in counters_of.get(cand) or []:
|
||||
if edge["key"] == ek:
|
||||
beats.append({"enemy": ek, "reason": edge.get("reason") or ""})
|
||||
for edge in countered_by.get(cand) or []:
|
||||
if edge["key"] == ek:
|
||||
beaten_by.append({"enemy": ek, "reason": edge.get("reason") or ""})
|
||||
for ak in allies:
|
||||
for edge in synergies_of.get(cand) or []:
|
||||
if edge["key"] == ak:
|
||||
with_allies.append({"ally": ak, "reason": edge.get("reason") or ""})
|
||||
score = len(beats) - len(beaten_by) + len(with_allies)
|
||||
if score == 0 and not beats and not beaten_by and not with_allies:
|
||||
continue
|
||||
scored.append((score, cand, {
|
||||
"beats": beats,
|
||||
"beaten_by": beaten_by,
|
||||
"with": with_allies,
|
||||
}))
|
||||
|
||||
scored.sort(key=lambda t: (t[0], t[1]), reverse=True)
|
||||
out = []
|
||||
for rank, (score, key, detail) in enumerate(scored[: max(0, top_n)], start=1):
|
||||
out.append({
|
||||
"key": key,
|
||||
"name_loc": names.get(key, key),
|
||||
"rank": rank,
|
||||
"score": score,
|
||||
**detail,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_RELATIONS",
|
||||
"DEFAULT_ROLE_TAGS",
|
||||
"ally_keys",
|
||||
"candidates_for_position",
|
||||
"enemy_keys",
|
||||
"load_relations",
|
||||
"suggest_top",
|
||||
]
|
||||
Reference in New Issue
Block a user