Add AI Director defense and style metrics
This commit is contained in:
parent
d6fec5029e
commit
5947ea048f
@ -95,7 +95,7 @@ Use this loop for all long-running AI intelligence work. The goal is not just "n
|
||||
|
||||
4. Classify the problem type.
|
||||
- Correctness: failed games, exceptions, null references, action timeout, action budget stop, no-effect successful actions, illegal repeated actions.
|
||||
- Intelligence: weak expansion, low city count, poor unit count, bad attrition, obvious idle/economic waste, bad hero task timing, over-defense, under-attack.
|
||||
- Intelligence: weak expansion, low city count, poor unit count, bad attrition, city loss, long capital threat, empty threatened cities, obvious idle/economic waste, bad hero task timing, weak hero style expression, weak special-unit skill expression, over-defense, under-attack.
|
||||
- Performance: low actions/sec, high avg game runtime, high decision p95/max, too many actions per player turn, oversized JSONL output.
|
||||
- Noise: legal repeated city upgrades, normal tactical move/attack chains, expected Steam warmup messages, incomplete logs from killed batches.
|
||||
|
||||
@ -124,6 +124,7 @@ Primary guardrails:
|
||||
- `repeated` should be `0` after excluding known legal repeats such as same-turn `CityLevelUpAction:Park` consuming city upgrade points.
|
||||
- `maxActions/playerTurn` should stay well below the forced-stop budget; investigate anything above `80`.
|
||||
- No AI loop, forced AI stop, fatal exception, or unresolved null reference is acceptable.
|
||||
- `cityLost` and `capitalOwnershipChanged` are high-severity intelligence/correctness signals; inspect the TurnStart-to-TurnStart city signature before changing combat weights.
|
||||
|
||||
Intelligence targets for compact 17-player, 20x20, 20-turn Director batches:
|
||||
|
||||
@ -132,6 +133,9 @@ Intelligence targets for compact 17-player, 20x20, 20-turn Director batches:
|
||||
- Track max city count and alive `>=3` city ratio as snowball signals, but do not overfit to one high-roll player.
|
||||
- Unit count and score p10/p50/p90 should not collapse while expansion improves.
|
||||
- Attrition should be interpreted with context: more kills and more deaths may indicate stronger aggression, not necessarily worse play.
|
||||
- Defense should be read from `Defense:` in the batch analyzer: `cityLost=0` is the target for short compact batches; rising `capitalThreatTurns`, `emptyThreatTurns`, or `worsened > resolved` means Emergency, city production, or Hold/Front logic needs review.
|
||||
- Hero style should not collapse to only `General`; `style buckets` should show expected class/faction roles such as Defense, Recovery, Burst, Control, Summon, Economy, Mobility, and HeroLifecycle as heroes appear.
|
||||
- Special-unit expression should be checked through `Unit skill expression`: `skill-bearing actor actions`, `signature-changing actions`, and `actor signatures` reveal whether new faction units are actually participating.
|
||||
|
||||
Performance targets:
|
||||
|
||||
|
||||
@ -55,6 +55,60 @@ def average(values) -> float:
|
||||
return sum(values) / len(values)
|
||||
|
||||
|
||||
def contains_any(value: str, tokens) -> bool:
|
||||
if not value:
|
||||
return False
|
||||
lower = value.lower()
|
||||
return any(token.lower() in lower for token in tokens if token)
|
||||
|
||||
|
||||
def classify_style_bucket(reason: str, action: str) -> str:
|
||||
text = f"{reason or ''} {action or ''}"
|
||||
if contains_any(text, ("LowHp", "Recover", "Heal", "Eirin", "Sanae", "Patchouli", "Absorb", "Revive")):
|
||||
return "Recovery"
|
||||
if contains_any(text, ("Defense", "Defend", "Protect", "Guard", "MoveToCity", "Aunn", "Meiling", "Sakuya", "Reimu", "ShakeOff", "Unsit")):
|
||||
return "Defense"
|
||||
if contains_any(text, ("Kill", "Flandre", "Assassin", "AttackValue", "LocalBattle", "Boom", "Mokou", "Reisen", "Yuugi")):
|
||||
return "Burst"
|
||||
if contains_any(text, ("Ban", "Fear", "Control", "Satori", "Koishi", "Sumireko", "Orb", "Ground")):
|
||||
return "Control"
|
||||
if contains_any(text, ("Summon", "CreateMini", "Suwako", "Snake", "BonePile", "Mini")):
|
||||
return "Summon"
|
||||
if contains_any(text, ("Economy", "CityExp", "Corpse", "KanakoSit", "Rin", "Tewi", "Kaguya")):
|
||||
return "Economy"
|
||||
if contains_any(text, ("MoveAgain", "MoveToFront", "Retreat", "Mobility", "Aya", "Kasen")):
|
||||
return "Mobility"
|
||||
if contains_any(text, ("SelectHero", "SpawnHero", "FinishLowestTask")):
|
||||
return "HeroLifecycle"
|
||||
return "General"
|
||||
|
||||
|
||||
def is_defensive_style(bucket: str) -> bool:
|
||||
return bucket in ("Defense", "Recovery")
|
||||
|
||||
|
||||
def is_hero_action(action: dict) -> bool:
|
||||
if not action:
|
||||
return False
|
||||
actor_unit_type = action.get("actorUnitType") or ""
|
||||
actor_giant_type = action.get("actorGiantType") or ""
|
||||
giant_type = action.get("giantType") or ""
|
||||
return (
|
||||
actor_unit_type == "Giant"
|
||||
or actor_giant_type not in ("", "None")
|
||||
or giant_type not in ("", "None")
|
||||
)
|
||||
|
||||
|
||||
def compact_skill_signature(signature: str) -> str:
|
||||
if not signature:
|
||||
return ""
|
||||
parts = signature.split("|")
|
||||
if len(parts) <= 4:
|
||||
return signature
|
||||
return "|".join(parts[:4]) + "|..."
|
||||
|
||||
|
||||
def percentile(values, ratio: float) -> float:
|
||||
values = sorted(value for value in values if value is not None)
|
||||
if not values:
|
||||
@ -197,6 +251,27 @@ def analyze_logs(log_paths):
|
||||
"ready_hero_task_delta": 0,
|
||||
"forced_hero_task_delta": 0,
|
||||
"hero_task_progress_delta": 0,
|
||||
"critical_city_threat_turns": 0,
|
||||
"capital_threat_turns": 0,
|
||||
"empty_threatened_city_turns": 0,
|
||||
"empty_threatened_city_total": 0,
|
||||
"city_threat_resolved": 0,
|
||||
"city_threat_worsened": 0,
|
||||
"city_lost": 0,
|
||||
"city_gained": 0,
|
||||
"capital_ownership_changed": 0,
|
||||
"emergency_decisions": 0,
|
||||
"emergency_executions": 0,
|
||||
"defender_return": 0,
|
||||
"hero_defensive_use": 0,
|
||||
"defense_reasons": Counter(),
|
||||
"defense_actions": Counter(),
|
||||
"hero_style_buckets": Counter(),
|
||||
"hero_style_reasons": Counter(),
|
||||
"hero_style_actions": Counter(),
|
||||
"unit_skill_actions": Counter(),
|
||||
"unit_skill_changed_actions": Counter(),
|
||||
"actor_skill_signatures": Counter(),
|
||||
}
|
||||
|
||||
for path in log_paths:
|
||||
@ -223,6 +298,8 @@ def analyze_logs(log_paths):
|
||||
stable_rows_by_turn = defaultdict(list)
|
||||
turn_rows = defaultdict(list)
|
||||
decision_lane_by_turn_action = {}
|
||||
decision_reason_by_turn_action = {}
|
||||
last_turn_summary_by_player = {}
|
||||
for row in rows:
|
||||
event_type = row.get("eventType", "")
|
||||
if event_type == "Decision":
|
||||
@ -242,15 +319,50 @@ def analyze_logs(log_paths):
|
||||
stable_key(action),
|
||||
)
|
||||
decision_lane_by_turn_action[turn_action_key] = lane
|
||||
decision_reason_by_turn_action[turn_action_key] = reason
|
||||
if lane == "Emergency":
|
||||
aggregate["emergency_decisions"] += 1
|
||||
aggregate["defense_reasons"][reason] += 1
|
||||
aggregate["defense_actions"][action_key(action)] += 1
|
||||
if lane in ("HeroManagement", "HeroPlaybook"):
|
||||
aggregate["hero_lane_counts"][lane] += 1
|
||||
aggregate["hero_reasons"][reason] += 1
|
||||
aggregate["hero_actions"][action_key(action)] += 1
|
||||
key = action_key(action)
|
||||
aggregate["hero_actions"][key] += 1
|
||||
bucket = classify_style_bucket(reason, key)
|
||||
aggregate["hero_style_buckets"][bucket] += 1
|
||||
aggregate["hero_style_reasons"][f"{bucket}:{reason}"] += 1
|
||||
aggregate["hero_style_actions"][f"{bucket}:{key}"] += 1
|
||||
if decision.get("isFallback"):
|
||||
aggregate["fallback_reasons"][reason] += 1
|
||||
aggregate["fallback_actions"][action_key(action)] += 1
|
||||
continue
|
||||
|
||||
if event_type == "TurnStart":
|
||||
turn_summary = row.get("turnSummary") or {}
|
||||
player_id = row.get("playerId", 0)
|
||||
if int(turn_summary.get("criticalCityThreatCount") or 0) > 0:
|
||||
aggregate["critical_city_threat_turns"] += 1
|
||||
if int(turn_summary.get("capitalThreatCount") or 0) > 0:
|
||||
aggregate["capital_threat_turns"] += 1
|
||||
empty_threatened = int(turn_summary.get("emptyThreatenedCityCount") or 0)
|
||||
if empty_threatened > 0:
|
||||
aggregate["empty_threatened_city_turns"] += 1
|
||||
aggregate["empty_threatened_city_total"] += empty_threatened
|
||||
|
||||
previous = last_turn_summary_by_player.get(player_id)
|
||||
if previous:
|
||||
previous_cities = int(previous.get("cityCount") or 0)
|
||||
current_cities = int(turn_summary.get("cityCount") or 0)
|
||||
if current_cities < previous_cities:
|
||||
aggregate["city_lost"] += previous_cities - current_cities
|
||||
if current_cities > previous_cities:
|
||||
aggregate["city_gained"] += current_cities - previous_cities
|
||||
if (previous.get("capitalCityIdsSignature") or "") != (turn_summary.get("capitalCityIdsSignature") or ""):
|
||||
aggregate["capital_ownership_changed"] += 1
|
||||
last_turn_summary_by_player[player_id] = turn_summary
|
||||
continue
|
||||
|
||||
if event_type != "Execution":
|
||||
continue
|
||||
|
||||
@ -263,6 +375,7 @@ def analyze_logs(log_paths):
|
||||
key = action_key(action)
|
||||
turn_action_key = (player_id, row.get("playerTurn", 0), stable_key(action))
|
||||
decision_lane = decision_lane_by_turn_action.get(turn_action_key, "")
|
||||
decision_reason = decision_reason_by_turn_action.get(turn_action_key, "")
|
||||
aggregate["player_actions"][player_id] += 1
|
||||
if decision_lane in ("HeroManagement", "HeroPlaybook"):
|
||||
aggregate["hero_executed_actions"][key] += 1
|
||||
@ -277,6 +390,31 @@ def analyze_logs(log_paths):
|
||||
aggregate["ready_hero_task_delta"] += int(delta.get("readyHeroTaskDelta") or 0)
|
||||
aggregate["forced_hero_task_delta"] += int(delta.get("forcedHeroTaskDelta") or 0)
|
||||
aggregate["hero_task_progress_delta"] += int(delta.get("heroTaskProgressDelta") or 0)
|
||||
if delta.get("cityThreatResolved"):
|
||||
aggregate["city_threat_resolved"] += 1
|
||||
if delta.get("cityThreatWorsened"):
|
||||
aggregate["city_threat_worsened"] += 1
|
||||
if decision_lane == "Emergency":
|
||||
aggregate["emergency_executions"] += 1
|
||||
if decision_lane == "Emergency" and key.startswith("UnitMove"):
|
||||
aggregate["defender_return"] += 1
|
||||
if is_hero_action(action):
|
||||
bucket = classify_style_bucket(decision_reason, key)
|
||||
if is_defensive_style(bucket) or decision_lane == "Emergency":
|
||||
aggregate["hero_defensive_use"] += 1
|
||||
|
||||
before = execution.get("before") or {}
|
||||
skill_signature = before.get("unitSkillSignature") or ""
|
||||
if skill_signature:
|
||||
aggregate["unit_skill_actions"][key] += 1
|
||||
aggregate["actor_skill_signatures"][compact_skill_signature(skill_signature)] += 1
|
||||
if (
|
||||
delta.get("unitSkillSignatureChanged")
|
||||
or delta.get("targetUnitSkillSignatureChanged")
|
||||
or int(delta.get("unitSkillDelta") or 0) != 0
|
||||
or int(delta.get("targetUnitSkillDelta") or 0) != 0
|
||||
):
|
||||
aggregate["unit_skill_changed_actions"][key] += 1
|
||||
|
||||
if delta.get("targetUnitDied"):
|
||||
aggregate["kills"] += 1
|
||||
@ -329,6 +467,17 @@ def quality_warnings(batch, metrics, log_metrics):
|
||||
warnings.append(f"NO_EFFECT_ACTIONS: count={log_metrics['no_effect']}.")
|
||||
if log_metrics["repeated_stable"] > 0:
|
||||
warnings.append(f"REPEATED_ACTIONS: count={log_metrics['repeated_stable']}.")
|
||||
if log_metrics["city_lost"] > 0:
|
||||
warnings.append(f"CITY_LOSS: cityLost={log_metrics['city_lost']}.")
|
||||
if log_metrics["capital_ownership_changed"] > 0:
|
||||
warnings.append(f"CAPITAL_CHANGED: count={log_metrics['capital_ownership_changed']}.")
|
||||
defense_opportunities = log_metrics["critical_city_threat_turns"] + log_metrics["empty_threatened_city_turns"]
|
||||
if defense_opportunities > 0 and log_metrics["emergency_decisions"] == 0:
|
||||
warnings.append(f"NO_EMERGENCY_RESPONSE: defenseOpportunities={defense_opportunities}.")
|
||||
if log_metrics["city_threat_worsened"] > log_metrics["city_threat_resolved"] + max(2, defense_opportunities // 4):
|
||||
warnings.append(
|
||||
f"DEFENSE_WORSENING: worsened={log_metrics['city_threat_worsened']} resolved={log_metrics['city_threat_resolved']}."
|
||||
)
|
||||
if log_metrics["decide_ms"]:
|
||||
p95 = percentile(log_metrics["decide_ms"], 0.95)
|
||||
if p95 > 60:
|
||||
@ -412,6 +561,22 @@ def to_jsonable(value):
|
||||
|
||||
def compact_log_metrics(log_metrics, top):
|
||||
decide_ms = log_metrics["decide_ms"]
|
||||
defense_opportunities = log_metrics["critical_city_threat_turns"] + log_metrics["empty_threatened_city_turns"]
|
||||
emergency_response_rate = (
|
||||
min(1.0, log_metrics["emergency_decisions"] / defense_opportunities)
|
||||
if defense_opportunities > 0
|
||||
else 0.0
|
||||
)
|
||||
defense_score = (
|
||||
log_metrics["city_threat_resolved"] * 3
|
||||
+ log_metrics["emergency_executions"]
|
||||
+ log_metrics["defender_return"]
|
||||
+ log_metrics["hero_defensive_use"] * 2
|
||||
- log_metrics["city_lost"] * 8
|
||||
- log_metrics["capital_threat_turns"] * 2
|
||||
- log_metrics["empty_threatened_city_turns"] * 2
|
||||
- log_metrics["city_threat_worsened"] * 2
|
||||
)
|
||||
return {
|
||||
"logs": [str(path) for path in log_metrics["logs"]],
|
||||
"rows": log_metrics["rows"],
|
||||
@ -424,6 +589,26 @@ def compact_log_metrics(log_metrics, top):
|
||||
"self_deaths": log_metrics["self_deaths"],
|
||||
"city_owner_changes": log_metrics["city_owner_changes"],
|
||||
"target_city_owner_changes": log_metrics["target_city_owner_changes"],
|
||||
"defense": {
|
||||
"critical_city_threat_turns": log_metrics["critical_city_threat_turns"],
|
||||
"capital_threat_turns": log_metrics["capital_threat_turns"],
|
||||
"empty_threatened_city_turns": log_metrics["empty_threatened_city_turns"],
|
||||
"empty_threatened_city_total": log_metrics["empty_threatened_city_total"],
|
||||
"defense_opportunity_turns": defense_opportunities,
|
||||
"city_threat_resolved": log_metrics["city_threat_resolved"],
|
||||
"city_threat_worsened": log_metrics["city_threat_worsened"],
|
||||
"city_lost": log_metrics["city_lost"],
|
||||
"city_gained": log_metrics["city_gained"],
|
||||
"capital_ownership_changed": log_metrics["capital_ownership_changed"],
|
||||
"emergency_decisions": log_metrics["emergency_decisions"],
|
||||
"emergency_executions": log_metrics["emergency_executions"],
|
||||
"defender_return": log_metrics["defender_return"],
|
||||
"hero_defensive_use": log_metrics["hero_defensive_use"],
|
||||
"emergency_response_rate": emergency_response_rate,
|
||||
"defense_score": defense_score,
|
||||
"top_reasons": log_metrics["defense_reasons"].most_common(top),
|
||||
"top_actions": log_metrics["defense_actions"].most_common(top),
|
||||
},
|
||||
"decision_time": {
|
||||
"avg_ms": average(decide_ms),
|
||||
"p95_ms": percentile(decide_ms, 0.95),
|
||||
@ -436,6 +621,9 @@ def compact_log_metrics(log_metrics, top):
|
||||
"top_reasons": log_metrics["hero_reasons"].most_common(top),
|
||||
"top_actions": log_metrics["hero_actions"].most_common(top),
|
||||
"top_executed_actions": log_metrics["hero_executed_actions"].most_common(top),
|
||||
"style_buckets": log_metrics["hero_style_buckets"].most_common(top),
|
||||
"style_reasons": log_metrics["hero_style_reasons"].most_common(top),
|
||||
"style_actions": log_metrics["hero_style_actions"].most_common(top),
|
||||
"hero_delta": log_metrics["hero_delta"],
|
||||
"selected_hero_delta": log_metrics["selected_hero_delta"],
|
||||
"hero_task_delta": log_metrics["hero_task_delta"],
|
||||
@ -449,6 +637,11 @@ def compact_log_metrics(log_metrics, top):
|
||||
"top_executed_actions": log_metrics["fallback_executed_actions"].most_common(top),
|
||||
"top_no_effect_actions": log_metrics["fallback_no_effect_actions"].most_common(top),
|
||||
},
|
||||
"unit_skill_expression": {
|
||||
"top_actions": log_metrics["unit_skill_actions"].most_common(top),
|
||||
"top_changed_actions": log_metrics["unit_skill_changed_actions"].most_common(top),
|
||||
"top_actor_signatures": log_metrics["actor_skill_signatures"].most_common(top),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -508,6 +701,36 @@ def print_report(batch_path, batch, metrics, log_metrics, warnings, top):
|
||||
f"cityOwnerChanges={log_metrics['city_owner_changes']} "
|
||||
f"targetCityOwnerChanges={log_metrics['target_city_owner_changes']}"
|
||||
)
|
||||
defense_opportunities = log_metrics["critical_city_threat_turns"] + log_metrics["empty_threatened_city_turns"]
|
||||
emergency_response_rate = (
|
||||
min(1.0, log_metrics["emergency_decisions"] / defense_opportunities)
|
||||
if defense_opportunities > 0
|
||||
else 0.0
|
||||
)
|
||||
defense_score = (
|
||||
log_metrics["city_threat_resolved"] * 3
|
||||
+ log_metrics["emergency_executions"]
|
||||
+ log_metrics["defender_return"]
|
||||
+ log_metrics["hero_defensive_use"] * 2
|
||||
- log_metrics["city_lost"] * 8
|
||||
- log_metrics["capital_threat_turns"] * 2
|
||||
- log_metrics["empty_threatened_city_turns"] * 2
|
||||
- log_metrics["city_threat_worsened"] * 2
|
||||
)
|
||||
print(
|
||||
"Defense: "
|
||||
f"score={defense_score:.1f} "
|
||||
f"criticalThreatTurns={log_metrics['critical_city_threat_turns']} "
|
||||
f"capitalThreatTurns={log_metrics['capital_threat_turns']} "
|
||||
f"emptyThreatTurns={log_metrics['empty_threatened_city_turns']} "
|
||||
f"resolved={log_metrics['city_threat_resolved']} "
|
||||
f"worsened={log_metrics['city_threat_worsened']} "
|
||||
f"cityLost={log_metrics['city_lost']} "
|
||||
f"cityGained={log_metrics['city_gained']} "
|
||||
f"emergencyResponse={emergency_response_rate:.1%} "
|
||||
f"defenderReturn={log_metrics['defender_return']} "
|
||||
f"heroDefense={log_metrics['hero_defensive_use']}"
|
||||
)
|
||||
if log_metrics["decide_ms"]:
|
||||
print(
|
||||
"Decision time: "
|
||||
@ -546,6 +769,26 @@ def print_report(batch_path, batch, metrics, log_metrics, warnings, top):
|
||||
print(f" {count:>5} {key}")
|
||||
else:
|
||||
print(" executed actions: <none>")
|
||||
if log_metrics["hero_style_buckets"]:
|
||||
print(" style buckets:")
|
||||
for key, count in log_metrics["hero_style_buckets"].most_common(top):
|
||||
print(f" {count:>5} {key}")
|
||||
else:
|
||||
print(" style buckets: <none>")
|
||||
|
||||
print("Unit skill expression:")
|
||||
if log_metrics["unit_skill_actions"]:
|
||||
print(" skill-bearing actor actions:")
|
||||
for key, count in log_metrics["unit_skill_actions"].most_common(top):
|
||||
print(f" {count:>5} {key}")
|
||||
else:
|
||||
print(" skill-bearing actor actions: <none>")
|
||||
if log_metrics["unit_skill_changed_actions"]:
|
||||
print(" signature-changing actions:")
|
||||
for key, count in log_metrics["unit_skill_changed_actions"].most_common(top):
|
||||
print(f" {count:>5} {key}")
|
||||
else:
|
||||
print(" signature-changing actions: <none>")
|
||||
|
||||
print("Fallback:")
|
||||
if log_metrics["fallback_actions"]:
|
||||
|
||||
@ -13,6 +13,8 @@ ZERO_DELTA_FIELDS = (
|
||||
"scoreDelta",
|
||||
"sightGridDelta",
|
||||
"cityDelta",
|
||||
"cityLostDelta",
|
||||
"cityGainedDelta",
|
||||
"unitDelta",
|
||||
"heroDelta",
|
||||
"selectedHeroDelta",
|
||||
@ -23,6 +25,9 @@ ZERO_DELTA_FIELDS = (
|
||||
"heroTaskProgressDelta",
|
||||
"criticalCityThreatDelta",
|
||||
"cityThreatDelta",
|
||||
"capitalThreatDelta",
|
||||
"criticalCapitalThreatDelta",
|
||||
"emptyThreatenedCityDelta",
|
||||
"unitHealthDelta",
|
||||
"unitSkillDelta",
|
||||
"unitGridBuildingLevelDelta",
|
||||
@ -110,6 +115,12 @@ def has_no_effect_delta(execution: dict) -> bool:
|
||||
return False
|
||||
if delta.get("cityOwnerChanged") or delta.get("targetCityOwnerChanged"):
|
||||
return False
|
||||
if delta.get("cityOwnershipSignatureChanged") or delta.get("capitalOwnershipSignatureChanged"):
|
||||
return False
|
||||
if delta.get("cityThreatResolved") or delta.get("cityThreatWorsened"):
|
||||
return False
|
||||
if delta.get("cityThreatSignatureChanged") or delta.get("criticalCityThreatSignatureChanged"):
|
||||
return False
|
||||
if delta.get("unitSkillSignatureChanged") or delta.get("targetUnitSkillSignatureChanged"):
|
||||
return False
|
||||
grid_change_fields = (
|
||||
|
||||
@ -575,17 +575,40 @@ AI 可以重新实现,但必须遵守 TH1 的游戏架构:
|
||||
| 外交行为 | 高好感不乱开战,必要时建立使馆或结盟 |
|
||||
| 性能 | 单个 AI 动作决策无明显卡顿 |
|
||||
|
||||
当前批跑调优采用以下基线指标,不用单局主观感觉判断 AI 是否变聪明:
|
||||
当前批跑调优采用以下基线指标,不用单局主观感觉判断 AI 是否变聪明。指标只用于诊断和调参,不反向要求 AI 套统一万能公式。
|
||||
|
||||
| 指标组 | 观察项 | 目标 |
|
||||
|---|---|---|
|
||||
| 正确性 | failedGames、noEffect、repeated、maxActions/playerTurn | 必须为 0 或远低于强制停止线 |
|
||||
| 扩张 | aliveAvgCities、alive>=2、alive>=3、maxCities | 二城率和三城率稳定提升,不能靠单个高滚玩家掩盖整体弱扩张 |
|
||||
| 战斗 | UnitAttack 数、kills、actingUnitDeaths、PriorityTactic/Tactic 占比 | 有战果且不过度白送,城市威胁能被处理 |
|
||||
| 英雄 | selected、spawned、HeroManagement、HeroPlaybook、英雄专属 reason/action | 已选择英雄应尽快上场,英雄动作应体现个人机制 |
|
||||
| 防守 | cityLost、capitalThreatTurns、emptyThreatenedCityTurns、resolved/worsened、emergencyResponse、defenderReturn | 少丢城,首都少长期受压,危险城市有回防或生产响应 |
|
||||
| 英雄 | selected、spawned、HeroManagement、HeroPlaybook、英雄专属 reason/action、styleBuckets | 已选择英雄应尽快上场,英雄动作应体现个人机制 |
|
||||
| 特色单位 | unitSkillActions、unitSkillChangedActions、actorSkillSignatures | 新阵营小兵和特殊单位要通过技能签名、特殊动作、状态变化展示存在感 |
|
||||
| Fallback | Fallback 总数、Fallback actionType、Fallback noEffect | 越接近 0 越好;非 0 时优先把有意义动作归入正式车道 |
|
||||
| 性能 | actions/sec、avgGame、decision avg/p95/max | 先保证聪明,再把明显尖峰纳入下一轮优化 |
|
||||
|
||||
防守指标解释:
|
||||
|
||||
| 指标 | 含义 |
|
||||
|---|---|
|
||||
| cityLost | 同一玩家两次回合开始之间城市数减少,代表上一轮循环没守住 |
|
||||
| capitalThreatTurns | 回合开始时首都处于城市威胁范围 |
|
||||
| emptyThreatenedCityTurns | 回合开始时有受威胁城市中心没有己方或同盟单位驻守 |
|
||||
| resolved/worsened | 某个行动后城市威胁数量、危急威胁或 dangerScore 改善/恶化 |
|
||||
| emergencyResponse | 有防守机会时 Emergency 决策的覆盖率 |
|
||||
| defenderReturn | Emergency 中实际执行回防移动的次数 |
|
||||
|
||||
英雄和特色单位指标解释:
|
||||
|
||||
| 指标 | 含义 |
|
||||
|---|---|
|
||||
| styleBuckets | 把英雄 reason/action 归为 Defense、Recovery、Burst、Control、Summon、Economy、Mobility、HeroLifecycle、General |
|
||||
| heroDefensiveUse | 英雄在防守、治疗、保护或 Emergency 中参与的次数 |
|
||||
| unitSkillActions | 带技能签名的单位实际执行了哪些动作 |
|
||||
| unitSkillChangedActions | 动作前后改变了单位或目标技能签名的动作 |
|
||||
| actorSkillSignatures | 最常参与行动的技能组合,辅助判断新阵营小兵是否真的被用到 |
|
||||
|
||||
问题定位:
|
||||
|
||||
| 现象 | 优先检查 |
|
||||
|
||||
@ -2552,12 +2552,142 @@ Growth: 只遍历合法动作
|
||||
|
||||
## 18. 测试脚本标准
|
||||
|
||||
### 18.0 批量诊断指标
|
||||
|
||||
批量指标不参与单次决策,只服务测试循环。
|
||||
|
||||
```text
|
||||
结构 DefenseMetrics:
|
||||
CriticalCityThreatTurns
|
||||
CapitalThreatTurns
|
||||
EmptyThreatenedCityTurns
|
||||
CityLost
|
||||
CityGained
|
||||
CityThreatResolved
|
||||
CityThreatWorsened
|
||||
EmergencyDecisions
|
||||
EmergencyExecutions
|
||||
DefenderReturn
|
||||
HeroDefensiveUse
|
||||
EmergencyResponseRate
|
||||
DefenseScore
|
||||
```
|
||||
|
||||
```text
|
||||
函数 BuildTurnDefenseSnapshot(map, player):
|
||||
snapshot.CityIds = 当前 player 拥有城市 Id 排序签名
|
||||
snapshot.CapitalCityIds = 当前 player 首都 Id 排序签名
|
||||
snapshot.CityThreatCount = 受威胁城市数量
|
||||
snapshot.CriticalCityThreatCount = 危急城市数量
|
||||
snapshot.CapitalThreatCount = 受威胁首都数量
|
||||
snapshot.EmptyThreatenedCityCount = 受威胁且城市中心无己方/同盟单位驻守的城市数量
|
||||
snapshot.MaxCityDangerScore = 所有城市威胁中的最高危险值
|
||||
返回 snapshot
|
||||
```
|
||||
|
||||
```text
|
||||
函数 BuildActionDefenseDelta(before, after):
|
||||
delta.CityThreatResolved =
|
||||
before.CityThreatCount > 0
|
||||
且 (
|
||||
after.CriticalCityThreatCount < before.CriticalCityThreatCount
|
||||
或 after.CityThreatCount < before.CityThreatCount
|
||||
或 after.MaxCityDangerScore < before.MaxCityDangerScore
|
||||
)
|
||||
|
||||
delta.CityThreatWorsened =
|
||||
after.CriticalCityThreatCount > before.CriticalCityThreatCount
|
||||
或 after.CityThreatCount > before.CityThreatCount
|
||||
或 after.EmptyThreatenedCityCount > before.EmptyThreatenedCityCount
|
||||
或 after.MaxCityDangerScore > before.MaxCityDangerScore
|
||||
|
||||
返回 delta
|
||||
```
|
||||
|
||||
城市丢失不从单个 action 直接统计,按同一玩家两次 `TurnStart` 对比:
|
||||
|
||||
```text
|
||||
函数 AccumulateCityLoss(previousTurnStart, currentTurnStart):
|
||||
如果 current.CityCount < previous.CityCount:
|
||||
CityLost += previous.CityCount - current.CityCount
|
||||
如果 current.CityCount > previous.CityCount:
|
||||
CityGained += current.CityCount - previous.CityCount
|
||||
如果 current.CapitalCityIds != previous.CapitalCityIds:
|
||||
CapitalOwnershipChanged += 1
|
||||
```
|
||||
|
||||
防守综合分只用于批量对比:
|
||||
|
||||
```text
|
||||
DefenseScore =
|
||||
CityThreatResolved * 3
|
||||
+ EmergencyExecutions
|
||||
+ DefenderReturn
|
||||
+ HeroDefensiveUse * 2
|
||||
- CityLost * 8
|
||||
- CapitalThreatTurns * 2
|
||||
- EmptyThreatenedCityTurns * 2
|
||||
- CityThreatWorsened * 2
|
||||
```
|
||||
|
||||
```text
|
||||
EmergencyResponseRate =
|
||||
EmergencyDecisions / max(1, CriticalCityThreatTurns + EmptyThreatenedCityTurns)
|
||||
```
|
||||
|
||||
英雄风格表达按 reason/action 归桶:
|
||||
|
||||
```text
|
||||
函数 ClassifyHeroStyle(reason, actionKey):
|
||||
如果 包含 LowHp、Recover、Heal、Eirin、Sanae、Patchouli、Absorb、Revive:
|
||||
返回 Recovery
|
||||
如果 包含 Defense、Protect、Guard、MoveToCity、Aunn、Meiling、Sakuya、Reimu:
|
||||
返回 Defense
|
||||
如果 包含 Kill、Flandre、AttackValue、Boom、Mokou、Reisen、Yuugi:
|
||||
返回 Burst
|
||||
如果 包含 Ban、Fear、Control、Satori、Koishi、Sumireko、Orb、Ground:
|
||||
返回 Control
|
||||
如果 包含 Summon、CreateMini、Suwako、Snake、BonePile、Mini:
|
||||
返回 Summon
|
||||
如果 包含 Economy、CityExp、Corpse、KanakoSit、Rin、Tewi、Kaguya:
|
||||
返回 Economy
|
||||
如果 包含 MoveAgain、MoveToFront、Retreat、Mobility、Aya、Kasen:
|
||||
返回 Mobility
|
||||
如果 包含 SelectHero、SpawnHero、FinishLowestTask:
|
||||
返回 HeroLifecycle
|
||||
返回 General
|
||||
```
|
||||
|
||||
特色单位表达按技能签名统计:
|
||||
|
||||
```text
|
||||
函数 AccumulateUnitSkillExpression(execution):
|
||||
before = execution.Before
|
||||
action = execution.Action
|
||||
delta = execution.Delta
|
||||
|
||||
如果 before.UnitSkillSignature 为空:
|
||||
返回
|
||||
|
||||
UnitSkillActions[action.ActionKey] += 1
|
||||
ActorSkillSignatures[Compact(before.UnitSkillSignature)] += 1
|
||||
|
||||
如果 delta.UnitSkillSignatureChanged
|
||||
或 delta.TargetUnitSkillSignatureChanged
|
||||
或 delta.UnitSkillDelta != 0
|
||||
或 delta.TargetUnitSkillDelta != 0:
|
||||
UnitSkillChangedActions[action.ActionKey] += 1
|
||||
```
|
||||
|
||||
### 18.1 城市防守
|
||||
|
||||
```text
|
||||
给 AI 城市附近放敌军。
|
||||
期望:
|
||||
Emergency 返回攻击威胁、回防、或危险城市生产。
|
||||
批量日志中 cityLost 尽量为 0。
|
||||
capitalThreatTurns 不应长期升高。
|
||||
resolved 应高于 worsened。
|
||||
```
|
||||
|
||||
### 18.2 占领扩张
|
||||
@ -2574,6 +2704,17 @@ Growth: 只遍历合法动作
|
||||
给 AI 放残血友军、敌方英雄、地面攻击目标、自爆窗口。
|
||||
期望:
|
||||
HeroPlaybook 选择对应英雄动作。
|
||||
styleBuckets 中出现对应英雄风格,不应长期只有 General。
|
||||
```
|
||||
|
||||
### 18.3.1 特色小兵机制
|
||||
|
||||
```text
|
||||
给 AI 放置新阵营特色小兵和普通小兵混合场景。
|
||||
期望:
|
||||
unitSkillActions 中出现特色小兵动作。
|
||||
actorSkillSignatures 能看到该阵营技能组合。
|
||||
如果技能会改变状态,unitSkillChangedActions 应出现对应 action。
|
||||
```
|
||||
|
||||
### 18.4 战线移动
|
||||
|
||||
@ -34,6 +34,8 @@ AI 测试必须回答四个问题:
|
||||
| 扩张 | `aliveAvgCities`、`alive>=2`、`alive>=3`、`maxCities` | 看整体城市数和二城/三城率是否提升 |
|
||||
| 战斗 | `UnitAttack`、`kills`、`actingUnitDeaths`、`PriorityTactic/Tactic` | 看是否有战果,是否过度送兵 |
|
||||
| 英雄 | `selected`、`spawned`、`HeroManagement`、`HeroPlaybook`、英雄 reason/action | 看英雄是否上场、是否用个人机制 |
|
||||
| 防守 | `cityLost`、`capitalThreatTurns`、`emptyThreatenedCityTurns`、`resolved/worsened`、`emergencyResponse` | 看是否少丢城、是否响应危险城市 |
|
||||
| 特色单位 | `unitSkillActions`、`unitSkillChangedActions`、`actorSkillSignatures` | 看英雄/小兵技能是否真的参与行动 |
|
||||
| Fallback | Fallback 总数、actionType、no-effect | 用来发现规则缺口 |
|
||||
| 性能 | `actions/sec`、`avgGame`、`decision avg/p95/max` | 只处理明显尖峰,不牺牲聪明度做极端优化 |
|
||||
|
||||
@ -280,7 +282,7 @@ playerGrowth
|
||||
资源:coin / techPoint / culture / cultureCardCount
|
||||
规模:cityCount / unitCount / heroCount
|
||||
军力:selfMilitary / enemyMilitary
|
||||
威胁:criticalCityThreatCount / cityThreatCount / maxCityDangerScore
|
||||
威胁:criticalCityThreatCount / cityThreatCount / capitalThreatCount / emptyThreatenedCityCount / maxCityDangerScore
|
||||
关键对象:单位血量、位置、死亡,城市归属、等级
|
||||
```
|
||||
|
||||
@ -289,11 +291,13 @@ playerGrowth
|
||||
```text
|
||||
netActionDelta
|
||||
coinDelta / techPointDelta / cultureDelta
|
||||
cityDelta / unitDelta / heroDelta
|
||||
cityDelta / cityLostDelta / cityGainedDelta / unitDelta / heroDelta
|
||||
selfMilitaryDelta / enemyMilitaryDelta
|
||||
criticalCityThreatDelta / cityThreatDelta
|
||||
criticalCityThreatDelta / cityThreatDelta / capitalThreatDelta / emptyThreatenedCityDelta
|
||||
cityThreatResolved / cityThreatWorsened
|
||||
unitMoved / unitDied / targetUnitDied
|
||||
cityOwnerChanged / targetCityOwnerChanged
|
||||
unitSkillSignatureChanged / targetUnitSkillSignatureChanged
|
||||
```
|
||||
|
||||
重点看:
|
||||
@ -305,9 +309,29 @@ netActionDelta 是否推进
|
||||
delta 是否符合动作意图
|
||||
攻击是否造成 targetUnitHealthDelta 或 targetUnitDied
|
||||
回防是否降低 criticalCityThreatDelta 或 maxCityDangerScoreDelta
|
||||
防守行动是否让 cityThreatResolved=true,或至少不让 cityThreatWorsened=true
|
||||
占领是否造成 cityOwnerChanged 或 cityDelta
|
||||
特色单位行动是否带有 unitSkillSignature,并在需要时改变 skill signature
|
||||
```
|
||||
|
||||
### 5.7 batch diagnostics
|
||||
|
||||
`batch_summary.json` 的 `diagnostics` 会聚合单局 JSONL,重点字段:
|
||||
|
||||
| 字段 | 用途 |
|
||||
|---|---|
|
||||
| cityLostCount / cityGainedCount | 同一玩家两次 TurnStart 之间的城市减少/增加 |
|
||||
| capitalThreatTurnCount | 首都处于威胁中的回合开始次数 |
|
||||
| emptyThreatenedCityTurnCount | 受威胁且城市中心无人驻守的回合开始次数 |
|
||||
| cityThreatResolvedCount / cityThreatWorsenedCount | 行动后城市威胁改善/恶化次数 |
|
||||
| emergencyResponseRate | 防守机会中 Emergency 决策覆盖率 |
|
||||
| defenderReturnCount | Emergency 中执行回防移动的次数 |
|
||||
| heroDefensiveUse | 英雄参与防守、保护、治疗或 Emergency 的次数 |
|
||||
| defenseScore | 防守综合分,用于同参数批跑前后对比 |
|
||||
| topHeroStyleBuckets | 英雄风格分布 |
|
||||
| topUnitSkillActionTypes | 带技能签名单位执行的动作 |
|
||||
| topActorSkillSignatures | 最常行动的技能组合 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 单回合阅读流程
|
||||
@ -582,9 +606,14 @@ ActionPool 最大数量
|
||||
| 执行失败率 | execution.executed | 参数或同步问题 |
|
||||
| 候选生成量 | actionPool.all | 性能风险 |
|
||||
| 城市威胁变化 | execution.delta.criticalCityThreatDelta | 防守效果 |
|
||||
| 城市丢失 | 相邻 TurnStart 的 cityCount / cityIdsSignature | 防守结果 |
|
||||
| 首都压力 | turnSummary.capitalThreatCount | 防守风险 |
|
||||
| 空城压力 | turnSummary.emptyThreatenedCityCount | 驻防质量 |
|
||||
| 军力交换 | selfMilitaryDelta / enemyMilitaryDelta | 战斗收益 |
|
||||
| 扩张收益 | cityDelta / cityOwnerChanged | 占领能力 |
|
||||
| 英雄存活 | heroDelta / unitDied | 英雄保命 |
|
||||
| 英雄风格 | Hero reason/action -> styleBuckets | 英雄个性表达 |
|
||||
| 特色单位表达 | unitSkillSignature / unitSkillChangedActions | 小兵技能是否发挥 |
|
||||
| 重复动作 | action.stableKey | 循环风险 |
|
||||
|
||||
自动归因规则:
|
||||
@ -608,6 +637,12 @@ NoAction率低但想要的动作不在 lanes
|
||||
Emergency 后 criticalCityThreatDelta 不下降
|
||||
=> 防守动作没有真正解决问题,调整 Emergency 行动优先级
|
||||
|
||||
cityLost 高或 capitalThreatTurns 长期高
|
||||
=> 城市威胁识别、Emergency 响应、城市生产和回防目标优先级需要调整
|
||||
|
||||
emptyThreatenedCityTurns 高
|
||||
=> AI 可能只扩张不驻防,检查 Front/Hold、EmergencyMove、城市训练防守兵
|
||||
|
||||
Tactic 后 enemyMilitaryDelta 不下降且 selfMilitaryDelta 下降
|
||||
=> 攻击评分低估反击或高估输出
|
||||
|
||||
@ -617,6 +652,12 @@ UnitOpportunity 长期不触发 Capture/Examine/Gather
|
||||
HeroPlaybook 候选少或长期无效
|
||||
=> 英雄规则条件或目标策略需要补
|
||||
|
||||
styleBuckets 长期只有 General
|
||||
=> 英雄 Playbook 缺专属 reason/action,或分类规则缺失,需要补英雄机制表达
|
||||
|
||||
unitSkillActions 低
|
||||
=> 特色小兵没进入行动主体,检查训练、Front、UnitOpportunity 和 Tactic 对该兵种的使用
|
||||
|
||||
decideMs 尖峰且 actionPool.all 高
|
||||
=> 限制候选数量、Front/DevelopmentTarget TopN 或移动枚举
|
||||
```
|
||||
|
||||
@ -13,7 +13,7 @@ namespace Logic.AI.Director
|
||||
#if TH1_AI_DIRECTOR_DIAGNOSTICS || UNITY_EDITOR
|
||||
public static class AIDirectorDiagnostics
|
||||
{
|
||||
private const string SchemaVersion = "1.3";
|
||||
private const string SchemaVersion = "1.4";
|
||||
private const int MaxItemsPerSection = 32;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@ -217,7 +217,7 @@ namespace Logic.AI.Director
|
||||
priority = diagnostic.priority,
|
||||
lane = candidate?.Lane.ToString() ?? AIDirectorLane.None.ToString(),
|
||||
reason = candidate?.Reason ?? string.Empty,
|
||||
action = BuildActionSnapshot(candidate?.AIAction),
|
||||
action = BuildActionSnapshot(candidate?.AIAction),
|
||||
scoreTerms = BuildScoreTerms(candidate?.ScoreTerms)
|
||||
};
|
||||
}
|
||||
@ -294,7 +294,7 @@ namespace Logic.AI.Director
|
||||
if (action?.ActionLogic?.ActionId == null) return null;
|
||||
var id = action.ActionLogic.ActionId;
|
||||
var param = action.Param;
|
||||
return new ActionSnapshot
|
||||
var snapshot = new ActionSnapshot
|
||||
{
|
||||
stableKey = AIDirectorActionIndex.StableActionKey(action),
|
||||
actionType = id.ActionType.ToString(),
|
||||
@ -320,6 +320,28 @@ namespace Logic.AI.Director
|
||||
targetGridId = param?.TargetGridId ?? 0,
|
||||
targetPlayerId = param?.TargetPlayerId ?? 0
|
||||
};
|
||||
FillActionUnitTypeSnapshot(param?.UnitData, true, snapshot);
|
||||
FillActionUnitTypeSnapshot(param?.TargetUnitData, false, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static void FillActionUnitTypeSnapshot(UnitData unit, bool self, ActionSnapshot snapshot)
|
||||
{
|
||||
if (unit == null || snapshot == null) return;
|
||||
if (self)
|
||||
{
|
||||
snapshot.actorUnitType = unit.UnitType.ToString();
|
||||
snapshot.actorGiantType = unit.GiantType.ToString();
|
||||
snapshot.actorUnitLevel = unit.UnitLevel;
|
||||
snapshot.actorChessType = unit.ChessType.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.targetActorUnitType = unit.UnitType.ToString();
|
||||
snapshot.targetActorGiantType = unit.GiantType.ToString();
|
||||
snapshot.targetActorUnitLevel = unit.UnitLevel;
|
||||
snapshot.targetActorChessType = unit.ChessType.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<CityThreatSnapshot> BuildCityThreats(List<AIDirectorCityThreat> threats)
|
||||
@ -582,6 +604,7 @@ namespace Logic.AI.Director
|
||||
using var cityHandle = THCollectionPool.GetListHandle<CityData>(out var cities);
|
||||
map.GetCityDataListByPlayerId(player.Id, cities);
|
||||
probe.CityCount = cities.Count;
|
||||
FillCitySignatureProbe(cities, probe);
|
||||
|
||||
using var unitHandle = THCollectionPool.GetListHandle<UnitData>(out var units);
|
||||
map.GetUnitDataListByPlayerId(player.Id, units);
|
||||
@ -608,6 +631,12 @@ namespace Logic.AI.Director
|
||||
var enemyCount = 0;
|
||||
var enemyPower = 0f;
|
||||
var defenderPower = 0f;
|
||||
var hasCityCenterDefender = cityGrid.RealUnit(map, out var cityCenterUnit)
|
||||
&& cityCenterUnit != null
|
||||
&& cityCenterUnit.IsAlive()
|
||||
&& map.GetPlayerDataByUnitId(cityCenterUnit.Id, out var cityCenterOwner)
|
||||
&& cityCenterOwner != null
|
||||
&& map.SameUnion(player.Id, cityCenterOwner.Id);
|
||||
|
||||
if (map.UnitMap?.UnitList != null)
|
||||
{
|
||||
@ -631,17 +660,62 @@ namespace Logic.AI.Director
|
||||
|
||||
if (enemyCount <= 0) continue;
|
||||
probe.CityThreatCount++;
|
||||
AddUniqueId(probe.ThreatenedCityIds, city.Id);
|
||||
if (city.IsCapital) probe.CapitalThreatCount++;
|
||||
if (!hasCityCenterDefender) probe.EmptyThreatenedCityCount++;
|
||||
var danger = enemyPower - defenderPower;
|
||||
if (danger > probe.MaxCityDangerScore) probe.MaxCityDangerScore = danger;
|
||||
if (danger > 0f) probe.CriticalCityThreatCount++;
|
||||
if (danger > 0f)
|
||||
{
|
||||
probe.CriticalCityThreatCount++;
|
||||
AddUniqueId(probe.CriticalThreatenedCityIds, city.Id);
|
||||
if (city.IsCapital) probe.CriticalCapitalThreatCount++;
|
||||
}
|
||||
}
|
||||
|
||||
probe.ThreatenedCityIdsSignature = BuildIdSignature(probe.ThreatenedCityIds);
|
||||
probe.CriticalThreatenedCityIdsSignature = BuildIdSignature(probe.CriticalThreatenedCityIds);
|
||||
|
||||
if (probe.CriticalCityThreatCount > 0) probe.StrategicPosture = AIDirectorStrategicPosture.Defense.ToString();
|
||||
else if (probe.EnemyMilitary > 0f && probe.SelfMilitary >= probe.EnemyMilitary) probe.StrategicPosture = AIDirectorStrategicPosture.Attack.ToString();
|
||||
else probe.StrategicPosture = AIDirectorStrategicPosture.Development.ToString();
|
||||
return probe;
|
||||
}
|
||||
|
||||
private static void FillCitySignatureProbe(List<CityData> cities, AIDirectorOutcomeProbe probe)
|
||||
{
|
||||
if (probe == null) return;
|
||||
using var ownedHandle = THCollectionPool.GetListHandle<uint>(out var ownedIds);
|
||||
using var capitalHandle = THCollectionPool.GetListHandle<uint>(out var capitalIds);
|
||||
if (cities != null)
|
||||
{
|
||||
foreach (var city in cities)
|
||||
{
|
||||
if (city == null) continue;
|
||||
ownedIds.Add(city.Id);
|
||||
if (city.IsCapital) capitalIds.Add(city.Id);
|
||||
}
|
||||
}
|
||||
|
||||
probe.OwnedCityIdsSignature = BuildIdSignature(ownedIds);
|
||||
probe.CapitalCityIdsSignature = BuildIdSignature(capitalIds);
|
||||
}
|
||||
|
||||
private static void AddUniqueId(List<uint> values, uint id)
|
||||
{
|
||||
if (id == 0 || values == null || values.Contains(id)) return;
|
||||
values.Add(id);
|
||||
}
|
||||
|
||||
private static string BuildIdSignature(List<uint> values)
|
||||
{
|
||||
if (values == null || values.Count <= 0) return string.Empty;
|
||||
values.Sort();
|
||||
using var handle = THCollectionPool.GetListHandle<string>(out var parts);
|
||||
foreach (var value in values) parts.Add(value.ToString());
|
||||
return string.Join("|", parts);
|
||||
}
|
||||
|
||||
private static void FillHeroOutcomeProbe(MapData map, PlayerData player, AIDirectorOutcomeProbe probe)
|
||||
{
|
||||
var heroData = player?.PlayerHeroData;
|
||||
@ -844,6 +918,8 @@ namespace Logic.AI.Director
|
||||
playerScore = probe.PlayerScore,
|
||||
sightGridCount = probe.SightGridCount,
|
||||
cityCount = probe.CityCount,
|
||||
ownedCityIdsSignature = probe.OwnedCityIdsSignature,
|
||||
capitalCityIdsSignature = probe.CapitalCityIdsSignature,
|
||||
unitCount = probe.UnitCount,
|
||||
heroCount = probe.HeroCount,
|
||||
selectedHeroCount = probe.SelectedHeroCount,
|
||||
@ -856,6 +932,11 @@ namespace Logic.AI.Director
|
||||
enemyMilitary = probe.EnemyMilitary,
|
||||
criticalCityThreatCount = probe.CriticalCityThreatCount,
|
||||
cityThreatCount = probe.CityThreatCount,
|
||||
capitalThreatCount = probe.CapitalThreatCount,
|
||||
criticalCapitalThreatCount = probe.CriticalCapitalThreatCount,
|
||||
emptyThreatenedCityCount = probe.EmptyThreatenedCityCount,
|
||||
threatenedCityIdsSignature = probe.ThreatenedCityIdsSignature,
|
||||
criticalThreatenedCityIdsSignature = probe.CriticalThreatenedCityIdsSignature,
|
||||
maxCityDangerScore = probe.MaxCityDangerScore,
|
||||
strategicPosture = probe.StrategicPosture,
|
||||
unitId = probe.UnitId,
|
||||
@ -922,6 +1003,10 @@ namespace Logic.AI.Director
|
||||
scoreDelta = after.PlayerScore - before.PlayerScore,
|
||||
sightGridDelta = after.SightGridCount - before.SightGridCount,
|
||||
cityDelta = after.CityCount - before.CityCount,
|
||||
cityLostDelta = Mathf.Max(0, before.CityCount - after.CityCount),
|
||||
cityGainedDelta = Mathf.Max(0, after.CityCount - before.CityCount),
|
||||
cityOwnershipSignatureChanged = before.OwnedCityIdsSignature != after.OwnedCityIdsSignature,
|
||||
capitalOwnershipSignatureChanged = before.CapitalCityIdsSignature != after.CapitalCityIdsSignature,
|
||||
unitDelta = after.UnitCount - before.UnitCount,
|
||||
heroDelta = after.HeroCount - before.HeroCount,
|
||||
selectedHeroDelta = after.SelectedHeroCount - before.SelectedHeroCount,
|
||||
@ -934,6 +1019,19 @@ namespace Logic.AI.Director
|
||||
enemyMilitaryDelta = after.EnemyMilitary - before.EnemyMilitary,
|
||||
criticalCityThreatDelta = after.CriticalCityThreatCount - before.CriticalCityThreatCount,
|
||||
cityThreatDelta = after.CityThreatCount - before.CityThreatCount,
|
||||
capitalThreatDelta = after.CapitalThreatCount - before.CapitalThreatCount,
|
||||
criticalCapitalThreatDelta = after.CriticalCapitalThreatCount - before.CriticalCapitalThreatCount,
|
||||
emptyThreatenedCityDelta = after.EmptyThreatenedCityCount - before.EmptyThreatenedCityCount,
|
||||
cityThreatSignatureChanged = before.ThreatenedCityIdsSignature != after.ThreatenedCityIdsSignature,
|
||||
criticalCityThreatSignatureChanged = before.CriticalThreatenedCityIdsSignature != after.CriticalThreatenedCityIdsSignature,
|
||||
cityThreatResolved = before.CityThreatCount > 0
|
||||
&& (after.CriticalCityThreatCount < before.CriticalCityThreatCount
|
||||
|| after.CityThreatCount < before.CityThreatCount
|
||||
|| after.MaxCityDangerScore + 0.001f < before.MaxCityDangerScore),
|
||||
cityThreatWorsened = after.CriticalCityThreatCount > before.CriticalCityThreatCount
|
||||
|| after.CityThreatCount > before.CityThreatCount
|
||||
|| after.EmptyThreatenedCityCount > before.EmptyThreatenedCityCount
|
||||
|| after.MaxCityDangerScore > before.MaxCityDangerScore + 0.001f,
|
||||
maxCityDangerScoreDelta = after.MaxCityDangerScore - before.MaxCityDangerScore,
|
||||
unitMoved = before.UnitGridId != after.UnitGridId,
|
||||
unitHealthDelta = after.UnitHealth - before.UnitHealth,
|
||||
@ -1098,6 +1196,8 @@ namespace Logic.AI.Director
|
||||
public int playerScore;
|
||||
public int sightGridCount;
|
||||
public int cityCount;
|
||||
public string ownedCityIdsSignature;
|
||||
public string capitalCityIdsSignature;
|
||||
public int unitCount;
|
||||
public int heroCount;
|
||||
public int selectedHeroCount;
|
||||
@ -1110,6 +1210,11 @@ namespace Logic.AI.Director
|
||||
public float enemyMilitary;
|
||||
public int criticalCityThreatCount;
|
||||
public int cityThreatCount;
|
||||
public int capitalThreatCount;
|
||||
public int criticalCapitalThreatCount;
|
||||
public int emptyThreatenedCityCount;
|
||||
public string threatenedCityIdsSignature;
|
||||
public string criticalThreatenedCityIdsSignature;
|
||||
public float maxCityDangerScore;
|
||||
public string strategicPosture;
|
||||
public uint unitId;
|
||||
@ -1173,6 +1278,10 @@ namespace Logic.AI.Director
|
||||
public int scoreDelta;
|
||||
public int sightGridDelta;
|
||||
public int cityDelta;
|
||||
public int cityLostDelta;
|
||||
public int cityGainedDelta;
|
||||
public bool cityOwnershipSignatureChanged;
|
||||
public bool capitalOwnershipSignatureChanged;
|
||||
public int unitDelta;
|
||||
public int heroDelta;
|
||||
public int selectedHeroDelta;
|
||||
@ -1185,6 +1294,13 @@ namespace Logic.AI.Director
|
||||
public float enemyMilitaryDelta;
|
||||
public int criticalCityThreatDelta;
|
||||
public int cityThreatDelta;
|
||||
public int capitalThreatDelta;
|
||||
public int criticalCapitalThreatDelta;
|
||||
public int emptyThreatenedCityDelta;
|
||||
public bool cityThreatSignatureChanged;
|
||||
public bool criticalCityThreatSignatureChanged;
|
||||
public bool cityThreatResolved;
|
||||
public bool cityThreatWorsened;
|
||||
public float maxCityDangerScoreDelta;
|
||||
public bool unitMoved;
|
||||
public int unitHealthDelta;
|
||||
@ -1289,6 +1405,14 @@ namespace Logic.AI.Director
|
||||
public uint unitLevel;
|
||||
public string wonderType;
|
||||
public string skillType;
|
||||
public string actorUnitType;
|
||||
public string actorGiantType;
|
||||
public uint actorUnitLevel;
|
||||
public string actorChessType;
|
||||
public string targetActorUnitType;
|
||||
public string targetActorGiantType;
|
||||
public uint targetActorUnitLevel;
|
||||
public string targetActorChessType;
|
||||
public string mainObjectType;
|
||||
public uint playerId;
|
||||
public uint unitId;
|
||||
|
||||
@ -536,6 +536,10 @@ namespace Logic.AI.Director
|
||||
public int PlayerScore;
|
||||
public int SightGridCount;
|
||||
public int CityCount;
|
||||
public string OwnedCityIdsSignature;
|
||||
public string CapitalCityIdsSignature;
|
||||
public readonly List<uint> ThreatenedCityIds = new();
|
||||
public readonly List<uint> CriticalThreatenedCityIds = new();
|
||||
public int UnitCount;
|
||||
public int HeroCount;
|
||||
public int SelectedHeroCount;
|
||||
@ -548,6 +552,11 @@ namespace Logic.AI.Director
|
||||
public float EnemyMilitary;
|
||||
public int CriticalCityThreatCount;
|
||||
public int CityThreatCount;
|
||||
public int CapitalThreatCount;
|
||||
public int CriticalCapitalThreatCount;
|
||||
public int EmptyThreatenedCityCount;
|
||||
public string ThreatenedCityIdsSignature;
|
||||
public string CriticalThreatenedCityIdsSignature;
|
||||
public float MaxCityDangerScore;
|
||||
public string StrategicPosture;
|
||||
public uint UnitId;
|
||||
|
||||
@ -674,7 +674,17 @@ namespace TH1_Logic.Editor
|
||||
var fallbackReasons = new Dictionary<string, int>();
|
||||
var fallbackExecutedActionTypes = new Dictionary<string, int>();
|
||||
var fallbackNoEffectActionTypes = new Dictionary<string, int>();
|
||||
var defenseReasons = new Dictionary<string, int>();
|
||||
var defenseActionTypes = new Dictionary<string, int>();
|
||||
var heroStyleBuckets = new Dictionary<string, int>();
|
||||
var heroStyleReasons = new Dictionary<string, int>();
|
||||
var heroStyleActionTypes = new Dictionary<string, int>();
|
||||
var unitSkillActionTypes = new Dictionary<string, int>();
|
||||
var unitSkillChangedActionTypes = new Dictionary<string, int>();
|
||||
var actorSkillSignatures = new Dictionary<string, int>();
|
||||
var decisionLaneByAction = new Dictionary<string, string>();
|
||||
var decisionReasonByAction = new Dictionary<string, string>();
|
||||
var lastTurnSummaryByPlayer = new Dictionary<uint, JToken>();
|
||||
|
||||
try
|
||||
{
|
||||
@ -713,19 +723,32 @@ namespace TH1_Logic.Editor
|
||||
Increment(reasons, reason);
|
||||
Increment(selectedActionTypes, actionType);
|
||||
Increment(laneActionTypes, $"{lane}:{actionType}");
|
||||
if (!string.IsNullOrEmpty(turnActionKey)) decisionLaneByAction[turnActionKey] = lane;
|
||||
if (!string.IsNullOrEmpty(turnActionKey))
|
||||
{
|
||||
decisionLaneByAction[turnActionKey] = lane;
|
||||
decisionReasonByAction[turnActionKey] = reason;
|
||||
}
|
||||
|
||||
if (lane == "Emergency")
|
||||
{
|
||||
summary.emergencyDecisions++;
|
||||
Increment(defenseReasons, reason);
|
||||
Increment(defenseActionTypes, actionKey);
|
||||
}
|
||||
|
||||
if (lane == "HeroManagement")
|
||||
{
|
||||
summary.heroManagementDecisions++;
|
||||
Increment(heroReasons, reason);
|
||||
Increment(heroActionTypes, actionKey);
|
||||
IncrementHeroStyle(heroStyleBuckets, heroStyleReasons, heroStyleActionTypes, reason, actionKey);
|
||||
}
|
||||
else if (lane == "HeroPlaybook")
|
||||
{
|
||||
summary.heroPlaybookDecisions++;
|
||||
Increment(heroReasons, reason);
|
||||
Increment(heroActionTypes, actionKey);
|
||||
IncrementHeroStyle(heroStyleBuckets, heroStyleReasons, heroStyleActionTypes, reason, actionKey);
|
||||
if (reason.StartsWith("HeroPlaybook.", StringComparison.Ordinal)) summary.genericHeroDecisions++;
|
||||
else summary.heroRuleDecisions++;
|
||||
}
|
||||
@ -737,6 +760,35 @@ namespace TH1_Logic.Editor
|
||||
Increment(fallbackActionTypes, actionKey);
|
||||
}
|
||||
}
|
||||
else if (eventType == "TurnStart")
|
||||
{
|
||||
var turnSummary = record["turnSummary"];
|
||||
if (turnSummary == null) continue;
|
||||
|
||||
var playerId = record.Value<uint?>("playerId") ?? 0;
|
||||
if ((turnSummary.Value<int?>("criticalCityThreatCount") ?? 0) > 0) summary.criticalCityThreatTurnCount++;
|
||||
if ((turnSummary.Value<int?>("capitalThreatCount") ?? 0) > 0) summary.capitalThreatTurnCount++;
|
||||
var emptyThreatenedCityCount = turnSummary.Value<int?>("emptyThreatenedCityCount") ?? 0;
|
||||
if (emptyThreatenedCityCount > 0)
|
||||
{
|
||||
summary.emptyThreatenedCityTurnCount++;
|
||||
summary.emptyThreatenedCityTotal += emptyThreatenedCityCount;
|
||||
}
|
||||
|
||||
if (lastTurnSummaryByPlayer.TryGetValue(playerId, out var previous))
|
||||
{
|
||||
var previousCityCount = previous.Value<int?>("cityCount") ?? 0;
|
||||
var currentCityCount = turnSummary.Value<int?>("cityCount") ?? 0;
|
||||
if (currentCityCount < previousCityCount) summary.cityLostCount += previousCityCount - currentCityCount;
|
||||
if (currentCityCount > previousCityCount) summary.cityGainedCount += currentCityCount - previousCityCount;
|
||||
|
||||
var previousCapital = previous.Value<string>("capitalCityIdsSignature") ?? string.Empty;
|
||||
var currentCapital = turnSummary.Value<string>("capitalCityIdsSignature") ?? string.Empty;
|
||||
if (!string.Equals(previousCapital, currentCapital, StringComparison.Ordinal)) summary.capitalOwnershipChangedCount++;
|
||||
}
|
||||
|
||||
lastTurnSummaryByPlayer[playerId] = turnSummary;
|
||||
}
|
||||
else if (eventType == "Execution")
|
||||
{
|
||||
summary.executions++;
|
||||
@ -759,6 +811,7 @@ namespace TH1_Logic.Editor
|
||||
|
||||
var turnActionKey = BuildTurnActionKey(record, action);
|
||||
decisionLaneByAction.TryGetValue(turnActionKey, out var executedLane);
|
||||
decisionReasonByAction.TryGetValue(turnActionKey, out var executedReason);
|
||||
if (executedLane == "HeroManagement" || executedLane == "HeroPlaybook")
|
||||
{
|
||||
Increment(heroExecutedActionTypes, actionKey);
|
||||
@ -777,6 +830,34 @@ namespace TH1_Logic.Editor
|
||||
summary.readyHeroTaskDelta += delta?.Value<int?>("readyHeroTaskDelta") ?? 0;
|
||||
summary.forcedHeroTaskDelta += delta?.Value<int?>("forcedHeroTaskDelta") ?? 0;
|
||||
summary.heroTaskProgressDelta += delta?.Value<int?>("heroTaskProgressDelta") ?? 0;
|
||||
summary.cityThreatResolvedCount += delta?.Value<bool?>("cityThreatResolved") == true ? 1 : 0;
|
||||
summary.cityThreatWorsenedCount += delta?.Value<bool?>("cityThreatWorsened") == true ? 1 : 0;
|
||||
if (executedLane == "Emergency") summary.emergencyExecutions++;
|
||||
if (executedLane == "Emergency" && actionKey == "UnitMove")
|
||||
{
|
||||
summary.defenderReturnCount++;
|
||||
}
|
||||
|
||||
if (IsHeroAction(action))
|
||||
{
|
||||
var bucket = ClassifyStyleBucket(executedReason, actionKey);
|
||||
if (IsDefensiveStyle(bucket) || executedLane == "Emergency") summary.heroDefensiveUse++;
|
||||
}
|
||||
|
||||
var before = execution?["before"];
|
||||
var skillSignature = before?.Value<string>("unitSkillSignature") ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(skillSignature))
|
||||
{
|
||||
Increment(unitSkillActionTypes, actionKey);
|
||||
Increment(actorSkillSignatures, CompactSkillSignature(skillSignature));
|
||||
if (delta?.Value<bool?>("unitSkillSignatureChanged") == true
|
||||
|| delta?.Value<bool?>("targetUnitSkillSignatureChanged") == true
|
||||
|| (delta?.Value<int?>("unitSkillDelta") ?? 0) != 0
|
||||
|| (delta?.Value<int?>("targetUnitSkillDelta") ?? 0) != 0)
|
||||
{
|
||||
Increment(unitSkillChangedActionTypes, actionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((execution?.Value<bool?>("executed") ?? false) && !HasMeaningfulDelta(delta))
|
||||
@ -806,10 +887,30 @@ namespace TH1_Logic.Editor
|
||||
summary.topHeroReasons = TopCounts(heroReasons, 16);
|
||||
summary.topHeroActionTypes = TopCounts(heroActionTypes, 12);
|
||||
summary.topHeroExecutedActionTypes = TopCounts(heroExecutedActionTypes, 12);
|
||||
summary.topDefenseReasons = TopCounts(defenseReasons, 12);
|
||||
summary.topDefenseActionTypes = TopCounts(defenseActionTypes, 12);
|
||||
summary.topHeroStyleBuckets = TopCounts(heroStyleBuckets, 12);
|
||||
summary.topHeroStyleReasons = TopCounts(heroStyleReasons, 16);
|
||||
summary.topHeroStyleActionTypes = TopCounts(heroStyleActionTypes, 12);
|
||||
summary.topUnitSkillActionTypes = TopCounts(unitSkillActionTypes, 12);
|
||||
summary.topUnitSkillChangedActionTypes = TopCounts(unitSkillChangedActionTypes, 12);
|
||||
summary.topActorSkillSignatures = TopCounts(actorSkillSignatures, 12);
|
||||
summary.topFallbackReasons = TopCounts(fallbackReasons, 12);
|
||||
summary.topFallbackActionTypes = TopCounts(fallbackActionTypes, 12);
|
||||
summary.topFallbackExecutedActionTypes = TopCounts(fallbackExecutedActionTypes, 12);
|
||||
summary.fallbackNoEffectActionTypes = TopCounts(fallbackNoEffectActionTypes, 12);
|
||||
summary.defenseOpportunityTurns = summary.criticalCityThreatTurnCount + summary.emptyThreatenedCityTurnCount;
|
||||
summary.emergencyResponseRate = summary.defenseOpportunityTurns <= 0
|
||||
? 0f
|
||||
: Mathf.Clamp01((float)summary.emergencyDecisions / summary.defenseOpportunityTurns);
|
||||
summary.defenseScore = summary.cityThreatResolvedCount * 3f
|
||||
+ summary.emergencyExecutions
|
||||
+ summary.defenderReturnCount
|
||||
+ summary.heroDefensiveUse * 2f
|
||||
- summary.cityLostCount * 8f
|
||||
- summary.capitalThreatTurnCount * 2f
|
||||
- summary.emptyThreatenedCityTurnCount * 2f
|
||||
- summary.cityThreatWorsenedCount * 2f;
|
||||
return summary;
|
||||
}
|
||||
|
||||
@ -843,6 +944,76 @@ namespace TH1_Logic.Editor
|
||||
: $"{actionType}:{subType}";
|
||||
}
|
||||
|
||||
private static void IncrementHeroStyle(
|
||||
Dictionary<string, int> buckets,
|
||||
Dictionary<string, int> reasons,
|
||||
Dictionary<string, int> actions,
|
||||
string reason,
|
||||
string actionKey)
|
||||
{
|
||||
var bucket = ClassifyStyleBucket(reason, actionKey);
|
||||
Increment(buckets, bucket);
|
||||
Increment(reasons, $"{bucket}:{reason}");
|
||||
Increment(actions, $"{bucket}:{actionKey}");
|
||||
}
|
||||
|
||||
private static string ClassifyStyleBucket(string reason, string actionKey)
|
||||
{
|
||||
var text = $"{reason ?? string.Empty} {actionKey ?? string.Empty}";
|
||||
if (ContainsAny(text, "LowHp", "Recover", "Heal", "Eirin", "Sanae", "Patchouli", "Absorb", "Revive"))
|
||||
return "Recovery";
|
||||
if (ContainsAny(text, "Defense", "Defend", "Protect", "Guard", "MoveToCity", "Aunn", "Meiling", "Sakuya", "Reimu", "ShakeOff", "Unsit"))
|
||||
return "Defense";
|
||||
if (ContainsAny(text, "Kill", "Flandre", "Assassin", "AttackValue", "LocalBattle", "Boom", "Mokou", "Reisen", "Yuugi"))
|
||||
return "Burst";
|
||||
if (ContainsAny(text, "Ban", "Fear", "Control", "Satori", "Koishi", "Sumireko", "Orb", "Ground"))
|
||||
return "Control";
|
||||
if (ContainsAny(text, "Summon", "CreateMini", "Suwako", "Snake", "BonePile", "Mini"))
|
||||
return "Summon";
|
||||
if (ContainsAny(text, "Economy", "CityExp", "Corpse", "KanakoSit", "Rin", "Tewi", "Kaguya"))
|
||||
return "Economy";
|
||||
if (ContainsAny(text, "MoveAgain", "MoveToFront", "Retreat", "Mobility", "Aya", "Kasen"))
|
||||
return "Mobility";
|
||||
if (ContainsAny(text, "SelectHero", "SpawnHero", "FinishLowestTask"))
|
||||
return "HeroLifecycle";
|
||||
return "General";
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string value, params string[] tokens)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value) || tokens == null) return false;
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(token) && value.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsDefensiveStyle(string bucket)
|
||||
{
|
||||
return bucket == "Defense" || bucket == "Recovery";
|
||||
}
|
||||
|
||||
private static bool IsHeroAction(JToken action)
|
||||
{
|
||||
if (action == null) return false;
|
||||
var actorUnitType = action.Value<string>("actorUnitType") ?? string.Empty;
|
||||
var actorGiantType = action.Value<string>("actorGiantType") ?? string.Empty;
|
||||
var giantType = action.Value<string>("giantType") ?? string.Empty;
|
||||
return actorUnitType == "Giant"
|
||||
|| (!string.IsNullOrEmpty(actorGiantType) && actorGiantType != "None")
|
||||
|| (!string.IsNullOrEmpty(giantType) && giantType != "None");
|
||||
}
|
||||
|
||||
private static string CompactSkillSignature(string signature)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(signature)) return string.Empty;
|
||||
var parts = signature.Split('|');
|
||||
if (parts.Length <= 4) return signature;
|
||||
return string.Join("|", parts.Take(4)) + "|...";
|
||||
}
|
||||
|
||||
private static bool HasMeaningfulDelta(JToken delta)
|
||||
{
|
||||
if (delta == null) return false;
|
||||
@ -1116,6 +1287,22 @@ namespace TH1_Logic.Editor
|
||||
public int readyHeroTaskDelta;
|
||||
public int forcedHeroTaskDelta;
|
||||
public int heroTaskProgressDelta;
|
||||
public int criticalCityThreatTurnCount;
|
||||
public int capitalThreatTurnCount;
|
||||
public int emptyThreatenedCityTurnCount;
|
||||
public int emptyThreatenedCityTotal;
|
||||
public int defenseOpportunityTurns;
|
||||
public int cityThreatResolvedCount;
|
||||
public int cityThreatWorsenedCount;
|
||||
public int cityLostCount;
|
||||
public int cityGainedCount;
|
||||
public int capitalOwnershipChangedCount;
|
||||
public int emergencyDecisions;
|
||||
public int emergencyExecutions;
|
||||
public int defenderReturnCount;
|
||||
public int heroDefensiveUse;
|
||||
public float emergencyResponseRate;
|
||||
public float defenseScore;
|
||||
public float avgDecideMs;
|
||||
public float p95DecideMs;
|
||||
public float maxDecideMs;
|
||||
@ -1137,6 +1324,14 @@ namespace TH1_Logic.Editor
|
||||
public List<BatchCountMetric> topHeroReasons = new();
|
||||
public List<BatchCountMetric> topHeroActionTypes = new();
|
||||
public List<BatchCountMetric> topHeroExecutedActionTypes = new();
|
||||
public List<BatchCountMetric> topDefenseReasons = new();
|
||||
public List<BatchCountMetric> topDefenseActionTypes = new();
|
||||
public List<BatchCountMetric> topHeroStyleBuckets = new();
|
||||
public List<BatchCountMetric> topHeroStyleReasons = new();
|
||||
public List<BatchCountMetric> topHeroStyleActionTypes = new();
|
||||
public List<BatchCountMetric> topUnitSkillActionTypes = new();
|
||||
public List<BatchCountMetric> topUnitSkillChangedActionTypes = new();
|
||||
public List<BatchCountMetric> topActorSkillSignatures = new();
|
||||
public List<BatchCountMetric> topFallbackReasons = new();
|
||||
public List<BatchCountMetric> topFallbackActionTypes = new();
|
||||
public List<BatchCountMetric> topFallbackExecutedActionTypes = new();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user