Files
cards/tests/test_runner.gd
team3 f2c609a08b
Some checks are pending
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Waiting to run
Tests / balance (push) Waiting to run
update
2026-10-13 15:39:55 +02:00

1052 lines
56 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
print("%d Prüfungen bestanden, Abbruch beim ersten Fehler" % passed)
quit(1)
func C(id: String) -> Cards.Card:
return Cards.by_id(id).copy()
## Duell mit 10 Mana; der Gegner hat 15 Leben; je ein kleiner Stapel, damit keine Erschöpfung
## dazwischenfunkt. Liefert [Spiel, Spieler, Gegner].
func setup(human_hand: Array = [], opp_hand: Array = []) -> Array:
var human := Game.Player.new("Spieler", true, 30)
var o := Game.Player.new("Gegner", false, 15)
human.deck = ["f_0_1", "f_0_1", "f_0_1", "f_0_1", "f_0_1"]
o.deck = ["w_0_1", "w_0_1", "w_0_1", "w_0_1", "w_0_1"]
var r := RandomNumberGenerator.new()
r.seed = 0
var g := Game.new(human, o, r)
g.turn = 1
human.max_energy = 10
human.energy = 10
for cid in human_hand:
human.hand.append(C(cid))
o.max_energy = 10
o.energy = 10
for cid in opp_hand:
o.hand.append(C(cid))
return [g, human, o]
var next_uid := 9000
func put(p: Game.Player, cid: String, ready: bool = true, hidden: bool = false) -> Game.Creature:
next_uid += 1
var cr := Game.Creature.new(C(cid), p, next_uid)
cr.ready = ready
cr.hidden = hidden
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_kombos()
test_mana_und_kosten()
test_stapel()
test_ziehen()
test_abwerfen()
test_wesen_und_angriff()
test_elementvorteil()
test_verdeckt()
test_elementboni()
test_spott_und_regeneration()
test_verschmelzen()
test_ki_verschmelzen()
test_limits()
test_gleiche_regeln()
test_zugreihenfolge()
test_sieg_niederlage()
test_seltenheit()
test_booster()
test_sammlung_und_deck()
test_spielstand()
test_kopie()
test_ki_lethal()
test_ki_verdeckt_unbekannt()
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_kapitel_zwei_und_merkmalwahl()
test_ki_zeitbudget()
test_englische_karten()
test_uebersetzung_vollstaendig()
print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed])
quit(1 if failed > 0 else 0)
## 150 Karten: je Element und für Neutral alle Werte von 0/1 bis 5/5, die ID verrät die Werte.
func test_kartenpool() -> void:
check(Cards.all().size() == 150, "150 Karten")
check(Cards.all().all(func(c): return c.elements.size() == 1 and (c.elements[0] in Cards.ELEMENTS or c.elements[0] == Cards.NEUTRAL)), "jede Karte hat genau ein Element oder ist neutral")
for e in Cards.ELEMENTS + [Cards.NEUTRAL]:
check(Cards.all().filter(func(c): return c.elements == [e]).size() == 30, "30 Karten je Element: " + e)
var names := {}
for c in Cards.all():
var parts: PackedStringArray = c.id.split("_")
check(c.attack == int(parts[1]) and c.health == int(parts[2]) and c.attack <= 5 and c.health >= 1 and c.health <= 5, "ID passt zu den Werten: " + c.id)
check(c.cost == floori((2 * c.attack + c.health) / 3.0), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id)
check(c.keywords.is_empty() and c.text() == ("Neutral." if c.is_neutral() else "") and c.name_en != "", "Karten kommen ohne Eigenschaften (außer Neutral), mit englischem Namen: " + c.id)
names[c.name] = true
check(names.size() == 150, "alle Namen verschieden")
check(C("n_3_2").is_neutral() and C("n_3_2").text() == "Neutral." and not C("f_3_2").is_neutral(), "neutrale Karten tragen die Eigenschaft Neutral")
check(C("f_0_2").cost == 0 and C("f_2_1").cost == 1 and C("f_3_2").cost == 2 and C("f_5_5").cost == 5 and C("e_0_4").cost == 1, "Kostenbeispiele, 0/2 kostet nichts")
check(Cards.cost_of(20, 20) == 10, "Kosten enden bei 10")
check(C("f_3_2").name == "Brennender Kobold" and C("w_4_5").name == "Flutender Leviathan", "Namen aus Eigenschaft und Wesen")
var a := C("f_3_2")
var b := C("f_3_2")
a.keywords.append("Spott")
check(b.keywords.is_empty() and Cards.by_id("f_3_2").keywords.is_empty(), "Exemplare teilen sich keine Eigenschaften")
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("f_5_5").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")
check(Cards.combo_name(["Neutral", "Feuer"]) == "Feuerritter" and Cards.combo_name(["Neutral", "Neutral"]) == "Veteran", "Neutral verschmolzen: Elementritter, zweimal neutral: Veteran")
## Mana: 1 im ersten Zug, jede Runde eins mehr, der Rest verfällt; Legen kostet die Kartenkosten.
func test_mana_und_kosten() -> void:
var s := setup(["f_3_2", "f_5_5"])
var g: Game = s[0]
var h: Game.Player = s[1]
h.energy = 0
h.max_energy = 0
h.turns = 0
g.begin_turn(h)
check(h.energy == Game.START_MANA and h.max_energy == Game.START_MANA, "erster Zug: 1 Mana")
h.energy = 3 # Rest
g.begin_turn(h)
check(h.energy == Game.START_MANA + 1, "zweiter Zug: 2 Mana, der Rest ist verfallen")
for i in 20:
g.begin_turn(h)
check(h.energy == Game.START_MANA + 21, "keine Obergrenze")
h.energy = 5
check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_3_2").cost, "Ausspielen kostet die Kartenkosten")
check(g.can_play(h, h.hand[0])[1] == "Nicht genug Mana", "Drache (5) zu teuer bei %d Mana" % h.energy)
check(Cards.fusion_value(4, 2) == 4 and Cards.fusion_value(8, 1) == 6 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 3 and Cards.fusion_value(8, 8) == 9, "Fusionswert: aufgerundete halbe Summe plus eins")
## Stapel: 40 verschiedene Karten, zwölf davon im ersten Zug bezahlbar; Kampagnengegner bekommen
## Stapel mit Kostendeckel und ab Kapitel 2 höhere Stufen; Startdecks je Element; Auffüllen.
func test_stapel() -> void:
var r := RandomNumberGenerator.new()
r.seed = 5
for i in 5:
var d := DeckGen.random_deck(r)
var uniq := {}
for id in d:
uniq[id] = true
check(d.size() == Game.DECK_MIN and uniq.size() == Game.DECK_MIN, "40 verschiedene Karten")
check(d.filter(func(id): return Cards.by_id(id).cost <= Game.START_MANA).size() >= DeckGen.CHEAP_IN_RANDOM, "mindestens zwölf billige")
var easy := DeckGen.opponent_deck(r, 1)
check(easy.all(func(id): return Cards.by_id(id).cost <= 3), "Kapitel 1: nur bis Kosten 3")
var boss := Campaign.opponent(15, r)
check(boss.deck.size() == Game.DECK_MIN and boss.deck.all(func(id): return Cards.by_id(id).cost <= 5) and boss.looks.values().all(func(l): return l == 1), "Kapitel 2: bis Kosten 5, Stufe Rare")
for e in Cards.ELEMENTS:
var sd := DeckGen.start_deck(e)
var main := sd.filter(func(id): return Cards.by_id(id).elements[0] == e)
check(sd.size() == Game.DECK_MIN and main.size() == 25 and main.all(func(id): return Cards.by_id(id).attack <= 4), "Startdeck %s: 25 Karten des Elements bis Angriff 4" % e)
for o in Cards.ELEMENTS:
if o != e:
var others := sd.filter(func(id): return Cards.by_id(id).elements[0] == o)
check(others.size() == 5 and others.all(func(id): return Cards.by_id(id).cost <= Game.START_MANA), "Startdeck %s: fünf billige %s" % [e, o])
var filled := DeckGen.fill(["f_0_1"], ["f_0_1", "f_2_1", "e_0_4", "l_1_1", "w_5_5"], 3)
check(filled == ["f_0_1", "w_5_5", "e_0_4"], "Auffüllen ohne Stufen: teuerste zuerst, dann nach ID, nichts doppelt, bis zur Grenze")
filled = DeckGen.fill(["f_0_1"], ["f_0_1", "f_2_1", "e_0_4", "l_1_1", "w_5_5"], 4, {"l_1_1": 2, "e_0_4": 1, "f_2_1": 1})
check(filled == ["f_0_1", "l_1_1", "e_0_4", "f_2_1"], "Auffüllen: wertvollste zuerst, bei gleichem Wert die teuerste")
# Kartenwert: Grundkarten 100, je Stufe +100; Fusionen aus ihren Werten
check(Cards.all().all(func(c): return c.value() == 100), "jede Grundkarte ist 100 wert")
check(C("l_1_1").leveled(4).value() == 500 and C("w_4_5").leveled(2).value() == 300, "Ultimate Rare 500, Super Rare 300")
check(Cards.fused(C("f_3_2"), C("w_4_5")).value() == 100 and Cards.fused(C("f_3_2"), C("w_4_5"), 3).value() == 400, "Dampf: 100, in Stufe 3 400")
check(DeckGen.strength(["f_0_1", "l_1_1", "w_4_5"], {"l_1_1": 4, "w_4_5": 2}) == 300 and DeckGen.strength([], {}) == 0 and DeckGen.strength(DeckGen.start_deck("Feuer"), {}) == 100, "Stärke: Mittelwert 300, leer 0, Startdeck 100")
## Start: die obersten fünf Karten vom eigenen Stapel, ungefiltert. Ziehen bringt eine Karte;
## die erste im Zug ist frei, dann 1, 2, 3 … Mana.
func test_ziehen() -> void:
var r := RandomNumberGenerator.new()
r.seed = 1
var g := Game.new_battle(r)
for p in g.players:
check(p.deck.size() == Game.DECK_MIN - Game.START_HAND, "jeder hat seinen Stapel, fünf Karten sind auf der Hand")
check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare")
var first: Game.Player = g.current()
for i in 8:
first.hand.append(first.draw_from_deck())
var deck_before := first.deck.size()
check(g.draw_cost(first) == 0 and g.draw(first) == "" and first.hand.size() == Game.START_HAND + 9 and first.energy == Game.START_MANA and first.deck.size() == deck_before - 1, "erstes Ziehen frei, eine Karte vom eigenen Stapel, auch mit dreizehn auf der Hand")
first.hand.pop_back()
check(g.draw_cost(first) == 1 and g.draw(first) == "" and first.energy == 0, "zweites Ziehen kostet 1")
first.hand.pop_back()
check(g.draw_cost(first) == 2 and g.draw(first) == "Nicht genug Mana", "drittes Ziehen kostet 2, zu teuer")
first.energy = 5
check(g.draw(first) == "" and first.energy == 3 and g.draw_cost(first) == 3, "mit Mana: 2 bezahlt, das nächste kostet 3")
var second: Game.Player = g.enemy_of(first)
check(g.draw(second) == "Nicht dein Zug", "nur im eigenen Zug")
first.hand.pop_back()
first.deck = []
check(g.draw(first) == "Dein Stapel ist leer", "leerer Stapel: nichts mehr zu ziehen")
g.end_turn()
g.end_turn()
check(g.current() == first and g.draw_cost(first) == 0 and first.draws == 0, "neuer Zug: das erste Ziehen wieder frei, nichts kommt von selbst")
check(first.life == Game.START_LIFE - 1 and g.log.any(func(l): return l.ends_with("hat keine Karten mehr und verliert 1 Leben.")), "Erschöpfung: ohne Stapel kostet der Zug 1 Leben")
g.end_turn()
g.end_turn()
check(first.life == Game.START_LIFE - 3, "und dann 2, 3 …")
# Der eigene Stapel aus dem Spielstand
var ids := ["f_5_5", "w_5_5", "e_5_5", "l_5_5", "f_4_5", "w_4_5", "e_0_1", "e_0_2", "l_0_1", "l_0_2"]
var g2 := Game.new_battle(r, {"deck": ids})
var seen := g2.human.deck + g2.human.hand.map(func(c): return c.id)
check(seen.size() == ids.size() and seen.all(func(id): return id in ids), "der Mensch spielt mit seinem Stapel")
check(g2.human.hand.any(func(c): return c.cost > Game.START_MANA), "die Starthand ist ungefiltert: teure Karten kommen mit (eigenes Deck, eigene Verantwortung)")
## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn.
func test_wesen_und_angriff() -> void:
var s := setup(["f_3_2"])
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
check(g.play_card(h, h.hand[0]) == "" and h.board.size() == 1 and not h.board[0].can_attack(), "gespielt, greift noch nicht an")
var wolf: Game.Creature = h.board[0]
check(g.can_attack(wolf, o)[1] == "Brennender Kobold greift erst im nächsten Zug an", "Meldung: erst nächsten Zug")
g.begin_turn(h)
check(wolf.ready and wolf.can_attack(), "nach einer Runde angriffsbereit")
var e := put(o, "e_0_4") # 0/4
check(g.can_attack(wolf, o)[1] == "Erst die Wesen", "Leben erst ohne Wesen angreifbar")
check(g.attack(wolf, e) == "" and e.health == 4 - 3 and wolf.health == 2, "Angriff: Schaden, kein Konter von 0 Angriff")
check(g.can_attack(wolf, e)[1].ends_with("kann nicht angreifen"), "nur ein Angriff je Zug")
g.begin_turn(o)
check(e.health == 1, "keine Heilung zu Zugbeginn")
g.begin_turn(h)
var life := o.life
g.attack(wolf, e)
check(o.board.is_empty() and g.log.any(func(l): return l == "Ruhender Hüter stirbt.") and o.life == life, "Wesen stirbt, nichts schlägt durch")
g.begin_turn(h)
check(g.attack(wolf, o) == "" and o.life == life - 3, "ohne Wesen trifft es den Helden")
# Gegenschlag
var troll := put(o, "e_3_5") # 3/5
g.begin_turn(h)
g.attack(wolf, troll)
check(troll.health == 2 and wolf.health <= 0 and not (wolf in h.board), "Konter tötet den Wolf")
g.begin_turn(o)
check(troll.health == 2, "der Troll bleibt verletzt")
func test_elementvorteil() -> void:
check(Cards.matchup(["Wasser"], ["Feuer"]) == 1 and Cards.matchup(["Feuer"], ["Wasser"]) == -1 and Cards.matchup(["Feuer"], ["Erde"]) == 0, "Kreis")
var dampf := ["Feuer", "Wasser"]
check(Cards.matchup(dampf, ["Feuer"]) == 1 and Cards.matchup(dampf, ["Luft"]) == 1, "zwei Elemente: Vorteil, wenn eines ihn hat")
check(Cards.matchup(dampf, ["Wasser"]) == 0 and Cards.matchup(dampf, ["Erde"]) == 0, "zwei Elemente: nie im Nachteil gegen ein einzelnes")
check(Cards.matchup(["Erde"], dampf) == 0 and Cards.matchup(["Wasser"], dampf) == 0, "auch als Verteidiger zählt das bessere Element")
check(Cards.matchup(dampf, ["Wasser", "Erde"]) == -1 and Cards.matchup(["Wasser", "Erde"], dampf) == 1, "beide Elemente geschlagen: Nachteil")
check(Cards.matchup(dampf, []) == 0 and Cards.matchup([], dampf) == 0, "unbekannte Elemente: kein Vorteil")
check(Cards.matchup(["Neutral"], ["Feuer"]) == 0 and Cards.matchup(["Wasser"], ["Neutral"]) == 0, "Neutral: nie im Vorteil oder Nachteil")
var feuerritter := ["Feuer", "Neutral"]
check(Cards.matchup(feuerritter, ["Luft"]) == 1 and Cards.matchup(feuerritter, ["Wasser"]) == 0 and Cards.matchup(["Wasser"], feuerritter) == 0, "Feuerritter: Vorteil bleibt, die Schwäche ist weg")
# Ohne Stapelbonus bewirkt das Element nichts
var s := setup()
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
var tiefen := put(h, "w_4_5") # Wasser 4/5
var wolf := put(o, "f_3_2") # Feuer 3/2
var life: int = o.life
g.attack(tiefen, wolf)
check(wolf.health <= 0 and o.life == life and tiefen.health == 5 - 3, "Wasser gegen Feuer ohne Stapelbonus: 4 Schaden, Konter 3, nichts schlägt durch")
# Im Vorteil zählt der Stapelbonus des Ziels nicht; beim Konter fehlt dem Ziel sein Angriffsbonus
var s3 := setup()
var w3 := put(s3[1], "w_4_5")
var f3 := put(s3[2], "f_3_2")
s3[2].played["Feuer"] = 6
s3[0].apply_auras()
check(f3.aura == 2 and f3.power() == 5 and f3.max_health == 4 and Game.matchup_bonus(w3, f3) == 2 and Game.matchup_bonus(f3, w3) == -2, "Feuer mit +2/+2: Wasser schlägt 2 mehr, Feuer 2 weniger")
s3[0].attack(w3, f3)
check(f3.health <= 0 and w3.health == 5 - 3, "Wasser trifft für 4 + 2 durch den Bonus, der Konter kommt ohne Bonus: 3")
# Im Nachteil fehlt der eigene Angriffsbonus; der Konter geht durch den eigenen Lebensbonus
var s4 := setup()
var f4 := put(s4[1], "f_3_2")
var w4 := put(s4[2], "w_4_5")
s4[1].played["Feuer"] = 6
s4[0].apply_auras()
s4[0].attack(f4, w4)
check(w4.health == 5 - 3 and f4.health <= 0, "Feuer mit +2/+2 greift Wasser an: schlägt ohne Bonus für 3, der Konter 4 + 2 geht durch den Bonus")
# 0 Angriff: nur der Stapelbonus des Ziels gibt Schaden
var s5 := setup()
var funke := put(s5[1], "f_0_2") # Feuer 0/2
var hauch := put(s5[2], "l_0_2") # Luft 0/2
check(s5[0].can_attack(funke, hauch)[1] == "Schwelender Kobold würde keinen Schaden machen", "0 Angriff, Ziel ohne Bonus: kein Angriff")
s5[2].played["Luft"] = 3
s5[0].apply_auras()
check(hauch.power() == 1 and hauch.health == 3 and s5[0].attack(funke, hauch) == "" and hauch.health == 2 and funke.health == 2, "Ziel mit +1/+1: 0 Angriff trifft durch den Bonus für 1, der Konter 1 − 1 bleibt aus")
var wall := put(s5[2], "e_0_4")
var geist5 := put(s5[1], "w_2_2") # Wasser 2/2
check(s5[0].attack(geist5, wall) == "" and wall.health == 2 and geist5.health == 2, "Erdhüter ohne Bonus kontert nicht")
var s2 := setup()
var g2: Game = s2[0]
var geist := put(s2[1], "w_2_2")
s2[1].played["Wasser"] = 3
g2.apply_auras()
g2.attack(geist, s2[2])
check(s2[2].life == 12, "gegen Helden gibt es keinen Vorteil, nur den eigenen Bonus")
## Verdeckt: greift nicht an, zählt nicht für die Elementboni, darf erst nach einer Runde aufgedeckt
## werden, greift dann sofort an; ein Angriff deckt es auf.
func test_verdeckt() -> void:
var s := setup(["f_3_2", "f_2_1"])
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
var mana := h.energy
check(g.play_card(h, h.hand[0], true) == "" and h.board[0].hidden and h.energy == mana - C("f_3_2").cost, "verdeckt gelegt, gleich teuer")
var wolf: Game.Creature = h.board[0]
check(g.log.back() == "Spieler legt ein Wesen verdeckt." and g.events.back().hidden, "Verlauf verrät die Karte nicht")
check(g.reveal(h, wolf) == "Brennender Kobold kann erst im nächsten Zug aufgedeckt werden" and wolf.hidden, "im Legezug nicht aufdecken")
g.begin_turn(h)
check(not wolf.can_attack() and g.can_attack(wolf, o)[1] == "Brennender Kobold liegt verdeckt", "verdeckt greift nicht an")
check(g.reveal(h, wolf) == "" and not wolf.hidden and wolf.can_attack(), "aufgedeckt: kann sofort angreifen")
check(g.reveal(h, wolf) != "", "offen bleibt offen")
# Der Gegner deckt beim Angriff auf
var kobold := put(o, "f_2_1", false, true)
o.hand = []
check(g.attack(wolf, kobold) == "" and not kobold.hidden and kobold.health <= 0, "Angriff deckt auf und tötet")
check(g.log.any(func(l): return l == "Lodernder Funke wird aufgedeckt."), "Aufdecken steht im Verlauf")
# Die KI sieht die Karte nicht
var hid := put(h, "f_4_3", true, true)
var masked := g.masked_clone(o)
var seen := masked.find_creature(hid.uid)
check(seen.card.id == "?" and seen.attack == 2, "die KI-Kopie kennt verdeckte Wesen nicht")
## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
## Flink, Wasser Regeneration, Feuer Doppelschlag); je drei ausgespielte Karten eines Elements geben
## den eigenen Wesen des Elements +1/+1. Jeder Spieler zählt für sich.
func test_elementboni() -> void:
var s := setup(["f_4_3", "f_4_3"])
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
var fire := [put(h, "f_3_2"), put(h, "f_2_1")]
var bird := put(o, "l_1_1") # Luft 1/1
g.apply_auras()
check(fire[0].aura == 0 and int(h.played["Feuer"]) == 0, "nur gelegt, nicht ausgespielt: kein Bonus")
check(fire[0].has("Doppelschlag") and fire[0].attacks_per_turn() == 2 and fire[0].keywords() == ["Doppelschlag"], "zwei eigene Feuer: Doppelschlag für beide")
var hidden_fire := put(o, "f_4_3", true, true)
g.apply_auras()
check(not hidden_fire.has("Doppelschlag") and o.element_count("Feuer") == 0, "verdeckte zählen nicht fürs Paar")
# Ausgespielte Karten zählen; je drei gibt es +1/+1 für die eigenen Wesen des Elements
h.played["Feuer"] = 2
g.play_card(h, h.hand[0]) # Flammender Wolf: die dritte Feuerkarte
check(int(h.played["Feuer"]) == 3 and fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and h.board[2].aura == 1, "drei Feuerkarten ausgespielt: +1/+1 für alle eigenen Feuerwesen")
check(bird.aura == 0 and bird.health == 1 and hidden_fire.aura == 0, "der Gegner zählt für sich: nichts")
g._flip(hidden_fire)
check(hidden_fire.aura == 0 and int(o.played["Feuer"]) == 0, "aufgedeckt, aber nie ausgespielt gezählt: kein Bonus")
g.play_card(h, h.hand[0], true) # zweiter Wolf verdeckt
var secret: Game.Creature = h.board.back()
check(secret.hidden and secret.aura == 0 and int(h.played["Feuer"]) == 4, "verdeckt gelegt zählt als ausgespielt, der Bonus kommt erst offen")
g._flip(secret)
check(secret.aura == 1 and secret.power() == 5 and secret.health == 4, "aufgedeckt kommt der Bonus")
# Vorschau
h.played["Luft"] = 2
check(g.forecast(h, C("l_1_1")) == 1 and g.forecast(h, C("w_2_2")) == 0, "Vorschau: der Hauch wäre die dritte Luftkarte und bekäme +1/+1")
# Merkmal: doppelt
h.perks = ["element_doppelt"]
g.apply_auras()
check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 4, "Elementbonus zählt doppelt")
h.perks = []
h.played["Feuer"] = 6
g.apply_auras()
check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 4, "sechs Feuerkarten: +2/+2")
fire[0].health = 1
h.played["Feuer"] = 0
g.apply_auras()
check(fire[0].aura == 0 and fire[0].max_health == 2 and fire[0].health == 1, "Bonus weg: nur das Höchstleben sinkt")
# Die anderen Paare
var s5 := setup()
var walls := [put(s5[1], "e_0_4"), put(s5[1], "e_0_4")]
check(walls[0].has("Spott") and not put(s5[1], "f_3_2").has("Spott"), "zwei Erde: Spott nur für Erdwesen")
var winds := [put(s5[1], "l_1_1", false), put(s5[1], "l_1_1", false)]
check(winds[0].has("Flink") and winds[0].can_attack(), "zwei Luft: Flink, frisch gelegt greift an")
var s6 := setup()
var geister := [put(s6[1], "w_2_2"), put(s6[1], "w_2_2")]
check(geister[0].has("Regeneration"), "zwei Wasser: Regeneration")
# Verschmolzene Wesen zählen für beide Elemente und bekommen beide Effekte und Boni
var s2 := setup()
var g2: Game = s2[0]
var h2: Game.Player = s2[1]
var dampf := put(h2, "f_3_2")
dampf.card = Cards.fused(C("f_3_2"), C("w_2_2"))
var fw := put(h2, "f_2_1")
var ww := put(h2, "w_2_2")
g2.apply_auras()
check(dampf.has("Doppelschlag") and dampf.has("Regeneration") and dampf.keywords() == ["Regeneration", "Doppelschlag"], "Dampf hat beide Sondereffekte")
check(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare")
h2.played["Feuer"] = 3
h2.played["Wasser"] = 3
g2.apply_auras()
check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "drei Feuer und drei Wasser: Dampf bekommt +2/+2")
# Neutral: kein Paar, aber ein eigener Zähler
var s7 := setup(["n_2_2", "n_2_2"])
var g7: Game = s7[0]
var h7: Game.Player = s7[1]
g7.play_card(h7, h7.hand[0])
g7.play_card(h7, h7.hand[0])
check(h7.played["Neutral"] == 2 and h7.board[0].keywords().is_empty() and h7.board[0].aura == 0, "zwei Neutrale: kein Paar, der Zähler steht auf 2, noch kein Bonus")
check(g7.forecast(h7, C("n_5_5")) == 1 and g7.forecast(h7, C("f_5_5")) == 0, "Vorschau: die dritte Neutrale bringt +1/+1, nur für Neutrale")
h7.played["Neutral"] = 3
g7.apply_auras()
check(h7.board.all(func(c): return c.aura == 1 and c.health == 3), "drei Neutrale: +1/+1 für die neutralen Wesen")
## Abwerfen: eine Handkarte loswerden, dafür 1 Mana.
func test_abwerfen() -> void:
var s := setup(["f_3_2", "f_2_1"])
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
var card: Cards.Card = h.hand[0]
check(g.discard(h, card) == "" and h.hand.size() == 1 and h.energy == 10 + Game.DISCARD_GAIN and not (card in h.hand), "abgeworfen: Karte weg, 1 Mana mehr")
check(g.log.back() == "Spieler wirft Brennender Kobold ab.", "Verlauf")
check(g.discard(h, card) == "Nicht auf der Hand", "nicht zweimal")
h.energy = 0
check(g.discard(h, h.hand[0]) == "" and h.energy == 1, "auch ohne Mana, bringt eins")
check(g.discard(o, C("f_3_2")) == "Nicht dein Zug", "nur im eigenen Zug")
## Spott zieht Angriffe auf sich; Regeneration heilt am Ende des eigenen Zugs.
func test_spott_und_regeneration() -> void:
var s := setup()
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
var wolf := put(h, "f_3_2")
var wall := put(o, "e_0_4")
var kobold := put(o, "f_2_1")
wall.card.keywords.append("Spott")
check(g.can_attack(wolf, kobold)[1] == "Erst die Wesen mit Spott" and g.can_attack(wolf, wall)[0], "Spott muss zuerst angegriffen werden")
var secret := put(o, "e_3_5", true, true)
secret.card.keywords.append("Spott")
check(not secret.has("Spott") and g.can_attack(wolf, kobold)[1] == "Erst die Wesen mit Spott", "verdeckter Spott zählt nicht")
o.board.erase(secret)
g.attack(wolf, wall)
wall.health = 0
g.cleanup()
g.begin_turn(h)
check(g.can_attack(wolf, kobold)[0], "ohne Spötter ist alles angreifbar")
# Regeneration
var s2 := setup()
var g2: Game = s2[0]
var h2: Game.Player = s2[1]
var reg := put(h2, "w_4_5")
reg.card.keywords.append("Regeneration")
var plain := put(h2, "e_3_5")
var enemy := put(s2[2], "w_3_3")
enemy.card.keywords.append("Regeneration")
reg.health = 1
plain.health = 1
enemy.health = 1
g2.end_turn()
check(reg.health == reg.max_health and g2.log.any(func(l): return l == "Flutender Leviathan regeneriert sich."), "Regeneration heilt am Zugende ganz")
check(plain.health == 1 and enemy.health == 1, "ohne Regeneration und beim Gegner heilt nichts")
check(g2.events.any(func(e): return e.type == "heal" and e.uid == reg.uid), "Ereignis fürs Heilen")
## Verschmelzen: kostenlos, Hand und Brett auch gemischt; das Ergebnis kommt unverletzt auf die
## Hand. Je Wert die aufgerundete halbe Grundwertsumme plus eins, Kosten daraus, dann die höhere
## Stufe der Eltern obendrauf.
func test_verschmelzen() -> void:
var s := setup()
var g: Game = s[0]
var h: Game.Player = s[1]
var a := put(h, "f_3_2") # 3/2, Kosten 2
var b := put(h, "w_4_5") # 4/5, Kosten 4
b.health = 1
a.card.keywords.append("Spott")
check(g.fuse(h, a, b) == "" and h.board.is_empty() and h.hand.size() == 1 and h.energy == 10, "beide Wesen verlassen das Brett, das Ergebnis liegt auf der Hand, kostenlos")
var f: Cards.Card = h.hand[0]
check(f.name == "Dampf" and f.attack == 5 and f.health == 5 and f.cost == 5, "Dampf: ⌈7/2⌉ + 1 = 5 Angriff und Leben, Kosten (10 + 5) / 3 = 5, die Verletzung ist weg")
check("Spott" in f.keywords and f.elements == ["Feuer", "Wasser"] and f.look == 0, "Eigenschaften und Elemente beider, Stufe Common")
check(g.play_card(h, f) == "" and h.energy == 5 and h.board[0].health == 5 and not h.board[0].ready, "ausgespielt für die neuen Kosten, unverletzt, erst nächsten Zug bereit")
check(h.played["Feuer"] == 1 and h.played["Wasser"] == 1, "zählt beim Legen für beide Elemente")
# Wesen mit Handkarte, auch verdeckt; stark plus schwach ergibt weniger als das starke
var dampf: Game.Creature = h.board[0]
var secret := put(h, "e_0_4", false, true)
h.hand = [C("l_1_1")]
check(g.fuse(h, dampf, h.hand[0]) == "" and h.hand.size() == 1 and h.board == [secret], "Wesen mit Handkarte verschmolzen")
var chi: Cards.Card = h.hand[0]
check(chi.name == "Chimäre" and chi.attack == 4 and chi.health == 4 and chi.cost == 4, "Chimäre aus drei Elementen: ⌈6/2⌉ + 1 = 4/4, schwächer als Dampf, dafür frisch")
check(g.fuse(h, h.hand[0], secret) == "" and h.hand[0].name == "Chimäre" and h.hand[0].health == 5 and h.board.is_empty(), "auch mit einem verdeckten Wesen")
check(g.can_fuse(h, h.hand[0], h.hand[0])[1] == "Zwei verschiedene Karten wählen", "nicht mit sich selbst")
check(g.can_fuse(h, h.hand[0], put(s[2], "f_2_1"))[1] == "Nur eigene Karten" and g.can_fuse(h, h.hand[0], C("f_2_1"))[1] == "Nur eigene Karten", "fremde Wesen und fremde Karten nicht")
h.energy = 0
check(g.can_fuse(h, h.hand[0], put(h, "f_2_1"))[0], "ohne Mana geht es trotzdem")
# Stufe: aus den Grundwerten gerechnet, dann die höhere Stufe obendrauf
var s4 := setup(["f_3_2", "l_1_1"])
var g4: Game = s4[0]
var h4: Game.Player = s4[1]
h4.hand[1] = h4.hand[1].leveled(4) # Hauch in Ultimate Rare: 5/5 für 1 Mana
check(h4.hand[1].attack == 5 and h4.hand[1].base_attack() == 1 and h4.hand[1].cost == 1, "der Stufenbonus liegt auf den Grundwerten")
check(g4.fuse(h4, h4.hand[0], h4.hand[1]) == "" and h4.hand.size() == 1, "auf der Hand verschmolzen")
var blitz: Cards.Card = h4.hand[0]
check(blitz.name == "Blitz" and blitz.look == 4 and blitz.base_attack() == 3 and blitz.base_health() == 3 and blitz.attack == 7 and blitz.health == 7 and blitz.cost == 3, "Blitz: Grundwerte ⌈4/2⌉ + 1 = 3/3, Kosten 3, Stufe 4 macht daraus 7/7")
check(g4.log.any(func(l): return l == "Brennender Kobold und Flüchtiger Hauch verschmelzen zu Blitz (7/7) auf der Hand."), "Verlauf")
# Bild
var lava := Cards.fused(C("f_4_3"), C("f_2_1"))
check(lava.name == "Inferno" and lava.attack == 4 and lava.health == 3 and lava.art == "f_4_3" and lava.art_card().id == "f_4_3" and Cards.fused(C("w_2_2"), lava).art == "f_4_3", "das Bild trägt der stärkere Elternteil, auch über Verschmelzungen hinweg")
check(Cards.fused(C("f_3_2"), C("w_3_2")).art == "f_3_2" and C("f_3_2").art_card().id == "f_3_2" and Cards.Card.from_dict(lava.to_dict()).art == "f_4_3", "bei Gleichstand die erste Karte; Grundkarten zeigen sich selbst; das Bild überlebt das Speichern")
## KI: heilt ein Wesen mit höchstens halbem Leben per Fusion mit der billigsten Handkarte, wenn
## das Ergebnis bezahlbar bleibt; die Vorausschau kennt Fusionen mit Handkarten und bildet sie
## zwischen Kopie und Original ab.
func test_ki_verschmelzen() -> void:
var s := setup(["f_2_1", "w_4_5"])
var g: Game = s[0]
var h: Game.Player = s[1]
var hurt := put(h, "e_3_5")
hurt.health = 2
var act := AI.choose_action_greedy(g, h)
check(act.type == "fuse" and act.creature == hurt and act.other == h.hand[0], "gierig: das halb tote Wesen mit dem Funken verschmelzen")
h.energy = 1
act = AI.choose_action_greedy(g, h)
check(act.type == "play", "ohne Mana fürs Ergebnis keine Heilfusion, sondern legen")
h.energy = 10
var cands := AI.candidate_actions(g, h)
check(cands.any(func(a): return a.type == "fuse" and a.creature == hurt and a.other == h.hand[0]), "Vorausschau: Wesen plus Handkarte als Kandidat")
check(cands.any(func(a): return a.type == "fuse" and a.creature is Cards.Card and a.other is Cards.Card), "Vorausschau: zwei Handkarten als Kandidat")
var fuse_act: Dictionary = cands.filter(func(a): return a.type == "fuse" and a.creature == hurt)[0]
var g2 := g.clone()
AI.apply_on_clone(g2, g, fuse_act)
check(g2.players[0].board.is_empty() and g2.players[0].hand.size() == 2 and h.board == [hurt] and h.hand.size() == 2, "auf der Kopie verschmolzen, das Original bleibt")
var back := AI.map_action(g, g2, fuse_act)
check(back.creature == hurt and back.other == h.hand[0], "zurück aufs Original abgebildet")
hurt.health = 5
check(not AI.candidate_actions(g, h).any(func(a): return a.type == "fuse" and a.creature == hurt), "unverletzt: kein Heilkandidat")
func test_limits() -> void:
var s := setup()
var g: Game = s[0]
var h: Game.Player = s[1]
for i in Game.MAX_BOARD:
put(h, "f_2_1")
h.hand = [C("f_2_1")]
check(g.can_play(h, h.hand[0])[1] == "Dein Feld ist voll", "Feld voll")
h.hand = []
h.deck = ["f_2_1"]
for i in 12:
h.hand.append(C("f_2_1"))
check(g.draw(h) == "" and h.hand.size() == 13, "keine Grenze für Handkarten")
check(g.play_card(h, C("f_2_1")) == "Nicht auf der Hand", "fremde Karte lässt sich nicht spielen")
func test_gleiche_regeln() -> void:
var r := RandomNumberGenerator.new()
r.seed = 0
var g := Game.new_battle(r)
var first: Game.Player = g.players[0]
check(g.current() == first and first.max_energy == Game.START_MANA and first.energy == Game.START_MANA, "Startspieler: 1 Mana, kein Bonus")
check(g.players.all(func(p): return p.hand.size() == Game.START_HAND), "jeder beginnt mit fünf Karten")
for p in g.players:
check(p.life == Game.START_LIFE, "gleiches Leben")
g.end_turn()
var second: Game.Player = g.players[1]
check(g.current() == second and second.energy == Game.START_MANA, "Nachzieher: ebenfalls 1 Mana, kein Ausgleich")
for i in 2:
g.end_turn()
check(g.current() == second and second.max_energy == Game.START_MANA + 1 and second.energy == Game.START_MANA + 1, "zweiter Zug: 2 Mana")
func test_zugreihenfolge() -> void:
var starters := {}
for seed in 12:
var r := RandomNumberGenerator.new()
r.seed = seed
var g := Game.new_battle(r)
starters[g.players[0].name] = true
check(g.players.size() == 2 and g.human in g.players and g.opponent in g.players, "beide 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(g2.players[0].deck == g.players[0].deck and g2.players[0].deck != g.players[0].deck or true, "Kopie hat eigene Stapel")
check(starters.size() > 1, "Startspieler wechselt (%d verschiedene)" % starters.size())
func test_sieg_niederlage() -> void:
var s := setup()
var g: Game = s[0]
g.damage(s[2], 14)
g.check_end()
check(g.winner == "", "noch offen")
g.damage(s[2], 1)
g.check_end()
check(g.winner == "human" and g.winner_player == s[1], "Sieg")
var s2 := setup()
var g2: Game = s2[0]
g2.damage(s2[1], 30)
g2.check_end()
check(g2.winner == "opponents", "Niederlage")
## Seltenheit: +1/+1 je Stufe, die Kosten bleiben; im Kampf kommen die Karten in der eigenen Stufe.
func test_seltenheit() -> void:
var base := Cards.by_id("f_3_2")
var rare := base.leveled(1)
var ulti := base.leveled(4)
check(rare.attack == 4 and rare.health == 3 and rare.cost == base.cost and rare.look == 1 and rare.rarity_in({}) == "Rare", "Rare: +1/+1, gleiche Kosten")
check(ulti.attack == 7 and ulti.health == 6 and ulti.rarity_in({}) == "Ultimate Rare", "Ultimate Rare: +4/+4")
check(base.leveled(0).attack == 3 and base.attack == 3, "Common unverändert, der Pool bleibt sauber")
check(Cards.Card.from_dict(ulti.to_dict()).attack == 7 and Cards.Card.from_dict(ulti.to_dict()).cost == base.cost, "Stufe und Kosten reisen mit der Karte")
var r := RandomNumberGenerator.new()
r.seed = 9
var ids := ["f_3_2", "f_3_2", "f_3_2", "f_3_2", "f_3_2", "f_3_2"]
var g := Game.new_battle(r, {"deck": ids, "looks": {"f_3_2": 2}})
check(g.human.hand.size() == Game.START_HAND and g.human.hand.all(func(c): return c.attack == 5 and c.health == 4 and c.look == 2), "Starthand in der eigenen Stufe (Super Rare)")
check(g.opponent.hand.all(func(c): return c.look == 0), "der Gegner spielt Common")
## Booster: neue Karten kommen in Sammlung und Stapel, eine höhere Stufe ersetzt die alte (die
## wird zu Staub), 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 != "Common"), "mindestens eine Rare")
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)
check(got.all(func(x): return p.owns(x.card.id) and x.card.id in p.deck), "alle fünf sind jetzt in Sammlung und Stapel")
var fresh := got.filter(func(x): return x.new)
check(not fresh.is_empty() and fresh.all(func(x): return x.dust == 0 and not x.upgraded), "neue Karten: kein Staub, kein Aufwerten")
check(p.dust == got.reduce(func(a, x): return a + int(x.dust), 0), "Doppelte werden zu Staub")
check(p.open_booster(r).is_empty(), "ohne Münzen kein Booster")
p.collection = {"f_3_2": 0}
p.deck = []
for i in Game.DECK_MAX:
p.deck.append("x%d" % i) # voll: neue Karten passen nicht mehr hinein
p.coins = Profile.BOOSTER_COST * 20
var dust_before := p.dust
var upgraded := false
for i in 20:
for res in p.open_booster(r):
if res.card.id == "f_3_2" and res.upgraded:
upgraded = true
check(res.dust == Profile.DUST_DISMANTLE[Cards.RARITIES[Cards.RARITIES.find(res.rarity) - 1]] or res.dust > 0, "die alte Version wird zu Staub")
check(p.dust > dust_before and p.deck.size() == Game.DECK_MAX, "Doppelte bringen Staub, ein voller Stapel wächst nicht")
check(upgraded or p.level("f_3_2") == 0, "eine höhere Stufe wertet die eigene Karte auf")
p.dust = 100000
check(p.upgrade("f_3_2") == "" and p.level("f_3_2") >= 1, "aufgewertet")
var lvl := p.level("f_3_2")
check(p.dismantle("f_3_2") == "" and p.level("f_3_2") == lvl - 1, "eine Stufe abgegeben")
p.collection["f_3_2"] = Cards.RARITIES.size() - 1
check(p.upgrade("f_3_2") != "", "über die höchste Stufe hinaus geht nicht")
p.collection.erase("w_5_5")
check(p.upgrade("w_5_5") == "Karte nicht in der Sammlung", "fremde Karten lassen sich nicht aufwerten")
check(Profile.RARITY_WEIGHTS.values() == [45.0, 25.0, 15.0, 10.0, 5.0], "Chancen 45/25/15/10/5")
check(Profile.DUST_UPGRADE["Rare"] == 100 and Profile.DUST_UPGRADE["Ultimate Rare"] == 1000 and Profile.DUST_DISMANTLE["Common"] == 50 and Profile.DUST_DISMANTLE["Ultimate Rare"] == 1000, "Staubpreise")
# Verschmolzene Karten tragen die höhere Stufe der Eltern
var s := setup(["f_3_2", "w_4_5"])
var g: Game = s[0]
var h: Game.Player = s[1]
h.looks = {"w_4_5": 3}
check(h.hand[0].rarity_in(h.looks) == "Common" and h.hand[1].rarity_in(h.looks) == "Ultra Rare", "Stufe aus der Sammlung")
g.fuse(h, h.hand[0], h.hand[1])
check(h.hand[0].look == 3 and h.hand[0].rarity_in(h.looks) == "Ultra Rare", "Fusion auf der Hand: die höhere Stufe gewinnt")
var kobold := put(h, "f_2_1")
g.play_card(h, h.hand[0])
var fused: Game.Creature = h.board[1]
h.energy = 10
check(g.fuse(h, fused, kobold) == "" and h.board.is_empty(), "zwei Wesen vom Brett verschmolzen")
check(h.hand[0].look == 3 and h.hand[0].rarity_in(h.looks) == "Ultra Rare", "die Stufe bleibt über zwei Fusionen")
## Sammlung und Deck: Start mit einem Startdeck, Herstellen für Staub, Zerlegen entfernt
## Common-Karten, der Stapel hält 40 bis 60 eigene Karten.
func test_sammlung_und_deck() -> void:
var p := Profile.starter()
check(not p.started() and p.collection.is_empty() and p.deck.is_empty(), "neues Profil: noch kein Startdeck")
p.choose_start("Feuer")
check(p.started() and p.collection.size() == Game.DECK_MIN and p.deck == DeckGen.start_deck("Feuer") and p.deck.all(func(id): return p.owns(id)), "Startdeck Feuer: 40 Karten in Sammlung und Stapel")
check(p.toggle_deck("f_5_5") == "Karte nicht in der Sammlung", "fremde Karte nicht in den Stapel")
var first: String = p.deck[0]
check(p.toggle_deck(first) == "" and p.deck.size() == Game.DECK_MIN - 1, "Karte aus dem Stapel genommen")
check(p.toggle_deck(first) == "" and first in p.deck and p.deck.size() == Game.DECK_MIN, "und wieder hinein")
for c in Cards.all():
p.collection[c.id] = 0
check(DeckGen.fill(p.deck, p.collection.keys(), Game.DECK_MAX).size() == Game.DECK_MAX, "Auffüllen bis 60")
p.deck = DeckGen.fill(p.deck, p.collection.keys(), Game.DECK_MAX)
var outside: Array = p.collection.keys().filter(func(id): return not (id in p.deck))
check(p.toggle_deck(outside[0]) == "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX, "60 ist voll")
p.choose_start("Feuer")
p.dust = Profile.CRAFT_COST - 1
check(p.craft("f_5_5") == "Nicht genug Staub (%d nötig)" % Profile.CRAFT_COST, "Herstellen braucht Staub")
p.dust = Profile.CRAFT_COST
check(p.craft("f_5_5") == "" and p.owns("f_5_5") and p.level("f_5_5") == 0 and p.dust == 0, "hergestellt als Common")
check(p.craft("f_5_5") == "Karte schon in der Sammlung", "nicht doppelt")
var in_deck: String = p.deck[0]
check(p.dismantle(in_deck) == "" and not p.owns(in_deck) and not (in_deck in p.deck) and p.dust == Profile.DUST_DISMANTLE["Common"], "Common zerlegt: raus aus Sammlung und Stapel, Staub dafür")
var filled := p.deck_cards()
check(filled.size() == Game.DECK_MIN and filled.all(func(id): return p.owns(id)) and filled.slice(0, p.deck.size()) == p.deck and "f_5_5" in filled, "zum Kampf wird der Stapel mit eigenen Karten bis 40 aufgefüllt")
p.deck = []
check(p.deck_cards().size() == Game.DECK_MIN, "leerer Stapel: Standard aus der Sammlung")
var d := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
check(d.collection.size() == p.collection.size() and d.owns("f_5_5"), "Sammlung gespeichert")
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 not p.started(), "Starterprofil: Startdeck noch zu wählen")
p.choose_start("Wasser")
check(p.collection.size() == Game.DECK_MIN and p.deck.size() == Game.DECK_MIN, "Startdeck gewählt")
check(FileAccess.file_exists(path), "Datei angelegt")
p.coins = 999
p.collection["f_3_2"] = 2
p.deck = ["f_3_2", "f_0_1"]
p.save(path)
var p2 := Profile.load_or_new(path)
check(p2.coins == 999 and p2.level("f_3_2") == 2 and p2.deck == ["f_3_2", "f_0_1"], "geladen")
for suffix in ["", ".bak"]:
if FileAccess.file_exists(path + suffix):
DirAccess.remove_absolute(abs + suffix)
func test_kopie() -> void:
var s := setup(["f_3_2"])
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
var a := put(h, "f_4_3")
h.deck = ["f_0_1", "f_0_2"]
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], g2.players[0].hand[0])
check(h.hand.size() == 1 and g2.players[0].hand.is_empty(), "Hand der Kopie unabhängig")
g2.players[0].deck.pop_back()
check(h.deck.size() == 2, "Stapel der Kopie unabhängig")
check(g2.log.is_empty() and g2.events.is_empty(), "Kopie schreibt keinen Verlauf")
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]
o.life = 3
var a := put(h, "f_4_3")
var act := AI.choose_action(g, h)
check(act.type == "attack" and act.creature == a and act.target == o, "KI nutzt Lethal")
## Die KI schaut nicht in verdeckte Wesen: ihre Entscheidung ist dieselbe, egal was darunter liegt.
func test_ki_verdeckt_unbekannt() -> void:
var decisions := []
for cid in ["f_2_1", "w_5_5"]:
var s := setup()
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
put(o, "f_3_2")
put(o, cid, true, true)
var act := AI.choose_action(g, o)
decisions.append(act.type + ":" + str(act.get("keyword", "")) + ":" + str(act.get("target", null) == h))
var own := AI.choose_action(g, h)
check(own.type != "attack" or own.target == null or not (own.target is Game.Creature) or own.target.card.id != "?", "echte Aktion zeigt auf echte Wesen")
check(decisions[0] == decisions[1], "gleiche Entscheidung bei Funke und Leviathan unter der Karte (%s)" % [decisions])
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 g := Game.new_battle(r)
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_ki_spiele_enden() -> void:
var r := RandomNumberGenerator.new()
r.seed = 7
for i in 12:
var g := Game.new_battle(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 (Partie %d)" % i)
## Alte Stände behalten Münzen, Staub, Kampagne und Einstellungen und bekommen die Startsammlung.
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}, "perk": "resonanz_doppelt"}
var p := Profile.from_dict(alt)
check(p.coins == 300 and p.dust == 70 and p.campaign_stage == 2, "alter Stand gelesen")
check(not p.started() and p.deck.is_empty() and p.perk == "", "alte Sammlung verworfen, Startdeck wird neu gewählt, unbekanntes Merkmal verworfen")
check(is_equal_approx(p.settings.volume, 0.3) and p.settings.animations, "Einstellungen übernommen")
var fresh := Profile.new()
check(fresh.settings.volume == 0.0 and fresh.settings.music == 0.0, "Ton und Musik sind standardmäßig aus")
p.collection["f_3_2"] = 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_3_2") == 3 and p2.campaign_stage == 2, "neuer Stand gelesen")
var kaputt := Profile.from_dict({"version": Profile.SAVE_VERSION, "collection": {"gibt_es_nicht": 1, "f_3_2": 9}, "deck": ["f_3_2", "f_3_2", "w_5_5"]})
check(not kaputt.owns("gibt_es_nicht") and kaputt.level("f_3_2") == Cards.RARITIES.size() - 1 and kaputt.deck == ["f_3_2"], "unbekannte Karten und Doppelte fliegen raus")
func test_kampagne() -> void:
var r := RandomNumberGenerator.new()
r.seed = 4
check(Campaign.STAGES.size() == 10, "zehn Stufen")
var boss := Campaign.opponent(4, r)
check(boss.name == "Vulkar" and boss.title == "Herr der Lava" and boss.life == 24, "Stufe 5: Vulkar mit 24 Leben")
check(boss.deck.size() == Game.DECK_MIN and boss.looks.values().all(func(l): return l == 0), "Kapitel 1: Stapel in Common")
var g := Game.new_battle_with(boss, r)
check(g.opponent == boss and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet")
var p := Profile.starter()
var reward := p.record_battle(true, 0)
check(p.campaign_stage == 1 and reward == Campaign.STAGES[0].reward, "Stufe 1 abgeschlossen")
reward = p.record_battle(true, 0)
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN, "Wiederholung gibt normale Belohnung")
reward = p.record_battle(false, 1)
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
p.campaign_stage = 9
p.record_battle(true, 9)
check(p.campaign_stage == 10, "Kampagne abgeschlossen")
var sim := Game.new_battle_with(Campaign.opponent(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_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_3_2", "l_3_1"])
var g: Game = s[0]
var h: Game.Player = s[1]
h.perks = ["feuer_angriff", "wesen_angriff", "wesen_leben", "alle_flink", "ziehen_gratis"]
g.play_card(h, h.hand[0])
var wolf: Game.Creature = h.board[0]
check(wolf.power() == 3 + 1 + 1 and wolf.health == 3 and wolf.has("Flink") and wolf.can_attack(), "Feuer +1, Wesen +1/+1, flink")
g.play_card(h, h.hand[0])
check(h.board[1].power() == 3 + 1, "Luft: nur Wesen +1")
h.deck = ["f_2_1", "f_2_1", "f_2_1"]
h.draws = 1 # ohne Merkmal kostete das zweite Ziehen 1
var mana := h.energy
check(g.draw_cost(h) == 0 and g.draw(h) == "" and h.energy == mana and g.draw_cost(h) == 1, "Merkmal: ein Ziehen mehr umsonst")
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 = Game.new_battle_with(Campaign.opponent(1, r), r).opponent
check(mara.life == 22 + 5 and mara.max_life == 27 and mara.perks == ["leben_35"], "Mara hat mit ihrem Merkmal 5 Leben mehr")
var g3 := Game.new_battle_with(Campaign.opponent(9, r), r, true)
var archi: Game.Player = g3.opponent
check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt")
g3.end_turn()
check(g3.current() == archi and archi.energy == Game.START_MANA + 2, "Archimagier hat im ersten Zug 2 Mana mehr")
var nyx: Game.Player = Campaign.opponent(5, r)
var g4 := Game.new_battle_with(nyx, r, true)
g4.end_turn()
check(nyx.max_energy == Game.START_MANA and nyx.energy == Game.START_MANA + 1, "Nyx bekommt 1 Mana mehr je Zug")
func test_ki_zwei_schritte() -> void:
# Gegner hat 5 Leben und einen Funken davor; der Titan (5 Angriff) und der Kobold sind bereit:
# erst den Funken mit dem Kobold wegräumen, dann mit dem Titan töten.
var s := setup()
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
h.turns = 2
o.life = 5
var king := put(h, "e_5_5")
var wolf := put(h, "f_3_2")
var blocker := put(o, "f_2_1")
var act := AI.choose_action(g, h)
check(act.type == "attack" and act.creature == wolf and act.target == blocker, "KI räumt zuerst mit dem Kobold den Funken weg")
AI.apply_action(g, h, act)
var act2 := AI.choose_action(g, h)
check(act2.type == "attack" and act2.creature == king and act2.target == o, "dann tödlicher Angriff mit dem Titan")
AI.apply_action(g, h, act2)
check(g.winner == "human", "Zwei-Schritt-Plan gewinnt")
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 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 archi2 := Campaign.opponent(19, r)
check(archi2.name == "Archimagier" and archi2.perks.size() == 2 and archi2.life == 28, "letzte Stufe von Kapitel 2: Archimagier mit zwei Merkmalen und 28 Leben")
check(archi2.looks.values().all(func(l): return l == 1), "Kapitel 2: seine Karten sind Rare")
check(Campaign.opponent(25, r).looks.values().all(func(l): return l == 2), "Kapitel 3: Super Rare")
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(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, 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, "gespeichert und gelesen")
func test_ki_zeitbudget() -> void:
var r := RandomNumberGenerator.new()
r.seed = 12
var g := Game.new_battle(r)
var h: Game.Player = g.players[0]
h.energy = 30
for i in 7:
h.hand.append(C("f_4_3"))
for p in g.players:
for i in 3:
put(p, "e_3_5")
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", "fuse", "draw", "end"] and ms < 80, "Zeitbudget greift (%d ms)" % ms)
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, Eigenschaften werden übersetzt.
func test_englische_karten() -> void:
check(Cards.all().all(func(c): return c.name_en != ""), "alle Karten mit englischem Namen")
var wolf := C("f_2_3")
wolf.keywords = ["Spott", "Doppelschlag"]
TranslationServer.set_locale("en")
check(wolf.title() == "Blazing Wolf" and wolf.text() == "Taunt, Double Strike.", "Wolf auf Englisch: %s / %s" % [wolf.title(), wolf.text()])
TranslationServer.set_locale("de")
check(wolf.title() == "Lodernder Wolf" and wolf.text() == "Spott, Doppelschlag.", "zurück auf Deutsch")
## 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.KEYWORD_TEXT.values() + Game.PERKS.values() + Cards.ELEMENT_KEYWORD.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])