This commit is contained in:
team3
2026-09-16 13:21:45 +02:00
parent 8a188c5d5c
commit 49e8f2d95e
27 changed files with 1772 additions and 265 deletions

View File

@@ -39,6 +39,16 @@ func _initialize() -> void:
test_pairing_known_early()
test_recap()
test_bot_arrange()
test_element_forms()
test_difficulty()
test_settings()
test_healer_arrange()
test_undo_sell()
test_pve_rounds()
test_stats()
test_save_load()
test_counters()
test_bot_counter_pick()
print("")
if failures == 0:
print("ALLE TESTS BESTANDEN")
@@ -704,3 +714,321 @@ func test_bot_arrange() -> void:
avg += c
avg /= dps_cols.size()
check(avg > 3.0, "Schützen meiden die Seite des gegnerischen Tanks (Mittel %.1f)" % avg)
func _duel(my_el: int, my_role: int, foes: Array) -> Combat:
var a := PlayerState.new(0, "A", false)
a.board[Vector2i(3, 0)] = Avatar.new(my_role, [my_el] as Array[int], 1)
var b := PlayerState.new(1, "B", false)
for i in foes.size():
b.board[foes[i]] = Avatar.new(Data.Role.TANK, [Data.Element.WIND] as Array[int], 1)
return Combat.new(a, b, 77)
func _unit(c: Combat, team: int, index: int = 0) -> Combat.CUnit:
var n := 0
for u in c.units:
if u.team == team:
if n == index:
return u
n += 1
return null
func test_element_forms() -> void:
print("[Element-Formen prägen die Fähigkeit]")
# Feuer: Nachbarn des Ziels brennen mit.
var c := _duel(Data.Element.FEUER, Data.Role.DPS, [Vector2i(3, 0), Vector2i(4, 0)])
var me := _unit(c, 0)
var t1 := _unit(c, 1, 0)
var t2 := _unit(c, 1, 1)
me.target = t1
c._cast(me)
check(t1.hp < t1.max_hp and t2.hp < t2.max_hp, "Flächenbrand trifft Ziel und Nachbar")
check(t2.burn_timer > 0.0, "Nachbar brennt")
# Erde: Gegner neben dem Ziel sind betäubt.
c = _duel(Data.Element.ERDE, Data.Role.HEILER, [Vector2i(3, 0), Vector2i(4, 0)])
me = _unit(c, 0)
t1 = _unit(c, 1, 0)
t2 = _unit(c, 1, 1)
me.target = t1
c._cast(me)
check(t1.stun_timer > 0.0 and t2.stun_timer > 0.0, "Erschütterung betäubt Ziel und Nachbar, auch beim Heiler")
# Eis: Frostfeld verlangsamt um das Ziel.
c = _duel(Data.Element.EIS, Data.Role.TANK, [Vector2i(3, 0), Vector2i(4, 0)])
me = _unit(c, 0)
t1 = _unit(c, 1, 0)
t2 = _unit(c, 1, 1)
me.target = t1
c._cast(me)
check(t1.slow >= 0.3 and t2.slow >= 0.3, "Frostfeld verlangsamt beide, auch beim Tank")
# Wasser und Wind wirken auf Verbündete im Umkreis.
var a := PlayerState.new(0, "A", false)
a.board[Vector2i(3, 1)] = Avatar.new(Data.Role.HEILER, [Data.Element.WASSER] as Array[int], 1)
a.board[Vector2i(2, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1)
var b := PlayerState.new(1, "B", false)
b.board[Vector2i(3, 0)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
c = Combat.new(a, b, 5)
var healer := _unit(c, 0, 0)
var ally := _unit(c, 0, 1)
if healer.avatar.role != Data.Role.HEILER:
var tmp := healer
healer = ally
ally = tmp
var mana0 := ally.mana
c._cast(healer)
check(ally.mana >= mana0 + 20.0 - 0.01, "Welle gibt dem Verbündeten 20 Mana")
c._cast(ally)
check(healer.haste >= 0.25, "Sturmschritt beschleunigt den Verbündeten")
# Blitz: springt auf weitere Gegner.
c = _duel(Data.Element.BLITZ, Data.Role.TANK, [Vector2i(3, 0), Vector2i(0, 3), Vector2i(6, 3)])
me = _unit(c, 0)
me.target = _unit(c, 1, 0)
c._cast(me)
var hurt := 0
for u in c.units:
if u.team == 1 and u.hp < u.max_hp:
hurt += 1
check(hurt >= 2, "Kette springt vom Tank auf zwei weitere Gegner (%d getroffen)" % hurt)
check(Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1).ability_text().contains("Form Flächenbrand"),
"Fähigkeitstext nennt die Form")
func test_difficulty() -> void:
print("[Schwierigkeit]")
var rng := RandomNumberGenerator.new()
var greed := {}
for d in 3:
rng.seed = 5
var sum := 0.0
for i in 30:
var ai := BotAI.new()
ai.setup(rng, d)
sum += ai.greed
greed[d] = sum / 30.0
check(greed[Data.Difficulty.LEICHT] > greed[Data.Difficulty.NORMAL] and greed[Data.Difficulty.NORMAL] > greed[Data.Difficulty.SCHWER],
"leicht hortet am meisten, schwer am wenigsten (%.2f / %.2f / %.2f)"
% [greed[0], greed[1], greed[2]])
var g := Game.new()
g.start(9, Data.Difficulty.SCHWER)
check(g.difficulty == Data.Difficulty.SCHWER and g.bots.values()[0].difficulty == Data.Difficulty.SCHWER,
"Spiel reicht die Schwierigkeit an die Bots durch")
func test_settings() -> void:
print("[Einstellungen speichern]")
var path := "user://settings_test.cfg"
var st := Settings.new()
st.volume = 0.35
st.difficulty = Data.Difficulty.SCHWER
st.save_to_disk(path)
var back := Settings.new()
back.load_from_disk(path)
check(is_equal_approx(back.volume, 0.35) and back.difficulty == Data.Difficulty.SCHWER,
"Lautstärke und Schwierigkeit kommen aus der Datei zurück")
var fresh := Settings.new()
fresh.load_from_disk("user://gibt_es_nicht.cfg")
check(is_equal_approx(fresh.volume, 0.8) and fresh.difficulty == Data.Difficulty.NORMAL,
"ohne Datei gelten die Standardwerte")
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
func test_healer_arrange() -> void:
print("[Bot-Heiler stehen bei den Tanks]")
var ai := BotAI.new()
var rng := RandomNumberGenerator.new()
rng.seed = 4
ai.setup(rng)
var me := PlayerState.new(1, "Bot", true)
me.level = 9
for i in 3:
me.board[Vector2i(i, 3)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
for i in 2:
me.board[Vector2i(3 + i, 3)] = Avatar.new(Data.Role.HEILER, [Data.Element.WASSER] as Array[int], 1)
for i in 2:
me.board[Vector2i(5 + i, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
ai.arrange(me, null)
var ok := true
for hex in me.board:
if me.board[hex].role != Data.Role.HEILER:
continue
var near := false
for other in me.board:
if me.board[other].role == Data.Role.TANK and Hex.distance(hex, other) <= Data.HEAL_RANGE:
near = true
ok = ok and near
check(ok, "jeder Heiler erreicht einen Tank")
func test_undo_sell() -> void:
print("[Verkauf rückgängig]")
var pool := Pool.new()
var p := PlayerState.new(0, "T", false)
p.gold = 5
var a := Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
a.invested = 2
pool.take(Data.Element.FEUER)
p.bench[2] = a
var left := pool.element_left[Data.Element.FEUER]
check(p.sell_bench(2, pool) and p.gold == 7 and pool.element_left[Data.Element.FEUER] == left + 1,
"Verkauf gibt Gold und Vorrat zurück")
check(p.undo_sell(pool), "Rückgängig geht")
check(p.bench[2] == a and p.gold == 5 and pool.element_left[Data.Element.FEUER] == left,
"Avatar liegt wieder da, Gold und Vorrat stimmen")
check(not p.undo_sell(pool), "zweites Rückgängig ist wirkungslos")
p.sell_bench(2, pool)
p.gold = 0
check(not p.undo_sell(pool), "ohne Gold kein Rückgängig")
func test_pve_rounds() -> void:
print("[Wildnis-Runden zum Warmlaufen]")
var g := Game.new()
g.start(61)
check(g.is_pve_round(), "Runde 1 ist eine Wildnis-Runde")
var opp := g.human_opponent()
check(opp != null and opp.pname == "Wildnis" and opp.board.size() == 1, "Gegner ist die Wildnis mit einer Einheit")
check(g.ranking().size() == 8, "die Wildnis zählt nicht als Spieler")
var hp0 := g.human.hp
g.start_combat()
g.skip_combat()
check(g.human.hp <= hp0, "gegen die Wildnis kann man Leben verlieren")
check(g.round_no == 2 and g.human_opponent().pname == "Wildnis" and g.human_opponent().board.size() == 2,
"Runde 2: Wildnis mit zwei Einheiten")
g.start_combat()
g.skip_combat()
check(not g.is_pve_round() and g.human_opponent().pname != "Wildnis", "ab Runde %d spielen die Spieler gegeneinander" % (Data.PVE_ROUNDS + 1))
var all_alive_before := g.alive_players().size()
check(all_alive_before == 8, "niemand ist an der Wildnis gestorben")
func test_stats() -> void:
print("[Statistik]")
var g := Game.new()
g.start(71)
var ai := BotAI.new()
ai.setup(g.rand)
for i in 5:
ai.take_turn(g.human, g.pool, g.rand)
g.start_combat()
g.skip_combat()
var st := g.human.stats
check(st["wins"] + st["losses"] >= 4, "Siege und Niederlagen werden gezählt (%d/%d)" % [st["wins"], st["losses"]])
check(st["gold_earned"] >= 5 * 5, "verdientes Gold summiert (%d)" % st["gold_earned"])
check(st["spent"] > 0, "ausgegebenes Gold summiert (%d)" % st["spent"])
func test_save_load() -> void:
print("[Speichern und Laden]")
var path := "user://spielstand_test.json"
var g := Game.new()
g.start(81)
var ai := BotAI.new()
ai.setup(g.rand)
for i in 4:
ai.take_turn(g.human, g.pool, g.rand)
g.start_combat()
g.skip_combat()
g.human.shop_locked = true
check(g.save(path), "Spielstand geschrieben")
check(Game.has_save(path), "Datei ist da")
var l := Game.load_from(path)
check(l != null, "Spielstand gelesen")
check(l.round_no == g.round_no and l.human.gold == g.human.gold and l.human.hp == g.human.hp,
"Runde, Gold, Leben stimmen")
check(l.human.board.size() == g.human.board.size() and l.human.level == g.human.level,
"Brett und Level stimmen")
check(l.human.shop_locked and l.human.shop.size() == g.human.shop.size(), "Shop samt Einfrieren stimmt")
var same_pool := true
for e in Data.ELEMENT_COUNT:
if l.pool.element_left[e] != g.pool.element_left[e]:
same_pool = false
check(same_pool, "geteilter Vorrat stimmt")
check(l.players.size() == 8 and l.bots.size() == 7 and l.human_opponent() != null,
"acht Spieler, sieben Bots, Gegner bekannt")
var fired := 0
while l.phase != Game.Phase.GAMEOVER and fired < 60:
l.start_combat()
if l.phase == Game.Phase.COMBAT:
l.skip_combat()
fired += 1
check(l.phase == Game.Phase.GAMEOVER or fired == 60, "geladene Partie läuft weiter (%d Runden)" % fired)
Game.delete_save(path)
check(not Game.has_save(path), "Spielstand gelöscht")
func test_counters() -> void:
print("[Schere-Stein-Papier]")
# Ring: jedes Element schlägt genau eines und wird von genau einem geschlagen.
var beaten := {}
for e in Data.ELEMENT_COUNT:
var target: int = Data.ELEMENT_BEATS[e]
check(target != e, "%s schlägt nicht sich selbst" % Data.ELEMENT_NAMES[e])
beaten[target] = beaten.get(target, 0) + 1
var ring_ok := beaten.size() == Data.ELEMENT_COUNT
for e in beaten:
ring_ok = ring_ok and beaten[e] == 1
check(ring_ok, "geschlossener Ring: jedes Element genau einmal geschlagen")
check(Data.beats_element(Data.Element.WASSER, Data.Element.FEUER) and not Data.beats_element(Data.Element.FEUER, Data.Element.WASSER),
"Wasser schlägt Feuer, nicht umgekehrt")
check(Data.beaten_by(Data.Element.FEUER) == Data.Element.WASSER, "Feuer wird von Wasser geschlagen")
check(Data.beats_role(Data.Role.TANK, Data.Role.DPS) and Data.beats_role(Data.Role.DPS, Data.Role.HEILER)
and Data.beats_role(Data.Role.HEILER, Data.Role.TANK), "Rollen-Dreieck Tank > DPS > Heiler > Tank")
check(is_equal_approx(Data.counter_multiplier(Data.Element.WASSER, Data.Role.TANK, Data.Element.FEUER, Data.Role.DPS), 1.35),
"doppelter Konter gibt +35%")
check(is_equal_approx(Data.counter_multiplier(Data.Element.FEUER, Data.Role.DPS, Data.Element.WASSER, Data.Role.TANK), 0.65),
"doppelt gekontert gibt -35%")
check(is_equal_approx(Data.counter_multiplier(Data.Element.FEUER, Data.Role.DPS, Data.Element.WIND, Data.Role.DPS), 1.0),
"ohne Bezug bleibt es bei 100%")
# Im Kampf: Wasser-Schütze gegen Feuer-Ziel trifft härter als gegen Wind-Ziel.
var a := PlayerState.new(0, "A", false)
a.board[Vector2i(3, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.WASSER] as Array[int], 1)
var b := PlayerState.new(1, "B", false)
b.board[Vector2i(3, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
var c := Combat.new(a, b, 3)
c.record_hits = true
var me: Combat.CUnit = c.units[0] if c.units[0].team == 0 else c.units[1]
var foe: Combat.CUnit = c.units[1] if c.units[0].team == 0 else c.units[0]
var hp0: float = foe.hp
c._apply_damage(me, foe, 100.0, false)
var dealt_vs_fire: float = hp0 - foe.hp
check(is_equal_approx(dealt_vs_fire, 100.0 * 1.2 * (1.0 - foe.armor)), "Wasser gegen Feuer: +20% (%.0f)" % dealt_vs_fire)
check(c.hits.size() == 1 and c.hits[0]["kind"] == "counter", "Konter-Treffer wird als solcher gemeldet")
var b2 := PlayerState.new(1, "B", false)
b2.board[Vector2i(3, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1)
var c2 := Combat.new(a, b2, 3)
var me2: Combat.CUnit = c2.units[0] if c2.units[0].team == 0 else c2.units[1]
var foe2: Combat.CUnit = c2.units[1] if c2.units[0].team == 0 else c2.units[0]
var hp1: float = foe2.hp
c2._apply_damage(me2, foe2, 100.0, false)
check(is_equal_approx(hp1 - foe2.hp, 100.0 * (1.0 - foe2.armor)), "Wasser gegen Wind: normal")
check(Avatar.new(Data.Role.TANK, [Data.Element.FEUER] as Array[int], 1).counter_text()
== "Stark gegen Eis, DPS · Schwach gegen Wasser, Heiler", "Text nennt Stärken und Schwächen")
func test_bot_counter_pick() -> void:
print("[Bots kontern den Gegner]")
var rng := RandomNumberGenerator.new()
rng.seed = 12
var ai := BotAI.new()
ai.setup(rng, Data.Difficulty.SCHWER)
var me := PlayerState.new(1, "Bot", true)
var opp := PlayerState.new(2, "Gegner", true)
for i in 3:
opp.board[Vector2i(i, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
ai.opponent = opp
check(ai._pick_element(me) == Data.Element.WASSER, "schwerer Bot wählt Wasser gegen ein Feuer-Team")
ai.opponent = null
check(ai.pref.has(ai._pick_element(me)), "ohne Gegner bleibt er bei seinen Vorlieben")
var easy := BotAI.new()
easy.setup(rng, Data.Difficulty.NORMAL)
easy.pref = [Data.Element.ERDE, Data.Element.EIS, Data.Element.BLITZ]
easy.opponent = opp
check(easy._pick_element(me) != Data.Element.WASSER, "normaler Bot kontert nur innerhalb seiner Vorlieben")
easy.pref = [Data.Element.WASSER, Data.Element.EIS, Data.Element.BLITZ]
check(easy._pick_element(me) == Data.Element.WASSER, "… und nimmt den Konter, wenn er dazu passt")