diff --git a/scripts/apricot-run.sh b/scripts/apricot-run.sh index e8d00a59..2c082623 100755 --- a/scripts/apricot-run.sh +++ b/scripts/apricot-run.sh @@ -469,9 +469,17 @@ case "${MODE}" in # 2026-04-18. Opt-in via env override; gpu-walltime flips # per-iteration as its explicit comparison. GPU_ENV="AI_GPU_ROLLOUT=${AI_GPU_ROLLOUT:-false}" - echo "[$(date +%H:%M:%S)] smoke batch: ${SEEDS} seeds T${TURNS} PARALLEL=${PARALLEL} ${GPU_ENV}" + # p2-44b: forward MC_AI_* diagnostic envs so instrumentation prints surface in game.log + MC_AI_ENV="" + for var in MC_AI_PROMOTION_DEBUG; do + val="${!var:-}" + if [[ -n "${val}" ]]; then + MC_AI_ENV="${MC_AI_ENV} ${var}=${val}" + fi + done + echo "[$(date +%H:%M:%S)] smoke batch: ${SEEDS} seeds T${TURNS} PARALLEL=${PARALLEL} ${GPU_ENV}${MC_AI_ENV}" ssh "${APRICOT}" "set -euo pipefail; cd '${SCRATCH_ABS}' && \ - AI_USE_MCTS=true ${GPU_ENV} PARALLEL=${PARALLEL} \ + AI_USE_MCTS=true ${GPU_ENV}${MC_AI_ENV} PARALLEL=${PARALLEL} \ bash tools/autoplay-batch.sh ${SEEDS} ${TURNS} ${RESULTS_ABS}/smoke 2>&1 | tail -30" ;; clan) diff --git a/src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd b/src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd index 4883d81b..b70d5033 100644 --- a/src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd +++ b/src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd @@ -47,14 +47,24 @@ static func dispatch_action( ## the audio chime, throne-room counter, and chronicle log all fire on the ## same path the human picker uses. static func dispatch_promotion_picked(fields: Dictionary, index_maps: Dictionary) -> bool: - var unit: RefCounted = resolve_unit(int(fields.get("unit_id", -1)), index_maps) - if unit == null or not unit.is_alive(): - return false + var debug: bool = OS.get_environment("MC_AI_PROMOTION_DEBUG") != "" + var uid: int = int(fields.get("unit_id", -1)) var promo_id: String = String(fields.get("promotion_id", "")) + if debug: + print("[promo-debug] dispatch_promotion_picked entry uid=", uid, " promo=", promo_id) + var unit: RefCounted = resolve_unit(uid, index_maps) + if unit == null or not unit.is_alive(): + if debug: + print("[promo-debug] dispatch_promotion_picked ABORT unit_null_or_dead uid=", uid) + return false if promo_id.is_empty(): + if debug: + print("[promo-debug] dispatch_promotion_picked ABORT empty_promo_id uid=", uid) return false unit.promote(promo_id) EventBus.unit_promoted.emit(unit, promo_id) + if debug: + print("[promo-debug] dispatch_promotion_picked SUCCESS uid=", uid, " promo=", promo_id, " new_vlvl=", unit.veteran_level) return true diff --git a/src/game/engine/src/modules/ai/ai_turn_bridge_state.gd b/src/game/engine/src/modules/ai/ai_turn_bridge_state.gd index a051cf30..9cea9278 100644 --- a/src/game/engine/src/modules/ai/ai_turn_bridge_state.gd +++ b/src/game/engine/src/modules/ai/ai_turn_bridge_state.gd @@ -91,6 +91,15 @@ static func build_tactical_state(focal: RefCounted) -> Dictionary: if p == null: continue players.append(player_to_dict(p)) + if OS.get_environment("MC_AI_PROMOTION_DEBUG") != "": + var total_with_choices: int = 0 + var total_units: int = 0 + for pdict: Dictionary in players: + for udict: Dictionary in pdict.get("units", []): + total_units += 1 + if not (udict.get("pending_promotion_choices", []) as Array).is_empty(): + total_with_choices += 1 + print("[promo-debug] build_tactical_state turn=", int(GameState.turn_number), " focal=", int(focal.index), " total_units=", total_units, " units_with_choices=", total_with_choices) return { "current_player": int(focal.index), "turn": int(GameState.turn_number), @@ -412,27 +421,45 @@ static func _load_clan_entry(clan_id: String) -> Dictionary: ## 4. The promotion's prerequisite (if any) must already be owned. ## 5. The promotion's level entry must equal the unit's next veteran level. static func _eligible_promotion_ids(unit: RefCounted) -> Array: + var debug: bool = OS.get_environment("MC_AI_PROMOTION_DEBUG") != "" var out: Array = [] - if unit == null or not unit.has_method("can_promote") or not unit.can_promote(): + if unit == null or not unit.has_method("can_promote"): + if debug: + print("[promo-debug] eligible: unit=null or no can_promote") + return out + var can: bool = unit.can_promote() + var uid: int = int(unit.get("id")) if "id" in unit else -1 + if not can: + if debug: + print("[promo-debug] eligible unit=", uid, " kind=", unit.unit_id, " xp=", unit.xp, " vlvl=", unit.veteran_level, " can_promote=false") return out var trees: Dictionary = DataLoader.get_promotion_trees() if trees.is_empty(): + if debug: + print("[promo-debug] eligible unit=", uid, " ABORT trees_empty") return out var owned: Array = Array(unit.promo_ids) var next_level: int = int(unit.veteran_level) + 1 var unit_data: Dictionary = unit.get_data() var unit_flags: Array = unit_data.get("flags", []) var unit_combat_type: String = String(unit_data.get("unit_type", "melee")) + var rejected_trees: Array = [] + var matched_trees: Array = [] + var no_level_match: Array = [] for tree_key: String in trees: var tree: Dictionary = trees[tree_key] as Dictionary if tree == null: continue if not _tree_applies_to(tree, unit_combat_type, unit_flags): + rejected_trees.append(tree_key) continue + matched_trees.append(tree_key) var levels: Array = tree.get("levels", []) + var any_level_match: bool = false for level_entry: Dictionary in levels: if int(level_entry.get("level", 0)) != next_level: continue + any_level_match = true for choice: Dictionary in level_entry.get("choices", []): var pid: String = String(choice.get("id", "")) if pid.is_empty() or owned.has(pid): @@ -441,6 +468,10 @@ static func _eligible_promotion_ids(unit: RefCounted) -> Array: if not prereq.is_empty() and prereq != "" and not owned.has(prereq): continue out.append(pid) + if not any_level_match: + no_level_match.append(tree_key) + if debug: + print("[promo-debug] eligible unit=", uid, " kind=", unit.unit_id, " xp=", unit.xp, " vlvl=", unit.veteran_level, " ctype=", unit_combat_type, " flags=", unit_flags, " next_lvl=", next_level, " matched=", matched_trees, " rejected=", rejected_trees, " no_lvl=", no_level_match, " out=", out) return out