- Challenge samt Bildschirm, Menüpunkt, Erfolg "Wochenheld" und Spielstandfeld entfernt; record_battle verliert den challenge-Parameter, die Bestenliste bleibt - Lobby: Erklärzeile unter dem Titel weg, "KI-Gegner (nur Host)" heißt jetzt "KI-Gegner" Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
1438 lines
62 KiB
GDScript
1438 lines
62 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 deck := []
|
|
for i in 10:
|
|
deck.append(C("f_kobold"))
|
|
var human := Game.Player.new("Du", deck, true, 30)
|
|
var opps := []
|
|
for i in n_opp:
|
|
opps.append(Game.Player.new("Gegner %d" % (i + 1), deck, 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_elementbooster_und_deckcode()
|
|
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_zauber_ziel()
|
|
test_flaechenschaden()
|
|
test_zurueck_auf_hand()
|
|
test_buff_heilung()
|
|
test_limits()
|
|
test_leeres_deck()
|
|
test_gleiche_regeln()
|
|
test_zugreihenfolge()
|
|
test_tote_gegner()
|
|
test_jeder_gegen_jeden()
|
|
test_gegnerzahl()
|
|
test_sieg_niederlage()
|
|
test_deckregeln()
|
|
test_gegnerdeck()
|
|
test_booster()
|
|
test_spielstand()
|
|
test_resonanz()
|
|
test_feldkombos()
|
|
test_schild_effekt_und_dreifach()
|
|
test_kopie()
|
|
test_ki_lethal()
|
|
test_ki_besser_als_gierig()
|
|
test_ki_spiele_enden()
|
|
test_spielstand_migration()
|
|
test_erfolge_und_tagesaufgabe()
|
|
test_kampagne()
|
|
test_fallen()
|
|
test_ki_sieht_fallen_nicht()
|
|
test_zufallssamen()
|
|
test_deck_slots_und_werkstatt()
|
|
test_spielstand_sicherung()
|
|
test_mulligan()
|
|
test_merkmale()
|
|
test_ki_zwei_schritte()
|
|
test_snapshot()
|
|
test_statistik_pity_tagesaufgaben()
|
|
test_deck_umbenennen_und_auffuellen()
|
|
test_neue_fallen()
|
|
test_kapitel_zwei_und_merkmalwahl()
|
|
test_bestenliste()
|
|
test_deckstaerke_und_schwierigkeit()
|
|
test_starter_gegen_passende_gegner()
|
|
test_kurve_statistik_vorschlag_verbesserung()
|
|
test_wasser_antworten()
|
|
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() == 118, "118 Karten")
|
|
for key in Cards.COMBOS:
|
|
var found: bool = Cards.all().any(func(c): return c.is_combo() and Cards.combo_key(c.elements) == key)
|
|
check(found, "Kombo vorhanden: " + key)
|
|
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.rarity in Cards.RARITIES, "Seltenheit " + c.id)
|
|
check(not c.is_creature() or c.health > 0, "Leben " + c.id)
|
|
check(C("el_koenig").text() == "Wächter, Flink, Schild. Beim Ausspielen: Alle befreundeten Wesen erhalten +1/+1.", "Kartentext")
|
|
check(C("f_funke").text() == "2 Schaden an ein Ziel.", "Zaubertext")
|
|
check(C("ww_eiszeit").text() == "Friere alle gegnerischen Wesen ein, Ziehe 1 Karte.", "Eiszeit-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(C("ff_phoenix").is_combo() and C("ff_phoenix").combo() == "Inferno", "Phönix ist Inferno")
|
|
check(not C("f_funke").is_combo(), "Funke keine Kombo")
|
|
|
|
|
|
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 == 8 and h.board.size() == 1, "Energie bezahlt")
|
|
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_eis"])
|
|
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_eis"), a) == "", "Eis 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_zauber_ziel() -> void:
|
|
var s := setup(["w_eis", "f_funke"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
check(not g.can_play(h, C("w_eis"))[0], "Eis ohne Ziel")
|
|
check(g.can_play(h, C("f_funke"))[0], "Funke mit Held als Ziel")
|
|
g.play_card(h, C("f_funke"), o)
|
|
check(o.life == 13, "2 Schaden")
|
|
|
|
|
|
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(["l_stoss"])
|
|
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("l_stoss"), 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(["e_haerten", "e_baum"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var a := put(h, "f_wolf")
|
|
h.life = 20
|
|
g.play_card(h, C("e_haerten"), a)
|
|
check(a.attack == 5 and a.health == 4, "+2/+2")
|
|
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"))
|
|
g.draw(h)
|
|
check(h.hand.size() == Game.MAX_HAND, "Hand voll verbrennt")
|
|
|
|
|
|
func test_leeres_deck() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
h.deck = []
|
|
g.draw(h, 3)
|
|
check(h.life == 30 - 6, "Ermüdung 1+2+3")
|
|
|
|
|
|
func test_gleiche_regeln() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 0
|
|
var g := Game.new_battle(Profile.starter().deck_cards(), 3, r)
|
|
var first: Game.Player = g.players[0]
|
|
check(g.current() == first and first.max_energy == 1 and first.energy == 1, "Startspieler: 1 Energie, kein Bonus")
|
|
check(first.hand.size() == Game.START_HAND + 1, "Startspieler 5 Handkarten")
|
|
for p in g.players:
|
|
check(p.life == Game.START_LIFE, "gleiches Leben")
|
|
if p != first:
|
|
check(p.hand.size() == Game.START_HAND + 1 and p.start_bonus == 1, "Nachzieher: Karte mehr, 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 Energie plus Ausgleich")
|
|
check(second.hand.size() == Game.START_HAND + 2, "Nachzieher 6 Handkarten nach dem Ziehen")
|
|
for i in 4:
|
|
g.end_turn()
|
|
check(g.current() == second and second.energy == 2 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(Profile.starter().deck_cards(), 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 = []
|
|
o1.mulligan_done = true
|
|
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")
|
|
var act := AI.choose_action(g, o1)
|
|
check(act.type == "end", "KI-Zug endet ohne weitere Aktion")
|
|
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(Profile.starter().deck_cards(), 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.hand.size() == Game.START_HAND + 1, "Handkarten 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")
|
|
|
|
|
|
func test_deckregeln() -> void:
|
|
var starter := Profile.starter().deck_cards()
|
|
check(Game.validate_deck(starter)[0], "Starterdeck gültig")
|
|
var d := []
|
|
for i in 19:
|
|
d.append(C("f_kobold"))
|
|
check(not Game.validate_deck(d)[0], "19 Karten ungültig")
|
|
d = []
|
|
for i in 4:
|
|
d.append(C("f_kobold"))
|
|
for i in 16:
|
|
d.append(C("f_wolf"))
|
|
check(not Game.validate_deck(d)[0], "4 Kopien ungültig")
|
|
d = []
|
|
for cid in ["f_kobold", "f_wolf", "f_asche", "f_funke", "w_geist", "w_reiter"]:
|
|
for i in 3:
|
|
d.append(C(cid))
|
|
d.append(C("e_wall"))
|
|
d.append(C("e_wall"))
|
|
check("Elemente" in Game.validate_deck(d)[1], "3 Elemente ungültig")
|
|
check(not Game.validate_deck(starter, {"f_kobold": 1})[0], "nicht besessen")
|
|
|
|
|
|
func test_gegnerdeck() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 1
|
|
for e in Cards.ELEMENTS:
|
|
var deck := Game.build_opponent_deck(r, [e])
|
|
check(Game.validate_deck(deck)[0], "Mono-Gegnerdeck gültig: " + e)
|
|
check(Game.deck_elements(deck) == [e], "nur Element " + e)
|
|
check(Game.validate_deck(Game.build_opponent_deck(r, ["Feuer", "Luft"]))[0], "Zwei-Element-Gegnerdeck")
|
|
|
|
|
|
func test_booster() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 3
|
|
for i in 50:
|
|
var cards := Profile.roll_booster(r)
|
|
check(cards.size() == 5, "5 Karten")
|
|
check(cards.any(func(c): return c.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 100")
|
|
var total := 0
|
|
for cid in p.collection:
|
|
total += p.collection[cid]
|
|
check(total == 5, "Sammlung +5")
|
|
check(p.open_booster(r).is_empty(), "ohne Münzen kein Booster")
|
|
|
|
|
|
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.deck_valid()[0], "Starterprofil gültig")
|
|
check(FileAccess.file_exists(path), "Datei angelegt")
|
|
p.coins = 999
|
|
p.save(path)
|
|
var p2 := Profile.load_or_new(path)
|
|
check(p2.coins == 999 and p2.deck.size() == 20 and p2.collection.size() == Cards.of_rarity("gewöhnlich").size() + 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(Game.build_opponent_deck(r, [pool[0], pool[1]]), 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() == "" and Cards.RESONANCE_TEXT.begins_with("Resonanz"), "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")
|
|
|
|
|
|
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 zieht, 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 hand_before := h.hand.size()
|
|
g.play_card(h, C("l_wind"))
|
|
check(h.hand.size() == hand_before, "Sturm zieht 1 (eine gespielt, eine gezogen)")
|
|
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")
|
|
# Kombokarte bringt ihr zweites Element mit
|
|
s = setup(["fw_wolke", "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_dreifach() -> void:
|
|
var s := setup(["e_steinhaut", "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("e_steinhaut"), a)
|
|
check(a.shield, "Steinhaut gibt Schild")
|
|
var sonne := C("fff_sonne")
|
|
check(sonne.rarity == "mythisch" and sonne.combo() == "Sonne" and sonne.is_combo(), "Dreifach-Kombo Sonne")
|
|
check(Cards.of_rarity("mythisch").size() == 4, "vier mythische Karten")
|
|
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")
|
|
check(Cards.combo_name(["Erde", "Erde", "Erde"]) == "Gebirge", "Dreifach-Name")
|
|
|
|
|
|
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(Game.build_opponent_deck(r, [pool[0], pool[1]]), 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])
|
|
|
|
|
|
func test_spielstand_migration() -> void:
|
|
var alt := {"coins": 300, "collection": {"f_kobold": 2, "unbekannt": 1}, "deck": ["f_kobold", "f_kobold"]}
|
|
var p := Profile.from_dict(alt)
|
|
check(p.coins == 300 and p.collection == {"f_kobold": 2} and p.deck.size() == 2, "Version 1 gelesen")
|
|
check(p.stats.games == 0 and p.achievements.is_empty() and p.campaign_stage == 0, "Standardwerte ergänzt")
|
|
check(p.settings.volume == 0.8 and p.settings.animations, "Einstellungen Standard")
|
|
p.stats.wins = 3
|
|
p.achievements.append("erster_sieg")
|
|
p.campaign_stage = 2
|
|
p.settings.volume = 0.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.stats.wins == 3 and p2.achievements == ["erster_sieg"] and p2.campaign_stage == 2, "Version 2 gelesen")
|
|
check(is_equal_approx(p2.settings.volume, 0.3), "Einstellungen gelesen")
|
|
|
|
|
|
func test_erfolge_und_tagesaufgabe() -> void:
|
|
Profile.today_override = "2026-09-16"
|
|
var p := Profile.starter()
|
|
var coins := p.coins
|
|
var reward := p.record_battle(true, 1, p.deck_cards())
|
|
check(reward == Profile.REWARD_WIN[1], "Duell-Belohnung")
|
|
check("erster_sieg" in p.achievements and "duellant" in p.achievements, "Erfolge Erster Sieg und Duellant")
|
|
check(not ("tischherr" in p.achievements), "Herr des Tisches noch offen")
|
|
check(p.coins == coins + reward + 100 + (p.daily_task().reward if p.daily.done else 0), "Münzen inklusive Erfolge")
|
|
check(p.notices.size() >= 2, "Meldungen vorhanden")
|
|
p.take_notices()
|
|
check(p.notices.is_empty(), "Meldungen abgeholt")
|
|
check(p.daily.date == "2026-09-16" and p.daily.task != "", "Tagesaufgabe gesetzt")
|
|
# Tagesaufgabe erfüllen (je nach Aufgabe: Sieg, zwei Kämpfe oder Booster)
|
|
var task := p.daily_task()
|
|
if not p.daily.done:
|
|
if task.kind == "game":
|
|
p.record_battle(false, 2, p.deck_cards())
|
|
elif task.kind == "booster":
|
|
p.coins = 100
|
|
p.open_booster()
|
|
check(p.daily.done, "Tagesaufgabe erfüllt: " + task.text)
|
|
var progress_before: int = p.daily.progress
|
|
p.record_battle(true, 1, p.deck_cards())
|
|
check(p.daily.progress == progress_before, "erfüllte Aufgabe zählt nicht weiter")
|
|
Profile.today_override = "2026-09-17"
|
|
p.refresh_daily()
|
|
check(p.daily.date == "2026-09-17" and not p.daily.done and p.daily.progress == 0, "neuer Tag, neue Aufgabe")
|
|
Profile.today_override = ""
|
|
# Mono-Deck-Erfolg
|
|
var mono := []
|
|
for cid in ["f_funke", "f_kobold", "f_wolf", "f_feuerball", "f_asche", "f_glutpfeil", "f_elementar"]:
|
|
for i in 3:
|
|
mono.append(Cards.by_id(cid))
|
|
mono = mono.slice(0, 20)
|
|
p.record_battle(true, 3, mono)
|
|
check("elementarist" in p.achievements and "tischherr" in p.achievements, "Elementarist und Herr des Tisches")
|
|
check(p.stats.wins == 3 and p.stats.wins_by_opp["1"] == 2, "Statistik")
|
|
|
|
|
|
func test_kampagne() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 4
|
|
check(Campaign.STAGES.size() == 10, "zehn Stufen")
|
|
var opps := Campaign.opponents(4, r)
|
|
check(opps.size() == 3 and opps[0].name == "Vulkar" and opps[0].title == "Herr der Lava", "Stufe 5: Vulkar mit 2 Begleitern")
|
|
check(Game.deck_elements(opps[0].deck).all(func(e): return e in ["Feuer", "Erde"]), "Boss-Deck aus seinen Elementen")
|
|
var g := Game.new_battle_with(Profile.starter().deck_cards(), 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, p.deck_cards(), 0)
|
|
check(p.campaign_stage == 1 and reward == Campaign.STAGES[0].reward, "Stufe 1 abgeschlossen")
|
|
reward = p.record_battle(true, 1, p.deck_cards(), 0)
|
|
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN[1], "Wiederholung gibt normale Belohnung")
|
|
reward = p.record_battle(false, 2, p.deck_cards(), 1)
|
|
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
|
|
p.campaign_stage = 9
|
|
p.record_battle(true, 5, p.deck_cards(), 9)
|
|
check(p.campaign_stage == 10 and "kampagnenheld" in p.achievements, "Kampagne abgeschlossen")
|
|
var sim := Game.new_battle_with(Profile.starter().deck_cards(), Campaign.opponents(0, r), 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_fallen() -> void:
|
|
var s := setup(["f_falle"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
check(C("f_falle").is_trap() and C("f_falle").text().begins_with("Falle. Wenn ein gegnerisches Wesen"), "Fallentext")
|
|
check(g.play_card(h, C("f_falle")) == "" and h.traps.size() == 1 and h.hand.is_empty() and h.energy == 8, "Falle gelegt")
|
|
check(g.log.back() == "Du legt eine Falle.", "Falle bleibt im Verlauf verdeckt")
|
|
g.end_turn()
|
|
var a := put(o, "e_troll")
|
|
check(g.attack(a, h) == "", "Angriff in die Feuerfalle")
|
|
check(h.traps.is_empty() and a.health == 3 and h.life == 27, "Feuerfalle: 3 Schaden, Angriff geht durch")
|
|
# Frostfalle bricht den Angriff ab
|
|
s = setup(["w_falle"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
g.play_card(h, C("w_falle"))
|
|
g.end_turn()
|
|
a = put(o, "e_troll")
|
|
g.attack(a, h)
|
|
check(h.life == 30 and a.frozen and a.attacks_left == 0, "Frostfalle: Angriff verpufft, Angreifer eingefroren")
|
|
# Erdwall beschwört einen Steinwall
|
|
s = setup(["e_falle"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
g.play_card(h, C("e_falle"))
|
|
g.end_turn()
|
|
a = put(o, "f_wolf")
|
|
g.attack(a, h)
|
|
check(h.board.size() == 1 and h.board[0].name == "Steinwall" and h.life == 27, "Erdwall beschwört Steinwall")
|
|
# Windfalle schickt ein ausgespieltes Wesen zurück
|
|
s = setup(["l_falle"], ["f_wolf"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
g.play_card(h, C("l_falle"))
|
|
g.end_turn()
|
|
g.play_card(o, C("f_wolf"))
|
|
check(o.board.is_empty() and C("f_wolf") in o.hand and o.energy == 8, "Windfalle: Wesen zurück, Energie weg")
|
|
# Höchstens zwei Fallen
|
|
s = setup(["f_falle", "f_falle", "f_falle"])
|
|
g = s[0]
|
|
h = s[1]
|
|
g.play_card(h, C("f_falle"))
|
|
g.play_card(h, C("f_falle"))
|
|
check(not g.can_play(h, C("f_falle"))[0] and "Fallen" in g.can_play(h, C("f_falle"))[1], "höchstens 2 Fallen")
|
|
# Falle schützt auch Wesen: toter Angreifer trifft nicht mehr
|
|
s = setup(["f_falle"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
g.play_card(h, C("f_falle"))
|
|
var w := put(h, "e_wall")
|
|
g.end_turn()
|
|
a = put(o, "f_wolf")
|
|
g.attack(a, w)
|
|
check(not (a in o.board) and w.health == 4, "Feuerfalle tötet den Wolf, Steinwall bleibt heil")
|
|
|
|
|
|
func test_ki_sieht_fallen_nicht() -> void:
|
|
var s := setup(["w_falle"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
g.play_card(h, C("w_falle"))
|
|
var hi := g.players.find(h)
|
|
var oi := g.players.find(o)
|
|
check(g.clone().players[hi].traps.size() == 1, "volle Kopie kennt Fallen")
|
|
var g2 := g.clone_for(o)
|
|
check(g2.players[hi].traps.is_empty() and g2.players[oi].traps == o.traps, "KI-Kopie kennt fremde Fallen nicht")
|
|
g.end_turn()
|
|
o.hand = []
|
|
o.mulligan_done = true
|
|
var a := put(o, "e_troll")
|
|
var act := AI.choose_action(g, o)
|
|
check(act.type == "attack" and act.target == h, "KI greift trotz verdeckter Falle an")
|
|
check(g.clone_for(h).players[hi].traps.size() == 1, "eigene Fallen bleiben in der eigenen Kopie")
|
|
|
|
|
|
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_deck_slots_und_werkstatt() -> void:
|
|
var p := Profile.starter()
|
|
check(p.decks.size() == Profile.DECK_SLOTS and p.deck.size() == 20 and p.decks[1].cards.is_empty(), "fünf Slots, Deck 1 gefüllt")
|
|
p.active_deck = 1
|
|
check(p.deck.is_empty() and not p.deck_valid()[0] and p.deck_valid(0)[0], "aktives Deck wechselt")
|
|
p.deck = ["f_kobold"]
|
|
check(p.decks[1].cards == ["f_kobold"], "Zuweisung geht in den Slot")
|
|
p.active_deck = 0
|
|
# Migration aus Version 2 (ein Deck)
|
|
var v2 := {"version": 2, "coins": 5, "collection": {"f_kobold": 3}, "deck": ["f_kobold", "f_kobold"]}
|
|
var m := Profile.from_dict(v2)
|
|
check(m.decks[0].cards == ["f_kobold", "f_kobold"] and m.active_deck == 0 and m.dust == 0, "Version 2 wird zu Slot 1")
|
|
var v3 := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
|
check(v3.decks[1].cards == ["f_kobold"] and v3.deck_name(1) == "Deck 2", "Version 3 gelesen")
|
|
# Werkstatt
|
|
p.collection["f_kobold"] = 5
|
|
check(p.dismantle("f_kobold") == "" and p.collection["f_kobold"] == 4 and p.dust == 5, "Zerlegen bringt Staub")
|
|
check(p.dismantle("f_kobold", 3) != "" and p.collection["f_kobold"] == 4, "Deckbedarf schützt Karten")
|
|
check(p.dismantle("ff_phoenix") != "", "nichts zu zerlegen")
|
|
p.dust = 100
|
|
check(p.craft("ff_phoenix") != "" and p.dust == 100, "zu wenig Staub")
|
|
p.dust = 1600
|
|
check(p.craft("ff_phoenix") == "" and p.collection["ff_phoenix"] == 1 and p.dust == 0, "Herstellen")
|
|
p.collection["f_wolf"] = 6
|
|
var got := p.dismantle_extras()
|
|
check(got == 20 and p.collection["f_wolf"] == 3 and p.collection["f_kobold"] == 3, "Überschuss zerlegt")
|
|
|
|
|
|
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_mulligan() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 9
|
|
var g := Game.new_battle(Profile.starter().deck_cards(), 1, r)
|
|
var first: Game.Player = g.players[0]
|
|
var hand_before := first.hand.duplicate()
|
|
var swap := [hand_before[0], hand_before[1]]
|
|
check(g.mulligan(first, swap) == "", "Mulligan erlaubt im ersten Zug")
|
|
check(first.hand.size() == hand_before.size() and first.deck.size() == 20 - hand_before.size(), "gleich viele Karten nachgezogen")
|
|
check(g.mulligan(first, [first.hand[0]]) != "", "nur einmal")
|
|
check(g.log.any(func(l): return "tauscht 2 Karten" in l), "Mulligan im Verlauf")
|
|
g.end_turn()
|
|
var second: Game.Player = g.players[1]
|
|
check(g.mulligan(first, []) != "", "nicht im fremden Zug")
|
|
check(g.mulligan(second, []) == "" and second.mulligan_done, "Hand behalten zählt als Mulligan")
|
|
for i in 2:
|
|
g.end_turn()
|
|
check(g.mulligan(g.current(), []) != "", "nicht mehr im zweiten Zug")
|
|
# KI tauscht teure Karten
|
|
var s := setup([], ["ff_phoenix", "f_kobold", "ww_leviathan"])
|
|
var g2: Game = s[0]
|
|
var o: Game.Player = s[2][0]
|
|
g2.end_turn()
|
|
var act := AI.choose_action(g2, o)
|
|
check(act.type == "mulligan" and act.cards.size() == 2 and not (C("f_kobold") in act.cards), "KI legt teure Karten zurück")
|
|
AI.apply_action(g2, o, act)
|
|
check(o.mulligan_done and AI.choose_action(g2, o).type != "mulligan", "danach normale Züge")
|
|
|
|
|
|
func test_merkmale() -> void:
|
|
var s := setup(["f_wolf", "l_falke", "e_wall", "f_funke"])
|
|
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", "zauber_schaden", "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")
|
|
var life_before: int = o.life
|
|
g.play_card(h, C("f_funke"), o)
|
|
check(o.life == life_before - 3, "Zauber +1 Schaden")
|
|
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, r)[0]
|
|
check(mara.life == 35 and mara.perks == ["leben_35"], "Mara hat 35 Leben")
|
|
var g3 := Game.new_battle_with(Profile.starter().deck_cards(), Campaign.opponents(9, r), 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.max_energy == 3, "Archimagier beginnt mit 3 Energie")
|
|
var nyx: Game.Player = Campaign.opponents(5, r)[0]
|
|
var g4 := Game.new_battle_with(Profile.starter().deck_cards(), [nyx], r, true)
|
|
var before: int = nyx.hand.size()
|
|
g4.end_turn()
|
|
check(nyx.hand.size() == before + 2, "Nyx zieht 2 Karten")
|
|
|
|
|
|
func test_ki_zwei_schritte() -> void:
|
|
# Gegner hat 5 Leben, eigenes Wesen 3 Angriff, Verhärten (+2) auf der Hand: erst verstärken, dann töten
|
|
var s := setup(["e_haerten"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
h.mulligan_done = true
|
|
h.turns = 2
|
|
o.life = 5
|
|
var a := put(h, "f_wolf")
|
|
var act := AI.choose_action(g, h)
|
|
check(act.type == "play" and act.card == C("e_haerten") and act.target == a, "KI verstärkt zuerst")
|
|
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_falle", "f_wolf"], ["w_eis"], 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_falle"))
|
|
g.play_card(h, C("f_wolf"))
|
|
put(o, "e_troll")
|
|
var hi := g.players.find(h)
|
|
var oi := g.players.find(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() and g2.players[hi].traps.size() == 1 and g2.players[hi].traps[0].id == "f_falle", "eigene Hand und Fallen sichtbar")
|
|
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0] != C("w_eis"), "fremde Hand nur als Anzahl")
|
|
check(g2.players[oi].deck.size() == o.deck.size(), "fremdes Deck nur als Anzahl")
|
|
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(not g2.can_play(g2.players[hi], C("fff_sonne"))[0] and g2.valid_targets(g2.players[hi], "enemy_creature").size() == 1, "Kopie kennt die Regeln")
|
|
var snap_o := g.snapshot(oi)
|
|
check(snap_o.players[hi].hand is int and snap_o.players[hi].traps is int and snap_o.players[oi].hand is Array, "Sicht des Gegners")
|
|
|
|
|
|
func test_statistik_pity_tagesaufgaben() -> void:
|
|
Profile.today_override = "2026-09-20"
|
|
var p := Profile.starter()
|
|
var deck := p.deck_cards()
|
|
p.record_battle(true, 3, deck, -1, ["f_kobold", "f_kobold", "f_funke"])
|
|
check(p.card_stats["f_kobold"].played == 2 and p.card_stats["f_funke"].played == 1, "gespielte Karten gezählt")
|
|
check(p.card_stats["f_kobold"].games == 1 and p.card_stats["f_kobold"].wins == 1 and p.card_stats["e_wall"].games == 1, "Partien und Siege je Deckkarte")
|
|
p.record_battle(false, 1, deck, -1, [])
|
|
check(p.card_stats["f_kobold"].games == 2 and p.card_stats["f_kobold"].wins == 1, "Niederlage zählt Partie")
|
|
var top := p.top_cards(3)
|
|
check(top.size() == 3 and top[0].wins == 1, "Bestenliste der Karten")
|
|
# Pity-Zähler
|
|
p.coins = 100000
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 1
|
|
p.booster_since_mythic = Profile.PITY_BOOSTERS - 1
|
|
var got := p.open_booster(r)
|
|
check(got.any(func(c): return c.rarity == "mythisch"), "nach %d Boostern garantiert mythisch" % Profile.PITY_BOOSTERS)
|
|
check(p.booster_since_mythic == 0, "Zähler zurückgesetzt")
|
|
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
|
check(v.card_stats["f_kobold"].played == 2 and v.booster_since_mythic == 0 and v.settings.has("music"), "Version 4 gelesen")
|
|
# Zwölf Tagesaufgaben, elementbezogen
|
|
check(Profile.DAILY_TASKS.size() == 12, "zwölf Tagesaufgaben")
|
|
var seen := {}
|
|
for day in range(1, 13):
|
|
Profile.today_override = "2026-10-%02d" % day
|
|
p.refresh_daily()
|
|
seen[p.daily.task] = true
|
|
check(seen.size() == 12, "jede Aufgabe kommt vor")
|
|
for t in Profile.DAILY_TASKS:
|
|
var q := Profile.starter()
|
|
q.daily = {"date": Profile.today(), "task": t.id, "progress": 0, "done": false}
|
|
var mono := []
|
|
for i in 20:
|
|
mono.append(Cards.by_id("w_reiter"))
|
|
match t.kind:
|
|
"win":
|
|
q.record_battle(true, 1, mono)
|
|
"game":
|
|
for i in t.goal:
|
|
q.record_battle(false, 1, mono)
|
|
"booster":
|
|
q.coins = 1000
|
|
for i in t.goal:
|
|
q.open_booster(r)
|
|
"craft":
|
|
q.dust = 10000
|
|
q.craft("f_kobold")
|
|
"win_vs3":
|
|
q.record_battle(true, 3, mono)
|
|
"win_element:Wasser":
|
|
q.record_battle(true, 1, mono)
|
|
"win_element:Feuer":
|
|
q.record_battle(true, 1, q.deck_cards())
|
|
"win_element:Erde":
|
|
q.record_battle(true, 1, q.deck_cards())
|
|
"win_element:Luft":
|
|
var luft := []
|
|
for i in 20:
|
|
luft.append(Cards.by_id("l_falke"))
|
|
q.record_battle(true, 1, luft)
|
|
_:
|
|
for i in t.goal:
|
|
q.daily_event(t.kind)
|
|
check(q.daily.done, "Tagesaufgabe erfüllbar: " + t.id)
|
|
Profile.today_override = ""
|
|
|
|
|
|
func test_deck_umbenennen_und_auffuellen() -> void:
|
|
var p := Profile.starter()
|
|
check(p.rename_deck(1, " Feuerdeck ") == "" and p.deck_name(1) == "Feuerdeck", "umbenannt")
|
|
check(p.rename_deck(1, "") != "" and p.rename_deck(1, "x".repeat(40)) != "", "Name geprüft")
|
|
check(p.autofill_deck(1) == "" and p.deck_valid(1)[0], "leerer Slot aufgefüllt und spielbar")
|
|
check(Game.deck_elements(p.deck_cards(1)).size() <= 2, "höchstens zwei Elemente")
|
|
p.decks[2].cards = ["l_falke", "l_wind"]
|
|
check(p.autofill_deck(2) == "" and p.deck_valid(2)[0], "Luft-Slot aufgefüllt")
|
|
check(p.deck_cards(2).all(func(c): return c.elements.all(func(e): return e in ["Luft", Game.deck_elements(p.deck_cards(2))[1] if Game.deck_elements(p.deck_cards(2)).size() > 1 else "Luft"])), "Auffüllen bleibt bei den Elementen")
|
|
var q := Profile.new()
|
|
q.collection = {"f_kobold": 1}
|
|
check(q.autofill_deck(0) != "", "zu wenig Karten: Fehlermeldung")
|
|
|
|
|
|
func test_neue_fallen() -> void:
|
|
check(Cards.all().filter(func(c): return c.is_trap()).size() == 12, "zwölf Fallen")
|
|
check(C("e_sandfalle").text() == "Falle. Wenn ein Gegner ein Wesen ausspielt: Es wird eingefroren.", "Fallentext je Auslöser")
|
|
check(C("f_aschefalle").text().ends_with("3 Schaden an dieses Wesen."), "Aschefalle-Text")
|
|
# Böenfalle: Angreifer zurück auf die Hand, Angriff verpufft
|
|
var s := setup(["l_boenfalle"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
g.play_card(h, C("l_boenfalle"))
|
|
g.end_turn()
|
|
var a := put(o, "e_troll")
|
|
g.attack(a, h)
|
|
check(o.board.is_empty() and C("e_troll") in o.hand and h.life == 30, "Böenfalle: Angreifer zurück, kein Schaden")
|
|
# Aschefalle: 3 Schaden an das ausgespielte Wesen
|
|
s = setup(["f_aschefalle"], ["f_wolf"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
g.play_card(h, C("f_aschefalle"))
|
|
g.end_turn()
|
|
g.play_card(o, C("f_wolf"))
|
|
check(o.board.is_empty(), "Aschefalle tötet den 3/2 Wolf beim Ausspielen")
|
|
# Sandfalle friert das ausgespielte Wesen ein, Spiegelfalle beschwört einen Windgeist
|
|
s = setup(["e_sandfalle", "l_spiegel"], ["e_troll", "w_reiter"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
g.play_card(h, C("e_sandfalle"))
|
|
g.play_card(h, C("l_spiegel"))
|
|
g.end_turn()
|
|
g.play_card(o, C("e_troll"))
|
|
check(o.board.size() == 1 and o.board[0].frozen and h.traps.size() == 1, "Sandfalle friert ein, eine Falle je Auslöser")
|
|
g.play_card(o, C("w_reiter")) # Wasser neben Erde: Schlamm heilt nur
|
|
check(h.board.size() == 1 and h.board[0].name == "Windgeist", "Spiegelfalle beschwört Windgeist beim zweiten Wesen")
|
|
check(h.traps.is_empty(), "beide Fallen verbraucht")
|
|
# Nebelfalle bricht ab und zieht, Dornenfalle verletzt und heilt
|
|
s = setup(["w_nebelfalle", "e_dornen"])
|
|
g = s[0]
|
|
h = s[1]
|
|
o = s[2][0]
|
|
g.play_card(h, C("w_nebelfalle"))
|
|
g.play_card(h, C("e_dornen"))
|
|
h.life = 20
|
|
g.end_turn()
|
|
a = put(o, "e_troll")
|
|
var hand_before := h.hand.size()
|
|
g.attack(a, h)
|
|
check(h.life == 20 and h.hand.size() == hand_before + 1 and a.health == 6, "Nebelfalle zuerst: verpufft, Karte gezogen")
|
|
var b := put(o, "f_wolf")
|
|
g.attack(b, h)
|
|
check(b.health <= 0 or not (b in o.board), "Dornenfalle: 2 Schaden töten den Wolf")
|
|
check(h.life == 23, "Dornenfalle heilt 3, Angriff entfällt")
|
|
|
|
|
|
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" and Campaign.target_rating(29, 1.0) == 4.4, "Kapitel 3 mit drei Merkmalen und höherer Zielstärke")
|
|
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, r)
|
|
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(p.deck_cards(), 1, r, {"human_perks": [p.perk]})
|
|
check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Du: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf")
|
|
p.campaign_stage = 19
|
|
p.record_battle(true, 5, p.deck_cards(), 19)
|
|
check(p.campaign_stage == 20 and "elementmeister" in p.achievements, "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_bestenliste() -> void:
|
|
Profile.today_override = "2026-09-16"
|
|
var mono := []
|
|
for i in 20:
|
|
mono.append(Cards.by_id("w_reiter"))
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 4
|
|
var g := Game.new_battle(mono, 3, r, {"everyone_perk": "start_energie", "start_life": 20})
|
|
check(g.players.all(func(p): return p.life == 20 and "start_energie" in p.perks), "Regelvariante: 20 Leben und Startenergie für alle")
|
|
check(g.players[0].max_energy == 3, "Startspieler hat 3 Energie")
|
|
var p := Profile.starter()
|
|
p.record_battle(true, 3, mono, -1, [], 7)
|
|
p.record_battle(true, 3, mono, -1, [], 9)
|
|
check(p.records.streak == 2 and p.records.best_streak == 2 and p.records.fastest_win == 7, "Serie und schnellster Sieg")
|
|
p.record_battle(false, 1, mono, -1, [], 12)
|
|
check(p.records.streak == 0 and p.records.best_streak == 2, "Niederlage bricht die Serie")
|
|
for i in 5:
|
|
p.record_battle(true, 1, mono, -1, [], 10)
|
|
check("serie" in p.achievements, "Siegesserie-Erfolg")
|
|
var path := "user://test_statistik.csv"
|
|
check(p.export_stats(path) == "", "Statistik exportiert")
|
|
var text := FileAccess.get_file_as_string(path)
|
|
check(text.begins_with("karte;seltenheit;gespielt;partien;siege") and "beste_serie;5" in text, "CSV-Inhalt")
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
|
Profile.today_override = ""
|
|
|
|
|
|
func test_deckstaerke_und_schwierigkeit() -> void:
|
|
check(is_equal_approx(Cards.card_value(C("f_kobold")), 1.0) and is_equal_approx(Cards.card_value(C("ff_phoenix")), 4.6), "Kartenwert aus Seltenheit, Schlüsselwörtern, Effekten")
|
|
var starter := Profile.starter().deck_cards()
|
|
var rs := Game.deck_rating(starter)
|
|
check(rs > 1.2 and rs < 1.5, "Starter mit zwei seltenen Karten bei etwa 1,3 (%.2f)" % rs)
|
|
check(Profile.starter().deck_valid()[0] and starter.any(func(c): return c.id == "f_drache") and starter.any(func(c): return c.id == "e_baum"), "Feuerdrache und Uralter Baum im Starterdeck")
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 5
|
|
for target in [1.2, 1.6, 2.2, 2.6]:
|
|
for elems in [["Feuer"], ["Wasser", "Luft"]]:
|
|
var d := Game.build_opponent_deck_rated(r, elems, target)
|
|
check(Game.validate_deck(d)[0] and absf(Game.deck_rating(d) - target) <= 0.25, "Zielstärke %.1f für %s erreicht (%.2f)" % [target, "+".join(elems), Game.deck_rating(d)])
|
|
var strong := Game.build_opponent_deck_rated(r, ["Feuer", "Luft"], 3.0)
|
|
check(Game.deck_rating(strong) >= 2.7, "Zielstärke 3,0 mit zwei Elementen nahezu erreichbar (%.2f)" % Game.deck_rating(strong))
|
|
var g := Game.new_battle(starter, 2, r, {"target_rating": 1.3})
|
|
for o in g.opponents:
|
|
check(absf(Game.deck_rating(o.deck + o.hand) - 1.3) <= 0.3, "Gegner auf Zielstärke (%.2f)" % Game.deck_rating(o.deck + o.hand))
|
|
check(is_equal_approx(Campaign.target_rating(0, 1.3), 1.3) and is_equal_approx(Campaign.target_rating(0, 2.5), 2.5) and is_equal_approx(Campaign.target_rating(9, 1.3), 2.6), "Kampagne: Stufe oder eigene Stärke, was höher ist")
|
|
var opps := Campaign.opponents(2, r, 1.3)
|
|
check(opps.size() == 2 and absf(Game.deck_rating(opps[0].deck) - 1.5) <= 0.25 and absf(Game.deck_rating(opps[1].deck) - 1.5) <= 0.3, "Borin und Gefolge auf 1,5")
|
|
var p := Profile.starter()
|
|
check(p.difficulty().name == "Normal" and is_equal_approx(p.target_rating(), rs), "Normal: Zielstärke gleich eigener Stärke")
|
|
p.settings.difficulty = "schwer"
|
|
check(is_equal_approx(p.target_rating(), rs * 1.2) and p.difficulty().smart, "Schwer: 1,2-fach mit Vorausschau-KI")
|
|
p.settings.difficulty = "leicht"
|
|
check(not p.difficulty().smart and is_equal_approx(p.target_rating(), rs * 0.9), "Leicht: einfache KI, 0,9-fach")
|
|
var reward := p.record_battle(true, 1, starter, -1, [], 5, p.difficulty().reward)
|
|
check(reward == int(round(Profile.REWARD_WIN[1] * 0.75)), "Leicht: weniger Münzen")
|
|
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
|
check(v.settings.difficulty == "leicht", "Schwierigkeit gespeichert")
|
|
v.settings.difficulty = "unsinn"
|
|
check(v.difficulty().name == "Normal", "unbekannte Schwierigkeit fällt auf Normal zurück")
|
|
|
|
|
|
func test_starter_gegen_passende_gegner() -> void:
|
|
check(Game.archetypes("leicht").size() == 5 and Game.archetypes("schwer").size() == 5 and Game.archetypes().size() == 10, "Archetypen geteilt")
|
|
var table: Dictionary = DeckGen.strength()
|
|
var keys: Array = table.keys()
|
|
keys.sort_custom(func(a, b): return table[a] < table[b])
|
|
check(keys[0] in Game.archetypes("leicht") and keys[-1] in Game.archetypes("schwer"), "schwächster Archetyp in Leicht, stärkster in Schwer")
|
|
check(Game.archetypes("leicht").all(func(k): return table[k] <= table[Game.archetypes("schwer")[0]]), "Hälften nach Stärke getrennt")
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 6
|
|
var starter := Profile.starter().deck_cards()
|
|
var rs := Game.deck_rating(starter)
|
|
var results := {}
|
|
for key in ["leicht", "schwer"]:
|
|
var d: Dictionary = Profile.DIFFICULTIES[key]
|
|
var wins := 0
|
|
var games := 24
|
|
for i in games:
|
|
var g := Game.new_battle(starter, 1, r, {"target_rating": rs * float(d.factor), "tier": d.tier})
|
|
var arch := Cards.combo_key(Game.deck_elements(g.opponents[0].deck + g.opponents[0].hand))
|
|
check(arch in Game.archetypes(d.tier), "Archetyp passt zur Stufe (%s)" % arch)
|
|
var guard := 0
|
|
while g.winner == "" and guard < 300:
|
|
var p := g.current()
|
|
AI.take_turn(g, p, (not d.smart) and not p.is_human)
|
|
guard += 1
|
|
if g.winner == "human":
|
|
wins += 1
|
|
results[key] = wins
|
|
check(results.leicht > results.schwer, "Leicht ist leichter als Schwer (%d zu %d von 24)" % [results.leicht, results.schwer])
|
|
check(results.leicht >= 16, "Leicht: Starter gewinnt meistens (%d von 24)" % results.leicht)
|
|
|
|
|
|
func test_kurve_statistik_vorschlag_verbesserung() -> void:
|
|
var starter := Profile.starter().deck_cards()
|
|
check(is_zero_approx(DeckGen.curve_penalty(starter)), "Starterkurve ohne Abzug")
|
|
var teuer := []
|
|
for i in 20:
|
|
teuer.append(Cards.by_id("f_zunge"))
|
|
check(DeckGen.curve_penalty(teuer) >= 0.4 - 0.001 and Game.deck_rating(teuer) < 1.0, "teures Deck: Kurvenabzug")
|
|
var stats := {"f_kobold": {"played": 30, "games": 20, "wins": 16}}
|
|
check(DeckGen.card_value(Cards.by_id("f_kobold"), stats) > Cards.card_value(Cards.by_id("f_kobold")), "gute Statistik hebt den Wert")
|
|
check(Game.deck_rating(starter, stats) > Game.deck_rating(starter), "Deckstärke mit Statistik")
|
|
var few := {"f_kobold": {"played": 3, "games": 3, "wins": 3}}
|
|
check(is_equal_approx(DeckGen.card_value(Cards.by_id("f_kobold"), few), 1.0), "zu wenig Partien: Basiswert")
|
|
# Vorschlag nach Serien
|
|
var p := Profile.starter()
|
|
var mono := starter
|
|
check(p.suggest_difficulty() == "", "kein Vorschlag am Anfang")
|
|
for i in 3:
|
|
p.record_battle(true, 1, mono, -1, [], 6, 1.0, 4)
|
|
check(p.suggest_difficulty() == "", "drei knappe Siege: noch kein Vorschlag")
|
|
p.records.streak = 0
|
|
p.records.margins = []
|
|
for i in 3:
|
|
p.record_battle(true, 1, mono, -1, [], 6, 1.0, 20)
|
|
check(p.suggest_difficulty() == "schwer" and p.notices.any(func(n): return n.begins_with("Tipp: 3 Siege")), "drei deutliche Siege: Schwer vorschlagen")
|
|
p.records.streak = 0
|
|
p.records.margins = []
|
|
for i in 5:
|
|
p.record_battle(true, 1, mono, -1, [], 6, 1.0, 2)
|
|
check(p.suggest_difficulty() == "schwer" and p.records.margins.size() == 5, "fünf Siege egal wie knapp")
|
|
p.settings.difficulty = "schwer"
|
|
p.take_notices()
|
|
for i in 3:
|
|
p.record_battle(false, 1, mono)
|
|
check(p.records.loss_streak == 3 and p.suggest_difficulty() == "normal", "drei Niederlagen: Normal vorschlagen")
|
|
p.settings.difficulty = "leicht"
|
|
check(p.suggest_difficulty() == "", "auf Leicht kein Abstieg")
|
|
# Deck verbessern
|
|
var q := Profile.starter()
|
|
check(q.improve_deck_with("ff_phoenix", true) != "", "nicht besessen")
|
|
q.collection["ff_phoenix"] = 1
|
|
check(q.improve_deck_with("ff_phoenix", true) == "", "Phönix passt (Feuer)")
|
|
q.collection["ww_leviathan"] = 1
|
|
check(q.improve_deck_with("ww_leviathan", true) != "", "Leviathan passt nicht (drittes Element)")
|
|
check(q.deck_suggestions(["ff_phoenix", "ww_leviathan", "f_kobold"]) == ["ff_phoenix"], "Vorschläge gefiltert")
|
|
var before := q.deck.duplicate()
|
|
check(q.improve_deck_with("ff_phoenix") == "" and q.deck.count("ff_phoenix") == 1 and q.deck.size() == 20 and q.deck_valid()[0], "Phönix eingetauscht")
|
|
check(Game.deck_rating(q.deck_cards()) > Game.deck_rating(before.map(func(id): return Cards.by_id(id))), "Deck stärker")
|
|
check(q.improve_deck_with("ff_phoenix", true) != "", "zweite Kopie nicht möglich")
|
|
|
|
|
|
func test_wasser_antworten() -> void:
|
|
var s := setup(["w_ebbe", "w_tiefenstoss", "w_strudel"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2][0]
|
|
var t := put(o, "e_troll")
|
|
g.play_card(h, C("w_ebbe"), t)
|
|
check(t.health == 5 and t.frozen, "Ebbe: 1 Schaden und Eis")
|
|
g.play_card(h, C("w_tiefenstoss"), t)
|
|
check(t.health == 2, "Tiefenstoß: 3 Schaden")
|
|
var hand := h.hand.size()
|
|
g.play_card(h, C("w_strudel"), t)
|
|
check(o.board.is_empty() and C("e_troll") in o.hand and h.hand.size() == hand, "Strudel: zurück auf die Hand, eine Karte gezogen")
|
|
check(C("w_ebbe").rarity == "gewöhnlich" and Profile.starter().collection.has("w_ebbe"), "Ebbe in der Startsammlung")
|
|
|
|
|
|
func test_ki_zeitbudget() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 12
|
|
var g := Game.new_battle(Profile.starter().deck_cards(), 5, r)
|
|
var h: Game.Player = g.players[0]
|
|
h.mulligan_done = true
|
|
h.energy = 10
|
|
h.max_energy = 10
|
|
for i in 8:
|
|
h.hand.append(Cards.by_id("f_funke"))
|
|
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").combo() == "Sturm", "Gischtläufer für Sturm")
|
|
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(Profile.starter().to_dict())))
|
|
check(v.settings.has("big_font") and not v.tutorial2_done and v.records.margins is Array, "Version 5 mit neuen Feldern")
|
|
|
|
|
|
## Elementbooster enthalten nur Karten des Elements; Deck-Codes sind umkehrbar und robust.
|
|
## 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_funke").text()
|
|
TranslationServer.set_locale("en")
|
|
var funke := Cards.by_id("f_funke")
|
|
var falle := Cards.by_id("w_falle")
|
|
var wall := Cards.by_id("e_wall")
|
|
check(funke.title() == "Spark" and funke.text() == "2 damage to a target.", "Funke auf Englisch: %s / %s" % [funke.title(), funke.text()])
|
|
check(falle.text().begins_with("Trap. When an enemy creature attacks") and "The attack fizzles" in falle.text(), "Fallentext auf Englisch: %s" % falle.text())
|
|
check(wall.text() == "Guardian." and Cards.by_id("l_spiegel").text().ends_with("Summon a Wind Spirit."), "Schlüsselwort und Beschwörung auf Englisch")
|
|
TranslationServer.set_locale("de")
|
|
check(funke.title() == "Funke" and funke.text() == de, "zurück auf Deutsch")
|
|
|
|
|
|
func test_elementbooster_und_deckcode() -> void:
|
|
var p := Profile.starter()
|
|
p.coins = 500
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 5
|
|
var got: Array = p.open_booster(r, "Wasser")
|
|
check(got.size() == 5 and got.all(func(c): return "Wasser" in c.elements) and p.coins == 500 - Profile.ELEMENT_BOOSTER_COST, "Wasser-Booster: fünf Wasserkarten, %d Münzen" % Profile.ELEMENT_BOOSTER_COST)
|
|
p.coins = 100
|
|
check(p.open_booster(r, "Feuer").is_empty() and p.coins == 100, "Elementbooster braucht %d Münzen" % Profile.ELEMENT_BOOSTER_COST)
|
|
for e in Cards.ELEMENTS:
|
|
for rar in Profile.RARITY_WEIGHTS:
|
|
check(not Profile.booster_pool(rar, e).is_empty(), "%s hat %s Karten" % [e, rar])
|
|
var deck: Array = Profile.starter().deck
|
|
var code := Profile.deck_code(deck)
|
|
check(code.begins_with(Profile.CODE_PREFIX) and Profile.parse_deck_code(code).size() == 20, "Deck-Code hin und zurück")
|
|
var back: Array = Profile.parse_deck_code(code)
|
|
back.sort()
|
|
var orig: Array = deck.duplicate()
|
|
orig.sort()
|
|
check(back == orig, "Deck-Code enthält genau die Karten")
|
|
check(Profile.parse_deck_code("kaputt").is_empty() and Profile.parse_deck_code("DINGER1:x_nix*2").is_empty()
|
|
and Profile.parse_deck_code("DINGER1:f_funke*9").size() == Game.MAX_COPIES, "ungültige Codes leer, Kopien gedeckelt")
|
|
check(Cards.by_id("w_flutfalle").is_trap() and Cards.by_id("fl_sturmfalke").combo() == "Blitz" and Cards.by_id("el_sandsturm").is_combo(), "neue Karten im Pool")
|
|
|
|
|
|
## 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(Profile.starter().deck_cards(), 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 ids.call(a.deck) == ids.call(b.deck) \
|
|
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 not (p.traps is Array) and not (p.deck is Array)), "Zuschauer-Sicht ohne Hände, Decks und Fallen")
|
|
|
|
|
|
## Jeder tr("...")-Text in der Oberfläche und jeder Anleitungsschritt 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 + ["Wesen", "Zauber", "Falle"] + Cards.TRIGGER_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))
|
|
for stepset in [BattleTutorial.STEPS1, BattleTutorial.STEPS2]:
|
|
for st in stepset:
|
|
if not keys.has(st[0]):
|
|
missing.append(st[0])
|
|
check(missing.is_empty(), "fehlende Übersetzungen: %s" % [missing])
|