extends RefCounted ## Deterministic finite planning; no access to the opponent's current orders. const MAX_WORKERS_LEARNING := 4 const MAX_ROSTER_LEARNING := 12 func plan(team: int, wave: int, opponent_snapshot: Dictionary, last_report: Dictionary, level: int = GameState.AI_STANDARD) -> void: if not EconomyManager.build_open or EconomyManager.current_wave != wave: return if wave == 1: _order(team, {"type": "build", "type_id": "g03", "cell": Vector2i(2, 0)}) _order(team, {"type": "build", "type_id": "g05", "cell": Vector2i(2, 2)}) _order(team, {"type": "build", "type_id": "g01", "cell": Vector2i(3, 0)}) return if level == GameState.AI_LEARNING: _plan_learning(team, wave) return var natural_count: int = maxi(1, int(last_report.get("natural_count", 1))) var leak_ratio: float = float(last_report.get("natural_leaks", 0)) / float(natural_count) var weak_front: bool = float(last_report.get("front_lifetime", 90.0)) < 12.0 or leak_ratio > 0.10 var own: Dictionary = EconomyManager.get_team(team) if own.is_empty(): return # Leave at least 60 gold for defense; newly ordered workers cannot produce now. var cost: int = EconomyManager.worker_cost(team) if not weak_front and wave <= DataCatalog.wave_count() - 3 and int(own["workers"]) + int(own["pending_workers"]) < 8: if int(own["gold"]) >= cost + 60 and _front_count(own["roster"]) >= 2: _order(team, {"type": "worker"}) var armor: int = _dominant_wave_armor(wave) # Fixed upper bound also makes pathological or full boards terminate immediately. for _step: int in 16: own = EconomyManager.get_team(team) var roster: Array = own["roster"] var front: int = _front_count(roster) var desired_front: int = maxi(2, ceili(float(roster.size()) * 0.35)) var purchased: bool = false if front < desired_front: purchased = _build(team, "g03" if front % 2 == 0 else "g01", own) elif roster.size() >= 3: purchased = _upgrade(team, own, armor, weak_front) if not purchased: var preferred: Array[String] = _buy_preferences(roster, armor, weak_front) for id: String in preferred: if _build(team, id, own): purchased = true break if not purchased: break _plan_sends(team, wave, opponent_snapshot) func _plan_learning(team: int, wave: int) -> void: # Fixed, readable behaviour: no reactive counters, no saved-up attacks. var own: Dictionary = EconomyManager.get_team(team) if own.is_empty(): return var cost: int = EconomyManager.worker_cost(team) if wave <= DataCatalog.wave_count() - 3 and int(own["workers"]) + int(own["pending_workers"]) < MAX_WORKERS_LEARNING: if int(own["gold"]) >= cost + 60 and _front_count(own["roster"]) >= 2: _order(team, {"type": "worker"}) var rotation: Array[String] = ["g03", "g01", "g05", "g07", "g09"] var upgrade_order: Array[String] = ["g03", "g05", "g01", "g07", "g09"] for _step: int in 16: own = EconomyManager.get_team(team) var roster: Array = own["roster"] var purchased: bool = false if roster.size() >= 6: purchased = _upgrade_any(team, own, upgrade_order) if not purchased and roster.size() < MAX_ROSTER_LEARNING: purchased = _build(team, rotation[roster.size() % rotation.size()], own) if not purchased: break _plan_simple_sends(team, wave) func _upgrade_any(team: int, own: Dictionary, order: Array[String]) -> bool: for id: String in order: var upgraded: Dictionary = DataCatalog.get_unit(DataCatalog.UNITS[DataCatalog.for_team(id, team)]["upgrade_id"]) if int(own["gold"]) < int(upgraded["cost"]): continue for entry: Dictionary in own["roster"]: if entry["type_id"] == DataCatalog.for_team(id, team): return _order(team, {"type": "upgrade", "uid": entry["uid"]}) return false func _plan_simple_sends(team: int, wave: int) -> void: # Sends only in every second allowed wave; the budget is spent immediately. if wave < 3 or wave % 2 == 0: return for _step: int in 8: var own: Dictionary = EconomyManager.get_team(team) var reserved: int = EconomyManager.reserved_sparks(team) var available: int = mini(int(own["sparks"]) - reserved, EconomyManager.send_budget(wave) - reserved) if own["send_queue"].size() >= 8 or available < 20: break var send_id: String = "s01" if available < 40 else "s02" if not _order(team, {"type": "queue_send", "type_id": send_id}): break func _buy_preferences(roster: Array, armor: int, weak_front: bool) -> Array[String]: var front: int = _front_count(roster) if front < 2 or (weak_front and front * 2 < roster.size()): return ["g03", "g01", "g05"] var support: int = _count_types(roster, ["g09", "g10"]) if roster.size() >= 4 and support * 4 < roster.size() and not weak_front: return ["g09", "g05", "g07", "g01"] if armor == 2 or _count_types(roster, ["g07", "g08"]) == 0: return ["g07", "g05", "g01"] return ["g05", "g07", "g01"] func _upgrade(team: int, own: Dictionary, armor: int, weak_front: bool) -> bool: var order: Array[String] = ["g05", "g03", "g07", "g01", "g09"] if weak_front: order = ["g03", "g01", "g05", "g07", "g09"] elif armor == 2: order = ["g07", "g05", "g03", "g01", "g09"] for id: String in order: var upgraded: Dictionary = DataCatalog.get_unit(DataCatalog.UNITS[DataCatalog.for_team(id, team)]["upgrade_id"]) if int(own["gold"]) < int(upgraded["cost"]): continue for entry: Dictionary in own["roster"]: if entry["type_id"] == DataCatalog.for_team(id, team): return _order(team, {"type": "upgrade", "uid": entry["uid"]}) return false func _build(team: int, id: String, own: Dictionary) -> bool: var definition: Dictionary = DataCatalog.get_unit(DataCatalog.for_team(id, team)) if int(own["gold"]) < int(definition["cost"]): return false var cell: Vector2i = _free_cell(own["roster"], id) if cell.x < 0: return false return _order(team, {"type": "build", "type_id": id, "cell": cell}) func _free_cell(roster: Array, id: String) -> Vector2i: var rows: Array[int] = [2, 3, 4, 5, 6, 7, 1, 0] if id in ["g01", "g03"]: rows = [0, 1, 2, 3, 4, 5, 6, 7] elif id == "g09": rows = [1, 2, 3, 4, 5, 6, 7, 0] for row: int in rows: for column: int in [2, 3, 1, 4, 0, 5]: var candidate: Vector2i = Vector2i(column, row) var occupied: bool = false for entry: Dictionary in roster: if entry["cell"] == candidate: occupied = true break if not occupied: return candidate return Vector2i(-1, -1) func _plan_sends(team: int, wave: int, opponent_snapshot: Dictionary) -> void: if wave < 3: return var public_roster: Array = opponent_snapshot.get("roster", []) var upgraded: int = 0 var pierce_damage: float = 0.0 for entry: Dictionary in public_roster: var definition: Dictionary = DataCatalog.get_unit(str(entry.get("type_id", ""))) if definition.is_empty(): continue if str(definition["upgrade_id"]).is_empty(): upgraded += 1 if int(definition["attack_type"]) == 1: pierce_damage += float(definition["damage"]) for _step: int in 8: var own: Dictionary = EconomyManager.get_team(team) var reserved: int = EconomyManager.reserved_sparks(team) var available: int = mini(int(own["sparks"]) - reserved, EconomyManager.send_budget(wave) - reserved) if own["send_queue"].size() >= 8 or available < 20: break var send_id: String = "s01" if wave >= 11 and available >= 200 and _count_types(public_roster, ["g10"]) > 0: send_id = "s05" elif wave >= 6 and available >= 130 and _front_count(public_roster) >= 3: send_id = "s04" elif available >= 80 and upgraded >= 2 and public_roster.size() <= 6: send_id = "s03" elif available >= 40 and pierce_damage < 40.0: send_id = "s02" if not _order(team, {"type": "queue_send", "type_id": send_id}): break func _dominant_wave_armor(wave: int) -> int: var armor_weight: Array[float] = [0.0, 0.0, 0.0] for enemy: Dictionary in DataCatalog.get_attackers(wave): armor_weight[int(enemy["armor_type"])] += float(enemy["max_hp"]) var result: int = 0 for index: int in range(1, 3): if armor_weight[index] > armor_weight[result]: result = index return result func _front_count(roster: Array) -> int: return _count_types(roster, ["g01", "g02", "g03", "g04"]) func _count_types(roster: Array, ids: Array) -> int: var count: int = 0 for entry: Dictionary in roster: var id: String = str(entry.get("type_id", "")) if id.begins_with("w"): id = "g" + id.substr(1) if ids.has(id): count += 1 return count func _order(team: int, command: Dictionary) -> bool: if command.has("type_id"): command["type_id"] = DataCatalog.for_team(str(command["type_id"]), team) return bool(EconomyManager.apply_command(team, command)["ok"])