saumwacht-godot/tools/luneraria/boss_scenarios.gd
Martin Stickelbroeck f53859a298 M2: Helden, Items, Talente, Angriffschampions und Bossgrotte
Drei Helden mit Nähe-/Teilnahme-XP, Talenten und drei Itemplätzen,
drei Angriffschampions mit Buchung n -> n+1, Bossgrotte mit Streitfall,
ruhendem Boss, idempotenter Eroberung, einmaliger Entsendung für das
Siegerteam, hohem Kopfgeld und Sperre bis Sieg+4. HUD-Reiter, Porträt
aus echtem Modell, Grotten-Asset, Bots für alle Systeme. Regeln M2 73/73,
M1 91/91, App M2 15/15 und M1 38/38, Bossszenarien dokumentiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:33:11 +02:00

108 lines
4.9 KiB
GDScript

extends Node
## Boss strategy comparison from one identical mid-game state (seed replay to round 7).
## Variants in round 8 for Blau (and Rot for the contest); rounds 8-11 are then played by
## the ordinary bots without the boss-hunting profile, so only the scenario differs.
## Run: godot --headless --path . res://tools/luneraria/boss_scenarios.tscn -- --seed=3
const SCENARIOS: Array[String] = ["kein_versuch", "gescheitert", "erfolgreich", "umkaempft", "ueberinvestition"]
func _ready() -> void:
call_deferred("run")
func profile(state: LuneMatch, slot: int) -> String:
var name: String = LuneBot.profile_for(state, slot)
return "ausgewogen" if name == "bossjagd" else name
func replay(seed: int) -> LuneMatch:
var state := LuneMatch.new()
state.setup({"seed": seed, "human_slot": -1})
while state.round_number < 8 and state.phase != LuneMatch.Phase.OVER:
for slot: int in range(6):
LuneBot.plan(state, slot, profile(state, slot))
state.begin_combat()
state.finish_combat()
return state
func send(state: LuneMatch, slot: int, count: int, strongest: bool, hero: bool) -> int:
var roster: Array = (state.players[slot].draft.roster as Array).duplicate()
roster.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return int(a.seq) < int(b.seq) if int(a.invested) == int(b.invested) else (int(a.invested) > int(b.invested)) == strongest)
var value: int = 0
for index: int in range(mini(count, roster.size())):
if state.submit(slot, {"type": "assign", "uid": roster[index].uid, "lane": "grotto"}).ok:
value += int(roster[index].invested)
var champion: Dictionary = state.players[slot].draft.hero
if hero and not champion.is_empty():
state.submit(slot, {"type": "assign", "uid": champion.uid, "lane": "grotto"})
return value
func building_hp(state: LuneMatch, team: int) -> float:
var total: float = 0.0
for id: String in state.building_hp:
if id.begins_with("blau" if team == 0 else "rot"):
total += float(state.building_hp[id])
return total
func team_gold(state: LuneMatch, team: int) -> int:
var total: int = 0
for slot: int in range(team * 3, team * 3 + 3):
total += int(state.players[slot].gold)
return total
func run() -> void:
var seed: int = 3
for arg: String in OS.get_cmdline_user_args():
if arg.begins_with("--seed="):
seed = int(arg.substr(7))
var results: Array = []
for scenario: String in SCENARIOS:
var state: LuneMatch = replay(seed)
var start := {"blau_hp": building_hp(state, 0), "rot_hp": building_hp(state, 1), "blau_gold": team_gold(state, 0), "rot_gold": team_gold(state, 1)}
var row := {"scenario": scenario, "sent_value": 0, "rot_sent_value": 0}
for slot: int in range(6):
LuneBot.plan(state, slot, profile(state, slot))
match scenario:
"gescheitert": row.sent_value = send(state, 1, 3, false, false)
"erfolgreich": row.sent_value = send(state, 1, 8, true, true)
"umkaempft":
row.sent_value = send(state, 1, 8, true, true)
row.rot_sent_value = send(state, 4, 8, true, true)
"ueberinvestition":
row.sent_value = send(state, 1, 12, true, true)
row.sent_value += send(state, 0, 4, true, false) + send(state, 2, 4, true, false)
var blau_mid_before: int = state.towers_alive(0, "mid")
state.begin_combat()
state.finish_combat()
row.grotto = str(state.last_settlement.boss)
row.claim_team = int(state.boss.claim.team) if not (state.boss.claim as Dictionary).is_empty() else -1
row.blau_mid_towers_lost_r8 = blau_mid_before - state.towers_alive(0, "mid")
row.blau_hp_after_r8 = building_hp(state, 0) - float(start.blau_hp)
var rot_hp_before_r9: float = building_hp(state, 1)
var boss_bounty_rot: int = 0
for extra: int in range(3):
if state.phase == LuneMatch.Phase.OVER:
break
for slot: int in range(6):
LuneBot.plan(state, slot, profile(state, slot))
state.begin_combat()
var deployed: bool = state.combat.units.any(func(u: LuneUnit) -> bool: return u.kind == "boss" and u.zone == "lane")
state.finish_combat()
if deployed:
row.boss_round = state.combat.round_number
row.rot_hp_lost_boss_round = rot_hp_before_r9 - building_hp(state, 1)
for event: Dictionary in state.combat.events:
if event.kind == "boss" and event.zone == "lane":
boss_bounty_rot = int(event.bounty)
row.boss_killed = boss_bounty_rot > 0
row.boss_bounty_to_rot = boss_bounty_rot
row.blau_hp_delta = building_hp(state, 0) - float(start.blau_hp)
row.rot_hp_delta = building_hp(state, 1) - float(start.rot_hp)
row.blau_gold_delta = team_gold(state, 0) - int(start.blau_gold)
row.rot_gold_delta = team_gold(state, 1) - int(start.rot_gold)
row.phase = "vorbei" if state.phase == LuneMatch.Phase.OVER else "läuft"
row.winner = state.winner
results.append(row)
print("SCENARIO ", JSON.stringify(row))
var file := FileAccess.open("res://docs/luneraria/logs/m2/boss-scenarios-seed%d.json" % seed, FileAccess.WRITE)
file.store_string(JSON.stringify(results, "\t"))
file.close()
get_tree().quit()