707 lines
26 KiB
GDScript
707 lines
26 KiB
GDScript
extends SceneTree
|
|
|
|
## Kopflose Prüfläufe für die Kernlogik.
|
|
|
|
var failures := 0
|
|
|
|
|
|
func check(cond: bool, msg: String) -> void:
|
|
if cond:
|
|
print(" OK ", msg)
|
|
else:
|
|
failures += 1
|
|
print(" FAIL ", msg)
|
|
|
|
|
|
func _initialize() -> void:
|
|
test_hex()
|
|
test_upgrade_chain()
|
|
test_pool_shared()
|
|
test_combat_runs()
|
|
test_full_game()
|
|
test_placements()
|
|
test_trait_effect()
|
|
test_buy_specialisation()
|
|
test_own_shop()
|
|
test_move_slot()
|
|
test_interest()
|
|
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")
|
|
else:
|
|
print("FEHLER: ", failures)
|
|
quit(1 if failures > 0 else 0)
|
|
|
|
|
|
func test_hex() -> void:
|
|
print("[Hex]")
|
|
check(Hex.distance(Vector2i(0, 0), Vector2i(0, 0)) == 0, "Distanz zu sich selbst = 0")
|
|
check(Hex.distance(Vector2i(0, 0), Vector2i(1, 0)) == 1, "Nachbar rechts = 1")
|
|
for n in Hex.neighbors(Vector2i(2, 2)):
|
|
if Hex.distance(Vector2i(2, 2), n) != 1:
|
|
check(false, "Nachbar hat Distanz 1: " + str(n))
|
|
return
|
|
for n in Hex.neighbors(Vector2i(2, 3)):
|
|
if Hex.distance(Vector2i(2, 3), n) != 1:
|
|
check(false, "Nachbar (ungerade Zeile) hat Distanz 1: " + str(n))
|
|
return
|
|
check(true, "alle 6 Nachbarn haben Distanz 1 (gerade und ungerade Zeile)")
|
|
|
|
|
|
func test_upgrade_chain() -> void:
|
|
print("[Aufwerten]")
|
|
var pool := Pool.new()
|
|
var p := PlayerState.new(0, "Test", false)
|
|
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))
|
|
check(ok, "Feuer-Avatar von der Bank aufstellen")
|
|
check(p.unlocked[Data.Element.FEUER], "Feuer erscheint danach im eigenen Shop")
|
|
|
|
var hex := Vector2i(3, 1)
|
|
check(not p.can_upgrade(hex), "ohne Futter kein Upgrade")
|
|
check(p.upgrade_missing(hex).get(Data.Element.FEUER, 0) == 2, "es fehlen 2 Feuer-Avatare")
|
|
|
|
p.bench[0] = Avatar.new(Data.Role.TANK, [Data.Element.FEUER], 1)
|
|
p.bench[1] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
|
check(p.can_upgrade(hex), "mit 2 Feuer-Avataren ist 2-Star möglich")
|
|
check(p.upgrade(hex, Data.Element.WIND, pool), "Upgrade auf 2-Star mit Wind als 2. Element")
|
|
var a: Avatar = p.board[hex]
|
|
check(a.star == 2 and a.elements == [Data.Element.FEUER, Data.Element.WIND], "2-Star Feuer/Wind")
|
|
check(p.bench[0] == null and p.bench[1] == null, "Futter wurde verbraucht")
|
|
|
|
var miss := p.upgrade_missing(hex)
|
|
check(miss.get(Data.Element.FEUER, 0) == 3 and miss.get(Data.Element.WIND, 0) == 3,
|
|
"3-Star braucht 3 Feuer und 3 Wind")
|
|
for i in 3:
|
|
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
|
for i in range(3, 6):
|
|
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.WIND], 1)
|
|
check(p.can_upgrade(hex), "mit 3+3 Futter ist 3-Star möglich")
|
|
check(p.upgrade(hex, Data.Element.BLITZ, pool), "Upgrade auf 3-Star mit Blitz")
|
|
a = p.board[hex]
|
|
check(a.star == 3 and a.elements.size() == 3, "3-Star mit drei Elementen")
|
|
check(p.upgrade_cost().is_empty() if false else a.upgrade_cost().is_empty(), "3-Star ist Maximum")
|
|
var s1 := Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1).base_stats()
|
|
check(a.base_stats()["hp"] > s1["hp"] * 3.0, "Stats steigen deutlich pro Stern")
|
|
|
|
|
|
func test_pool_shared() -> void:
|
|
print("[Geteilter Pool]")
|
|
var pool := Pool.new()
|
|
var p := PlayerState.new(0, "Test", false)
|
|
p.unlock(Data.Element.FEUER)
|
|
var taken := 0
|
|
while pool.take(Data.Element.FEUER):
|
|
taken += 1
|
|
check(taken == Data.POOL_PER_ELEMENT, "Pro Element nur %d Avatare insgesamt" % Data.POOL_PER_ELEMENT)
|
|
check(p.shop_elements(pool).is_empty(), "leergekauftes Element verschwindet aus dem Shop")
|
|
|
|
|
|
func test_combat_runs() -> void:
|
|
print("[Kampf]")
|
|
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))
|
|
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))
|
|
var c := Combat.new(a, b, 12345)
|
|
check(c.units.size() == 8, "8 Einheiten in der Arena")
|
|
var team0 := 0
|
|
for u in c.units:
|
|
if u.team == 0:
|
|
team0 += 1
|
|
check(team0 == 4, "4 Einheiten pro Team")
|
|
c.run_to_end()
|
|
check(c.finished, "Kampf endet")
|
|
check(c.winner in [-1, 0, 1], "Ergebnis ist gültig")
|
|
print(" Sieger: ", c.winner, " Überlebende: ", c.survivors, " Dauer: %.1fs" % c.time)
|
|
|
|
# Trait-Wirkung: Feuer 4 muss den Angriff erhöhen.
|
|
var counts := a.trait_counts()
|
|
check(counts.get(Data.Element.FEUER, 0) == 4, "4 Feuer-Einheiten aufgestellt")
|
|
check(Data.trait_tier(Data.Element.FEUER, 4) == 2, "Feuer-Trait auf Stufe 2")
|
|
|
|
|
|
func test_full_game() -> void:
|
|
print("[Kompletter Loop]")
|
|
var g := Game.new()
|
|
g.start(999)
|
|
check(g.players.size() == 8, "8 Spieler (du + 7 Bots)")
|
|
check(g.human.hp == 100 and g.human.gold == Data.START_GOLD, "Start: 100 Leben, %d Gold" % Data.START_GOLD)
|
|
check(g.human.shop.size() == Data.SHOP_SLOTS, "Shop hat %d Plätze" % Data.SHOP_SLOTS)
|
|
var only_neutral := true
|
|
for e in g.human.shop:
|
|
if e.element >= 0:
|
|
only_neutral = false
|
|
check(true, "Shop-Angebot Runde 1 (nur neutral: %s)" % str(only_neutral))
|
|
|
|
# Menschlichen Spieler wie einen Bot spielen lassen, damit der Loop läuft.
|
|
var ai := BotAI.new()
|
|
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.start_combat()
|
|
if g.phase == Game.Phase.COMBAT:
|
|
g.skip_combat()
|
|
guard += 1
|
|
check(g.phase == Game.Phase.GAMEOVER, "Spiel endet (Runden gespielt: %d)" % g.round_no)
|
|
var alive := g.alive_players()
|
|
check(alive.size() == 1, "genau ein Gewinner")
|
|
if alive.size() == 1:
|
|
print(" Sieger: ", alive[0].pname, " mit ", alive[0].hp, " Leben")
|
|
var sum_hp := 0
|
|
for p in g.players:
|
|
sum_hp += p.hp
|
|
check(sum_hp < 8 * Data.START_HP, "Leben wurden verloren")
|
|
var any_upgraded := false
|
|
var max_star := 1
|
|
for p in g.players:
|
|
for u in p.board_units():
|
|
max_star = maxi(max_star, u.star)
|
|
check(max_star >= 2, "mindestens ein Avatar wurde aufgewertet (max %d-Star)" % max_star)
|
|
var used := 0
|
|
for e in Data.ELEMENT_COUNT:
|
|
used += Data.POOL_PER_ELEMENT - g.pool.element_left[e]
|
|
check(used > 0, "Bots verbrauchen den geteilten Pool (%d Avatare gekauft)" % used)
|
|
|
|
|
|
func test_placements() -> void:
|
|
print("[Platzierungen]")
|
|
var g := Game.new()
|
|
g.start(4242)
|
|
var ai := BotAI.new()
|
|
ai.setup(g.rand)
|
|
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.start_combat()
|
|
if g.phase == Game.Phase.COMBAT:
|
|
g.skip_combat()
|
|
guard += 1
|
|
var seen := {}
|
|
var ok := true
|
|
for p in g.players:
|
|
if p.placement < 1 or p.placement > 8 or seen.has(p.placement):
|
|
ok = false
|
|
print(" ", p.pname, " Platz ", p.placement, " hp ", p.hp)
|
|
seen[p.placement] = true
|
|
check(ok, "jeder Spieler hat genau einen Platz 1..8")
|
|
check(seen.size() == 8, "acht verschiedene Plätze vergeben")
|
|
|
|
|
|
func test_trait_effect() -> void:
|
|
print("[Trait-Wirkung]")
|
|
var pool := Pool.new()
|
|
# Team mit 4x Feuer -> Trait Stufe 2 (+26% Angriff).
|
|
var strong := PlayerState.new(0, "Feuer4", false)
|
|
for i in 4:
|
|
strong.board[Vector2i(i, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
|
# Team mit nur 1x Feuer -> kein Trait.
|
|
var weak := PlayerState.new(1, "Gemischt", false)
|
|
weak.board[Vector2i(0, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
|
for i in range(1, 4):
|
|
weak.board[Vector2i(i, 1)] = Avatar.new(Data.Role.DPS, [i], 1)
|
|
|
|
var c := Combat.new(strong, weak, 777)
|
|
var atk_strong := 0.0
|
|
var atk_weak := 0.0
|
|
for u in c.units:
|
|
if u.avatar.has_element(Data.Element.FEUER):
|
|
if u.team == 0:
|
|
atk_strong = u.atk
|
|
else:
|
|
atk_weak = u.atk
|
|
check(atk_strong > atk_weak * 1.2,
|
|
"Feuer-Trait erhöht den Angriff (%d statt %d)" % [int(atk_strong), int(atk_weak)])
|
|
|
|
# Kombi-Trait: Feuer 2 + Wind 2 muss Feuersturm auslösen.
|
|
var combo := PlayerState.new(2, "Kombi", false)
|
|
for i in 2:
|
|
combo.board[Vector2i(i, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
|
combo.board[Vector2i(i, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.WIND], 1)
|
|
var keys := []
|
|
for x in combo.active_combos():
|
|
keys.append(x["key"])
|
|
check(keys.has("feuersturm"), "Feuer 2 + Wind 2 aktiviert Feuersturm")
|
|
check(not keys.has("frost"), "Wasser/Eis bleibt ohne die Elemente inaktiv")
|
|
|
|
# Erde-Trait muss die Lebenspunkte im Kampf erhöhen.
|
|
var earth := PlayerState.new(3, "Erde4", false)
|
|
for i in 4:
|
|
earth.board[Vector2i(i, 1)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE], 1)
|
|
var c2 := Combat.new(earth, weak, 778)
|
|
var base_hp: float = Avatar.new(Data.Role.TANK, [Data.Element.ERDE], 1).base_stats()["hp"]
|
|
var got := 0.0
|
|
for u in c2.units:
|
|
if u.team == 0:
|
|
got = u.max_hp
|
|
check(got > base_hp * 1.2, "Erde-Trait erhöht Leben (%d statt %d)" % [int(got), int(base_hp)])
|
|
|
|
|
|
func test_buy_specialisation() -> void:
|
|
print("[Spezialisierung beim Kauf]")
|
|
var pool := Pool.new()
|
|
var p := PlayerState.new(0, "Test", false)
|
|
p.gold = 20
|
|
p.shop = [ShopEntry.new(Data.Role.TANK, -1)]
|
|
check(p.shop[0].element < 0, "Shop bietet einen neutralen Avatar an")
|
|
check(not p.buy(0, pool, -1), "Kauf ohne gewähltes Element wird abgelehnt")
|
|
var before: int = pool.element_left[Data.Element.BLITZ]
|
|
check(p.buy(0, pool, Data.Element.BLITZ), "Kauf mit Blitz als Spezialisierung")
|
|
var a: Avatar = p.bench[0]
|
|
check(a != null and a.elements == [Data.Element.BLITZ], "Avatar liegt als Blitz auf der Bank")
|
|
check(pool.element_left[Data.Element.BLITZ] == before - 1,
|
|
"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))
|
|
check(p.unlocked[Data.Element.BLITZ], "erst das Aufstellen schaltet Blitz im Shop frei")
|
|
|
|
|
|
func test_own_shop() -> void:
|
|
print("[Shop zeigt nur eigene Elemente]")
|
|
var pool := Pool.new()
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = 7
|
|
var me := PlayerState.new(0, "Ich", false)
|
|
var other := PlayerState.new(1, "Bot", true)
|
|
other.unlock(Data.Element.EIS)
|
|
me.unlock(Data.Element.FEUER)
|
|
var mine := me.shop_elements(pool)
|
|
var theirs := other.shop_elements(pool)
|
|
check(mine.size() == 1 and mine[0] == Data.Element.FEUER, "eigener Shop kennt nur Feuer")
|
|
check(theirs.size() == 1 and theirs[0] == Data.Element.EIS, "der Bot sieht nur sein eigenes Eis")
|
|
var seen := {}
|
|
for i in 200:
|
|
me.roll_shop(pool, rng)
|
|
for e in me.shop:
|
|
seen[e.element] = true
|
|
check(not seen.has(Data.Element.EIS), "fremdes Eis taucht im eigenen Shop nicht auf")
|
|
check(seen.has(Data.Element.FEUER) and seen.has(-1), "eigener Shop zeigt Feuer und Neutral")
|
|
|
|
|
|
func test_move_slot() -> void:
|
|
print("[Verschieben zwischen Bank und Board]")
|
|
var p := PlayerState.new(0, "Test", false)
|
|
var f := Avatar.new(Data.Role.TANK, [Data.Element.FEUER] as Array[int], 1)
|
|
var w := Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1)
|
|
p.bench[0] = f
|
|
p.bench[1] = w
|
|
var b0 := {"kind": "bench", "index": 0}
|
|
var b1 := {"kind": "bench", "index": 1}
|
|
var b3 := {"kind": "bench", "index": 3}
|
|
var h1 := {"kind": "board", "hex": Vector2i(2, 1)}
|
|
var h2 := {"kind": "board", "hex": Vector2i(4, 2)}
|
|
|
|
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), "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), "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), "Board -> Bank")
|
|
check(p.bench[3] == w and p.board.is_empty(), "Wind liegt wieder auf der Bank")
|
|
|
|
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 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)}):
|
|
placed += 1
|
|
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")
|
|
|
|
|
|
func test_interest() -> void:
|
|
print("[Zinsen]")
|
|
check(Data.interest(0) == 0, "0 Gold -> 0 Zins")
|
|
check(Data.interest(9) == 0, "9 Gold -> 0 Zins")
|
|
check(Data.interest(10) == 1, "10 Gold -> 1 Zins")
|
|
check(Data.interest(35) == 3, "35 Gold -> 3 Zins")
|
|
check(Data.interest(49) == 4, "49 Gold -> 4 Zins")
|
|
check(Data.interest(50) == 5, "50 Gold -> 5 Zins")
|
|
check(Data.interest(200) == 5, "über 50 Gold bleibt es bei 5 Zins")
|
|
|
|
var p := PlayerState.new(0, "Test", false)
|
|
p.gold = 0
|
|
check(p.round_income() == Data.BASE_INCOME, "ohne Gold nur die 5 Grundgold")
|
|
p.gold = 30
|
|
check(p.round_income() == Data.BASE_INCOME + 3, "30 Gold -> 5 + 3 Zins")
|
|
p.gold = 50
|
|
p.register_result(true)
|
|
check(p.round_income() == Data.BASE_INCOME + Data.WIN_GOLD + 5,
|
|
"50 Gold mit Sieg -> 5 + 1 + 5 Zins")
|
|
for i in 4:
|
|
p.register_result(true)
|
|
check(p.round_income() == Data.BASE_INCOME + Data.WIN_GOLD + 5 + Data.streak_gold(5),
|
|
"Serie kommt oben drauf")
|
|
|
|
|
|
func test_prices() -> void:
|
|
print("[Preise]")
|
|
check(Data.PRICE_NONE == 1, "neutraler Avatar kostet 1 Gold")
|
|
check(Data.PRICE_ELEMENT == 2, "Element-Avatar kostet 2 Gold")
|
|
var pool := Pool.new()
|
|
var p := PlayerState.new(0, "Test", false)
|
|
p.gold = 10
|
|
p.shop = [ShopEntry.new(Data.Role.TANK, -1), ShopEntry.new(Data.Role.DPS, Data.Element.EIS)]
|
|
check(p.shop[0].price() == 1 and p.shop[1].price() == 2, "Shop rechnet mit 1 und 2 Gold")
|
|
p.buy(0, pool, Data.Element.FEUER)
|
|
check(p.gold == 9, "neutraler Kauf zieht 1 Gold ab")
|
|
p.buy(1, pool, -1)
|
|
check(p.gold == 7, "Element-Kauf zieht 2 Gold ab")
|
|
p.sell_bench(0, pool)
|
|
check(p.gold == 8, "Verkauf gibt den Kaufpreis zurück")
|
|
|
|
|
|
func test_pool_pressure() -> void:
|
|
print("[Vorrat hält eine ganze Partie]")
|
|
var lowest := Data.POOL_PER_ELEMENT
|
|
var early_dry := 0
|
|
for run in 4:
|
|
var g := Game.new()
|
|
g.start(4000 + run)
|
|
var ai := BotAI.new()
|
|
ai.setup(g.rand)
|
|
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.start_combat()
|
|
if g.phase == Game.Phase.COMBAT:
|
|
g.skip_combat()
|
|
for e in Data.ELEMENT_COUNT:
|
|
lowest = mini(lowest, g.pool.element_left[e])
|
|
if g.round_no <= 15:
|
|
for e in Data.ELEMENT_COUNT:
|
|
if g.pool.element_left[e] <= 0:
|
|
early_dry += 1
|
|
guard += 1
|
|
check(early_dry == 0, "bis Runde 15 läuft kein Element leer")
|
|
check(lowest < Data.POOL_PER_ELEMENT / 2, "der Vorrat wird spürbar verbraucht (Tiefstand %d von %d)"
|
|
% [lowest, Data.POOL_PER_ELEMENT])
|
|
|
|
|
|
func test_neutral_chance() -> void:
|
|
print("[Neutral-Chance sinkt je Element]")
|
|
check(is_equal_approx(Data.neutral_chance(0), 1.0), "ohne eigene Elemente nur Neutral")
|
|
check(is_equal_approx(Data.neutral_chance(1), 0.75), "1 Element -> 75% Neutral")
|
|
check(is_equal_approx(Data.neutral_chance(2), 0.5625), "2 Elemente -> 56%")
|
|
check(Data.neutral_chance(6) > 0.0, "Neutral verschwindet nie ganz (%.1f%%)"
|
|
% (Data.neutral_chance(6) * 100.0))
|
|
|
|
var pool := Pool.new()
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = 99
|
|
var quotes := []
|
|
for count in range(0, 5):
|
|
var p := PlayerState.new(0, "Test", false)
|
|
for e in count:
|
|
p.unlock(e)
|
|
var neutral := 0
|
|
var total := 0
|
|
for i in 400:
|
|
p.roll_shop(pool, rng)
|
|
for entry in p.shop:
|
|
total += 1
|
|
if entry.element < 0:
|
|
neutral += 1
|
|
quotes.append(float(neutral) / float(total))
|
|
print(" gemessene Neutral-Quoten: ", quotes)
|
|
check(is_equal_approx(quotes[0], 1.0), "0 Elemente: alles neutral")
|
|
var falling := true
|
|
for i in range(1, quotes.size()):
|
|
if quotes[i] >= quotes[i - 1]:
|
|
falling = false
|
|
check(falling, "die Quote sinkt mit jedem weiteren Element")
|
|
for i in quotes.size():
|
|
if absf(quotes[i] - Data.neutral_chance(i)) > 0.05:
|
|
check(false, "Quote bei %d Elementen weicht ab (%.2f statt %.2f)"
|
|
% [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)
|