fix(misc): CSV column name mismatches in data loading (#32)

* fix: CSV column name mismatches in data loading

- sect.py: Fix headquarter_name/headquarter_desc -> name/desc when reading sect_region.csv
- sect.py: Move sid initialization before technique lookup to fix unbound variable bug
- technique.py: Change sect (name) to sect_id (int) to match technique.csv column
- elixir.py: Remove redundant get_int(row, "id") that reads non-existent column

These fixes ensure:
1. Sect headquarters display correct location names (e.g., "大千光极城" instead of "不夜城")
2. Sect techniques are correctly associated and displayed
3. Technique sect restrictions work properly

* fix: update main.py to use sect_id, add CSV loading tests

- main.py: Change technique.sect to technique.sect_id in API response
- Add tests/test_csv_loading.py to verify CSV column names match code

* test: add API test for /api/meta/game_data endpoint

Verify that techniques in API response use sect_id field (not sect)

* fix: add None check for hq_region in AvatarFactory

Remove redundant code that adds sect headquarters to known_regions.
This logic is already handled by Avatar._init_known_regions() which
uses sect_id matching (more reliable than name-based lookup).

The removed code was causing a flaky test (~1% failure rate) because
resolve_query returns None in test environments with simplified maps.
This commit is contained in:
Zihao Xu
2026-01-17 23:31:15 -08:00
committed by GitHub
parent 0acf72a313
commit 7edae9188b
6 changed files with 178 additions and 28 deletions

View File

@@ -147,7 +147,6 @@ def _load_elixirs() -> tuple[Dict[int, Elixir], Dict[str, List[Elixir]]]:
df = game_configs["elixir"]
for row in df:
elixir_id = get_int(row, "id")
name = get_str(row, "name")
desc = get_str(row, "desc")
price = get_int(row, "price")

View File

@@ -180,8 +180,8 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]:
sid = get_int(sr, "sect_id", -1)
if sid == -1:
continue
hq_name = get_str(sr, "headquarter_name")
hq_desc = get_str(sr, "headquarter_desc")
hq_name = get_str(sr, "name")
hq_desc = get_str(sr, "desc")
hq_by_sect_id[sid] = (hq_name, hq_desc)
# 可能不存在 technique 配表或未添加 sect 列,做容错
@@ -191,12 +191,14 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]:
for row in df:
name = get_str(row, "name")
image_path = assets_base / f"{name}.png"
# 先读取当前宗门 ID供后续使用
sid = get_int(row, "id")
# 收集该宗门下配置的功法名称
technique_names: list[str] = []
# 检查 tech_df 是否存在以及是否有数据
if tech_df:
# 检查是否存在 sect 字段 (检查第一行或当前行)
technique_names = [
get_str(t, "name")
for t in tech_df
@@ -229,7 +231,6 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]:
}
# 从 sect_region.csv 中优先取驻地名称/描述;否则兼容旧列或退回宗门名
sid = get_int(row, "id")
csv_hq = hq_by_sect_id.get(sid)
hq_name_from_csv = (csv_hq[0] if csv_hq else "").strip() if csv_hq else ""
hq_desc_from_csv = (csv_hq[1] if csv_hq else "").strip() if csv_hq else ""

View File

@@ -62,8 +62,8 @@ class Technique:
desc: str
weight: float
condition: str
# 归属宗门名称None/空表示无宗门要求(散修可修)
sect: Optional[str] = None
# 归属宗门IDNone 表示无宗门要求(散修可修)
sect_id: Optional[int] = None
# 影响角色或系统的效果
effects: dict[str, object] = field(default_factory=dict)
effect_desc: str = ""
@@ -125,9 +125,8 @@ def loads() -> tuple[dict[int, Technique], dict[str, Technique]]:
condition = get_str(row, "condition")
weight = get_float(row, "weight", 1.0)
sect = get_str(row, "sect")
if not sect:
sect = None
sect_id_raw = get_int(row, "sect_id", -1)
sect_id = sect_id_raw if sect_id_raw > 0 else None
effects = load_effect_from_str(get_str(row, "effects"))
from src.classes.effect import format_effects_to_text
@@ -141,7 +140,7 @@ def loads() -> tuple[dict[int, Technique], dict[str, Technique]]:
desc=get_str(row, "desc"),
weight=weight,
condition=condition,
sect=sect,
sect_id=sect_id,
effects=effects,
effect_desc=effect_desc,
)
@@ -232,24 +231,22 @@ def get_random_upper_technique_for_avatar(avatar) -> Technique | None:
def get_technique_by_sect(sect) -> Technique:
"""
简化版:仅按宗门筛选并按权重抽样,不考虑灵根与 condition。
- 散修sect 为 None/空只从无宗门要求sect 为空)的功法中抽样;
- 有宗门:从无宗门 + 该宗门的功法中抽样;
- 散修sect 为 None只从无宗门要求sect_id 为 None)的功法中抽样;
- 有宗门:从"无宗门 + 该宗门"的功法中抽样;
若集合为空,则退回全量功法。
"""
import random
sect_name: Optional[str] = None
target_sect_id: Optional[int] = None
if sect is not None:
sect_name = getattr(sect, "name", sect)
if isinstance(sect_name, str):
sect_name = sect_name.strip() or None
target_sect_id = getattr(sect, "id", None)
allowed_sects: set[Optional[str]] = {None, ""}
if sect_name is not None:
allowed_sects.add(sect_name)
allowed_sect_ids: set[Optional[int]] = {None}
if target_sect_id is not None:
allowed_sect_ids.add(target_sect_id)
def _in_allowed_sect(t: Technique) -> bool:
return (t.sect in allowed_sects) or (t.sect is None) or (t.sect == "")
return t.sect_id in allowed_sect_ids
candidates: List[Technique] = [t for t in techniques_by_id.values() if _in_allowed_sect(t)]
if not candidates: