1119 lines
46 KiB
GDScript
1119 lines
46 KiB
GDScript
extends SceneTree
|
|
## Regel-, KI- und Spielstand-Tests ohne Fenster:
|
|
## godot --headless --path . -s tests/test_runner.gd
|
|
|
|
var passed := 0
|
|
var failed := 0
|
|
|
|
|
|
func check(cond: bool, msg: String) -> void:
|
|
if cond:
|
|
passed += 1
|
|
else:
|
|
failed += 1
|
|
printerr("FEHLER: " + msg)
|
|
|
|
|
|
func C(id: String) -> Cards.Card:
|
|
return Cards.by_id(id)
|
|
|
|
|
|
func setup(human_hand: Array = [], opp_hand: Array = [], n_opp: int = 1) -> Array:
|
|
var human := Game.Player.new("Spieler", true, 30)
|
|
var opps := []
|
|
for i in n_opp:
|
|
opps.append(Game.Player.new("Gegner %d" % (i + 1), false, 15))
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 0
|
|
var g := Game.new(human, opps, r)
|
|
g.turn = 1
|
|
human.max_energy = 10
|
|
human.energy = 10
|
|
for cid in human_hand:
|
|
human.hand.append(C(cid))
|
|
for o in opps:
|
|
o.max_energy = 10
|
|
o.energy = 10
|
|
for cid in opp_hand:
|
|
o.hand.append(C(cid))
|
|
return [g, human, opps]
|
|
|
|
|
|
var next_uid := 9000
|
|
|
|
|
|
func put(p: Game.Player, cid: String, ready: bool = true) -> Game.Creature:
|
|
next_uid += 1
|
|
var cr := Game.Creature.new(C(cid), p, next_uid)
|
|
cr.ready = ready
|
|
cr.attacks_left = cr.attacks_per_turn() if ready else 0
|
|
p.board.append(cr)
|
|
return cr
|
|
|
|
|
|
func _initialize() -> void:
|
|
TranslationServer.set_locale("de") # das Spiel setzt die Sprache aus den Einstellungen; hier fehlt der Einstieg
|
|
test_kartenpool()
|
|
test_kampf_speichern()
|
|
test_englische_karten()
|
|
test_kombos()
|
|
test_wesen_spielen()
|
|
test_flink()
|
|
test_waechter()
|
|
test_waechter_nur_eigener_besitzer()
|
|
test_kampf_beidseitig()
|
|
test_schild()
|
|
test_doppelschlag()
|
|
test_einfrieren()
|
|
test_effekt_ziel()
|
|
test_flaechenschaden()
|
|
test_zurueck_auf_hand()
|
|
test_buff_heilung()
|
|
test_limits()
|
|
test_grenzfaelle()
|
|
test_grenzfaelle_2()
|
|
test_laden()
|
|
test_mana_effekt()
|
|
test_gleiche_regeln()
|
|
test_zugreihenfolge()
|
|
test_tote_gegner()
|
|
test_jeder_gegen_jeden()
|
|
test_gegnerzahl()
|
|
test_sieg_niederlage()
|
|
test_booster()
|
|
test_spielstand()
|
|
test_resonanz()
|
|
test_elementvorteil()
|
|
test_feld()
|
|
test_verschmelzen()
|
|
test_feldkombos()
|
|
test_schild_effekt_und_sonne()
|
|
test_kopie()
|
|
test_ki_lethal()
|
|
test_ki_besser_als_gierig()
|
|
test_ki_spiele_enden()
|
|
test_spielstand_migration()
|
|
test_kampagne()
|
|
test_zufallssamen()
|
|
test_spielstand_sicherung()
|
|
test_merkmale()
|
|
test_ki_zwei_schritte()
|
|
test_snapshot()
|
|
test_kapitel_zwei_und_merkmalwahl()
|
|
test_ki_zeitbudget()
|
|
test_uebersetzung_vollstaendig()
|
|
print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed])
|
|
quit(1 if failed > 0 else 0)
|
|
|
|
|
|
func test_kartenpool() -> void:
|
|
check(Cards.all().size() == 77, "77 Karten")
|
|
check(Cards.all().all(func(c): return c.elements.size() == 1 and c.elements[0] in Cards.ELEMENTS), "jede Karte hat genau ein Element")
|
|
for e in Cards.ELEMENTS:
|
|
check(Cards.all().any(func(c): return c.elements == [e]), "Element vorhanden: " + e)
|
|
for c in Cards.all():
|
|
check(c.health > 0 and c.cost > 0, "Werte " + c.id)
|
|
check(C("el_koenig").text() == "Wächter, Flink, Schild. Beim Ausspielen: Alle befreundeten Wesen erhalten +1/+1.", "Kartentext")
|
|
check(C("f_ritter").text() == "Flink. Beim Ausspielen: 1 Schaden an ein Ziel.", "Kartentext mit Effekt")
|
|
check(C("www_ozean").text() == "Wächter. Beim Ausspielen: Friere alle gegnerischen Wesen ein, Erhalte 3 Mana.", "Ozean-Text")
|
|
|
|
|
|
func test_kombos() -> void:
|
|
check(Cards.combo_name(["Feuer", "Feuer"]) == "Inferno", "Feuer+Feuer = Inferno")
|
|
check(Cards.combo_name(["Luft", "Feuer"]) == "Blitz", "Luft+Feuer = Blitz")
|
|
check(not C("ff_phoenix").is_combo() and Cards.fusion_name(["Feuer"]) == "Inferno", "Kombos gibt es nur beim Verschmelzen")
|
|
check(Cards.combo_name(["Feuer", "Wasser", "Erde"]) == "Chimäre", "drei Elemente sind eine Chimäre")
|
|
|
|
|
|
func test_wesen_spielen() -> void:
|
|
var s := setup(["f_wolf"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
check(g.play_card(h, C("f_wolf")) == "", "Wolf spielen")
|
|
check(h.energy == 10 and h.board.size() == 1, "Ausspielen kostet nichts")
|
|
check(not h.board[0].can_attack(), "Erschöpfung")
|
|
g.end_turn()
|
|
g.end_turn()
|
|
check(h.board[0].can_attack(), "bereit im nächsten Zug")
|
|
|
|
|
|
func test_flink() -> void:
|
|
var s := setup(["l_falke"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
g.play_card(h, C("l_falke"))
|
|
check(g.attack(h.board[0], o) == "", "Flink greift sofort an")
|
|
check(o.life == 12, "Falke 3 Schaden")
|
|
|
|
|
|
func test_waechter() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var a := put(h, "f_wolf")
|
|
var w := put(o, "e_wall")
|
|
check(g.attack(a, o) != "", "Wächter blockt Angriff auf Held")
|
|
check(g.attack(a, w) == "", "Wächter angreifen")
|
|
check(w.health == 1 and a.health == 2, "Schaden verrechnet")
|
|
|
|
|
|
func test_waechter_nur_eigener_besitzer() -> void:
|
|
var s := setup([], [], 2)
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var a := put(h, "f_wolf")
|
|
put(s[2][0], "e_wall")
|
|
check(g.attack(a, s[2][1]) == "" and s[2][1].life == 12, "Wächter schützt nur Besitzer")
|
|
|
|
|
|
func test_kampf_beidseitig() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var a := put(h, "f_asche")
|
|
var d := put(o, "w_reiter")
|
|
g.attack(a, d)
|
|
check(not (d in o.board) and not (a in h.board), "beide sterben")
|
|
check("Aschekrieger stirbt." in g.log, "Log")
|
|
|
|
|
|
func test_schild() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var sh := put(o, "ee_bastion")
|
|
var a := put(h, "f_asche")
|
|
g.attack(a, sh)
|
|
check(sh.health == 7 and not sh.shield, "Schild blockt einmal")
|
|
var b := put(h, "f_wolf")
|
|
g.attack(b, sh)
|
|
check(sh.health == 4, "zweiter Schaden zählt")
|
|
|
|
|
|
func test_doppelschlag() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var a := put(h, "l_schlange")
|
|
g.attack(a, o)
|
|
g.attack(a, o)
|
|
check(o.life == 11, "zwei Angriffe")
|
|
check(g.attack(a, o) != "", "kein dritter Angriff")
|
|
|
|
|
|
func test_einfrieren() -> void:
|
|
var s := setup([], ["w_frost"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var a := put(h, "f_wolf")
|
|
g.end_turn()
|
|
check(g.play_card(o, C("w_frost"), a) == "", "Frostmagier spielen")
|
|
check(a.frozen, "eingefroren")
|
|
g.end_turn()
|
|
check(not a.frozen and not a.can_attack(), "taut auf, setzt aus")
|
|
g.end_turn()
|
|
g.end_turn()
|
|
check(a.can_attack(), "wieder bereit")
|
|
|
|
|
|
func test_effekt_ziel() -> void:
|
|
var s := setup(["w_frost", "f_ritter"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
check(g.play_card(h, C("w_frost")) == "" and h.board.size() == 1, "ohne Ziel kommt das Wesen trotzdem, der Effekt verpufft")
|
|
g.play_card(h, C("f_ritter"), o)
|
|
check(o.life == 14 and h.board.size() == 2, "Ritter macht 1 Schaden an den Helden")
|
|
|
|
|
|
func test_flaechenschaden() -> void:
|
|
var s := setup(["ff_phoenix"], [], 2)
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o1: Game.Player = s[2][0]
|
|
var o2: Game.Player = s[2][1]
|
|
put(o1, "f_kobold")
|
|
put(o2, "e_golem")
|
|
g.play_card(h, C("ff_phoenix"))
|
|
check(o1.life == 11 and o2.life == 11, "Helden 4 Schaden")
|
|
check(o1.board.is_empty() and o2.board[0].health == 1, "Wesen getroffen")
|
|
|
|
|
|
func test_zurueck_auf_hand() -> void:
|
|
var s := setup(["wl_nebel"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var d := put(o, "e_troll")
|
|
g.play_card(h, C("wl_nebel"), d)
|
|
check(o.board.is_empty() and C("e_troll") in o.hand, "zurück auf Hand")
|
|
|
|
|
|
func test_buff_heilung() -> void:
|
|
var s := setup(["el_koenig", "e_baum"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var a := put(h, "f_wolf")
|
|
h.life = 20
|
|
h.energy = 20
|
|
g.play_card(h, C("el_koenig"))
|
|
check(a.attack == 4 and a.health == 3, "der König verstärkt alle eigenen Wesen um +1/+1")
|
|
g.play_card(h, C("e_baum"))
|
|
check(h.life == 25, "Heilung")
|
|
|
|
|
|
func test_limits() -> void:
|
|
var s := setup(["f_kobold"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
for i in Game.MAX_BOARD:
|
|
put(h, "f_kobold")
|
|
check(g.can_play(h, C("f_kobold"))[1] == "Dein Feld ist voll", "Feld voll")
|
|
h.hand = []
|
|
for i in Game.MAX_HAND:
|
|
h.hand.append(C("f_kobold"))
|
|
h.energy = 10
|
|
h.shop = [C("f_kobold")]
|
|
check(g.buy(h, 0) == "Hand voll" and h.hand.size() == Game.MAX_HAND, "volle Hand kauft nicht")
|
|
|
|
|
|
## Grenzfälle, die schon einmal falsch waren.
|
|
func test_grenzfaelle() -> void:
|
|
var s := setup(["f_wolf"], [], 2)
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o1: Game.Player = s[2][0]
|
|
var o2: Game.Player = s[2][1]
|
|
# Nur Karten von der eigenen Hand
|
|
check(not g.can_play(h, C("e_troll"))[0] and g.play_card(h, C("e_troll")) == "Nicht auf der Hand", "fremde Karte lässt sich nicht spielen")
|
|
check(g.play_card(h, C("f_wolf")) == "" and h.board.size() == 1, "eigene Handkarte geht")
|
|
# Besiegte Spieler nehmen ihre Wesen mit
|
|
var dead := put(o1, "e_troll")
|
|
o1.life = 0
|
|
g.cleanup()
|
|
check(o1.board.is_empty() and not o1.alive() and g.log.any(func(l): return l.find("verschwindet mit") >= 0), "besiegter Spieler räumt sein Feld")
|
|
check(not (o1 in g.enemies_of(h)) and o2 in g.enemies_of(h), "Besiegte sind keine Ziele mehr")
|
|
check(dead != null, "Wesen war vorher da")
|
|
# Verschmolzene Karte auf der Hand übersteht den Spielstand
|
|
var a := put(h, "f_kobold")
|
|
var b := put(h, "w_geist")
|
|
h.energy = 10
|
|
check(g.fuse(h, a, b) == "", "verschmolzen")
|
|
var fused: Game.Creature = h.board[h.board.size() - 1]
|
|
h.hand.append(fused.card)
|
|
var back := NetState.from_snapshot(JSON.parse_string(JSON.stringify(g.snapshot(-1))))
|
|
check(back.players[0].hand.any(func(c): return c.name == fused.card.name), "verschmolzene Karte übersteht die Hand im Spielstand")
|
|
|
|
|
|
## Zweite Runde Grenzfälle: Konter ohne Angriff, Chimäre, Ereignisse mit verschmolzener Karte,
|
|
## ausgeschiedener Netzwerkspieler mitten im eigenen Zug.
|
|
func test_grenzfaelle_2() -> void:
|
|
var s := setup([], [], 2)
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o1: Game.Player = s[2][0]
|
|
# Ein Wesen ohne Angriff schlägt nicht zurück, auch nicht mit Elementvorteil
|
|
var wall := put(o1, "e_wall") # Erde 0/4, Erde schlägt Wasser
|
|
var reiter := put(h, "w_reiter") # Wasser 3/3
|
|
g.attack(reiter, wall)
|
|
check(reiter.health == 3 and wall.health == 4 - 3 + 1, "Steinwall kontert nicht, nimmt aber 1 weniger")
|
|
# Drei Elemente ergeben eine Chimäre: erst Feuer und Erde zu Lava, dann Lava und Wasser
|
|
var fire := put(h, "f_wolf")
|
|
var earth := put(h, "e_kiesel")
|
|
h.energy = 10
|
|
check(g.fuse(h, fire, earth) == "", "Wolf und Kiesel verschmelzen")
|
|
var lava: Game.Creature = h.board[h.board.size() - 1]
|
|
check(lava.name == "Lava" and lava.card.elements == ["Feuer", "Erde"], "zwei Elemente heißen nach ihrer Kombo")
|
|
var water := put(h, "w_geist")
|
|
check(g.fuse(h, lava, water) == "", "Lava und Flussgeist verschmelzen")
|
|
var chimera: Game.Creature = h.board[h.board.size() - 1]
|
|
check(chimera.name == "Chimäre" and chimera.card.elements.size() == 3 and chimera.card.combo() == "Chimäre", "drei Elemente heißen Chimäre")
|
|
# Ein "play"-Ereignis mit verschmolzener Karte übersteht den Spielstand
|
|
h.hand.append(chimera.card)
|
|
h.board.erase(chimera)
|
|
g.events.clear()
|
|
check(g.play_card(h, chimera.card) == "", "Chimäre von der Hand gespielt")
|
|
var back := NetState.from_snapshot(JSON.parse_string(JSON.stringify(g.snapshot(-1))))
|
|
check(back.events.any(func(e): return e.type == "play" and e.card.name == "Chimäre"), "Ereignis mit verschmolzener Karte übertragen")
|
|
# Netzwerk: wer während des eigenen Zugs ausscheidet, blockiert das Spiel nicht
|
|
var net := Net.new()
|
|
net.is_host = true
|
|
net.game = g
|
|
net.rejoin_seconds = 0.0
|
|
net.waiting = {"Spieler": {"seat": g.players.find(h), "since": 0.0}}
|
|
o1.is_human = true # noch ein Mensch am Tisch, sonst wäre die Partie schlicht verloren
|
|
check(g.current() == h, "Spieler ist dran")
|
|
net.tick()
|
|
check(not h.alive() and h.board.is_empty() and g.current() != h, "Ausgeschiedener räumt das Feld und der Zug geht weiter")
|
|
net.free()
|
|
|
|
|
|
## Laden: kaufen kostet Mana, ausspielen nichts, nicht Ausgegebenes bleibt liegen.
|
|
func test_laden() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
h.max_energy = 3
|
|
g.roll_shop(h)
|
|
check(h.shop.size() == Game.SHOP_SIZE, "fünf Angebote")
|
|
check(h.shop.all(func(c): return c.cost <= h.max_energy + 1), "nur Karten bis Einkommen plus eins")
|
|
for i in 20: # der erste Zug darf nie tot sein
|
|
var poor := Game.Player.new("Arm", false, 30)
|
|
poor.max_energy = 1
|
|
g.roll_shop(poor)
|
|
check(poor.shop.any(func(c): return c.cost <= 1), "immer ein bezahlbares Angebot")
|
|
h.energy = 20
|
|
var card: Cards.Card = h.shop[0]
|
|
var before: int = h.energy
|
|
check(g.buy(h, 0) == "" and h.hand == [card], "gekauft, liegt auf der Hand")
|
|
check(h.energy == before - card.cost and h.shop[0] == null, "Mana bezahlt, Platz leer")
|
|
check(g.buy(h, 0) != "" and g.buy(h, 99) != "", "leerer und unbekannter Platz")
|
|
before = h.energy
|
|
var gain: int = 0 # manche Wesen geben beim Ausspielen Mana
|
|
for fx in card.effects:
|
|
if fx.kind == "mana":
|
|
gain += fx.amount
|
|
check(g.play_card(h, card) == "" and h.energy == before + gain, "Ausspielen kostet nichts")
|
|
before = h.energy
|
|
check(g.reroll(h) == "" and h.energy == before - Game.REROLL_COST and h.shop.size() == Game.SHOP_SIZE, "neu gewürfelt")
|
|
h.energy = 0
|
|
check(g.reroll(h) != "" and g.buy(h, 0) != "", "ohne Mana geht nichts")
|
|
h.energy = 100
|
|
for i in Game.MAX_HAND:
|
|
h.hand.append(C("f_kobold"))
|
|
check(g.buy(h, 1) != "", "volle Hand kauft nicht")
|
|
# Teure Karten tauchen erst mit dem Einkommen auf
|
|
h.max_energy = Game.MAX_ENERGY
|
|
g.roll_shop(h)
|
|
check(DeckGen.pool(1).all(func(c): return c.cost <= 2) and DeckGen.pool(Game.MAX_ENERGY).size() == Cards.all().size(),
|
|
"Angebot wächst mit dem Einkommen")
|
|
# Nicht ausgegebenes Mana bleibt liegen
|
|
var s2 := setup()
|
|
var g2: Game = s2[0]
|
|
var h2: Game.Player = s2[1]
|
|
h2.energy = 0
|
|
h2.max_energy = 0
|
|
g2.begin_turn(h2)
|
|
check(h2.energy == 1 and h2.max_energy == 1, "erste Runde: 1 Mana")
|
|
g2.begin_turn(h2)
|
|
check(h2.energy == 3 and h2.max_energy == 2, "gespartes Mana bleibt liegen")
|
|
|
|
|
|
## "Erhalte Mana" statt Nachziehen: Kartenvorteil ist im Laden Manavorteil.
|
|
func test_mana_effekt() -> void:
|
|
var s := setup(["w_geist"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var before: int = h.energy
|
|
g.play_card(h, C("w_geist"))
|
|
check(h.energy == before + 1, "Flussgeist gibt 1 Mana")
|
|
check(C("w_geist").text() == "Beim Ausspielen: Erhalte 1 Mana.", "Kartentext: " + C("w_geist").text())
|
|
check(Cards.FIELD_COMBO_TEXT["Sturm"] == "Erhalte 1 Mana", "Sturm gibt Mana")
|
|
|
|
|
|
|
|
func test_gleiche_regeln() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 0
|
|
var g := Game.new_battle(3, r)
|
|
var first: Game.Player = g.players[0]
|
|
check(g.current() == first and first.max_energy == 1 and first.energy == 1, "Startspieler: 1 Mana, kein Bonus")
|
|
check(first.shop.size() == Game.SHOP_SIZE and first.hand.is_empty(), "jeder beginnt mit einem Laden und leerer Hand")
|
|
for p in g.players:
|
|
check(p.life == Game.START_LIFE, "gleiches Leben")
|
|
if p != first:
|
|
check(p.start_bonus == 1, "Nachzieher: Bonus offen")
|
|
g.end_turn()
|
|
var second: Game.Player = g.players[1]
|
|
check(g.current() == second and second.max_energy == 1 and second.energy == 2, "Nachzieher: 1 Mana plus Ausgleich")
|
|
for i in 4:
|
|
g.end_turn()
|
|
check(g.current() == second and second.max_energy == 2, "Ausgleich nur einmal")
|
|
|
|
|
|
func test_zugreihenfolge() -> void:
|
|
var starters := {}
|
|
for seed in 12:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = seed
|
|
var g := Game.new_battle(2, r)
|
|
starters[g.players[0].name] = true
|
|
check(g.players.size() == 3 and g.human in g.players and g.opponents.size() == 2, "alle am Tisch")
|
|
for i in g.players.size():
|
|
check(g.players[i].order == i, "Reihenfolge nummeriert")
|
|
check(g.log[0].begins_with("Zugreihenfolge:"), "Reihenfolge im Verlauf")
|
|
var g2 := g.clone()
|
|
check(g2.players.map(func(p): return p.name) == g.players.map(func(p): return p.name), "Kopie behält Reihenfolge")
|
|
check(starters.size() > 1, "Startspieler wechselt (%d verschiedene)" % starters.size())
|
|
|
|
|
|
func test_tote_gegner() -> void:
|
|
var s := setup([], [], 2)
|
|
var g: Game = s[0]
|
|
s[2][0].life = 0
|
|
g.end_turn()
|
|
check(g.current() == s[2][1], "toter Gegner übersprungen")
|
|
g.end_turn()
|
|
check(g.current() == s[1] and g.turn == 2, "zurück beim Spieler, Zug 2")
|
|
|
|
|
|
func test_jeder_gegen_jeden() -> void:
|
|
var s := setup([], [], 2)
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o1: Game.Player = s[2][0]
|
|
var o2: Game.Player = s[2][1]
|
|
check(g.enemies_of(o1) == [h, o2], "Gegner bekämpft Spieler und anderen Gegner")
|
|
check(g.enemies_of(h) == [o1, o2], "Spieler bekämpft alle Gegner")
|
|
g.end_turn()
|
|
o1.hand = []
|
|
var a := put(o1, "f_wolf")
|
|
check(g.can_attack(a, o2)[0], "Gegner darf Gegner angreifen")
|
|
check(g.attack(a, o2) == "" and o2.life == 12, "Gegner trifft Gegner")
|
|
check(AI.choose_action(g, o1).type == "buy", "KI kauft aus ihrem Laden")
|
|
o1.shop = []
|
|
check(AI.choose_action(g, o1).type == "end", "ohne Angebot und ohne Angriff endet der Zug")
|
|
o2.life = 2
|
|
var b := put(o1, "f_kobold")
|
|
check(AI.choose_action(g, o1).target == o2, "KI nutzt Lethal gegen anderen Gegner")
|
|
|
|
|
|
func test_gegnerzahl() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 2
|
|
for n in range(1, Game.MAX_OPPONENTS + 1):
|
|
var g := Game.new_battle(n, r)
|
|
check(g.opponents.size() == n, "%d Gegner" % n)
|
|
check(g.players.all(func(o): return o.life == Game.START_LIFE), "gleiches Leben bei %d" % n)
|
|
check(g.players[0].max_energy == 1 and g.current() == g.players[0], "Startspieler bei %d" % n)
|
|
check(g.human.shop.size() == Game.SHOP_SIZE, "Laden bei %d" % n)
|
|
check(Profile.REWARD_WIN.has(n), "Belohnung bei %d" % n)
|
|
|
|
|
|
func test_sieg_niederlage() -> void:
|
|
var s := setup([], [], 2)
|
|
var g: Game = s[0]
|
|
g.damage(s[2][0], 99)
|
|
g.check_end()
|
|
check(g.winner == "", "noch offen")
|
|
g.damage(s[2][1], 99)
|
|
g.check_end()
|
|
check(g.winner == "human", "Sieg")
|
|
var s2 := setup()
|
|
var g2: Game = s2[0]
|
|
g2.damage(s2[1], 30)
|
|
g2.check_end()
|
|
check(g2.winner == "opponents", "Niederlage")
|
|
|
|
|
|
## Booster geben Seltenheitsstufen: höhere werten auf, der Rest wird zu Staub.
|
|
func test_booster() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 3
|
|
for i in 50:
|
|
var rolled := Profile.roll_booster(r)
|
|
check(rolled.size() == 5, "5 Karten")
|
|
check(rolled.any(func(x): return x.rarity != "gewöhnlich"), "mindestens eine seltene")
|
|
var p := Profile.new()
|
|
p.coins = Profile.BOOSTER_COST
|
|
var got := p.open_booster(r)
|
|
check(p.coins == 0 and got.size() == 5, "Booster kostet %d" % Profile.BOOSTER_COST)
|
|
var upgraded := got.filter(func(x): return x.upgraded)
|
|
check(upgraded.all(func(x): return p.rarity(x.card.id) == x.rarity), "aufgewertete Karten tragen ihre Stufe")
|
|
check(p.dust == got.reduce(func(a, x): return a + int(x.dust), 0), "der Rest wird zu Staub")
|
|
check(p.open_booster(r).is_empty(), "ohne Münzen kein Booster")
|
|
# Aufwerten und zerlegen
|
|
p.dust = 100000
|
|
check(p.upgrade("f_wolf") == "" and p.level("f_wolf") >= 1, "aufgewertet")
|
|
var lvl := p.level("f_wolf")
|
|
check(p.dismantle("f_wolf") == "" and p.level("f_wolf") == lvl - 1, "eine Stufe abgegeben")
|
|
p.collection["f_wolf"] = Cards.RARITIES.size() - 1
|
|
check(p.upgrade("f_wolf") != "", "über die höchste Stufe hinaus geht nicht")
|
|
p.collection.erase("f_wolf")
|
|
check(p.dismantle("f_wolf") != "" and p.rarity("f_wolf") == "gewöhnlich", "gewöhnliche Karten gehören allen")
|
|
|
|
|
|
func test_spielstand() -> void:
|
|
var path := "user://test_spielstand.json"
|
|
var abs := ProjectSettings.globalize_path(path)
|
|
for suffix in ["", ".bak"]:
|
|
if FileAccess.file_exists(path + suffix):
|
|
DirAccess.remove_absolute(abs + suffix)
|
|
var p := Profile.load_or_new(path)
|
|
check(p.coins == Profile.START_COINS and p.collection.is_empty(), "Starterprofil: alle Karten gewöhnlich")
|
|
check(FileAccess.file_exists(path), "Datei angelegt")
|
|
p.coins = 999
|
|
p.collection["f_wolf"] = 2
|
|
p.save(path)
|
|
var p2 := Profile.load_or_new(path)
|
|
check(p2.coins == 999 and p2.level("f_wolf") == 2, "geladen")
|
|
for suffix in ["", ".bak"]:
|
|
if FileAccess.file_exists(path + suffix):
|
|
DirAccess.remove_absolute(abs + suffix)
|
|
|
|
|
|
func test_ki_lethal() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
o.life = 3
|
|
var a := put(h, "f_asche")
|
|
put(o, "w_reiter")
|
|
var act := AI.choose_action(g, h)
|
|
check(act.type == "attack" and act.creature == a and act.target == o, "KI nutzt Lethal")
|
|
|
|
|
|
func test_ki_spiele_enden() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 7
|
|
for n in [1, 3, 5]:
|
|
for i in 6:
|
|
var pool: Array = Cards.ELEMENTS.duplicate()
|
|
Game.shuffle(pool, r)
|
|
var g := Game.new_battle(n, r)
|
|
var guard := 0
|
|
while g.winner == "" and guard < 300:
|
|
AI.take_turn(g, g.current(), i % 2 == 0)
|
|
guard += 1
|
|
check(g.winner != "", "Spiel endet (%d Gegner)" % n)
|
|
|
|
|
|
func test_resonanz() -> void:
|
|
var s := setup(["f_wolf", "f_kobold", "e_wall"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
g.play_card(h, C("f_wolf"))
|
|
check(h.board[0].power() == 3, "allein keine Resonanz")
|
|
g.play_card(h, C("f_kobold"))
|
|
check(h.board[0].power() == 4 and h.board[1].power() == 3, "Feuer+Feuer: beide +1")
|
|
g.play_card(h, C("e_wall"))
|
|
check(h.board[2].power() == 0, "Erde allein: kein Bonus")
|
|
check(C("f_wolf").text() == "", "Resonanz ist Regel, kein Kartentext")
|
|
var third := put(h, "f_asche")
|
|
g.update_resonance()
|
|
check(h.board[0].power() == 5 and third.power() == 6, "drei Feuer-Wesen: +2")
|
|
put(h, "f_kobold")
|
|
put(h, "f_kobold")
|
|
put(h, "f_kobold")
|
|
g.update_resonance()
|
|
check(h.board[0].power() == 6 and Game.resonance_for(1) == 1 and Game.resonance_for(3) == 3 and Game.resonance_for(4) == 3, "Resonanz höchstens +3")
|
|
for i in [1, 3, 4, 5, 6]:
|
|
h.board[i].health = 0
|
|
g.cleanup()
|
|
check(h.board[0].power() == 3, "Resonanz sinkt mit den Partnern")
|
|
|
|
|
|
## Elementkreis: Wasser > Feuer > Luft > Erde > Wasser, im Vorteil +1 Schaden, im Nachteil -1.
|
|
func test_elementvorteil() -> void:
|
|
check(Cards.matchup(["Wasser"], ["Feuer"]) == 1 and Cards.matchup(["Feuer"], ["Wasser"]) == -1, "Wasser schlägt Feuer")
|
|
check(Cards.matchup(["Feuer"], ["Luft"]) == 1 and Cards.matchup(["Luft"], ["Erde"]) == 1 and Cards.matchup(["Erde"], ["Wasser"]) == 1, "der Kreis schließt sich")
|
|
check(Cards.matchup(["Feuer"], ["Feuer"]) == 0 and Cards.matchup(["Feuer"], ["Erde"]) == 0, "gleiche und unbeteiligte Elemente: kein Vorteil")
|
|
check(Cards.matchup(["Feuer", "Wasser"], ["Feuer"]) == 1, "Mehrfachelemente nehmen den besseren Wert")
|
|
check(Cards.matchup([], ["Feuer"]) == 0 and Cards.matchup(["Feuer"], []) == 0, "ohne Element kein Vorteil")
|
|
var s := setup(["f_ritter"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var mine := put(h, "w_reiter") # Wasser 3/3
|
|
var his := put(o, "f_wolf") # Feuer 3/2
|
|
g.attack(mine, his)
|
|
check(his.health == -2 and mine.health == 1, "im Vorteil: 1 mehr ausgeteilt, 1 weniger eingesteckt")
|
|
check(Game.matchup_bonus(mine, o) == 0, "gegen Helden gibt es keinen Vorteil")
|
|
var life := o.life
|
|
g.play_card(h, C("f_ritter"), o) # Feuerwesen, 1 Schaden an den Helden
|
|
check(o.life == life - 1, "Effekte auf Helden bleiben unverändert")
|
|
s = setup(["f_ritter"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
var air := put(o, "l_laeufer") # Luft 3/4, Feuer schlägt Luft
|
|
g.play_card(h, C("f_ritter"), air)
|
|
check(air.health == 2, "Feuereffekt gegen Luft: 1 Schaden mehr")
|
|
|
|
|
|
## Das Feld gehört allen am Tisch und kippt, sobald ein Element genug Karten gesehen hat.
|
|
func test_feld() -> void:
|
|
var s := setup(["f_ritter"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
check(g.field_element == "" and int(g.field_charge["Feuer"]) == 0, "das Feld beginnt neutral")
|
|
var wolf := put(h, "f_wolf")
|
|
for i in Cards.FIELD_CHARGE - 1:
|
|
g.charge_field(C("f_kobold"))
|
|
check(g.field_element == "", "drei Karten reichen noch nicht")
|
|
g.charge_field(C("f_kobold"))
|
|
g.update_resonance()
|
|
check(g.field_element == "Feuer" and int(g.field_charge["Feuer"]) == 0, "die vierte Feuerkarte kippt das Feld, die Zähler beginnen von vorn")
|
|
check(g.log.any(func(l): return l.begins_with("Das Feld wird zu Vulkan")), "Feldwechsel im Verlauf")
|
|
check(wolf.field_bonus == 1 and wolf.power() == 4, "Feuerwesen bekommen +1 Angriff")
|
|
var stone := put(h, "e_wall")
|
|
var his := put(o, "f_kobold")
|
|
g.update_resonance()
|
|
check(stone.field_bonus == 0, "andere Elemente bleiben unberührt")
|
|
check(his.field_bonus == 1, "das Feld gilt auch für die Gegner")
|
|
var before := int(g.field_charge["Feuer"])
|
|
g.play_card(h, C("f_ritter"), o)
|
|
check(int(g.field_charge["Feuer"]) == before + 1, "jede gespielte Karte lädt ihr Element")
|
|
for i in Cards.FIELD_CHARGE:
|
|
g.charge_field(C("w_reiter"))
|
|
check(g.field_element == "Wasser" and wolf.field_bonus == 0, "das Feld kippt weiter und nimmt den alten Bonus mit")
|
|
|
|
|
|
## Verschmelzen: zwei eigene Wesen werden eins, Werte Durchschnitt plus 1.
|
|
func test_verschmelzen() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var a := put(h, "f_wolf") # Feuer 3/2, Kosten 2
|
|
var b := put(h, "w_reiter") # Wasser 3/3, Kosten 2
|
|
check(Cards.fusion_cost(a.card, b.card) == 2 and Cards.fusion_cost(C("f_kobold"), C("f_asche")) == 2, "Kosten: Durchschnitt, aufgerundet")
|
|
check(g.can_fuse(h, a, a)[0] == false and g.can_fuse(h, a, put(o, "f_kobold"))[0] == false, "nur zwei verschiedene eigene Wesen")
|
|
h.energy = 1
|
|
check(g.can_fuse(h, a, b)[0] == false, "zu wenig Mana")
|
|
h.energy = 5
|
|
check(g.fuse(h, a, b) == "" and h.board.size() == 1, "aus zwei Wesen wird eines")
|
|
var cr: Game.Creature = h.board[0]
|
|
check(h.energy == 3, "die Verschmelzung kostet Mana")
|
|
check(cr.card.elements == ["Feuer", "Wasser"], "es erbt beide Elemente")
|
|
check(cr.name == "Dampf", "es heißt nach seiner Kombo")
|
|
check(cr.attack == 4 and cr.health == 3 and cr.max_health == 3, "Werte: Durchschnitt plus 1")
|
|
check(g.log.any(func(l): return l.find("verschmelzen zu Dampf") >= 0), "Verschmelzung im Verlauf")
|
|
# Schlüsselwörter beider Karten wandern mit
|
|
s = setup()
|
|
g = s[0]
|
|
h = s[1]
|
|
h.energy = 10
|
|
var wall := put(h, "e_wall") # Erde 0/4, Wächter
|
|
var wind := put(h, "l_wind") # Luft 1/1, Flink
|
|
check(g.fuse(h, wall, wind) == "", "Erde und Luft verschmelzen")
|
|
var sand: Game.Creature = h.board[0]
|
|
check(sand.name == "Sand" and sand.taunt() and "Flink" in sand.card.keywords, "Wächter und Flink bleiben erhalten")
|
|
check(sand.attack == 1 and sand.health == 3, "0/4 und 1/1 werden 1/3")
|
|
# Das verschmolzene Wesen übersteht Speichern und Netzwerk
|
|
var back := NetState.from_snapshot(NetState.snapshot(g, -1))
|
|
var copy: Game.Creature = back.players[0].board[0]
|
|
check(copy.name == "Sand" and copy.attack == 1 and copy.health == 3 and copy.taunt(), "verschmolzene Karte übersteht den Snapshot")
|
|
# Platz auf dem Feld: fünf Wesen, verschmelzen macht einen Platz frei
|
|
s = setup(["f_kobold"])
|
|
g = s[0]
|
|
h = s[1]
|
|
h.energy = 10
|
|
for i in Game.MAX_BOARD:
|
|
put(h, "f_kobold")
|
|
check(g.can_play(h, C("f_kobold"))[0] == false, "das Feld ist voll")
|
|
check(g.fuse(h, h.board[0], h.board[1]) == "" and g.can_play(h, C("f_kobold"))[0], "verschmelzen macht Platz")
|
|
|
|
|
|
func test_feldkombos() -> void:
|
|
# Dampf: Feuer neben Wasser friert das stärkste gegnerische Wesen ein
|
|
var s := setup(["w_reiter"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
put(h, "f_wolf")
|
|
var weak := put(o, "f_kobold")
|
|
var strong := put(o, "e_troll")
|
|
g.play_card(h, C("w_reiter"))
|
|
check(strong.frozen and not weak.frozen, "Dampf friert das stärkste Wesen")
|
|
check(g.log.any(func(l): return l.begins_with("Feldkombo Dampf")), "Feldkombo im Verlauf")
|
|
# Lava: 1 Schaden an das stärkste gegnerische Wesen
|
|
s = setup(["e_golem"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
put(h, "f_wolf")
|
|
var k := put(o, "f_kobold")
|
|
var big := put(o, "e_troll")
|
|
g.play_card(h, C("e_golem"))
|
|
check(k in o.board and big.health == 5, "Lava trifft nur das stärkste Wesen")
|
|
# Blitz: 2 Schaden an den schwächsten Gegner
|
|
s = setup(["l_wind"], [], 2)
|
|
g = s[0]
|
|
h = s[1]
|
|
put(h, "f_wolf")
|
|
s[2][1].life = 5
|
|
g.play_card(h, C("l_wind"))
|
|
check(s[2][1].life == 3 and s[2][0].life == 15, "Blitz trifft den schwächsten Gegner")
|
|
# Schlamm heilt, Sturm gibt Mana, Sand gibt +1/+1
|
|
s = setup(["e_wall", "l_wind", "w_reiter"])
|
|
g = s[0]
|
|
h = s[1]
|
|
h.life = 20
|
|
g.play_card(h, C("w_reiter"))
|
|
g.play_card(h, C("e_wall"))
|
|
check(h.life == 23, "Schlamm heilt um 3")
|
|
var mana_before := h.energy
|
|
g.play_card(h, C("l_wind"))
|
|
check(h.energy == mana_before + 1, "Sturm gibt 1 Mana")
|
|
check(h.board[2].attack == 1, "nur eine Feldkombo je Ausspielen")
|
|
s = setup(["l_wind"])
|
|
g = s[0]
|
|
h = s[1]
|
|
put(h, "e_wall")
|
|
g.play_card(h, C("l_wind"))
|
|
check(h.board[1].attack == 2 and h.board[1].health == 2, "Sand: neues Wesen erhält +1/+1")
|
|
# Gleiche Elemente lösen keine Feldkombo aus
|
|
s = setup(["f_kobold"])
|
|
g = s[0]
|
|
h = s[1]
|
|
put(h, "f_wolf")
|
|
g.play_card(h, C("f_kobold"))
|
|
check(not g.log.any(func(l): return l.begins_with("Feldkombo")), "Feuer+Feuer ist Resonanz, keine Feldkombo")
|
|
# Ein Feuerwesen neben Wasser löst Dampf aus
|
|
s = setup(["fe_golem"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
put(h, "w_reiter")
|
|
var t := put(o, "e_troll")
|
|
g.play_card(h, C("fe_golem"))
|
|
check(t.frozen, "Lava-Wesen neben Wasser löst Dampf aus")
|
|
|
|
|
|
func test_schild_effekt_und_sonne() -> void:
|
|
var s := setup(["el_duenen", "fff_sonne"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var a := put(h, "f_wolf")
|
|
g.play_card(h, C("el_duenen"), a)
|
|
check(a.shield, "Dünenläufer gibt Schild")
|
|
# Frischer Tisch: die Sonne soll allein wirken, ohne Feldkombo mit eigenen Wesen
|
|
s = setup(["fff_sonne"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
check(C("fff_sonne").elements == ["Feuer"] and C("fff_sonne").cost == 9, "Kern der Sonne ist ein teures Feuerwesen")
|
|
var troll := put(o, "e_troll")
|
|
g.play_card(h, C("fff_sonne"))
|
|
check(o.life == 10 and troll.health == 1, "Kern der Sonne: 5 Schaden an alle")
|
|
|
|
|
|
func test_kopie() -> void:
|
|
var s := setup(["f_wolf"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var a := put(h, "f_asche")
|
|
var g2 := g.clone()
|
|
var a2 := g2.find_creature(a.uid)
|
|
check(a2 != null and a2 != a and a2.owner() == g2.players[0], "Wesen kopiert mit neuem Besitzer")
|
|
g2.attack(a2, g2.players[1])
|
|
check(g2.players[1].life == 11 and o.life == 15, "Kopie verändert das Original nicht")
|
|
g2.play_card(g2.players[0], C("f_wolf"))
|
|
check(h.hand.size() == 1 and g2.players[0].hand.is_empty(), "Hand der Kopie unabhängig")
|
|
check(g2.log.is_empty() and g2.events.is_empty(), "Kopie schreibt keinen Verlauf")
|
|
|
|
|
|
func test_ki_besser_als_gierig() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 11
|
|
var smart := 0
|
|
var games := 40
|
|
for i in games:
|
|
var pool: Array = Cards.ELEMENTS.duplicate()
|
|
Game.shuffle(pool, r)
|
|
var g := Game.new_battle(1, r)
|
|
# abwechselnd beginnt die schlaue oder die gierige KI
|
|
var smart_is_human := i % 2 == 0
|
|
var guard := 0
|
|
while g.winner == "" and guard < 300:
|
|
var p := g.current()
|
|
AI.take_turn(g, p, p.is_human != smart_is_human)
|
|
guard += 1
|
|
if (g.winner == "human") == smart_is_human:
|
|
smart += 1
|
|
check(smart >= games / 2, "Vorausschau-KI ist mindestens so gut wie die gierige (%d von %d)" % [smart, games])
|
|
|
|
|
|
## Alte Stände behalten Münzen, Staub, Kampagne und Einstellungen; die alte Sammlung entfällt.
|
|
func test_spielstand_migration() -> void:
|
|
var alt := {"version": 5, "coins": 300, "dust": 70, "campaign_stage": 2,
|
|
"collection": {"f_kobold": 2}, "decks": [], "settings": {"volume": 0.3}}
|
|
var p := Profile.from_dict(alt)
|
|
check(p.coins == 300 and p.dust == 70 and p.campaign_stage == 2, "alter Stand gelesen")
|
|
check(p.collection.is_empty(), "Sammlung auf Standard")
|
|
check(is_equal_approx(p.settings.volume, 0.3) and p.settings.animations, "Einstellungen übernommen")
|
|
p.collection["f_wolf"] = 3
|
|
var d := p.to_dict()
|
|
check(d.version == Profile.SAVE_VERSION, "Version gespeichert")
|
|
var p2 := Profile.from_dict(JSON.parse_string(JSON.stringify(d)))
|
|
check(p2.level("f_wolf") == 3 and p2.campaign_stage == 2, "neuer Stand gelesen")
|
|
|
|
|
|
func test_kampagne() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 4
|
|
check(Campaign.STAGES.size() == 10, "zehn Stufen")
|
|
var opps := Campaign.opponents(4)
|
|
check(opps.size() == 3 and opps[0].name == "Vulkar" and opps[0].title == "Herr der Lava", "Stufe 5: Vulkar mit 2 Begleitern")
|
|
check(opps[0].elements == ["Feuer", "Erde"] and opps[0].shop.all(func(c): return c.elements.all(func(e): return e in ["Feuer", "Erde"])), "Boss handelt nur mit seinen Elementen")
|
|
var g := Game.new_battle_with(opps, r)
|
|
check(g.opponents.size() == 3 and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet")
|
|
var p := Profile.starter()
|
|
var reward := p.record_battle(true, 1, 0)
|
|
check(p.campaign_stage == 1 and reward == Campaign.STAGES[0].reward, "Stufe 1 abgeschlossen")
|
|
reward = p.record_battle(true, 1, 0)
|
|
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN[1], "Wiederholung gibt normale Belohnung")
|
|
reward = p.record_battle(false, 2, 1)
|
|
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
|
|
p.campaign_stage = 9
|
|
p.record_battle(true, 5, 9)
|
|
check(p.campaign_stage == 10, "Kampagne abgeschlossen")
|
|
var sim := Game.new_battle_with(Campaign.opponents(0), r)
|
|
var guard := 0
|
|
while sim.winner == "" and guard < 300:
|
|
AI.take_turn(sim, sim.current(), true)
|
|
guard += 1
|
|
check(sim.winner != "", "Kampagnenduell endet")
|
|
|
|
|
|
func test_zufallssamen() -> void:
|
|
Game.test_seed = 77
|
|
var a := Game.make_rng().randi()
|
|
Game.test_seed = 77
|
|
var b := Game.make_rng().randi()
|
|
Game.test_seed = 0
|
|
check(a == b, "fester Samen wiederholbar")
|
|
var c := Game.make_rng().randi()
|
|
var d := Game.make_rng().randi()
|
|
check(c != d, "ohne Samen echter Zufall")
|
|
|
|
|
|
func test_spielstand_sicherung() -> void:
|
|
var path := "user://test_sicherung.json"
|
|
var abs := ProjectSettings.globalize_path(path)
|
|
for suffix in ["", ".bak", ".corrupt"]:
|
|
if FileAccess.file_exists(path + suffix):
|
|
DirAccess.remove_absolute(abs + suffix)
|
|
var p := Profile.load_or_new(path)
|
|
p.coins = 123
|
|
p.save(path)
|
|
p.coins = 456
|
|
p.save(path)
|
|
check(FileAccess.file_exists(path + ".bak"), "Sicherung angelegt")
|
|
check(Profile.load_or_new(path).coins == 456 and Profile.load_or_new(path + ".bak").coins == 123, "Sicherung ist der Vorgänger")
|
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
|
f.store_string("{kaputt")
|
|
f.close()
|
|
check(Profile.load_or_new(path).coins == 123, "kaputter Stand: Sicherung greift")
|
|
for suffix in ["", ".bak", ".corrupt"]:
|
|
if FileAccess.file_exists(path + suffix):
|
|
DirAccess.remove_absolute(abs + suffix)
|
|
|
|
|
|
func test_merkmale() -> void:
|
|
var s := setup(["f_wolf", "l_falke", "e_wall"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
h.perks = ["feuer_angriff", "flink_angriff", "waechter_leben", "schild_wesen", "alle_flink", "resonanz_doppelt"]
|
|
g.play_card(h, C("f_wolf"))
|
|
var wolf: Game.Creature = h.board[0]
|
|
check(wolf.power() == 4 and wolf.shield and wolf.can_attack(), "Feuer +1, Schild, sofort bereit")
|
|
g.play_card(h, C("l_falke"))
|
|
check(h.board[1].power() == 4, "Flink +1")
|
|
g.play_card(h, C("e_wall"))
|
|
check(h.board[2].health == 6 and h.board[2].max_health == 6, "Wächter +2 Leben")
|
|
put(h, "f_kobold")
|
|
g.update_resonance()
|
|
check(wolf.resonance == 2 and wolf.power() == 6, "Resonanz doppelt")
|
|
for perk in Game.PERKS:
|
|
check(Campaign.STAGES.any(func(st): return st.perk == perk), "Merkmal vergeben: " + perk)
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 2
|
|
var mara: Game.Player = Campaign.opponents(1)[0]
|
|
check(mara.life == 35 and mara.perks == ["leben_35"], "Mara hat 35 Leben")
|
|
var g3 := Game.new_battle_with(Campaign.opponents(9), r, true)
|
|
var archi: Game.Player = g3.opponents[0]
|
|
check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt")
|
|
g3.end_turn()
|
|
check(g3.current() == archi and archi.energy == 3 + 1, "Archimagier beginnt mit 3 Mana (plus Ausgleich)")
|
|
var nyx: Game.Player = Campaign.opponents(5)[0]
|
|
var g4 := Game.new_battle_with([nyx], r, true)
|
|
g4.end_turn()
|
|
# Einkommen 1, dazu 1 Mana für ihr Merkmal und 1 als Ausgleich für den späteren Start
|
|
check(nyx.max_energy == 1 and nyx.energy == 3, "Nyx bekommt 1 Mana mehr als ihr Einkommen")
|
|
|
|
|
|
func test_ki_zwei_schritte() -> void:
|
|
# Gegner hat 5 Leben, das eigene Wesen kann noch nicht angreifen, der Wüstenkönig (flink, 5 Angriff)
|
|
# liegt auf der Hand: erst ausspielen, dann mit ihm töten.
|
|
var s := setup(["el_koenig"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
h.turns = 2
|
|
o.life = 5
|
|
put(h, "f_wolf", false)
|
|
var act := AI.choose_action(g, h)
|
|
check(act.type == "play" and act.card == C("el_koenig"), "KI spielt zuerst den König")
|
|
AI.apply_action(g, h, act)
|
|
var act2 := AI.choose_action(g, h)
|
|
check(act2.type == "attack" and act2.target == o, "dann tödlicher Angriff")
|
|
AI.apply_action(g, h, act2)
|
|
check(g.winner == "human", "Zwei-Schritt-Plan gewinnt")
|
|
|
|
|
|
func test_snapshot() -> void:
|
|
var s := setup(["f_wolf"], ["w_frost"], 2)
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
g.play_card(h, C("f_wolf"))
|
|
put(o, "e_troll")
|
|
var hi := g.players.find(h)
|
|
var oi := g.players.find(o)
|
|
g.roll_shop(h)
|
|
g.roll_shop(o)
|
|
var snap := g.snapshot(hi)
|
|
var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
|
|
var g2 := Game.from_snapshot(json)
|
|
check(g2.players.size() == 3 and g2.turn == g.turn and g2.current_idx == g.current_idx, "Grunddaten")
|
|
check(g2.players[hi].hand.is_empty(), "eigene Hand sichtbar")
|
|
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0] != C("w_frost"), "fremde Hand nur als Anzahl")
|
|
check(g2.players[oi].shop.is_empty() and g2.players[hi].shop.size() == Game.SHOP_SIZE, "eigener Laden sichtbar, fremder nicht")
|
|
var cr2 := g2.find_creature(h.board[0].uid)
|
|
check(cr2 != null and cr2.card.id == "f_wolf" and cr2.owner() == g2.players[hi] and not cr2.ready, "Wesen mit Besitzer und Zustand")
|
|
check(g2.events.size() == g.events.size() and g2.events.back().type == g.events.back().type, "Ereignisse übertragen")
|
|
check(g2.valid_targets(g2.players[hi], "enemy_creature").size() == 1 and g2.buy(g2.players[oi], 0) == "Nicht dein Zug", "Kopie kennt die Regeln")
|
|
var snap_o := g.snapshot(oi)
|
|
check(snap_o.players[hi].hand is int and snap_o.players[oi].hand is Array, "Sicht des Gegners")
|
|
|
|
|
|
func test_kapitel_zwei_und_merkmalwahl() -> void:
|
|
check(Campaign.total() == 30 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(29) == 3, "dreißig Stufen in drei Kapiteln")
|
|
check(Campaign.stage(25).perks.size() == 3 and Campaign.stage(25).name == "Nyx", "Kapitel 3 mit drei Merkmalen")
|
|
for st in Campaign.STAGES3:
|
|
for pk in st.perks:
|
|
check(Game.PERKS.has(pk), "Merkmal %s bekannt" % pk)
|
|
for i in Campaign.total():
|
|
var st := Campaign.stage(i)
|
|
check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1))
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 8
|
|
var opps := Campaign.opponents(19)
|
|
check(opps.size() == 5 and opps[0].name == "Archimagier" and opps[0].perks.size() == 3, "letzte Stufe: Archimagier mit drei Merkmalen, voller Tisch")
|
|
var p := Profile.starter()
|
|
check(not p.perk_unlocked(), "Merkmalwahl erst nach Kapitel 1")
|
|
p.campaign_stage = 10
|
|
check(p.perk_unlocked(), "nach Kapitel 1 freigeschaltet")
|
|
p.perk = "feuer_angriff"
|
|
var g := Game.new_battle(1, r, {"human_perks": [p.perk]})
|
|
check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Spieler: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf")
|
|
p.campaign_stage = 19
|
|
p.record_battle(true, 5, 19)
|
|
check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen")
|
|
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
|
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen")
|
|
|
|
|
|
func test_ki_zeitbudget() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 12
|
|
var g := Game.new_battle(5, r)
|
|
var h: Game.Player = g.players[0]
|
|
h.energy = 10
|
|
h.max_energy = 10
|
|
for i in 8:
|
|
h.hand.append(Cards.by_id("f_ritter"))
|
|
for p in g.players:
|
|
for i in 3:
|
|
put(p, "e_troll")
|
|
AI.time_budget_ms = 1
|
|
var t0 := Time.get_ticks_msec()
|
|
var act := AI.choose_action(g, h)
|
|
var ms := Time.get_ticks_msec() - t0
|
|
AI.time_budget_ms = 150
|
|
check(act.type in ["play", "attack", "end"] and ms < 80, "Zeitbudget greift (%d ms)" % ms)
|
|
check(Cards.by_id("wl_gischt").keywords == ["Flink"] and Cards.by_id("wl_gischt").elements == ["Wasser"], "Gischtläufer ist Wasser")
|
|
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(Profile.starter().to_dict())))
|
|
check(v.settings.has("big_font") and v.coins == Profile.START_COINS, "Starterstand gelesen")
|
|
|
|
|
|
## Englisch: jede Karte hat einen englischen Namen, Kartentexte und Schlüsselwörter werden übersetzt.
|
|
func test_englische_karten() -> void:
|
|
check(Cards.all().all(func(c): return c.name_en != ""), "alle Karten mit englischem Namen")
|
|
var de: String = Cards.by_id("f_ritter").text()
|
|
TranslationServer.set_locale("en")
|
|
var ritter := Cards.by_id("f_ritter")
|
|
var wall := Cards.by_id("e_wall")
|
|
check(ritter.title() == "Blazing Knight" and ritter.text() == "Swift. On play: 1 damage to a target.", "Ritter auf Englisch: %s / %s" % [ritter.title(), ritter.text()])
|
|
check(wall.text() == "Guardian.", "Schlüsselwort auf Englisch")
|
|
TranslationServer.set_locale("de")
|
|
check(ritter.title() == "Lodernder Ritter" and ritter.text() == de, "zurück auf Deutsch")
|
|
|
|
|
|
## Voller Spielstand (viewer -1) stellt Hände, Decks und Bretter wieder her; Zuschauer (-2) sehen nichts Verborgenes.
|
|
func test_kampf_speichern() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 7
|
|
var g := Game.new_battle(2, r)
|
|
g.start()
|
|
AI.take_turn(g, g.current())
|
|
var d: Dictionary = JSON.parse_string(JSON.stringify(g.snapshot(-1)))
|
|
var g2 := Game.from_snapshot(d)
|
|
var same := g2.players.size() == g.players.size()
|
|
for i in g.players.size():
|
|
var a: Game.Player = g.players[i]
|
|
var b: Game.Player = g2.players[i]
|
|
var ids := func(cs: Array): return cs.map(func(c): return c.id)
|
|
same = same and ids.call(a.hand) == ids.call(b.hand) \
|
|
and a.life == b.life and a.board.size() == b.board.size() and a.is_human == b.is_human
|
|
check(same and g2.current_idx == g.current_idx and g2.turn == g.turn and g2.human.is_human, "voller Spielstand: Hände, Decks und Bretter erhalten")
|
|
var guard := 0
|
|
while g2.winner == "" and guard < 300:
|
|
AI.take_turn(g2, g2.current())
|
|
guard += 1
|
|
check(g2.winner != "", "fortgesetzte Partie ist bis zum Ende spielbar")
|
|
var s2: Dictionary = g.snapshot(-2)
|
|
check(s2.players.all(func(p): return not (p.hand is Array) and p.shop.is_empty()), "Zuschauer-Sicht ohne Hände und Läden")
|
|
|
|
|
|
## Jeder tr("...")-Text in der Oberfläche hat einen englischen Eintrag.
|
|
func test_uebersetzung_vollstaendig() -> void:
|
|
var keys := {}
|
|
var csv := FileAccess.get_file_as_string("res://translations/ui.csv")
|
|
for line in csv.split("\n"):
|
|
if line.is_empty() or line.begins_with("keys,"):
|
|
continue
|
|
var key := line
|
|
if line.begins_with("\""):
|
|
key = line.substr(1, line.find("\",", 1) - 1)
|
|
else:
|
|
key = line.substr(0, line.find(","))
|
|
keys[key] = true
|
|
var missing := []
|
|
for k in Cards.COMBOS.values() + Cards.ELEMENTS + Cards.KEYWORDS + Cards.RARITIES + Cards.FIELD_NAMES.values() + Cards.FIELD_TEXT.values():
|
|
if not keys.has(k):
|
|
missing.append(k)
|
|
var d := DirAccess.open("res://scripts/ui")
|
|
d.list_dir_begin()
|
|
var name := d.get_next()
|
|
while name != "":
|
|
if name.ends_with(".gd"):
|
|
var src := FileAccess.get_file_as_string("res://scripts/ui/" + name)
|
|
var rx := RegEx.new()
|
|
rx.compile("tr\\(\"([^\"]+)\"\\)")
|
|
for m in rx.search_all(src):
|
|
var k: String = m.get_string(1)
|
|
if not keys.has(k) and not (k in missing):
|
|
missing.append(k)
|
|
name = d.get_next()
|
|
var rx_cards := RegEx.new()
|
|
rx_cards.compile("tr\\(\"([^\"]+)\"\\)")
|
|
for m in rx_cards.search_all(FileAccess.get_file_as_string("res://scripts/cards.gd")):
|
|
if not keys.has(m.get_string(1)) and not (m.get_string(1) in missing):
|
|
missing.append(m.get_string(1))
|
|
check(missing.is_empty(), "fehlende Übersetzungen: %s" % [missing])
|