Improve AI Director hero strategy metrics
This commit is contained in:
parent
7eca426175
commit
d6fec5029e
@ -130,6 +130,19 @@ def find_matching_logs(root: Path, batch_path: Path, batch, explicit_logs):
|
||||
result.append(path)
|
||||
return result
|
||||
|
||||
direct_logs = []
|
||||
for game in batch.get("results", []) or []:
|
||||
value = game.get("diagnosticsLogPath") or ""
|
||||
if not value:
|
||||
continue
|
||||
path = Path(value)
|
||||
if not path.is_absolute():
|
||||
path = root / path
|
||||
if path.exists():
|
||||
direct_logs.append(path)
|
||||
if direct_logs:
|
||||
return direct_logs
|
||||
|
||||
start, end = batch_local_window(batch_path, batch)
|
||||
log_dir = root / "Unity" / "Logs" / "AI_Director_Diagnostics"
|
||||
logs = sorted(log_dir.glob("ai_director_*.jsonl"), key=lambda p: p.stat().st_mtime)
|
||||
@ -170,6 +183,20 @@ def analyze_logs(log_paths):
|
||||
"decide_ms": [],
|
||||
"top_no_effect": Counter(),
|
||||
"top_repeated": Counter(),
|
||||
"hero_reasons": Counter(),
|
||||
"hero_actions": Counter(),
|
||||
"hero_executed_actions": Counter(),
|
||||
"hero_lane_counts": Counter(),
|
||||
"fallback_reasons": Counter(),
|
||||
"fallback_actions": Counter(),
|
||||
"fallback_executed_actions": Counter(),
|
||||
"fallback_no_effect_actions": Counter(),
|
||||
"hero_delta": 0,
|
||||
"selected_hero_delta": 0,
|
||||
"hero_task_delta": 0,
|
||||
"ready_hero_task_delta": 0,
|
||||
"forced_hero_task_delta": 0,
|
||||
"hero_task_progress_delta": 0,
|
||||
}
|
||||
|
||||
for path in log_paths:
|
||||
@ -195,16 +222,33 @@ def analyze_logs(log_paths):
|
||||
|
||||
stable_rows_by_turn = defaultdict(list)
|
||||
turn_rows = defaultdict(list)
|
||||
decision_lane_by_turn_action = {}
|
||||
for row in rows:
|
||||
event_type = row.get("eventType", "")
|
||||
if event_type == "Decision":
|
||||
decision = row.get("decision") or {}
|
||||
lane = decision.get("lane") or ""
|
||||
reason = decision.get("reason") or ""
|
||||
action = decision.get("action") or {}
|
||||
if lane and lane != "None":
|
||||
aggregate["lane_counts"][lane] += 1
|
||||
decide_ms = decision.get("decideMs")
|
||||
if isinstance(decide_ms, (int, float)):
|
||||
aggregate["decide_ms"].append(float(decide_ms))
|
||||
if decision.get("hasAction"):
|
||||
turn_action_key = (
|
||||
row.get("playerId", 0),
|
||||
row.get("playerTurn", 0),
|
||||
stable_key(action),
|
||||
)
|
||||
decision_lane_by_turn_action[turn_action_key] = lane
|
||||
if lane in ("HeroManagement", "HeroPlaybook"):
|
||||
aggregate["hero_lane_counts"][lane] += 1
|
||||
aggregate["hero_reasons"][reason] += 1
|
||||
aggregate["hero_actions"][action_key(action)] += 1
|
||||
if decision.get("isFallback"):
|
||||
aggregate["fallback_reasons"][reason] += 1
|
||||
aggregate["fallback_actions"][action_key(action)] += 1
|
||||
continue
|
||||
|
||||
if event_type != "Execution":
|
||||
@ -217,7 +261,22 @@ def analyze_logs(log_paths):
|
||||
delta = execution.get("delta") or {}
|
||||
player_id = row.get("playerId", 0)
|
||||
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, "")
|
||||
aggregate["player_actions"][player_id] += 1
|
||||
if decision_lane in ("HeroManagement", "HeroPlaybook"):
|
||||
aggregate["hero_executed_actions"][key] += 1
|
||||
if decision_lane == "Fallback":
|
||||
aggregate["fallback_executed_actions"][key] += 1
|
||||
if has_no_effect_delta(delta):
|
||||
aggregate["fallback_no_effect_actions"][key] += 1
|
||||
|
||||
aggregate["hero_delta"] += int(delta.get("heroDelta") or 0)
|
||||
aggregate["selected_hero_delta"] += int(delta.get("selectedHeroDelta") or 0)
|
||||
aggregate["hero_task_delta"] += int(delta.get("heroTaskDelta") or 0)
|
||||
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("targetUnitDied"):
|
||||
aggregate["kills"] += 1
|
||||
@ -372,6 +431,24 @@ def compact_log_metrics(log_metrics, top):
|
||||
},
|
||||
"top_actions": log_metrics["action_counts"].most_common(top),
|
||||
"top_lanes": log_metrics["lane_counts"].most_common(top),
|
||||
"hero": {
|
||||
"lane_counts": log_metrics["hero_lane_counts"].most_common(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),
|
||||
"hero_delta": log_metrics["hero_delta"],
|
||||
"selected_hero_delta": log_metrics["selected_hero_delta"],
|
||||
"hero_task_delta": log_metrics["hero_task_delta"],
|
||||
"ready_hero_task_delta": log_metrics["ready_hero_task_delta"],
|
||||
"forced_hero_task_delta": log_metrics["forced_hero_task_delta"],
|
||||
"hero_task_progress_delta": log_metrics["hero_task_progress_delta"],
|
||||
},
|
||||
"fallback": {
|
||||
"top_reasons": log_metrics["fallback_reasons"].most_common(top),
|
||||
"top_actions": log_metrics["fallback_actions"].most_common(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),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -447,6 +524,43 @@ def print_report(batch_path, batch, metrics, log_metrics, warnings, top):
|
||||
for key, count in log_metrics["lane_counts"].most_common(top):
|
||||
print(f" {count:>5} {key}")
|
||||
|
||||
print("Hero:")
|
||||
print(
|
||||
" deltas: "
|
||||
f"selected={log_metrics['selected_hero_delta']} "
|
||||
f"spawned={log_metrics['hero_delta']} "
|
||||
f"task={log_metrics['hero_task_delta']} "
|
||||
f"readyTask={log_metrics['ready_hero_task_delta']} "
|
||||
f"forcedTask={log_metrics['forced_hero_task_delta']} "
|
||||
f"taskProgress={log_metrics['hero_task_progress_delta']}"
|
||||
)
|
||||
if log_metrics["hero_reasons"]:
|
||||
print(" top reasons:")
|
||||
for key, count in log_metrics["hero_reasons"].most_common(top):
|
||||
print(f" {count:>5} {key}")
|
||||
else:
|
||||
print(" top reasons: <none>")
|
||||
if log_metrics["hero_executed_actions"]:
|
||||
print(" executed actions:")
|
||||
for key, count in log_metrics["hero_executed_actions"].most_common(top):
|
||||
print(f" {count:>5} {key}")
|
||||
else:
|
||||
print(" executed actions: <none>")
|
||||
|
||||
print("Fallback:")
|
||||
if log_metrics["fallback_actions"]:
|
||||
print(" selected actions:")
|
||||
for key, count in log_metrics["fallback_actions"].most_common(top):
|
||||
print(f" {count:>5} {key}")
|
||||
else:
|
||||
print(" selected actions: <none>")
|
||||
if log_metrics["fallback_no_effect_actions"]:
|
||||
print(" no-effect actions:")
|
||||
for key, count in log_metrics["fallback_no_effect_actions"].most_common(top):
|
||||
print(f" {count:>5} {key}")
|
||||
else:
|
||||
print(" no-effect actions: <none>")
|
||||
|
||||
print("Warnings:")
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
|
||||
@ -340,7 +340,21 @@ Development 的行为倾向:
|
||||
|
||||
## 9. 英雄策略
|
||||
|
||||
英雄不走普通单位公式。英雄由 HeroPlaybook 驱动。
|
||||
英雄不走普通单位公式。英雄分两层处理:
|
||||
|
||||
- HeroManagement 负责英雄体系节奏:选英雄、让已选择英雄尽快出场、再推进任务。
|
||||
- HeroPlaybook 负责场上英雄怎么发挥机制:治疗、保护、控场、地面攻击、主动技能、收割、站位。
|
||||
|
||||
HeroManagement 的节奏:
|
||||
|
||||
```text
|
||||
没有选择英雄
|
||||
→ 先选择最适合当前阵营和局势的英雄
|
||||
→ 已选择但未上场时,优先在合法城市训练/复活英雄
|
||||
→ 英雄已经能参与战局后,再推进可强制完成的英雄任务
|
||||
```
|
||||
|
||||
这个顺序保证 AI 不会只完成任务却长期没有英雄在棋盘上,也不会把英雄当作普通兵随缘训练。
|
||||
|
||||
HeroPlaybook 的判断顺序:
|
||||
|
||||
@ -472,11 +486,13 @@ Director 不直接推演行为结果,而是从合法 Action 池中选择。
|
||||
|---|---|
|
||||
| 单位战斗 | UnitAttack、UnitAttackAlly、UnitAttackGround |
|
||||
| 单位移动 | UnitMove |
|
||||
| 单位行为 | Capture、Examine、Gather、Recover、Upgrade、HeroUpgrade、CultureUnitUpgrade、英雄主动 |
|
||||
| 单位行为 | Capture、Examine、Gather、Recover、Upgrade、HeroUpgrade、CultureUnitUpgrade、ShipUpgrade、AbsorbMarker、英雄主动 |
|
||||
| 城市行为 | TrainUnit、CityLevelUpAction、CityAction、StartWonder、BuildWonder |
|
||||
| 地块行为 | Gain、Build、GridMisc |
|
||||
| 玩家行为 | LearnTech、PlayerAction;BuyCultureCard 暂不进入 AI 候选,等文化卡策略单独回归 |
|
||||
| 英雄管理 | SelectHero、FinishHeroTask、出场、复活 |
|
||||
| 英雄管理 | SelectHero、TrainUnit:Giant 出场/复活、FinishHeroTask |
|
||||
|
||||
UnitAttackAlly 只由 HeroPlaybook、支援战术或明确的友军互动规则使用,不进入 Fallback。友军目标动作如果落到兜底,通常说明它缺少英雄或支援规则。
|
||||
|
||||
危险动作默认不进入普通 AI 选择:
|
||||
|
||||
@ -559,6 +575,17 @@ AI 可以重新实现,但必须遵守 TH1 的游戏架构:
|
||||
| 外交行为 | 高好感不乱开战,必要时建立使馆或结盟 |
|
||||
| 性能 | 单个 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 | 已选择英雄应尽快上场,英雄动作应体现个人机制 |
|
||||
| Fallback | Fallback 总数、Fallback actionType、Fallback noEffect | 越接近 0 越好;非 0 时优先把有意义动作归入正式车道 |
|
||||
| 性能 | actions/sec、avgGame、decision avg/p95/max | 先保证聪明,再把明显尖峰纳入下一轮优化 |
|
||||
|
||||
问题定位:
|
||||
|
||||
| 现象 | 优先检查 |
|
||||
@ -568,7 +595,7 @@ AI 可以重新实现,但必须遵守 TH1 的游戏架构:
|
||||
| 英雄不放技能 | HeroState、HeroPlaybook、ActionPool |
|
||||
| 单位乱走 | Front、GridThreat、MoveTarget |
|
||||
| 过度防守 | CityThreat 是否把全玩家领土误算到每座城市 |
|
||||
| Fallback 偏高 | Recover、UnitAttackAlly、特殊 UnitAction 是否应并入 UnitOpportunity 或 HeroPlaybook |
|
||||
| Fallback 偏高 | Recover、特殊 UnitAction 是否应并入 UnitOpportunity 或 HeroPlaybook;UnitAttackAlly 不应出现在 Fallback |
|
||||
| 城市不发展 | CityPlan、Growth、ActionPool |
|
||||
| 科技乱学 | StrategicPosture、TechScore |
|
||||
| 回合慢 | ActionPool、移动候选、局部搜索半径 |
|
||||
|
||||
@ -1271,11 +1271,11 @@ SkillBase
|
||||
如果 candidate.IsValid:
|
||||
返回 candidate
|
||||
|
||||
candidate = TryForceFinishHeroTask(ctx)
|
||||
candidate = TrySpawnSelectedHero(ctx)
|
||||
如果 candidate.IsValid:
|
||||
返回 candidate
|
||||
|
||||
candidate = TryDeployOrReviveHero(ctx)
|
||||
candidate = TryForceFinishHeroTask(ctx)
|
||||
如果 candidate.IsValid:
|
||||
返回 candidate
|
||||
|
||||
@ -1290,7 +1290,58 @@ SkillBase
|
||||
返回 Candidate(action, HeroManagement, 890, "选择英雄")
|
||||
```
|
||||
|
||||
### 7.3 FinishHeroTask
|
||||
### 7.3 SpawnSelectedHero
|
||||
|
||||
```text
|
||||
函数 TrySpawnSelectedHero(ctx):
|
||||
如果 ctx.Player 没有已选择英雄:
|
||||
返回 None
|
||||
|
||||
best = None
|
||||
|
||||
对每个 cityPlan in ctx.Cache.CityPlans:
|
||||
对每个 action in ctx.Actions.ByCity[cityPlan.City.Id]:
|
||||
如果 !IsSelectedHeroSpawnAction(ctx.Player, action):
|
||||
继续
|
||||
|
||||
score = 880
|
||||
score += max(cityPlan.Priority, 0) * 0.08
|
||||
|
||||
如果 cityPlan.Kind == EmergencyDefense:
|
||||
score += 40
|
||||
如果 cityPlan.Kind == Mobilize:
|
||||
score += 30
|
||||
如果 cityPlan.Kind == Frontline:
|
||||
score += 25
|
||||
|
||||
candidate = Candidate(action, HeroManagement, score, "英雄出场")
|
||||
candidate.City = cityPlan.City
|
||||
candidate.TargetGrid = cityPlan.CityGrid
|
||||
best = MaxCandidate(best, candidate)
|
||||
|
||||
如果 best.IsValid:
|
||||
返回 best
|
||||
|
||||
对每个 action in ctx.Actions.CityActions:
|
||||
如果 IsSelectedHeroSpawnAction(ctx.Player, action):
|
||||
candidate = Candidate(action, HeroManagement, 880, "英雄出场")
|
||||
best = MaxCandidate(best, candidate)
|
||||
|
||||
返回 best
|
||||
```
|
||||
|
||||
```text
|
||||
函数 IsSelectedHeroSpawnAction(player, action):
|
||||
id = action.ActionId
|
||||
返回 id.ActionType == TrainUnit
|
||||
且 id.UnitType == Giant
|
||||
且 id.GiantType != None
|
||||
且 player.PlayerHeroData.HasHero(id.GiantType)
|
||||
```
|
||||
|
||||
英雄出场仍然走 TrainUnitAction 的 CheckCan 和 Execute。AI 只负责更早选择这个合法动作,不直接改变英雄状态。
|
||||
|
||||
### 7.4 FinishHeroTask
|
||||
|
||||
```text
|
||||
函数 TryForceFinishHeroTask(ctx):
|
||||
@ -1335,25 +1386,6 @@ SkillBase
|
||||
返回 false
|
||||
```
|
||||
|
||||
### 7.4 DeployOrReviveHero
|
||||
|
||||
```text
|
||||
函数 TryDeployOrReviveHero(ctx):
|
||||
best = None
|
||||
|
||||
对每个 cityPlan in ctx.Cache.CityPlans:
|
||||
如果 cityPlan.Kind == EmergencyDefense 且 城市无法安全出英雄:
|
||||
继续
|
||||
|
||||
对每个 action in ctx.Actions.ByCity[cityPlan.City.Id]:
|
||||
如果 action 是英雄出场或复活动作:
|
||||
score = 780 + ScoreHeroDeployValue(ctx, action, cityPlan)
|
||||
candidate = Candidate(action, HeroManagement, score, "英雄出场或复活")
|
||||
best = MaxCandidate(best, candidate)
|
||||
|
||||
返回 best
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Expansion Lane
|
||||
@ -1870,6 +1902,10 @@ Byakuren / Miko / Zanmu:
|
||||
返回 ScoreCultureUpgrade(ctx, unit, action)
|
||||
如果 type == Recover:
|
||||
返回 ScoreRecover(ctx, unit, action)
|
||||
如果 type == ShipUpgrade:
|
||||
返回 ScoreShipUpgrade(ctx, unit, action)
|
||||
如果 type == AbsorbMarker:
|
||||
返回 ScoreAbsorbMarker(ctx, unit, action)
|
||||
返回 0
|
||||
```
|
||||
|
||||
@ -1915,9 +1951,12 @@ Byakuren / Miko / Zanmu:
|
||||
```text
|
||||
函数 ScoreRecover(ctx, unit, action):
|
||||
hp = HealthRatio(unit)
|
||||
如果 hp > Config.LowHpRatio:
|
||||
hasNegativeState = unit 有 KomeijiFear 或 SAKUYATIRED
|
||||
如果 hp > Config.LowHpRatio 且 !hasNegativeState:
|
||||
返回 0
|
||||
score = 560 + (1 - hp) * 240
|
||||
score = 520 + (1 - hp) * 220
|
||||
如果 hasNegativeState:
|
||||
score += 160
|
||||
如果 unit 是英雄:
|
||||
score += 120
|
||||
如果 unit 当前能击杀高价值目标:
|
||||
@ -1925,6 +1964,36 @@ Byakuren / Miko / Zanmu:
|
||||
返回 score
|
||||
```
|
||||
|
||||
```text
|
||||
函数 ScoreShipUpgrade(ctx, unit, action):
|
||||
如果 action 不是合法船升级:
|
||||
返回 0
|
||||
cost = GetShipUpgradeCost(unit, action.ActionId.UnitType)
|
||||
如果 ctx.Player.Coin < cost:
|
||||
返回 0
|
||||
|
||||
score = 560 + UnitPower(unit) * 0.2
|
||||
如果 cost == 0:
|
||||
score += 120
|
||||
否则:
|
||||
score -= cost * 18
|
||||
如果 ctx.Cache.StrategicPosture 是 Expansion 或 Attack:
|
||||
score += 90
|
||||
返回 score
|
||||
```
|
||||
|
||||
```text
|
||||
函数 ScoreAbsorbMarker(ctx, unit, action):
|
||||
missing = 1 - HealthRatio(unit)
|
||||
如果 missing <= 0.05:
|
||||
返回 0
|
||||
|
||||
score = 500 + missing * 260
|
||||
如果 unit 是英雄:
|
||||
score += 100
|
||||
返回 score
|
||||
```
|
||||
|
||||
```text
|
||||
函数 ScoreUpgrade(ctx, unit, action):
|
||||
如果 unit 当前承担 Emergency 或可击杀高价值目标:
|
||||
@ -2230,16 +2299,17 @@ Byakuren / Miko / Zanmu:
|
||||
函数 TryFallback(ctx):
|
||||
order = [
|
||||
ctx.Actions.Attacks,
|
||||
ctx.Actions.UnitActions,
|
||||
ctx.Actions.Moves,
|
||||
ctx.Actions.AttackGrounds,
|
||||
ctx.Actions.CityActions,
|
||||
ctx.Actions.GridActions,
|
||||
ctx.Actions.PlayerActions,
|
||||
ctx.Actions.All
|
||||
ctx.Actions.UnitActions
|
||||
]
|
||||
|
||||
对每个 list in order:
|
||||
对每个 action in list:
|
||||
如果 action.ActionType == UnitAttackAlly:
|
||||
继续
|
||||
candidate = Candidate(action, Fallback, 1, "兜底合法动作")
|
||||
如果 candidate.IsValid:
|
||||
candidate.IsFallback = true
|
||||
|
||||
@ -26,6 +26,17 @@ AI 测试必须回答四个问题:
|
||||
| 评分错误 | 车道对了但目标很蠢 | 修同类动作排序 |
|
||||
| 执行失败 | `executed=false` | 查 `CompleteExecute`、参数刷新、网络状态 |
|
||||
|
||||
本轮自循环统一使用一组可比较指标,避免只凭单局观感判断聪明程度:
|
||||
|
||||
| 指标组 | 字段 | 判断方式 |
|
||||
|---|---|---|
|
||||
| 正确性 | `failedGames`、`noEffect`、`repeated`、`maxActions/playerTurn` | 先保证没有错误、死循环和无收益动作 |
|
||||
| 扩张 | `aliveAvgCities`、`alive>=2`、`alive>=3`、`maxCities` | 看整体城市数和二城/三城率是否提升 |
|
||||
| 战斗 | `UnitAttack`、`kills`、`actingUnitDeaths`、`PriorityTactic/Tactic` | 看是否有战果,是否过度送兵 |
|
||||
| 英雄 | `selected`、`spawned`、`HeroManagement`、`HeroPlaybook`、英雄 reason/action | 看英雄是否上场、是否用个人机制 |
|
||||
| Fallback | Fallback 总数、actionType、no-effect | 用来发现规则缺口 |
|
||||
| 性能 | `actions/sec`、`avgGame`、`decision avg/p95/max` | 只处理明显尖峰,不牺牲聪明度做极端优化 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 诊断开关
|
||||
@ -613,16 +624,24 @@ decideMs 尖峰且 actionPool.all 高
|
||||
自循环的固定流程:
|
||||
|
||||
```text
|
||||
1. 固定 10 个 seed、地图、阵营组合。
|
||||
2. 跑一批 JSONL。
|
||||
3. 聚合上面的指标。
|
||||
1. 固定 Seed、地图尺寸、玩家数、回合数、难度、动作预算。
|
||||
2. 用 Editor batch runner 跑一批 JSONL 和 batch_summary.json。
|
||||
3. 先用 analyze_ai_batch_quality.py 聚合指标,不直接打开大日志。
|
||||
4. 找异常最高的 3 类问题。
|
||||
5. 对每类问题抽 3-5 条 eventSequence 做人工复核。
|
||||
6. 判断改 18、19、代码还是 Action 生成。
|
||||
7. 修改后用同一批 seed 回归。
|
||||
7. 修改后用同一批 Seed 和同一批参数回归。
|
||||
8. 对比指标是否改善。
|
||||
```
|
||||
|
||||
固定 Seed 的含义:
|
||||
|
||||
```text
|
||||
Tools/RunAIDirectorBatch.ps1 -Seed 424242 ...
|
||||
```
|
||||
|
||||
BatchRunner 会在 Editor/batch 环境把 `MapData.Net.RandomSeed`、Unity 随机种子和地图高度噪声种子固定住。多局批跑时使用 `Seed + gameIndex`。`batch_summary.json` 中的 `diagnosticsLogPath` 是分析器匹配 JSONL 的权威路径,不用再按时间戳猜日志。
|
||||
|
||||
---
|
||||
|
||||
## 10. 问题回写流程
|
||||
@ -682,6 +701,13 @@ delta证据:
|
||||
开始调整 18/19 的策略思路
|
||||
```
|
||||
|
||||
推荐命令:
|
||||
|
||||
```powershell
|
||||
Tools/RunAIDirectorBatch.ps1 -Games 1 -Players 17 -Width 30 -Height 30 -Turns 12 -Seed 424242 -TimeoutSeconds 420 -MaxActions 9000 -MaxActionsPerPlayerTurn 120 -Difficulty LUNATIC -KeepGoing
|
||||
python .codex/skills/th1-ai-director/scripts/analyze_ai_batch_quality.py --batch Unity/Logs/AI_Batch/<timestamp>/batch_summary.json --top 12
|
||||
```
|
||||
|
||||
第四轮:
|
||||
|
||||
```text
|
||||
|
||||
@ -9,6 +9,7 @@ param(
|
||||
[int]$TimeoutSeconds = 1800,
|
||||
[int]$MaxActions = 20000,
|
||||
[int]$MaxActionsPerPlayerTurn = 260,
|
||||
[int]$Seed = 0,
|
||||
[string]$OutDir,
|
||||
[string]$Difficulty = "LUNATIC",
|
||||
[switch]$KeepGoing,
|
||||
@ -113,6 +114,7 @@ $args = @(
|
||||
"-aiBatchTimeoutSeconds", $TimeoutSeconds,
|
||||
"-aiBatchMaxActions", $MaxActions,
|
||||
"-aiBatchMaxActionsPerPlayerTurn", $MaxActionsPerPlayerTurn,
|
||||
"-aiBatchSeed", $Seed,
|
||||
"-aiBatchDifficulty", $Difficulty,
|
||||
"-aiBatchFailFast", $failFast,
|
||||
"-aiBatchCompactDiagnostics", $compactDiagnostics,
|
||||
|
||||
@ -257,7 +257,6 @@ namespace Logic.AI.Director
|
||||
public AIActionBase FindBestFallback()
|
||||
{
|
||||
return FindFirstFallbackAction(AttackActions)
|
||||
?? FindFirstFallbackAction(AttackAllyActions)
|
||||
?? FindFirstFallbackAction(AttackGroundActions)
|
||||
?? FindFirstFallbackAction(CityActions)
|
||||
?? FindFirstFallbackAction(GridActions)
|
||||
@ -457,6 +456,11 @@ namespace Logic.AI.Director
|
||||
var id = action?.ActionLogic?.ActionId;
|
||||
if (id == null) return false;
|
||||
|
||||
if (id.ActionType == CommonActionType.UnitAttackAlly)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id.ActionType == CommonActionType.UnitAction
|
||||
&& id.UnitActionType is UnitActionType.Examine
|
||||
or UnitActionType.KANAKOSIT
|
||||
|
||||
@ -327,6 +327,14 @@ namespace Logic.AI.Director
|
||||
return true;
|
||||
}
|
||||
|
||||
var heroSpawn = FindBestHeroSpawn(ctx, decision);
|
||||
if (heroSpawn.IsValid)
|
||||
{
|
||||
candidate = heroSpawn;
|
||||
decision.AddTrace($"HeroManagement: spawn hero {candidate.AIAction?.ActionLogic?.ActionId?.GiantType}.", ctx.Config.MaxCandidateTraceCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
var finishTask = ctx.ActionIndex.FindBestHeroTaskFinish(ctx.Player);
|
||||
candidate = ctx.ActionIndex.Candidate(finishTask, AIDirectorLane.HeroManagement, "HeroManagement.FinishLowestTask", 870f);
|
||||
AddTerms(candidate, ("base", 870f));
|
||||
@ -340,6 +348,59 @@ namespace Logic.AI.Director
|
||||
return false;
|
||||
}
|
||||
|
||||
private AIDirectorActionCandidate FindBestHeroSpawn(AIDirectorContext ctx, AIDirectorDecision decision)
|
||||
{
|
||||
AIDirectorActionCandidate best = AIDirectorActionCandidate.None;
|
||||
foreach (var plan in ctx.Cache.CityPlans)
|
||||
{
|
||||
if (plan?.City == null) continue;
|
||||
foreach (var action in ctx.ActionIndex.GetCityActions(plan.City))
|
||||
{
|
||||
if (!IsSelectedHeroSpawnAction(ctx.Player, action)) continue;
|
||||
|
||||
var cityPlanBonus = (plan.Priority > 0f ? plan.Priority : 0f) * 0.08f;
|
||||
var roleBonus = plan.Kind switch
|
||||
{
|
||||
AIDirectorCityPlanKind.EmergencyDefense => 40f,
|
||||
AIDirectorCityPlanKind.Mobilize => 30f,
|
||||
AIDirectorCityPlanKind.Frontline => 25f,
|
||||
_ => 0f
|
||||
};
|
||||
var priority = 880f + cityPlanBonus + roleBonus;
|
||||
var current = ctx.ActionIndex.Candidate(action, AIDirectorLane.HeroManagement, "HeroManagement.SpawnHero", priority);
|
||||
AddTerms(current, ("base", 880f), ("cityPlan", cityPlanBonus), ("role", roleBonus));
|
||||
current.City = current.City ?? plan.City;
|
||||
current.TargetGrid = current.TargetGrid ?? plan.CityGrid;
|
||||
RecordCandidate(ctx, decision, "SpawnHero", current, current.IsValid ? null : "CheckCanFailed");
|
||||
best = MaxCandidate(best, current);
|
||||
}
|
||||
}
|
||||
|
||||
if (best.IsValid) return best;
|
||||
|
||||
foreach (var action in ctx.ActionIndex.CityActions)
|
||||
{
|
||||
if (!IsSelectedHeroSpawnAction(ctx.Player, action)) continue;
|
||||
var current = ctx.ActionIndex.Candidate(action, AIDirectorLane.HeroManagement, "HeroManagement.SpawnHero", 880f);
|
||||
AddTerms(current, ("base", 880f));
|
||||
RecordCandidate(ctx, decision, "SpawnHero", current, current.IsValid ? null : "CheckCanFailed");
|
||||
best = MaxCandidate(best, current);
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static bool IsSelectedHeroSpawnAction(PlayerData player, AIActionBase action)
|
||||
{
|
||||
var id = action?.ActionLogic?.ActionId;
|
||||
if (id == null) return false;
|
||||
return id.ActionType == CommonActionType.TrainUnit
|
||||
&& id.UnitType == UnitType.Giant
|
||||
&& id.GiantType != GiantType.None
|
||||
&& player?.PlayerHeroData != null
|
||||
&& player.PlayerHeroData.HasHero(id.GiantType);
|
||||
}
|
||||
|
||||
private bool TryHeroPlaybookLane(AIDirectorContext ctx, AIDirectorDecision decision, out AIDirectorActionCandidate candidate)
|
||||
{
|
||||
candidate = AIDirectorActionCandidate.None;
|
||||
|
||||
@ -65,7 +65,9 @@ namespace Logic.AI.Director
|
||||
HeroUpgrade,
|
||||
Upgrade,
|
||||
CultureUnitUpgrade,
|
||||
Recover
|
||||
Recover,
|
||||
ShipUpgrade,
|
||||
AbsorbMarker
|
||||
}
|
||||
|
||||
public enum AIDirectorHeroRole
|
||||
|
||||
@ -39,7 +39,7 @@ namespace Logic.AI.Director
|
||||
foreach (var action in actions.UnitActions)
|
||||
{
|
||||
if (action?.Param?.UnitData == null || action.ActionLogic?.ActionId == null) continue;
|
||||
var type = GetOpportunityType(action.ActionLogic.ActionId.UnitActionType);
|
||||
var type = GetOpportunityType(action);
|
||||
if (type == AIDirectorUnitOpportunityType.None) continue;
|
||||
var value = ScoreUnitOpportunity(ctx, action, type);
|
||||
if (value <= 0f) continue;
|
||||
@ -511,9 +511,13 @@ namespace Logic.AI.Director
|
||||
};
|
||||
}
|
||||
|
||||
private AIDirectorUnitOpportunityType GetOpportunityType(UnitActionType type)
|
||||
private AIDirectorUnitOpportunityType GetOpportunityType(AIActionBase action)
|
||||
{
|
||||
return type switch
|
||||
var id = action?.ActionLogic?.ActionId;
|
||||
if (id == null) return AIDirectorUnitOpportunityType.None;
|
||||
if (UnitActionUpgradeHelper.IsShipUpgradeTarget(id.UnitType)) return AIDirectorUnitOpportunityType.ShipUpgrade;
|
||||
|
||||
return id.UnitActionType switch
|
||||
{
|
||||
UnitActionType.Capture => AIDirectorUnitOpportunityType.Capture,
|
||||
UnitActionType.Examine => AIDirectorUnitOpportunityType.Examine,
|
||||
@ -522,6 +526,7 @@ namespace Logic.AI.Director
|
||||
UnitActionType.Upgrade => AIDirectorUnitOpportunityType.Upgrade,
|
||||
UnitActionType.CultureUnitUpgrade => AIDirectorUnitOpportunityType.CultureUnitUpgrade,
|
||||
UnitActionType.Recover => AIDirectorUnitOpportunityType.Recover,
|
||||
UnitActionType.AbsorbRedMist or UnitActionType.HakureiAbsorbRune => AIDirectorUnitOpportunityType.AbsorbMarker,
|
||||
_ => AIDirectorUnitOpportunityType.None
|
||||
};
|
||||
}
|
||||
@ -539,6 +544,8 @@ namespace Logic.AI.Director
|
||||
AIDirectorUnitOpportunityType.Upgrade => 610f,
|
||||
AIDirectorUnitOpportunityType.CultureUnitUpgrade => 600f,
|
||||
AIDirectorUnitOpportunityType.Recover => ScoreRecover(ctx, unit),
|
||||
AIDirectorUnitOpportunityType.ShipUpgrade => ScoreShipUpgrade(ctx, action, unit),
|
||||
AIDirectorUnitOpportunityType.AbsorbMarker => ScoreAbsorbMarker(ctx, unit),
|
||||
_ => 0f
|
||||
};
|
||||
|
||||
@ -562,12 +569,37 @@ namespace Logic.AI.Director
|
||||
{
|
||||
if (unit == null) return 0f;
|
||||
var hp = AIDirectorMath.HealthRatio(unit);
|
||||
if (hp > ctx.Config.LowHealthRatio) return 0f;
|
||||
var score = 560f + (1f - hp) * 240f;
|
||||
var hasNegativeState = unit.GetSkill(SkillType.KomeijiFear, out _) || unit.GetSkill(SkillType.SAKUYATIRED, out _);
|
||||
if (hp > ctx.Config.LowHealthRatio && !hasNegativeState) return 0f;
|
||||
var score = 520f + (1f - hp) * 220f;
|
||||
if (hasNegativeState) score += 160f;
|
||||
if (unit.TreatedAsHero(ctx.Map, unit)) score += 120f;
|
||||
return score;
|
||||
}
|
||||
|
||||
private float ScoreShipUpgrade(AIDirectorContext ctx, AIActionBase action, UnitData unit)
|
||||
{
|
||||
if (unit == null || action?.ActionLogic?.ActionId == null) return 0f;
|
||||
if (!UnitActionUpgradeHelper.TryGetShipUpgradeCost(unit, action.ActionLogic.ActionId.UnitType, out var cost)) return 0f;
|
||||
if (ctx.Player.PlayerCoin < cost) return 0f;
|
||||
|
||||
var score = 560f + AIDirectorMath.UnitPower(unit) * 0.2f;
|
||||
if (cost == 0) score += 120f;
|
||||
else score -= cost * 18f;
|
||||
if (ctx.Cache.StrategicPosture is AIDirectorStrategicPosture.Expansion or AIDirectorStrategicPosture.Attack) score += 90f;
|
||||
return score;
|
||||
}
|
||||
|
||||
private float ScoreAbsorbMarker(AIDirectorContext ctx, UnitData unit)
|
||||
{
|
||||
if (unit == null) return 0f;
|
||||
var missing = 1f - AIDirectorMath.HealthRatio(unit);
|
||||
if (missing <= 0.05f) return 0f;
|
||||
var score = 500f + missing * 260f;
|
||||
if (unit.TreatedAsHero(ctx.Map, unit)) score += 100f;
|
||||
return score;
|
||||
}
|
||||
|
||||
private bool ExistsHighValueDevelopmentTargetNearCity(AIDirectorContext ctx, AIDirectorWorldCache cache)
|
||||
{
|
||||
foreach (var target in cache.DevelopmentTargets)
|
||||
|
||||
@ -21,10 +21,12 @@ namespace Logic.AI
|
||||
public static bool ForceAllPlayersAi;
|
||||
public static bool SkipPresentationWait;
|
||||
public static bool CompactDiagnostics;
|
||||
public static int RandomSeedOverride;
|
||||
#else
|
||||
public const bool ForceAllPlayersAi = false;
|
||||
public const bool SkipPresentationWait = false;
|
||||
public const bool CompactDiagnostics = false;
|
||||
public const int RandomSeedOverride = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@ -242,6 +242,13 @@ namespace TH1_Logic.Core
|
||||
{
|
||||
MapData = new MapData(MapConfig, NetMode.Single);
|
||||
MapData.Net.Mode = NetMode.Single;
|
||||
#if UNITY_EDITOR
|
||||
if (AIDirectorBatchRuntime.RandomSeedOverride != 0)
|
||||
{
|
||||
MapData.Net.RandomSeed = AIDirectorBatchRuntime.RandomSeedOverride;
|
||||
UnityEngine.Random.InitState(AIDirectorBatchRuntime.RandomSeedOverride);
|
||||
}
|
||||
#endif
|
||||
|
||||
//step #1 初始化Audio
|
||||
InitGameAudio();
|
||||
|
||||
@ -136,6 +136,7 @@ namespace TH1_Logic.Editor
|
||||
AIDirectorBatchRuntime.ForceAllPlayersAi = false;
|
||||
AIDirectorBatchRuntime.SkipPresentationWait = false;
|
||||
AIDirectorBatchRuntime.CompactDiagnostics = false;
|
||||
AIDirectorBatchRuntime.RandomSeedOverride = 0;
|
||||
#endif
|
||||
CompleteResult(result, gameDirectory);
|
||||
results.Add(result);
|
||||
@ -166,6 +167,7 @@ namespace TH1_Logic.Editor
|
||||
AIDirectorBatchRuntime.ForceAllPlayersAi = true;
|
||||
AIDirectorBatchRuntime.SkipPresentationWait = true;
|
||||
AIDirectorBatchRuntime.CompactDiagnostics = options.CompactDiagnostics;
|
||||
AIDirectorBatchRuntime.RandomSeedOverride = options.Seed == 0 ? 0 : options.Seed + gameIndex;
|
||||
#if TH1_AI_DIRECTOR_DIAGNOSTICS || UNITY_EDITOR
|
||||
AIDirectorDiagnostics.BeginNewSession();
|
||||
result.diagnosticsLogPath = AIDirectorDiagnostics.CurrentLogPath;
|
||||
@ -176,6 +178,7 @@ namespace TH1_Logic.Editor
|
||||
result.playerCount = (int)main.MapConfig.PlayerCount;
|
||||
result.width = (int)main.MapConfig.Width;
|
||||
result.height = (int)main.MapConfig.Height;
|
||||
result.randomSeed = AIDirectorBatchRuntime.RandomSeedOverride;
|
||||
result.aiKernel = AIKernelRegistry.CurrentKernelType.ToString();
|
||||
|
||||
Debug.Log($"[AI.Batch] Start game {gameIndex + 1}/{options.Games}: players={result.playerCount}, size={result.width}x{result.height}");
|
||||
@ -226,6 +229,7 @@ namespace TH1_Logic.Editor
|
||||
AIDirectorBatchRuntime.ForceAllPlayersAi = false;
|
||||
AIDirectorBatchRuntime.SkipPresentationWait = false;
|
||||
AIDirectorBatchRuntime.CompactDiagnostics = false;
|
||||
AIDirectorBatchRuntime.RandomSeedOverride = 0;
|
||||
#endif
|
||||
try
|
||||
{
|
||||
@ -397,10 +401,7 @@ namespace TH1_Logic.Editor
|
||||
{
|
||||
var civId = PickCivId(i + gameIndex, used);
|
||||
used.Add(civId);
|
||||
if (!config.SetSinglePlayerSlotCiv(i, civId, civId))
|
||||
{
|
||||
config.SetPlayerSlotRandomCiv(i, NetMode.Single);
|
||||
}
|
||||
config.SetSinglePlayerSlotCiv(i, civId, civId);
|
||||
}
|
||||
|
||||
config.EnsurePlayerSlots(NetMode.Single);
|
||||
@ -463,6 +464,7 @@ namespace TH1_Logic.Editor
|
||||
options.MaxActionsPerPlayerTurn = GetIntArg("-aiBatchMaxActionsPerPlayerTurn", options.MaxActionsPerPlayerTurn);
|
||||
options.StagnantFrameLimit = GetIntArg("-aiBatchStagnantFrames", options.StagnantFrameLimit);
|
||||
options.AnimationSpeed = GetFloatArg("-aiBatchAnimationSpeed", options.AnimationSpeed);
|
||||
options.Seed = GetIntArg("-aiBatchSeed", options.Seed);
|
||||
options.FailFast = GetBoolArg("-aiBatchFailFast", options.FailFast);
|
||||
options.DisableNearbySpawnPoints = GetBoolArg("-aiBatchDisableNearbySpawnPoints", options.DisableNearbySpawnPoints);
|
||||
options.CompactDiagnostics = GetBoolArg("-aiBatchCompactDiagnostics", options.CompactDiagnostics);
|
||||
@ -665,6 +667,14 @@ namespace TH1_Logic.Editor
|
||||
var executedActionTypes = new Dictionary<string, int>();
|
||||
var laneActionTypes = new Dictionary<string, int>();
|
||||
var noEffectActionTypes = new Dictionary<string, int>();
|
||||
var heroReasons = new Dictionary<string, int>();
|
||||
var heroActionTypes = new Dictionary<string, int>();
|
||||
var heroExecutedActionTypes = new Dictionary<string, int>();
|
||||
var fallbackActionTypes = new Dictionary<string, int>();
|
||||
var fallbackReasons = new Dictionary<string, int>();
|
||||
var fallbackExecutedActionTypes = new Dictionary<string, int>();
|
||||
var fallbackNoEffectActionTypes = new Dictionary<string, int>();
|
||||
var decisionLaneByAction = new Dictionary<string, string>();
|
||||
|
||||
try
|
||||
{
|
||||
@ -695,12 +705,37 @@ namespace TH1_Logic.Editor
|
||||
|
||||
var lane = decision.Value<string>("lane") ?? string.Empty;
|
||||
var reason = decision.Value<string>("reason") ?? string.Empty;
|
||||
var actionType = decision["action"]?.Value<string>("actionType") ?? string.Empty;
|
||||
var action = decision["action"];
|
||||
var actionType = action?.Value<string>("actionType") ?? string.Empty;
|
||||
var actionKey = BuildActionMetricKey(action);
|
||||
var turnActionKey = BuildTurnActionKey(record, action);
|
||||
Increment(lanes, lane);
|
||||
Increment(reasons, reason);
|
||||
Increment(selectedActionTypes, actionType);
|
||||
Increment(laneActionTypes, $"{lane}:{actionType}");
|
||||
if (decision.Value<bool?>("isFallback") ?? false) summary.fallbackDecisions++;
|
||||
if (!string.IsNullOrEmpty(turnActionKey)) decisionLaneByAction[turnActionKey] = lane;
|
||||
|
||||
if (lane == "HeroManagement")
|
||||
{
|
||||
summary.heroManagementDecisions++;
|
||||
Increment(heroReasons, reason);
|
||||
Increment(heroActionTypes, actionKey);
|
||||
}
|
||||
else if (lane == "HeroPlaybook")
|
||||
{
|
||||
summary.heroPlaybookDecisions++;
|
||||
Increment(heroReasons, reason);
|
||||
Increment(heroActionTypes, actionKey);
|
||||
if (reason.StartsWith("HeroPlaybook.", StringComparison.Ordinal)) summary.genericHeroDecisions++;
|
||||
else summary.heroRuleDecisions++;
|
||||
}
|
||||
|
||||
if (decision.Value<bool?>("isFallback") ?? false)
|
||||
{
|
||||
summary.fallbackDecisions++;
|
||||
Increment(fallbackReasons, reason);
|
||||
Increment(fallbackActionTypes, actionKey);
|
||||
}
|
||||
}
|
||||
else if (eventType == "Execution")
|
||||
{
|
||||
@ -708,6 +743,7 @@ namespace TH1_Logic.Editor
|
||||
var execution = record["execution"];
|
||||
var action = execution?["action"];
|
||||
var actionType = action?.Value<string>("actionType") ?? string.Empty;
|
||||
var actionKey = BuildActionMetricKey(action);
|
||||
Increment(executedActionTypes, actionType);
|
||||
|
||||
var playerTurnKey = $"{record.Value<uint?>("playerId") ?? 0}:{record.Value<uint?>("playerTurn") ?? 0}";
|
||||
@ -721,10 +757,33 @@ namespace TH1_Logic.Editor
|
||||
if (!executedStableKeysByPlayerTurn.Add(stableKeyInTurn)) summary.repeatedExecutions++;
|
||||
}
|
||||
|
||||
if ((execution?.Value<bool?>("executed") ?? false) && !HasMeaningfulDelta(execution["delta"]))
|
||||
var turnActionKey = BuildTurnActionKey(record, action);
|
||||
decisionLaneByAction.TryGetValue(turnActionKey, out var executedLane);
|
||||
if (executedLane == "HeroManagement" || executedLane == "HeroPlaybook")
|
||||
{
|
||||
Increment(heroExecutedActionTypes, actionKey);
|
||||
}
|
||||
else if (executedLane == "Fallback")
|
||||
{
|
||||
Increment(fallbackExecutedActionTypes, actionKey);
|
||||
}
|
||||
|
||||
var delta = execution?["delta"];
|
||||
if (execution?.Value<bool?>("executed") ?? false)
|
||||
{
|
||||
summary.heroDelta += delta?.Value<int?>("heroDelta") ?? 0;
|
||||
summary.selectedHeroDelta += delta?.Value<int?>("selectedHeroDelta") ?? 0;
|
||||
summary.heroTaskDelta += delta?.Value<int?>("heroTaskDelta") ?? 0;
|
||||
summary.readyHeroTaskDelta += delta?.Value<int?>("readyHeroTaskDelta") ?? 0;
|
||||
summary.forcedHeroTaskDelta += delta?.Value<int?>("forcedHeroTaskDelta") ?? 0;
|
||||
summary.heroTaskProgressDelta += delta?.Value<int?>("heroTaskProgressDelta") ?? 0;
|
||||
}
|
||||
|
||||
if ((execution?.Value<bool?>("executed") ?? false) && !HasMeaningfulDelta(delta))
|
||||
{
|
||||
summary.noEffectExecutions++;
|
||||
Increment(noEffectActionTypes, actionType);
|
||||
if (executedLane == "Fallback") Increment(fallbackNoEffectActionTypes, actionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -744,9 +803,46 @@ namespace TH1_Logic.Editor
|
||||
summary.topExecutedActionTypes = TopCounts(executedActionTypes, 12);
|
||||
summary.topLaneActionTypes = TopCounts(laneActionTypes, 20);
|
||||
summary.noEffectActionTypes = TopCounts(noEffectActionTypes, 12);
|
||||
summary.topHeroReasons = TopCounts(heroReasons, 16);
|
||||
summary.topHeroActionTypes = TopCounts(heroActionTypes, 12);
|
||||
summary.topHeroExecutedActionTypes = TopCounts(heroExecutedActionTypes, 12);
|
||||
summary.topFallbackReasons = TopCounts(fallbackReasons, 12);
|
||||
summary.topFallbackActionTypes = TopCounts(fallbackActionTypes, 12);
|
||||
summary.topFallbackExecutedActionTypes = TopCounts(fallbackExecutedActionTypes, 12);
|
||||
summary.fallbackNoEffectActionTypes = TopCounts(fallbackNoEffectActionTypes, 12);
|
||||
return summary;
|
||||
}
|
||||
|
||||
private static string BuildTurnActionKey(JToken record, JToken action)
|
||||
{
|
||||
var stableKey = action?.Value<string>("stableKey");
|
||||
if (string.IsNullOrEmpty(stableKey)) return string.Empty;
|
||||
return $"{record?.Value<uint?>("playerId") ?? 0}:{record?.Value<uint?>("playerTurn") ?? 0}:{stableKey}";
|
||||
}
|
||||
|
||||
private static string BuildActionMetricKey(JToken action)
|
||||
{
|
||||
if (action == null) return string.Empty;
|
||||
var actionType = action.Value<string>("actionType") ?? string.Empty;
|
||||
var subType = actionType switch
|
||||
{
|
||||
"UnitAction" => action.Value<string>("unitActionType") ?? string.Empty,
|
||||
"PlayerAction" => action.Value<string>("playerActionType") ?? string.Empty,
|
||||
"CityAction" => action.Value<string>("cityActionType") ?? string.Empty,
|
||||
"CityLevelUpAction" => action.Value<string>("cityLevelUpActionType") ?? string.Empty,
|
||||
"GridMisc" => action.Value<string>("gridMiscActionType") ?? string.Empty,
|
||||
"LearnTech" => action.Value<string>("techType") ?? string.Empty,
|
||||
"BuyCultureCard" => action.Value<string>("cultureCardType") ?? string.Empty,
|
||||
"Gain" => action.Value<string>("resourceType") ?? string.Empty,
|
||||
"TrainUnit" => action.Value<string>("unitType") ?? string.Empty,
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
return string.IsNullOrEmpty(subType) || subType == "None"
|
||||
? actionType
|
||||
: $"{actionType}:{subType}";
|
||||
}
|
||||
|
||||
private static bool HasMeaningfulDelta(JToken delta)
|
||||
{
|
||||
if (delta == null) return false;
|
||||
@ -910,6 +1006,7 @@ namespace TH1_Logic.Editor
|
||||
public bool DisableNearbySpawnPoints = false;
|
||||
public bool CompactDiagnostics = true;
|
||||
public bool AllSight = false;
|
||||
public int Seed = 0;
|
||||
public string OutputDirectory;
|
||||
public string ScenePath;
|
||||
public AIDifficult Difficulty = AIDifficult.LUNATIC;
|
||||
@ -928,6 +1025,7 @@ namespace TH1_Logic.Editor
|
||||
MaxActionsPerPlayerTurn = Mathf.Max(0, MaxActionsPerPlayerTurn);
|
||||
StagnantFrameLimit = Mathf.Max(0, StagnantFrameLimit);
|
||||
AnimationSpeed = Mathf.Max(1f, AnimationSpeed);
|
||||
Seed = Mathf.Max(0, Seed);
|
||||
if (string.IsNullOrWhiteSpace(OutputDirectory))
|
||||
{
|
||||
var root = Directory.GetParent(Application.dataPath).FullName;
|
||||
@ -969,6 +1067,7 @@ namespace TH1_Logic.Editor
|
||||
public int playerCount;
|
||||
public int width;
|
||||
public int height;
|
||||
public int randomSeed;
|
||||
public int frames;
|
||||
public int netActions;
|
||||
public uint maxPlayerTurn;
|
||||
@ -1004,9 +1103,19 @@ namespace TH1_Logic.Editor
|
||||
public int decisions;
|
||||
public int noActionDecisions;
|
||||
public int fallbackDecisions;
|
||||
public int heroManagementDecisions;
|
||||
public int heroPlaybookDecisions;
|
||||
public int heroRuleDecisions;
|
||||
public int genericHeroDecisions;
|
||||
public int executions;
|
||||
public int noEffectExecutions;
|
||||
public int repeatedExecutions;
|
||||
public int heroDelta;
|
||||
public int selectedHeroDelta;
|
||||
public int heroTaskDelta;
|
||||
public int readyHeroTaskDelta;
|
||||
public int forcedHeroTaskDelta;
|
||||
public int heroTaskProgressDelta;
|
||||
public float avgDecideMs;
|
||||
public float p95DecideMs;
|
||||
public float maxDecideMs;
|
||||
@ -1025,6 +1134,13 @@ namespace TH1_Logic.Editor
|
||||
public List<BatchCountMetric> topExecutedActionTypes = new();
|
||||
public List<BatchCountMetric> topLaneActionTypes = new();
|
||||
public List<BatchCountMetric> noEffectActionTypes = new();
|
||||
public List<BatchCountMetric> topHeroReasons = new();
|
||||
public List<BatchCountMetric> topHeroActionTypes = new();
|
||||
public List<BatchCountMetric> topHeroExecutedActionTypes = new();
|
||||
public List<BatchCountMetric> topFallbackReasons = new();
|
||||
public List<BatchCountMetric> topFallbackActionTypes = new();
|
||||
public List<BatchCountMetric> topFallbackExecutedActionTypes = new();
|
||||
public List<BatchCountMetric> fallbackNoEffectActionTypes = new();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@ -11,6 +11,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Logic.Pool;
|
||||
using Logic.AI;
|
||||
using RuntimeData;
|
||||
using TH1_Logic.Core;
|
||||
using Unity.VisualScripting;
|
||||
@ -156,12 +157,17 @@ namespace Logic
|
||||
//海陆层使用的地形高度数据
|
||||
private void GenerateHeightMap()
|
||||
{
|
||||
System.Random batchRandom = null;
|
||||
#if UNITY_EDITOR
|
||||
if (AIDirectorBatchRuntime.RandomSeedOverride != 0)
|
||||
batchRandom = new System.Random(AIDirectorBatchRuntime.RandomSeedOverride ^ 0x5F3759DF);
|
||||
#endif
|
||||
for (var x = 0; x < _width; x++)
|
||||
{
|
||||
for (var y = 0; y < _height; y++)
|
||||
{
|
||||
float seda = new System.Random().Next(1, 30);
|
||||
float sedb = new System.Random().Next(2, 40);
|
||||
float seda = batchRandom?.Next(1, 30) ?? new System.Random().Next(1, 30);
|
||||
float sedb = batchRandom?.Next(2, 40) ?? new System.Random().Next(2, 40);
|
||||
// 使用种子值来改变噪声的生成,保证每次生成的噪声不同
|
||||
var xCoord = (float)x / _width * Scale / seda;
|
||||
var yCoord = (float)y / _height * Scale / sedb;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user