Bots planen Items/Champions vor der Armee und helfen deterministisch, damit Spiegelmatches fair bleiben. Boss und Profile neu abgestimmt; Balance-, Stress- und Volks-Werkzeuge sowie Kartenprops ergänzt.
99 lines
5.6 KiB
GDScript
99 lines
5.6 KiB
GDScript
extends Node
|
|
## Stress tests, separate from balance: (1) very high wave indices must stay finite and
|
|
## capped; (2) a long mirror match must not grow memory or per-round cost. The QA round
|
|
## limit ends the run as "Testlimit erreicht / Partie nicht abgeschlossen".
|
|
## Run: godot --headless --path . res://tools/luneraria/stress_run.tscn
|
|
var checks: int = 0
|
|
var failures: Array[String] = []
|
|
|
|
func check(ok: bool, label: String) -> void:
|
|
checks += 1
|
|
if not ok:
|
|
failures.append(label)
|
|
printerr("STRESS_FAIL: ", label)
|
|
|
|
func _ready() -> void:
|
|
call_deferred("run")
|
|
|
|
func run() -> void:
|
|
var report := {"high_index": [], "long_run": {}}
|
|
for index: int in [100, 1000, 10000, 1000000]:
|
|
var state := LuneMatch.new()
|
|
state.setup({"seed": 5, "human_slot": -1})
|
|
state.round_number = index
|
|
for slot: int in range(6):
|
|
state.players[slot].draft.gold = 5000
|
|
LuneBot.plan(state, slot, "ausgewogen")
|
|
var started: int = Time.get_ticks_msec()
|
|
state.begin_combat()
|
|
var peak: int = state.combat.mobile_count()
|
|
var finite: bool = true
|
|
var steps: int = 0
|
|
while state.phase == LuneMatch.Phase.COMBAT:
|
|
state.step_combat()
|
|
steps += 1
|
|
if state.phase == LuneMatch.Phase.COMBAT:
|
|
peak = maxi(peak, state.combat.mobile_count())
|
|
if steps % 50 == 0:
|
|
for unit: LuneUnit in state.combat.units:
|
|
finite = finite and is_finite(unit.hp) and is_finite(unit.position.x) and is_finite(unit.position.y)
|
|
var cap: int = 6 * state.balance.wave_max_count + 6 * (state.balance.supply_cap + 1) + 6 * LuneContent.MAX_BOOKINGS + 2
|
|
check(finite, "wave %d: all values finite" % index)
|
|
check(peak <= cap, "wave %d: at most %d mobile units (peak %d)" % [index, cap, peak])
|
|
check(steps <= int(state.balance.combat_time_limit / state.balance.sim_step) + 1, "wave %d: combat phase terminates" % index)
|
|
var row := {"wave": index, "peak_mobile": peak, "steps": steps, "wall_ms": Time.get_ticks_msec() - started,
|
|
"end": state.last_settlement.get("end_reason", ""), "phase": "vorbei" if state.phase == LuneMatch.Phase.OVER else "läuft",
|
|
"monster_hp": LuneWaves.build(state.balance, index)[0].max_hp}
|
|
report.high_index.append(row)
|
|
print("STRESS_WAVE ", JSON.stringify(row))
|
|
# Long run over the whole loop (plan -> combat -> settlement). A normal mirror match ends
|
|
# in a draw after 7-14 waves, so this run uses a documented test fixture: before every
|
|
# round all towers are restored and both Ancients get 10^12 HP. It measures memory,
|
|
# object count and cost per round, not balance; the QA limit then ends the run as
|
|
# "Testlimit erreicht / Partie nicht abgeschlossen".
|
|
var state := LuneMatch.new()
|
|
state.setup({"seed": 3, "human_slot": -1, "bunds": ["wurzelbund", "wurzelbund", "wurzelbund", "wurzelbund", "wurzelbund", "wurzelbund"]})
|
|
var limit: int = 200
|
|
var round_ms: Array[float] = []
|
|
var memory: Array[float] = [Performance.get_monitor(Performance.MEMORY_STATIC) / 1048576.0]
|
|
var objects: Array[float] = [Performance.get_monitor(Performance.OBJECT_COUNT)]
|
|
for definition: Dictionary in state.map.buildings:
|
|
if definition.tier == "core":
|
|
definition.max_hp = 1.0e12
|
|
while state.phase != LuneMatch.Phase.OVER and state.round_number <= limit:
|
|
for definition: Dictionary in state.map.buildings:
|
|
state.building_hp[definition.id] = float(definition.max_hp)
|
|
var started: int = Time.get_ticks_usec()
|
|
for slot: int in range(6):
|
|
LuneBot.plan(state, slot, "verteidigung")
|
|
state.begin_combat()
|
|
state.finish_combat()
|
|
round_ms.append((Time.get_ticks_usec() - started) / 1000.0)
|
|
if round_ms.size() % 25 == 0:
|
|
memory.append(Performance.get_monitor(Performance.MEMORY_STATIC) / 1048576.0)
|
|
objects.append(Performance.get_monitor(Performance.OBJECT_COUNT))
|
|
print("STRESS_PROGRESS round %d · %.0f ms · %.1f MB · %d objects" % [state.round_number, round_ms[-1], memory[-1], objects[-1]])
|
|
var result: String = "Testlimit erreicht / Partie nicht abgeschlossen" if state.phase != LuneMatch.Phase.OVER else ("Unentschieden" if state.winner == LuneMatch.DRAW else "Sieg %s" % ("Blau" if state.winner == 0 else "Rot"))
|
|
var early: float = 0.0
|
|
var late: float = 0.0
|
|
var window: int = mini(10, round_ms.size())
|
|
for i: int in range(window):
|
|
early += round_ms[i] / float(window)
|
|
late += round_ms[round_ms.size() - 1 - i] / float(window)
|
|
check(round_ms.size() >= limit, "long run reaches the QA limit of %d rounds (%d played)" % [limit, round_ms.size()])
|
|
check(objects[-1] <= objects[0] * 1.05 + 100, "no object growth over %d rounds (%s)" % [round_ms.size(), str(objects)])
|
|
check(memory[-1] <= memory[0] * 1.10 + 8.0, "no memory growth over %d rounds (%s MB)" % [round_ms.size(), str(memory)])
|
|
check(result != "Sieg Blau" and result != "Sieg Rot", "mirror match never produces a one-sided winner")
|
|
var finite_gold: bool = state.players.all(func(p: Dictionary) -> bool: return int(p.gold) >= 0 and int(p.gold) < 1 << 40)
|
|
check(finite_gold, "gold stays a valid integer")
|
|
report.long_run = {"rounds": round_ms.size(), "result": result, "round_ms_first10": early, "round_ms_last10": late,
|
|
"memory_mb_every25": memory, "objects_every25": objects, "final_gold": state.players.map(func(p: Dictionary) -> int: return int(p.gold)),
|
|
"final_wave_monster_hp": LuneWaves.build(state.balance, state.round_number)[0].max_hp}
|
|
print("STRESS_LONG ", JSON.stringify(report.long_run))
|
|
report.checks = checks
|
|
report.failures = failures
|
|
var file := FileAccess.open("res://docs/luneraria/logs/m3/stress.json", FileAccess.WRITE)
|
|
file.store_string(JSON.stringify(report, "\t"))
|
|
file.close()
|
|
print("STRESS_%s: %d checks; %d failures" % ["PASS" if failures.is_empty() else "FAIL", checks, failures.size()])
|
|
get_tree().quit(0 if failures.is_empty() else 1)
|