Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
8.4 KiB
GDScript
213 lines
8.4 KiB
GDScript
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"])
|