fix(@projects/@magic-civilization): 🐛 AI military picker reads full unit catalog (was dead race.start_units)

_pick_buildable_military_unit_id iterated race_data.start_units — a field that no
longer exists on race JSON — so _queue_military never queued a unit and the AI
built no military. Iterate the loaded unit catalog instead (can_build() applies
race+tech gates), requiring a non-founder combat unit (attack>0) and excluding
wild creatures (faction "wild" — dire_bear is cost 0 and would always win).
Clears test_ai_turn_bridge_mcts.

Also marks test_no_unit_has_legacy_flags_field pending: it asserts flags/
can_found_city are "legacy", but the runtime still reads them — the unit_actions
migration is incomplete (+ the test uses the pre-move data/units path & array
format). Consistent with the suite's existing pending-stub convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-24 11:42:04 -04:00
parent 774ffe9620
commit ff0f8a4670
2 changed files with 23 additions and 25 deletions

View file

@ -839,14 +839,22 @@ static func _find_unit_type_by_flag(player: RefCounted, flag: String) -> String:
static func _pick_buildable_military_unit_id(city: RefCounted, player: RefCounted) -> String:
var race_data: Dictionary = DataLoader.get_race(player.race_id)
var start_units: Array = race_data.get("start_units", [])
## Cheapest buildable combat unit for the player. Iterates the full unit
## catalog (can_build() applies the race + tech gates); the old code read a
## race `start_units` field that no longer exists, so the AI never queued
## military. Require a non-founder combat unit (attack > 0) and exclude wild
## creatures (faction "wild" — e.g. dire_bear is cost 0 and would always win).
var best_id: String = ""
var best_cost: int = 2147483647
for uid: String in start_units:
var udata: Dictionary = DataLoader.get_unit(uid)
var units: Dictionary = DataLoader.get_data("units")
for uid: String in units:
var udata: Dictionary = units[uid] as Dictionary
if udata.is_empty() or udata.get("can_found_city", false):
continue
if int(udata.get("attack", 0)) <= 0:
continue
if String(udata.get("faction", "")) == "wild":
continue
if not city.can_build(uid, player):
continue
var cost: int = int(udata.get("cost", 999999))

View file

@ -47,27 +47,17 @@ func test_archer_has_ranged_keyword() -> void:
func test_no_unit_has_legacy_flags_field() -> void:
var units_dir: String = "res://public/games/age-of-dwarves/data/units/"
var dir: DirAccess = DirAccess.open(units_dir)
assert_not_null(dir, "units/ directory must be accessible")
if dir == null:
return
var legacy_keys: Array[String] = ["flags", "can_found_city", "can_build_improvements"]
dir.list_dir_begin()
var filename: String = dir.get_next()
while filename != "":
if filename.ends_with(".json") and filename != "manifest.json" and filename != "stub.json":
var path: String = units_dir + filename
var text: String = FileAccess.get_file_as_string(path)
var parsed: Array = JSON.parse_string(text) as Array
for entry: Dictionary in parsed:
for key: String in legacy_keys:
assert_false(
entry.has(key),
"%s must not have legacy field '%s'" % [filename, key]
)
filename = dir.get_next()
dir.list_dir_end()
# Guards a migration that is NOT complete, and predates two schema changes:
# units now live in public/resources/units/ (not .../data/units/) and are one
# JSON object per file (not an array). The runtime still reads can_found_city
# and flags (founder logic, the AI unit picker, build gating), so units
# legitimately carry these fields today — asserting their absence fails by
# design. Re-enable (and fix the path/format) once the inline flags are fully
# migrated to unit_actions.json and the fields are vestigial.
pending(
"unit_actions migration incomplete — flags/can_found_city still read by "
+ "the runtime; cannot assert their absence yet"
)
# ── get_keywords / get_keywords_str ──────────────────────────────────────────