feat(ai): add parity sampling for move validation

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-08 18:39:56 -07:00
parent 6a15d0a418
commit 88bb753876

View file

@ -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