Files
climperor/pc/item_suggest.py

299 lines
8.1 KiB
Python

"""Draft item suggestions: hero core builds + qualitative answers vs enemies.
Core items come from web/data/hero_items.json (relative popularity).
Answer items are rule-mapped from enemy tags / push-global archetypes.
Does not read hero_item_fears stats or STRATZ.
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import json
from functools import lru_cache
from draft_archetypes import detect_archetypes, tag_profile
from shared.grid import hero_table
from shared.hero_tags import tags_for_hero
from shared.paths import DATA, ROOT
DEFAULT_HERO_ITEMS = DATA / "hero_items.json"
DEFAULT_ITEMS_META = DATA / "items_meta.json"
# Heroes that commonly pick from fog / invis — qualitative only.
INVIS_HEROES = frozenset({
"riki",
"bounty_hunter",
"clinkz",
"weaver",
"nyx_assassin",
"templar_assassin",
"mirana",
"treant",
"windrunner",
"slark",
"invoker",
"sand_king",
})
# Soft heal / sustain cores — vessel answers.
HEAL_HEROES = frozenset({
"omniknight",
"winter_wyvern",
"bane",
"undying",
"abaddon",
"oracle",
"chen",
"io",
"wisp",
"dazzle",
"warlock",
})
# Fallback Chinese names when catalogs miss an entry.
_FALLBACK_NAMES = {
"black_king_bar": "黑皇杖",
"bfury": "狂战斧",
"disperser": "散魂剑",
"gem": "真视宝石",
"dust": "显影之尘",
"pipe": "洞察烟斗",
"eternal_shroud": "永世护盾",
"travel_boots": "远行鞋",
"crimson_guard": "赤红甲",
"spirit_vessel": "魂之灵瓮",
}
def _resolve_path(path: str | Path | None, default: Path) -> Path:
if not path:
return default
p = Path(path)
if not p.is_absolute():
p = ROOT / p
return p
@lru_cache(maxsize=4)
def _load_hero_items(path_str: str) -> dict:
path = Path(path_str)
if not path.exists():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
@lru_cache(maxsize=4)
def _load_items_meta(path_str: str) -> dict[str, str]:
path = Path(path_str)
if not path.exists():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
items = data.get("items") if isinstance(data, dict) else None
if not isinstance(items, dict):
return {}
out: dict[str, str] = {}
for row in items.values():
if not isinstance(row, dict):
continue
key = row.get("key")
if not key:
continue
name = row.get("name_loc") or row.get("dname") or key
out[str(key)] = str(name)
return out
def _tags_by_key() -> dict[str, list[str]]:
return {
h["key"]: list(h.get("tags") or []) or tags_for_hero(h["key"], h.get("roles"))
for h in hero_table()
}
def _name_for(
key: str,
*,
catalog: dict[str, dict],
meta_names: dict[str, str],
) -> str:
row = catalog.get(key) or {}
return (
str(row.get("name_loc") or "")
or meta_names.get(key)
or _FALLBACK_NAMES.get(key)
or key
)
def _catalog_from_hero_items(data: dict) -> dict[str, dict]:
"""id-str -> row and also key -> row for lookups."""
items = data.get("items") or {}
by_key: dict[str, dict] = {}
for row in items.values():
if isinstance(row, dict) and row.get("key"):
by_key[str(row["key"])] = row
return by_key
def core_items_for_hero(
self_hero: str,
data: dict,
*,
core_n: int = 3,
catalog: dict[str, dict] | None = None,
meta_names: dict[str, str] | None = None,
) -> list[dict]:
"""Top relative-popularity finished items for one hero."""
if not self_hero or core_n <= 0:
return []
by_hero = data.get("by_hero") or {}
rows = list(by_hero.get(self_hero) or [])
items_by_id = data.get("items") or {}
catalog = catalog if catalog is not None else _catalog_from_hero_items(data)
meta_names = meta_names or {}
out: list[dict] = []
seen: set[str] = set()
for row in rows:
if len(out) >= core_n:
break
iid = str(row.get("id") if isinstance(row, dict) else row)
meta = items_by_id.get(iid) or items_by_id.get(int(iid) if iid.isdigit() else iid) or {}
key = str(meta.get("key") or "")
if not key or key in seen:
continue
seen.add(key)
out.append({
"key": key,
"name_loc": _name_for(key, catalog=catalog, meta_names=meta_names),
"kind": "core",
"reason": "常用",
})
return out
def answer_items_for_enemies(
enemies: list[str],
*,
answer_n: int = 3,
catalog: dict[str, dict] | None = None,
meta_names: dict[str, str] | None = None,
tags_by_key: dict[str, list[str]] | None = None,
) -> list[dict]:
"""Qualitative counter items from enemy tags / archetypes."""
if answer_n <= 0 or not enemies:
return []
tags_by_key = tags_by_key or _tags_by_key()
catalog = catalog or {}
meta_names = meta_names or {}
profile = tag_profile(enemies, tags_by_key)
arches = detect_archetypes(enemies, tags_by_key)
candidates: list[tuple[str, str]] = []
control_n = int(profile.get("控制") or 0) + int(profile.get("先手") or 0)
if control_n >= 2:
candidates.append(("black_king_bar", "克控制"))
if int(profile.get("幻象") or 0) >= 1 or any(
"幻象" in (tags_by_key.get(e) or []) for e in enemies
):
candidates.append(("bfury", "清幻象"))
candidates.append(("disperser", "打幻象"))
if any(e in INVIS_HEROES for e in enemies):
candidates.append(("gem", "克隐身"))
candidates.append(("dust", "显影"))
if int(profile.get("爆发") or 0) >= 2:
candidates.append(("pipe", "克魔法"))
candidates.append(("eternal_shroud", "魔抗"))
if any(e in HEAL_HEROES for e in enemies):
candidates.append(("spirit_vessel", "克回复"))
if "push" in arches:
candidates.append(("crimson_guard", "抗推进"))
candidates.append(("travel_boots", "对推进"))
if "global" in arches:
candidates.append(("travel_boots", "对全球流"))
out: list[dict] = []
seen: set[str] = set()
for key, reason in candidates:
if key in seen:
continue
seen.add(key)
out.append({
"key": key,
"name_loc": _name_for(key, catalog=catalog, meta_names=meta_names),
"kind": "answer",
"reason": reason,
})
if len(out) >= answer_n:
break
return out
def suggest_items(
self_hero: str | None,
enemies: list[str] | None,
*,
core_n: int = 3,
answer_n: int = 3,
max_total: int = 6,
hero_items_path: str | Path | None = None,
items_meta_path: str | Path | None = None,
) -> list[dict]:
"""Merge answer items then core items; dedupe by key; soft-fail to []."""
if not self_hero or max_total <= 0:
return []
hi_path = _resolve_path(hero_items_path, DEFAULT_HERO_ITEMS)
meta_path = _resolve_path(items_meta_path, DEFAULT_ITEMS_META)
data = _load_hero_items(str(hi_path))
if not data:
return []
catalog = _catalog_from_hero_items(data)
meta_names = _load_items_meta(str(meta_path))
answers = answer_items_for_enemies(
list(enemies or []),
answer_n=answer_n,
catalog=catalog,
meta_names=meta_names,
)
cores = core_items_for_hero(
self_hero,
data,
core_n=core_n,
catalog=catalog,
meta_names=meta_names,
)
out: list[dict] = []
seen: set[str] = set()
for row in answers + cores:
key = row.get("key")
if not key or key in seen:
continue
seen.add(key)
out.append(row)
if len(out) >= max_total:
break
return out
__all__ = [
"DEFAULT_HERO_ITEMS",
"answer_items_for_enemies",
"core_items_for_hero",
"suggest_items",
]