This commit is contained in:
team3
2026-09-16 12:09:33 +02:00
parent 9410d18ea5
commit 8a188c5d5c
29 changed files with 2016 additions and 564 deletions

View File

@@ -28,6 +28,17 @@ func _initialize() -> void:
test_prices()
test_pool_pressure()
test_neutral_chance()
test_level_xp()
test_damage_scaling()
test_heal_range()
test_combat_hits()
test_plan_timer()
test_bots_reach_three_star()
test_shop_lock()
test_element_profiles()
test_pairing_known_early()
test_recap()
test_bot_arrange()
print("")
if failures == 0:
print("ALLE TESTS BESTANDEN")
@@ -58,7 +69,7 @@ func test_upgrade_chain() -> void:
p.gold = 100
# Beim Kauf gewählter Feuer-Avatar wird aufgestellt.
p.bench[0] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
var ok := p.place(0, Vector2i(3, 1), 10)
var ok := p.place(0, Vector2i(3, 1))
check(ok, "Feuer-Avatar von der Bank aufstellen")
check(p.unlocked[Data.Element.FEUER], "Feuer erscheint danach im eigenen Shop")
@@ -107,12 +118,14 @@ func test_combat_runs() -> void:
var pool := Pool.new()
var a := PlayerState.new(0, "A", false)
var b := PlayerState.new(1, "B", true)
a.level = 4
b.level = 4
for i in 4:
a.bench[i] = Avatar.new(i % 3, [Data.Element.FEUER] as Array[int], 1)
a.place(i, Vector2i(2 + i, i % 2), 20)
a.place(i, Vector2i(2 + i, i % 2))
for i in 4:
b.bench[i] = Avatar.new(i % 3, [Data.Element.EIS] as Array[int], 1)
b.place(i, Vector2i(2 + i, i % 2), 20)
b.place(i, Vector2i(2 + i, i % 2))
var c := Combat.new(a, b, 12345)
check(c.units.size() == 8, "8 Einheiten in der Arena")
var team0 := 0
@@ -149,7 +162,7 @@ func test_full_game() -> void:
ai.setup(g.rand)
var guard := 0
while g.phase != Game.Phase.GAMEOVER and guard < 200:
ai.take_turn(g.human, g.pool, g.rand, g.round_no)
ai.take_turn(g.human, g.pool, g.rand)
g.start_combat()
if g.phase == Game.Phase.COMBAT:
g.skip_combat()
@@ -184,7 +197,7 @@ func test_placements() -> void:
var guard := 0
while g.phase != Game.Phase.GAMEOVER and guard < 200:
if g.human.alive:
ai.take_turn(g.human, g.pool, g.rand, g.round_no)
ai.take_turn(g.human, g.pool, g.rand)
g.start_combat()
if g.phase == Game.Phase.COMBAT:
g.skip_combat()
@@ -265,7 +278,7 @@ func test_buy_specialisation() -> void:
"der Kauf zieht einen Blitz aus dem geteilten Vorrat (%d -> %d)"
% [before, pool.element_left[Data.Element.BLITZ]])
check(not p.unlocked[Data.Element.BLITZ], "gekauft allein schaltet den Shop noch nicht frei")
p.place(0, Vector2i(3, 1), 10)
p.place(0, Vector2i(3, 1))
check(p.unlocked[Data.Element.BLITZ], "erst das Aufstellen schaltet Blitz im Shop frei")
@@ -304,31 +317,31 @@ func test_move_slot() -> void:
var h1 := {"kind": "board", "hex": Vector2i(2, 1)}
var h2 := {"kind": "board", "hex": Vector2i(4, 2)}
check(p.move_slot(b0, h1, 10), "Bank -> Board")
check(p.move_slot(b0, h1), "Bank -> Board")
check(p.board.get(Vector2i(2, 1)) == f and p.bench[0] == null, "Feuer steht auf (2,1)")
check(p.unlocked[Data.Element.FEUER], "Aufstellen schaltet Feuer frei")
check(p.move_slot(h1, h2, 10), "Board -> Board")
check(p.move_slot(h1, h2), "Board -> Board")
check(p.board.get(Vector2i(4, 2)) == f and not p.board.has(Vector2i(2, 1)), "Feuer steht auf (4,2)")
check(p.move_slot(b1, h2, 10), "Bank auf ein belegtes Feld = Tausch")
check(p.move_slot(b1, h2), "Bank auf ein belegtes Feld = Tausch")
check(p.board.get(Vector2i(4, 2)) == w and p.bench[1] == f, "Wind steht, Feuer liegt auf der Bank")
check(p.move_slot(h2, b3, 10), "Board -> Bank")
check(p.move_slot(h2, b3), "Board -> Bank")
check(p.bench[3] == w and p.board.is_empty(), "Wind liegt wieder auf der Bank")
check(p.move_slot(b3, b1, 10), "Bank -> Bank ist ein Tausch")
check(p.move_slot(b3, b1), "Bank -> Bank ist ein Tausch")
check(p.bench[1] == w and p.bench[3] == f, "beide haben die Plätze getauscht")
# Kaderplätze bleiben begrenzt.
# Kaderplätze bleiben durch das Level begrenzt.
for i in 5:
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.EIS] as Array[int], 1)
var placed := 0
for i in 5:
if p.move_slot({"kind": "bench", "index": i}, {"kind": "board", "hex": Vector2i(i, 0)}, 1):
if p.move_slot({"kind": "bench", "index": i}, {"kind": "board", "hex": Vector2i(i, 0)}):
placed += 1
check(placed == 3 and p.board.size() == 3, "Runde 1 lässt nur 3 Einheiten zu (%d)" % placed)
check(not p.move_slot({"kind": "board", "hex": Vector2i(9, 9)}, b0, 10),
check(placed == 3 and p.board.size() == 3, "Level 3 lässt nur 3 Einheiten zu (%d)" % placed)
check(not p.move_slot({"kind": "board", "hex": Vector2i(9, 9)}, b0),
"Feld ausserhalb des Boards wird abgelehnt")
@@ -386,7 +399,7 @@ func test_pool_pressure() -> void:
var guard := 0
while g.phase != Game.Phase.GAMEOVER and guard < 200:
if g.human.alive:
ai.take_turn(g.human, g.pool, g.rand, g.round_no)
ai.take_turn(g.human, g.pool, g.rand)
g.start_combat()
if g.phase == Game.Phase.COMBAT:
g.skip_combat()
@@ -440,3 +453,254 @@ func test_neutral_chance() -> void:
% [i, quotes[i], Data.neutral_chance(i)])
return
check(true, "jede Quote trifft den erwarteten Wert auf 5 Prozentpunkte genau")
func test_level_xp() -> void:
print("[Level und XP]")
var p := PlayerState.new(0, "Test", false)
check(p.level == 3 and p.team_slots() == 3, "Start auf Level 3 = 3 Kaderplätze")
p.gold = 3
check(not p.buy_xp(), "ohne 4 Gold kein XP-Kauf")
p.gold = 8
check(p.buy_xp() and p.level == 4 and p.gold == 4, "4 Gold -> 4 XP -> Level 4")
check(p.buy_xp() and p.level == 4 and p.xp == 4, "weitere 4 XP reichen für Level 5 noch nicht (4/8)")
p.gain_xp(4)
check(p.level == 5 and p.xp == 0, "Rundenerfahrung füllt auf Level 5")
p.gain_xp(1000)
check(p.level == Data.MAX_LEVEL and p.xp == 0, "Level ist bei %d gedeckelt" % Data.MAX_LEVEL)
check(not p.can_buy_xp(), "auf Maximalstufe kein XP-Kauf mehr")
var g := Game.new()
g.start(11)
var lvl := g.human.level
g.start_combat()
g.skip_combat()
check(g.human.xp == Data.XP_PER_ROUND or g.human.level > lvl,
"jede Runde gibt %d XP" % Data.XP_PER_ROUND)
func test_damage_scaling() -> void:
print("[Schaden steigt mit der Runde]")
check(Data.round_base_damage(1) == 2, "Runde 1: Grundschaden 2")
check(Data.round_base_damage(9) == 4, "Runde 9: Grundschaden 4")
check(Data.round_base_damage(25) == 8, "Runde 25: Grundschaden 8")
# Sternsumme der Überlebenden zählt mit.
var pool := Pool.new()
var strong := PlayerState.new(0, "A", false)
for i in 4:
strong.board[Vector2i(i, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 3)
var weak := PlayerState.new(1, "B", false)
weak.board[Vector2i(3, 1)] = Avatar.new(Data.Role.HEILER, [Data.Element.EIS] as Array[int], 1)
var c := Combat.new(strong, weak, 5)
c.run_to_end()
check(c.winner == 0, "das starke Team gewinnt")
check(c.survivor_stars >= 3 * c.survivors and c.survivors > 0,
"Sternsumme der Überlebenden = %d bei %d Einheiten" % [c.survivor_stars, c.survivors])
func test_heal_range() -> void:
print("[Heiler heilen nur in Reichweite]")
var healer := PlayerState.new(0, "A", false)
healer.board[Vector2i(0, 3)] = Avatar.new(Data.Role.HEILER, [Data.Element.WASSER] as Array[int], 1)
healer.board[Vector2i(6, 0)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
var foe := PlayerState.new(1, "B", false)
foe.board[Vector2i(6, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.BLITZ] as Array[int], 1)
var c := Combat.new(healer, foe, 3)
var h: Combat.CUnit = null
var tank: Combat.CUnit = null
for u in c.units:
if u.team == 0 and u.avatar.role == Data.Role.HEILER:
h = u
if u.team == 0 and u.avatar.role == Data.Role.TANK:
tank = u
check(Hex.distance(h.pos, tank.pos) > Data.HEAL_RANGE, "Tank steht außer Heilreichweite")
var list := c._allies_by_missing_hp(0, h)
check(list.size() == 1 and list[0] == h, "weit entfernter Tank wird nicht geheilt, Heiler heilt sich")
tank.pos = Vector2i(1, 7)
list = c._allies_by_missing_hp(0, h)
check(list.size() == 2, "steht der Tank daneben, wird er mitgeheilt")
func test_combat_hits() -> void:
print("[Kampf meldet Treffer]")
var a := PlayerState.new(0, "A", false)
var b := PlayerState.new(1, "B", false)
for i in 3:
a.board[Vector2i(2 + i, 0)] = Avatar.new(i, [Data.Element.FEUER] as Array[int], 1)
b.board[Vector2i(2 + i, 0)] = Avatar.new(i, [Data.Element.WIND] as Array[int], 1)
var silent := Combat.new(a, b, 8)
silent.run_to_end()
check(silent.hits.is_empty(), "Bot-Kämpfe zeichnen nichts auf")
var loud := Combat.new(a, b, 8)
loud.record_hits = true
for i in 300:
loud.step(0.05)
var kinds := {}
for h in loud.hits:
kinds[h["kind"]] = kinds.get(h["kind"], 0) + 1
check(kinds.get("dmg", 0) > 0, "Schadenstreffer werden gemeldet (%d)" % kinds.get("dmg", 0))
check(kinds.get("heal", 0) > 0, "Heilungen werden gemeldet (%d)" % kinds.get("heal", 0))
check(kinds.get("cast", 0) > 0, "Zauber werden gemeldet (%d)" % kinds.get("cast", 0))
func test_plan_timer() -> void:
print("[Planungsuhr]")
var g := Game.new()
g.start(21)
check(is_equal_approx(g.plan_left, Data.PLAN_SECONDS), "Runde startet mit %d s" % int(Data.PLAN_SECONDS))
check(not g.tick_planning(10.0) and g.phase == Game.Phase.PLANNING, "nach 10 s noch Planung")
check(g.tick_planning(Data.PLAN_SECONDS), "Ablauf startet den Kampf von selbst")
check(g.phase == Game.Phase.COMBAT, "Phase ist Kampf")
g.skip_combat()
check(is_equal_approx(g.plan_left, Data.PLAN_SECONDS), "neue Runde setzt die Uhr zurück")
g.timer_enabled = false
check(not g.tick_planning(999.0) and g.phase == Game.Phase.PLANNING, "abgeschaltete Uhr läuft nicht ab")
func test_bots_reach_three_star() -> void:
print("[Bots erreichen 3-Star]")
var three := 0
var games := 4
for run in games:
var g := Game.new()
g.start(7000 + run)
var ai := BotAI.new()
ai.setup(g.rand)
var guard := 0
var seen := {}
while g.phase != Game.Phase.GAMEOVER and guard < 200:
if g.human.alive:
ai.take_turn(g.human, g.pool, g.rand)
for p in g.players:
for u in p.board_units():
if u.star == 3:
seen[u.id] = true
g.start_combat()
if g.phase == Game.Phase.COMBAT:
g.skip_combat()
guard += 1
three += seen.size()
check(three > 0, "in %d Partien entstanden %d 3-Star-Avatare" % [games, three])
func test_shop_lock() -> void:
print("[Shop einfrieren]")
var g := Game.new()
g.start(31)
var p := g.human
var sig := []
for e in p.shop:
sig.append([e.role, e.element])
p.shop_locked = true
g.start_combat()
g.skip_combat()
var same := p.shop.size() == sig.size()
for i in p.shop.size():
if same and (p.shop[i].role != sig[i][0] or p.shop[i].element != sig[i][1]):
same = false
check(same, "eingefrorener Shop bleibt über den Rundenwechsel gleich")
p.gold = 10
p.reroll(g.pool, g.rand)
check(not p.shop_locked, "Würfeln hebt das Einfrieren auf")
p.shop = [ShopEntry.new(0, -1)]
p.gold = 5
p.buy(0, g.pool, Data.Element.ERDE)
check(p.last_element == Data.Element.ERDE, "letzte Spezialisierung wird gemerkt")
func test_element_profiles() -> void:
print("[Elemente prägen die Werte]")
var feuer := Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1).base_stats()
var erde := Avatar.new(Data.Role.DPS, [Data.Element.ERDE] as Array[int], 1).base_stats()
var wind := Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1).base_stats()
var blitz := Avatar.new(Data.Role.DPS, [Data.Element.BLITZ] as Array[int], 1).base_stats()
check(feuer["atk"] > erde["atk"] * 1.2, "Feuer schlägt härter als Erde (%d vs %d)" % [int(feuer["atk"]), int(erde["atk"])])
check(erde["hp"] > feuer["hp"] * 1.25, "Erde hält mehr aus als Feuer (%d vs %d)" % [int(erde["hp"]), int(feuer["hp"])])
check(wind["aspd"] > erde["aspd"] * 1.25, "Wind ist schneller als Erde")
check(blitz["crit"] > 0.0 and feuer["crit"] == 0.0, "nur Blitz bringt eigene Krit-Chance")
var a := PlayerState.new(0, "A", false)
a.board[Vector2i(3, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.BLITZ] as Array[int], 1)
var b := PlayerState.new(1, "B", false)
b.board[Vector2i(3, 1)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
var c := Combat.new(a, b, 2)
var blitz_unit = null
for u in c.units:
if u.team == 0:
blitz_unit = u
check(blitz_unit.crit >= 0.15, "Krit-Profil kommt im Kampf an (%.2f)" % blitz_unit.crit)
func test_pairing_known_early() -> void:
print("[Gegner steht in der Planung fest]")
var g := Game.new()
g.start(41)
var opp := g.human_opponent()
check(opp != null and g.phase == Game.Phase.PLANNING, "in der Planung ist der Gegner bekannt: %s" % (opp.pname if opp else "-"))
g.start_combat()
var fought := g.human_opponent()
check(fought == opp or (fought != null and fought.pname.begins_with(opp.pname)),
"gekämpft wird gegen genau diesen Gegner")
g.skip_combat()
check(g.human_opponent() != null, "neue Runde, neuer Gegner bekannt")
# Jeder Bot kennt ebenfalls seinen Gegner.
var all_known := true
for p in g.alive_players():
if g.opponent_of(p) == null:
all_known = false
check(all_known, "jeder Spieler hat einen Gegner")
func test_recap() -> void:
print("[Rückblick]")
var g := Game.new()
g.start(51)
g.human.board[Vector2i(3, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 2)
g.human.board[Vector2i(2, 0)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
g.start_combat()
g.skip_combat()
var r := g.human_recap
check(not r.is_empty(), "Rückblick liegt vor")
check(r["total"] == 2 and r["alive"] <= 2, "zählt eigene Einheiten (%d, %d überlebt)" % [r["total"], r["alive"]])
check(r["opponent"] != "" and r.has("result") and r.has("damage"), "nennt Gegner, Ergebnis und Schaden")
check(r["best_dealt"] > 0 or r["best_healed"] > 0, "stärkste Einheit hat etwas beigetragen (%s)" % r["best_name"])
func test_bot_arrange() -> void:
print("[Bots stellen gegen den Gegner auf]")
var ai := BotAI.new()
var rng := RandomNumberGenerator.new()
rng.seed = 3
ai.setup(rng)
var me := PlayerState.new(1, "Bot", true)
me.level = 6
for i in 2:
me.board[Vector2i(i, 3)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
for i in 3:
me.board[Vector2i(2 + i, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
var opp := PlayerState.new(2, "Gegner", true)
# Gegnerische Schützen links (aus seiner Sicht), also rechts aus unserer.
for i in 3:
opp.board[Vector2i(0, i)] = Avatar.new(Data.Role.DPS, [Data.Element.BLITZ] as Array[int], 1)
opp.board[Vector2i(6, 0)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
ai.arrange(me, opp)
check(me.board.size() == 5, "alle Einheiten stehen wieder (%d)" % me.board.size())
var tanks_front := true
var dps_back := true
var tank_cols := []
var dps_cols := []
for hex in me.board:
var u: Avatar = me.board[hex]
if u.role == Data.Role.TANK:
tanks_front = tanks_front and hex.y == 0
tank_cols.append(hex.x)
else:
dps_back = dps_back and hex.y >= 2
dps_cols.append(hex.x)
check(tanks_front, "Tanks stehen vorn")
check(dps_back, "Schützen stehen hinten")
check(tank_cols.has(6), "ein Tank steht vor den gegnerischen Schützen (Spalte 6)")
var avg := 0.0
for c in dps_cols:
avg += c
avg /= dps_cols.size()
check(avg > 3.0, "Schützen meiden die Seite des gegnerischen Tanks (Mittel %.1f)" % avg)