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>
164 lines
7.4 KiB
GDScript
164 lines
7.4 KiB
GDScript
extends Node
|
|
## Headless balance sweep over complete AI-vs-AI matches.
|
|
## Run: godot --headless --path . res://tools/balance_sim.tscn
|
|
## Mirrors the WaveManager flow with both seats driven by SimpleAI levels;
|
|
## stepping, economy and combat are the production implementations.
|
|
|
|
const AIScript = preload("res://scripts/ai/simple_ai.gd")
|
|
const BattleScript = preload("res://scripts/combat/battle_controller.gd")
|
|
const STEP: float = 1.0 / 60.0
|
|
const LEVEL_NAMES: Array[String] = ["Lernmodus", "Standard"]
|
|
|
|
var violations: Array[String] = []
|
|
var _public_snapshots: Array = [{}, {}]
|
|
|
|
|
|
func _ready() -> void:
|
|
WaveManager.set_physics_process(false)
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var started := Time.get_ticks_msec()
|
|
var lines: Array[String] = []
|
|
lines.append("# Saumwacht · Balance-Sweep")
|
|
lines.append("")
|
|
lines.append("Erzeugt von `tools/balance_sim.gd`; KI-Partien mit der aktuellen")
|
|
lines.append("Wirtschaft und Kampfsimulation (fester 1/60-Schritt, feste Startzustände).")
|
|
for matchup: Array in [
|
|
["Standard gegen Standard", GameState.AI_STANDARD, GameState.AI_STANDARD],
|
|
["Lernmodus gegen Standard", GameState.AI_LEARNING, GameState.AI_STANDARD],
|
|
]:
|
|
lines.append_array(_play_match(matchup[0], matchup[1], matchup[2]))
|
|
lines.append("")
|
|
lines.append("Keine Verletzung der im Sweep geprüften Invarianten."
|
|
if violations.is_empty() else
|
|
"VERLETZUNGEN: " + "; ".join(violations))
|
|
print("\n".join(lines))
|
|
print("Balance-Sweep fertig in %.1f s." % ((Time.get_ticks_msec() - started) / 1000.0))
|
|
get_tree().quit(0 if violations.is_empty() else 1)
|
|
|
|
|
|
func _play_match(title: String, level0: int, level1: int) -> Array[String]:
|
|
GameState.reset_match()
|
|
EconomyManager.reset_match()
|
|
_public_snapshots = [EconomyManager.get_team(0), EconomyManager.get_team(1)]
|
|
GameState.wave = 1
|
|
var battle = BattleScript.new()
|
|
battle.render_enabled = false
|
|
add_child(battle)
|
|
var ai = AIScript.new()
|
|
var lines: Array[String] = []
|
|
lines.append("")
|
|
lines.append("## %s" % title)
|
|
lines.append("")
|
|
lines.append("| Welle | Dauer s | Lecks 0/1 | Kernschaden 0/1 | Abschussprämien 0/1 | Goldbestand 0/1 | Wächter 0/1 | Abgerechnet |")
|
|
lines.append("| ---: | ---: | ---: | ---: | ---: | ---: | ---: | :---: |")
|
|
var winner: int = GameState.ONGOING
|
|
var total_leaks: Array[int] = [0, 0]
|
|
var total_seconds: float = 0.0
|
|
while GameState.wave <= DataCatalog.wave_count():
|
|
var wave: int = GameState.wave
|
|
var report: Dictionary = _play_wave(battle, ai, wave, level0, level1)
|
|
total_leaks[0] += int(report.leaks[0])
|
|
total_leaks[1] += int(report.leaks[1])
|
|
total_seconds += float(report.combat_seconds)
|
|
_check_state(wave)
|
|
lines.append("| %d | %.1f | %d/%d | %d/%d | %d/%d | %d/%d | %d/%d | %s |" % [
|
|
wave, float(report.combat_seconds),
|
|
report.leaks[0], report.leaks[1],
|
|
report.core_damage[0], report.core_damage[1],
|
|
report.bounty[0], report.bounty[1],
|
|
EconomyManager.get_team(0).gold, EconomyManager.get_team(1).gold,
|
|
EconomyManager.get_team(0).roster.size(), EconomyManager.get_team(1).roster.size(),
|
|
"Ja" if report.settled else "Nein"])
|
|
winner = GameState.decide_winner(wave == DataCatalog.wave_count())
|
|
if winner != GameState.ONGOING:
|
|
break
|
|
GameState.wave += 1
|
|
var outcome: String
|
|
match winner:
|
|
GameState.DRAW: outcome = "Unentschieden"
|
|
0: outcome = "Front 0 gewinnt (%s)" % LEVEL_NAMES[level0]
|
|
1: outcome = "Front 1 gewinnt (%s)" % LEVEL_NAMES[level1]
|
|
_:
|
|
outcome = "Unbeendet"
|
|
violations.append("%s: kein Matchende innerhalb der vorgesehenen Wellen." % title)
|
|
lines.append("")
|
|
lines.append("**Ergebnis nach %d Wellen:** %s · Kern %d/%d HP · Lecks gesamt %d/%d · mittlere Kampfdauer %.1f s" % [
|
|
GameState.wave, outcome, GameState.core_hp[0], GameState.core_hp[1],
|
|
total_leaks[0], total_leaks[1], total_seconds / float(maxi(1, GameState.wave))])
|
|
battle.free()
|
|
return lines
|
|
|
|
|
|
func _play_wave(battle, ai, wave: int, level0: int, level1: int) -> Dictionary:
|
|
EconomyManager.open_build(wave)
|
|
ai.plan(0, wave, _public_snapshots[1], GameState.last_reports[0], level0)
|
|
ai.plan(1, wave, _public_snapshots[0], GameState.last_reports[1], level1)
|
|
_public_snapshots = [EconomyManager.get_team(0), EconomyManager.get_team(1)]
|
|
_check_build_orders(wave)
|
|
var outgoing: Array = EconomyManager.freeze(wave)
|
|
var attackers: Array = [DataCatalog.get_attackers(wave), DataCatalog.get_attackers(wave)]
|
|
var natural_count: int = attackers[0].size()
|
|
for sender: int in range(2):
|
|
for send_id: String in outgoing[sender]:
|
|
attackers[1 - sender].append(DataCatalog.get_send(send_id, wave))
|
|
battle.begin_battle([EconomyManager.get_team(0).roster, EconomyManager.get_team(1).roster],
|
|
attackers, wave)
|
|
var report: Dictionary = {}
|
|
var wave_core_damage: Array[int] = [0, 0]
|
|
while true:
|
|
report = battle.step(STEP)
|
|
if report.leak_damage[0] > 0 or report.leak_damage[1] > 0:
|
|
for team: int in range(2):
|
|
wave_core_damage[team] += int(report.leak_damage[team])
|
|
GameState.apply_core_damage(report.leak_damage)
|
|
if GameState.decide_winner(false) != GameState.ONGOING or bool(report.finished):
|
|
break
|
|
# WaveManager checks core death before settlement, including the final step.
|
|
var did_settle: bool = GameState.decide_winner(false) == GameState.ONGOING and bool(report.finished)
|
|
if did_settle:
|
|
var reports: Array = []
|
|
for team: int in range(2):
|
|
var settled: Dictionary = EconomyManager.settle(team, wave, int(report.bounties[team]))
|
|
settled["leaks"] = report.leaks[team]
|
|
settled["core_damage"] = wave_core_damage[team]
|
|
settled["combat_seconds"] = report.elapsed
|
|
settled["natural_count"] = natural_count
|
|
settled["natural_leaks"] = report.natural_leaks[team]
|
|
settled["front_lifetime"] = report.front_lifetime[team]
|
|
reports.append(settled)
|
|
GameState.last_reports = reports
|
|
if float(report.elapsed) > 90.001:
|
|
violations.append("Welle %d: Kampf über 90 s." % wave)
|
|
return {
|
|
"leaks": report.leaks, "core_damage": wave_core_damage,
|
|
"bounty": report.bounties, "combat_seconds": report.elapsed, "settled": did_settle}
|
|
|
|
|
|
func _check_build_orders(wave: int) -> void:
|
|
for team: int in range(2):
|
|
var own: Dictionary = EconomyManager.get_team(team)
|
|
var reserved: int = EconomyManager.reserved_sparks(team)
|
|
if own.send_queue.size() > EconomyManager.MAX_SENDS or reserved > EconomyManager.send_budget(wave):
|
|
violations.append("Welle %d: Team %d überschreitet ein Sendlimit." % [wave, team])
|
|
if reserved > int(own.sparks):
|
|
violations.append("Welle %d: Team %d reserviert mehr Funken als vorhanden." % [wave, team])
|
|
|
|
|
|
func _check_state(wave: int) -> void:
|
|
for team: int in range(2):
|
|
var own: Dictionary = EconomyManager.get_team(team)
|
|
if int(own.gold) < 0 or int(own.sparks) < 0:
|
|
violations.append("Welle %d: Team %d mit negativer Ressource." % [wave, team])
|
|
if own.roster.size() > EconomyManager.GRID_SIZE.x * EconomyManager.GRID_SIZE.y:
|
|
violations.append("Welle %d: Team %d überfüllt das Baufeld." % [wave, team])
|
|
var occupied: Array[Vector2i] = []
|
|
for entry: Dictionary in own.roster:
|
|
var cell: Vector2i = entry.cell
|
|
if occupied.has(cell) or cell.x < 0 or cell.y < 0 or cell.x >= EconomyManager.GRID_SIZE.x or cell.y >= EconomyManager.GRID_SIZE.y:
|
|
violations.append("Welle %d: Team %d hat eine ungültige Zellbelegung." % [wave, team])
|
|
occupied.append(cell)
|
|
if int(own.workers) + int(own.pending_workers) > EconomyManager.MAX_WORKERS or int(own.sparks) > EconomyManager.SPARK_CAP:
|
|
violations.append("Welle %d: Team %d überschreitet eine Ressourcengrenze." % [wave, team])
|