diff --git a/src/game/engine/src/modules/ai/ai_turn_bridge.gd b/src/game/engine/src/modules/ai/ai_turn_bridge.gd index b0926377..35ad0489 100644 --- a/src/game/engine/src/modules/ai/ai_turn_bridge.gd +++ b/src/game/engine/src/modules/ai/ai_turn_bridge.gd @@ -271,6 +271,11 @@ static func _replay_learned_actions(action_log: Array, player: RefCounted) -> in var applied: int = 0 var deferred_founds: int = 0 var dropped_variants: Array[String] = [] + # Gate (b) instrumentation — record (intended_axial, resulting_gdscript_pos) + # for the first few moves so the Inc-2 gate can prove the GDScript world + # actually moved to the policy's chosen target. Capped to keep the log tight. + var parity_samples: Array[String] = [] + const PARITY_SAMPLE_CAP: int = 6 for entry: Dictionary in action_log: var kind: String = String(entry.get("kind", "")) match kind: @@ -279,14 +284,24 @@ static func _replay_learned_actions(action_log: Array, player: RefCounted) -> in # the move-as-attack case at the target hex). var off: Vector2i = Vector2i(int(entry.get("to_col", 0)), int(entry.get("to_row", 0))) var ax: Vector2i = HexUtilsScript.offset_to_axial(off) + var uid: int = int(entry.get("unit_id", -1)) var move_json: String = JSON.stringify({ "MoveUnit": { - "unit_id": int(entry.get("unit_id", -1)), + "unit_id": uid, "to_hex": [ax.x, ax.y], } }) if DispatchScript.dispatch_action(move_json, player, index_maps, city_name): applied += 1 + # Position-parity proof: read the unit's resulting GDScript + # position and compare to the intended axial target. + if parity_samples.size() < PARITY_SAMPLE_CAP: + var u: RefCounted = (index_maps.get("units", {}) as Dictionary).get(uid) + if u != null: + var match_str: String = "OK" if u.position == ax else "MISMATCH" + parity_samples.append( + "u%d→intended%s got%s [%s]" % [uid, str(ax), str(u.position), match_str] + ) "queue": var queue_json: String = JSON.stringify({ "SetProduction": { @@ -302,10 +317,18 @@ static func _replay_learned_actions(action_log: Array, player: RefCounted) -> in dropped_variants.append(String(entry.get("variant", "?"))) _: dropped_variants.append(kind) + print("[p1-29k] replay applied=%d (move/attack/queue dispatched into GDScript)" % applied) + if not parity_samples.is_empty(): + print("[p1-29k] position_parity: %s" % str(parity_samples)) if deferred_founds > 0: print("[p1-29k] DEFERRED %d FoundCity action(s) to Inc-3 (p1-29j boundary)" % deferred_founds) if not dropped_variants.is_empty(): - print("[p1-29k] dropped %d uncovered variant(s): %s" % [dropped_variants.size(), str(dropped_variants)]) + # Summarize dropped-variant counts rather than dumping the full toggle + # storm (the policy oscillates Fortify↔Unfortify — see Inc-2 report). + var dropped_counts: Dictionary = {} + for v: String in dropped_variants: + dropped_counts[v] = int(dropped_counts.get(v, 0)) + 1 + print("[p1-29k] dropped %d uncovered action(s): %s" % [dropped_variants.size(), str(dropped_counts)]) return applied