Files
cards/tests/test_runner.gd
T
team3 1d563b9eec
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Skipped
Tests / balance (push) Skipped
update
2026-10-14 17:35:09 +02:00

1116 lines
64 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_einzelspieler_gegner()
test_skilltree()
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)
## 550 Karten: je Element und für Neutral alle Werte von 0/1 bis 10/10, die ID verrät die Werte.
func test_kartenpool() -> void:
check(Cards.all().size() == 550, "550 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() == 110, "110 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 <= 10 and c.health >= 1 and c.health <= 10, "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() == 550, "alle Namen verschieden")
check(C("f_10_10").cost == 10 and C("n_6_1").name == "Eiserner Knappe" and C("w_0_10").name == "Stiller Urleviathan", "10/10 kostet 10; neue Eigenschaften und Wesen ab 6")
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(h.cards_played == 1 and g.clone().human.cards_played == 1, "ausgespielte Karte gezählt, auch in der Kopie")
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
## Zufällige Stapel; Elementstapel der Kampagne in der Stufe des Kapitels; 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, "30 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 neutral := DeckGen.element_deck(Cards.NEUTRAL)
check(neutral.size() == 110 and neutral.all(func(id): return Cards.by_id(id).is_neutral()), "alle 110 neutralen Karten")
var start := DeckGen.curve_deck(Cards.NEUTRAL, r)
var per_cost := {}
var uniq2 := {}
for id in start:
per_cost[Cards.by_id(id).cost] = int(per_cost.get(Cards.by_id(id).cost, 0)) + 1
uniq2[id] = true
check(start.size() == Game.DECK_MIN and start.all(func(id): return Cards.by_id(id).is_neutral()) and per_cost.get(0, 0) == 2 and per_cost.get(3, 0) == 6 and per_cost.get(7, 0) == 1 and not per_cost.has(8), "Kurvenstapel: 30 Neutrale nach Manakurve (2 zu 0, 6 zu 3, 1 zu 7, nichts darüber)")
check(uniq2.size() == Game.DECK_MIN and DeckGen.curve_deck(Cards.NEUTRAL, r) != start, "nichts doppelt, jedes Mal anders gewürfelt")
var capped := DeckGen.curve_deck("Feuer", r, 3)
check(capped.size() == Game.DECK_MIN and capped.all(func(id): return Cards.by_id(id).cost <= 3 and Cards.by_id(id).elements[0] == "Feuer"), "Kostendeckel 3: 30 Feuerkarten bis Kosten 3")
var fenn := Campaign.opponent(0, r)
check(fenn.deck.size() == Game.DECK_MIN and fenn.deck.all(func(id): return Cards.by_id(id).is_neutral() and Cards.by_id(id).cost <= 3) and fenn.looks.values().all(func(l): return l == 0), "Kapitel 1, Stufe 1: 30 Neutrale bis Kosten 3 in Common")
var boss := Campaign.opponent(15, r)
check(boss.deck.size() == Game.DECK_MIN and boss.deck.all(func(id): return Cards.by_id(id).elements[0] == "Feuer" and Cards.by_id(id).cost <= 6) and boss.looks.values().all(func(l): return l == 0), "Kapitel 2, Stufe 6: Feuer bis Kosten 6 in Common")
check(Campaign.stage(0).max_cost == 3 and Campaign.stage(9).max_cost == 10 and Campaign.stage(14).max_cost == 5, "Kostendeckel steigt je Stufe von 3 auf 10")
for c in Campaign.CHAPTER_ELEMENTS.size():
var e: String = Campaign.CHAPTER_ELEMENTS[c]
var o := Campaign.opponent(c * Campaign.STAGES_PER_CHAPTER + 9, r)
check(o.deck.all(func(id): return Cards.by_id(id).elements[0] == e) and o.looks.values().all(func(l): return l == maxi(0, c - 1)), "Boss von Kapitel %d: nur %s in Stufe %d" % [c + 1, e, maxi(0, c - 1)])
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", "e_0_4", "f_2_1"], "Auffüllen ohne Stufen: gleicher Wert, dann die billigeren, nach ID, nichts doppelt, bis zur Grenze")
var full := DeckGen.fill([], neutral, Game.DECK_MIN)
var curve_counts: Array[int] = []
curve_counts.resize(11)
curve_counts.fill(0)
for id in full:
curve_counts[Cards.by_id(id).cost] += 1
check(full.size() == Game.DECK_MIN and curve_counts == DeckGen.CURVE, "Auffüllen aus 110 Neutralen folgt der Manakurve")
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.element_deck(Cards.NEUTRAL), {}) == 100, "Stärke: Mittelwert 300, leer 0, Startsammlung 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")
# Schaden: im Vorteil doppelt, im Nachteil halb (aufgerundet), gegen Helden 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
check(Game.matchup_bonus(tiefen, wolf) == 4 and Game.matchup_bonus(wolf, tiefen) == -1, "Wasser schlägt Feuer doppelt (4 → 8), Feuer kontert halb (3 → 2)")
var life: int = o.life
g.attack(tiefen, wolf)
check(wolf.health <= 0 and o.life == life and tiefen.health == 5 - 2, "8 Schaden töten den Wolf, der Konter bringt 2, nichts schlägt durch")
# Nachteil beim Angriff: halb aufgerundet; der Konter des Ziels kommt doppelt
var s4 := setup()
var f4 := put(s4[1], "f_3_2")
var w4 := put(s4[2], "w_4_5")
s4[0].attack(f4, w4)
check(w4.health == 5 - 2 and f4.health <= 0, "Feuer trifft Wasser für 2 (3 halbiert, aufgerundet), der Konter 4 × 2 tötet")
# Rundung und Null
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: auch im Vorteil kein Schaden")
var wall := put(s5[2], "w_5_5")
check(Game.matchup_bonus(put(s5[1], "f_1_1"), wall) == 0 and Game.matchup_bonus(put(s5[1], "f_2_1"), wall) == -1 and Game.matchup_bonus(put(s5[1], "f_5_1"), wall) == -2, "halbiert aufgerundet: 1 bleibt 1, 2 wird 1, 5 wird 3")
check(not s5[0].can_attack(put(s5[1], "l_0_2"), put(s5[2], "e_0_4"))[0], "0 Angriff im Vorteil (Luft gegen Erde): kein Angriff")
# Helden haben kein Element
var s2 := setup()
var geist := put(s2[1], "w_2_2")
s2[0].attack(geist, s2[2])
check(s2[2].life == 15 - 2, "gegen Helden gibt es keinen Vorteil")
# Merkmal: kein Nachteil
var s6 := setup()
var f6 := put(s6[1], "f_3_2")
var w6 := put(s6[2], "w_4_5")
s6[1].perks = ["kein_nachteil"]
check(Game.matchup_bonus(f6, w6) == 0 and Game.matchup_bonus(w6, f6) == 4, "Merkmal: kein Nachteil für die eigenen Wesen, der Gegner behält seinen Vorteil")
## 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")
## Elementpaare: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
## Flink, Wasser Regeneration, Feuer Doppelschlag). Jeder Spieler zählt für sich, verdeckte nicht.
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
check(fire[0].has("Doppelschlag") and fire[0].attacks_per_turn() == 2 and fire[0].keywords() == ["Doppelschlag"], "zwei eigene Feuer: Doppelschlag für beide")
check(not bird.has("Flink") and bird.power() == 1 and bird.health == 1, "der Gegner zählt für sich: nichts")
var hidden_fire := put(o, "f_4_3", true, true)
check(not hidden_fire.has("Doppelschlag") and o.element_count("Feuer") == 0, "verdeckte zählen nicht fürs Paar")
g.play_card(h, h.hand[0]) # Flammender Wolf
check(h.board[2].has("Doppelschlag") and h.board[2].power() == 4 and h.board[2].health == 3, "ausgespielt: Werte wie auf der Karte, dazu der Effekt")
g.play_card(h, h.hand[0], true) # zweiter Wolf verdeckt
var secret: Game.Creature = h.board.back()
check(secret.hidden and not secret.has("Doppelschlag") and h.element_count("Feuer") == 3, "verdeckt gelegt: ohne Effekt, zählt nicht")
g._flip(secret)
check(secret.has("Doppelschlag") and h.element_count("Feuer") == 4, "aufgedeckt zählt es und bekommt den Effekt")
# 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
var s2 := setup()
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")
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")
# Neutral: kein Paar
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.board.size() == 2 and h7.board[0].keywords().is_empty() and h7.board[0].health == 2, "zwei Neutrale: kein Paar, nichts ändert sich")
## 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")
# 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, "Feuer")
check(rolled.size() == 5 and rolled.all(func(x): return x.card.elements[0] == "Feuer"), "5 Feuerkarten")
check(rolled.any(func(x): return x.rarity != "Common"), "mindestens eine Rare")
var p := Profile.new()
p.coins = Profile.ELEMENT_BOOSTER_COST
check(p.open_booster("Feuer", r).is_empty() and p.coins == Profile.ELEMENT_BOOSTER_COST and not p.booster_unlocked("Feuer") and p.booster_unlocked(Cards.NEUTRAL), "Feuer-Booster erst ab Kapitel 2, neutral immer")
p.campaign_stage = Campaign.STAGES_PER_CHAPTER
check(p.booster_unlocked("Feuer") and not p.booster_unlocked("Wasser"), "Kapitel 2 offen: Feuer ja, Wasser nein")
var got := p.open_booster("Feuer", r)
check(p.coins == 0 and got.size() == 5 and got.all(func(x): return x.card.elements[0] == "Feuer"), "Feuer-Booster kostet %d" % Profile.ELEMENT_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("Feuer", r).is_empty(), "ohne Münzen kein Booster")
p.coins = Profile.BOOSTER_COST
check(p.open_booster(Cards.NEUTRAL, r).size() == 5 and p.coins == 0, "neutraler Booster kostet %d" % Profile.BOOSTER_COST)
p.coins = Profile.ELEMENT_BOOSTER_COST * 3
check(p.open_boosters("Feuer", 4, r).is_empty() and p.coins == Profile.ELEMENT_BOOSTER_COST * 3, "vier Packs: zu teuer, nichts passiert")
check(p.open_boosters("Feuer", 3, r).size() == 15 and p.coins == 0, "drei Packs auf einmal: 15 Karten, alles bezahlt")
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.ELEMENT_BOOSTER_COST * 20
var dust_before := p.dust
var upgraded := false
for i in 20:
for res in p.open_booster("Feuer", 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 den neutralen Karten, Herstellen für Staub, Zerlegen entfernt
## Common-Karten, der Stapel hält 30 bis 60 eigene Karten.
func test_sammlung_und_deck() -> void:
var p := Profile.starter()
check(p.collection.size() == 110 and p.deck.size() == Game.DECK_MIN and p.deck.all(func(id): return p.owns(id) and p.level(id) == 0 and Cards.by_id(id).is_neutral()), "neues Profil: alle 110 Neutralen in Common in der Sammlung, 30 im 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 = Profile.starter()
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, "zum Kampf wird der Stapel mit eigenen Karten bis 30 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 p.collection.size() == 110 and p.deck.size() == Game.DECK_MIN, "Starterprofil: neutrale Karten")
check(FileAccess.file_exists(path), "Datei angelegt")
p.coins = 999
p.collection["f_3_2"] = 2
p.collection["f_0_1"] = 0
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(p.deck.size() == Game.DECK_MIN and p.collection.size() == 110 and p.perk == "", "alte Sammlung verworfen, neutrale Startsammlung, 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_PER_CHAPTER == 10 and Campaign.total() == 50, "fünf Kapitel mit je zehn Stufen")
var boss := Campaign.opponent(4)
check(boss.name == "Vulkar" and boss.title == "Söldnerfürst" 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")
check(Profile.battle_coins(false, 10, 20, 8) == 190 and Profile.battle_coins(true, 10, 20, 8) == 380, "Münzen: 5 je Zug, Karte und schnellem Zug, Sieg verdoppelt")
check(Profile.battle_coins(true, 10, 20, 8, 1.5) == 570 and Profile.battle_coins(true, 10, 20, 8, 2.0) == 760 and Profile.battle_coins(false, 5, 10, 4, 2.0) == 190, "Schwierigkeit ×1,5 und ×2")
check(Profile.DIFFICULTIES.leicht.reward == 1.0 and Profile.DIFFICULTIES.normal.reward == 1.5 and Profile.DIFFICULTIES.schwer.reward == 2.0, "Münzfaktoren 1 / 1,5 / 2")
var p := Profile.starter()
var reward := p.record_battle(true, 380, 0)
check(p.campaign_stage == 1 and reward == 380 + Campaign.stage(0).reward and p.coins == Profile.START_COINS + reward, "Stufe 1 abgeschlossen: Münzen plus Stufenbelohnung")
reward = p.record_battle(true, 380, 0)
check(p.campaign_stage == 1 and reward == 380, "Wiederholung nur die Münzen des Kampfs")
reward = p.record_battle(false, 190, 1)
check(p.campaign_stage == 1 and reward == 190, "Niederlage schaltet nichts frei")
p.campaign_stage = 9
p.record_battle(true, 380, 9)
check(p.campaign_stage == 10, "Kampagne abgeschlossen")
var sim := Game.new_battle_with(Campaign.opponent(0), r)
var guard := 0
while sim.winner == "" and guard < 300:
AI.take_turn(sim, sim.current(), true)
guard += 1
check(sim.winner != "", "Kampagnenduell endet")
func test_zufallssamen() -> void:
Game.test_seed = 77
var a := Game.make_rng().randi()
Game.test_seed = 77
var b := Game.make_rng().randi()
Game.test_seed = 0
check(a == b, "fester Samen wiederholbar")
var c := Game.make_rng().randi()
var d := Game.make_rng().randi()
check(c != d, "ohne Samen echter Zufall")
func test_spielstand_sicherung() -> void:
var path := "user://test_sicherung.json"
var abs := ProjectSettings.globalize_path(path)
for suffix in ["", ".bak", ".corrupt"]:
if FileAccess.file_exists(path + suffix):
DirAccess.remove_absolute(abs + suffix)
var p := Profile.load_or_new(path)
p.coins = 123
p.save(path)
p.coins = 456
p.save(path)
check(FileAccess.file_exists(path + ".bak"), "Sicherung angelegt")
check(Profile.load_or_new(path).coins == 456 and Profile.load_or_new(path + ".bak").coins == 123, "Sicherung ist der Vorgänger")
var f := FileAccess.open(path, FileAccess.WRITE)
f.store_string("{kaputt")
f.close()
check(Profile.load_or_new(path).coins == 123, "kaputter Stand: Sicherung greift")
for suffix in ["", ".bak", ".corrupt"]:
if FileAccess.file_exists(path + suffix):
DirAccess.remove_absolute(abs + suffix)
func test_merkmale() -> void:
var s := setup(["f_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(range(Campaign.total()).any(func(i): return perk in Campaign.stage(i).perks), "Merkmal vergeben: " + perk)
var r := RandomNumberGenerator.new()
r.seed = 2
var mara: Game.Player = Game.new_battle_with(Campaign.opponent(1), 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, 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)
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() == 50 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(49) == 5, "fünfzig Stufen in fünf Kapiteln")
check(Campaign.stage(25).perks.size() == 2 and Campaign.stage(25).name == "Nyx" and Campaign.stage(25).element == "Wasser", "Kapitel 3: Wasser, zwei Merkmale")
check(Campaign.stage(45).perks.size() == 3 and Campaign.stage(40).element == "Erde" and Campaign.stage(49).reward == 1500 and Campaign.stage(10).reward == 350 and Campaign.stage(9).reward == 500, "Kapitel 5: Erde, drei Merkmale; Belohnungen 350 bis 1500, Boss 1 gibt 500")
check(Campaign.stage(0).perks == ["feuer_angriff"] and Campaign.stage(23).perks == ["ziehen_gratis", "kein_nachteil"] and Campaign.stage(49).perks == ["start_energie", "wesen_angriff", "extra_mana"], "Merkmale je drei Plätze versetzt, nichts doppelt")
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)
check(archi2.name == "Archimagier" and archi2.perks.size() == 1 and archi2.life == 28, "letzte Stufe von Kapitel 2: Archimagier mit einem Merkmal und 28 Leben")
check(archi2.looks.values().all(func(l): return l == 0) and archi2.deck.all(func(id): return Cards.by_id(id).elements[0] == "Feuer"), "Kapitel 2: seine Karten sind Feuer in Common")
check(Campaign.opponent(25).looks.values().all(func(l): return l == 1) and Campaign.opponent(45).looks.values().all(func(l): return l == 3), "Kapitel 3 Rare, Kapitel 5 Ultra 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, 380, 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")
## Einzelspieler: der Gegner spielt ein offenes Element in einer Stufe relativ zu deiner Deckstärke.
func test_einzelspieler_gegner() -> void:
var r := RandomNumberGenerator.new()
r.seed = 21
var p := Profile.starter()
check(p.deck_level() == 0 and p.unlocked_elements() == [Cards.NEUTRAL] and p.unlocked_cards().size() == 110 and p.unlocked_cards().all(func(c): return c.is_neutral()), "Start: Stufe Common, nur Neutral offen, 110 Karten sichtbar")
check(Profile.DIFFICULTIES.leicht.level == -1 and Profile.DIFFICULTIES.normal.level == 0 and Profile.DIFFICULTIES.schwer.level == 1, "Versatz -1 / 0 / +1")
p.settings.difficulty = "schwer"
var o := p.single_opponent(r)
check(o.deck.size() == Game.DECK_MIN and o.deck.all(func(id): return Cards.by_id(id).is_neutral()) and o.looks.values().all(func(l): return l == 1) and o.life == Game.START_LIFE, "Schwer: 30 Neutrale, eine Stufe über dir, 30 Leben")
check(o.perks.size() == 1 and Game.PERKS.has(o.perks[0]) and o.title == Cards.NEUTRAL, "Schwer: ein zufälliges Merkmal, Element als Titel")
p.settings.difficulty = "leicht"
o = p.single_opponent(r)
check(o.looks.values().all(func(l): return l == 0) and o.perks.is_empty(), "Leicht: nicht unter Common, kein Merkmal")
for id in p.collection:
p.collection[id] = 3
check(p.deck_level() == 3 and p.opponent_level() == 2, "Ultra-Rare-Deck: Stufe 3, Leicht eine darunter")
p.settings.difficulty = "normal"
check(p.opponent_level() == 3 and p.single_opponent(r).looks.values().all(func(l): return l == 3), "Normal: deine Stufe")
for id in p.deck.slice(0, 3):
p.collection[id] = 4
check(DeckGen.strength(p.deck_cards(), p.collection) == 410 and p.deck_level() == 3, "410 rundet auf Stufe 3")
p.campaign_stage = Campaign.STAGES_PER_CHAPTER
check(p.unlocked_elements() == [Cards.NEUTRAL, "Feuer"] and p.unlocked_cards().size() == 220, "Kapitel 2 offen: Feuer dazu, 220 Karten sichtbar")
var seen := {}
for i in 30:
var e: String = Cards.by_id(p.single_opponent(r).deck[0]).elements[0]
seen[e] = true
check(seen.keys().size() == 2 and seen.has("Feuer") and seen.has(Cards.NEUTRAL), "Gegner würfelt aus Neutral und Feuer")
p.campaign_stage = Campaign.total()
check(p.unlocked_elements() == Array(Campaign.CHAPTER_ELEMENTS), "alles offen: alle fünf")
var g := Game.new_battle_with(p.single_opponent(r), r, false, {"deck": p.deck_cards(), "looks": p.looks()})
check(g.opponent.deck.size() + g.opponent.hand.size() == Game.DECK_MIN and g.opponent.hand.all(func(c): return c.look == 3), "im Kampf: 30 Karten in Stufe 3")
## Skilltree: Münzen sind Erfahrung, je Level ein Punkt, Skills wirken auf Booster, Staub, Münzen und Kampf.
func test_skilltree() -> void:
check(Skills.xp_for_level(1) == 100 and Skills.xp_for_level(2) == 300 and Skills.xp_for_level(5) == 1500 and Skills.level_for(0) == 0 and Skills.level_for(299) == 1 and Skills.level_for(300) == 2, "Level brauchen 100, 300, 600 … Erfahrung")
var p := Profile.starter()
check(p.player_level() == 0 and p.skill_points() == 0 and p.learn("packglueck") == "Kein Skillpunkt frei", "Start: Level 0, kein Punkt")
var reward := p.record_battle(true, 380)
check(reward == 380 and p.xp == 380 and p.player_level() == 2 and p.skill_points() == 2 and p.take_notices() == ["Level 2 erreicht: +2 Skillpunkte"], "380 Münzen: Level 2, zwei Punkte, Meldung")
check(p.learn("packglueck") == "" and p.skill("packglueck") == 1 and p.skill_points() == 1, "Packglück gelernt")
check(p.learn("gratisziehen") == "" and p.learn("gratisziehen") == "Schon auf der höchsten Stufe" and p.skill_points() == 0, "Freies Ziehen hat nur eine Stufe")
check(p.battle_perks() == ["ziehen_gratis"], "Kampfskill wird im Kampf zum Merkmal")
p.perk = "leben_35"
p.campaign_stage = Campaign.STAGES_PER_CHAPTER
check(p.battle_perks() == ["leben_35", "ziehen_gratis"], "das gewählte Merkmal kommt dazu")
p.xp = 100000
for i in 3:
p.learn("muenzen")
p.learn("staub")
p.learn("packpreis")
p.learn("packpreis")
check(is_equal_approx(p.coin_factor(), 1.3) and p.pack_price(Cards.NEUTRAL) == 80 and p.pack_price("Feuer") == 160 and p.dust_gain(100) == 175, "Münzsegen ×1,3, Händler −20 %, Staubsammler +75 %")
var coins := p.coins
check(p.record_battle(false, 200) == 260 and p.coins == coins + 260, "260 statt 200 Münzen")
p.coins = 80
check(p.open_booster(Cards.NEUTRAL).size() == 5 and p.coins == 0, "neutraler Booster kostet mit Händler 80")
var rr := RandomNumberGenerator.new()
rr.seed = 9
var commons := 0
var commons0 := 0
for i in 200:
commons += Profile.roll_booster(rr, Cards.NEUTRAL, 3).filter(func(x): return x.rarity == "Common").size()
commons0 += Profile.roll_booster(rr, Cards.NEUTRAL, 0).filter(func(x): return x.rarity == "Common").size()
check(commons < commons0 * 0.7, "Packglück 3: deutlich weniger Common (%d statt %d)" % [commons, commons0])
p.collection["n_0_1"] = 1
var dust := p.dust
check(p.dismantle("n_0_1") == "" and p.dust == dust + 175, "Zerlegen einer Rare mit Staubsammler 3: 175 statt 100")
var d := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
check(d.xp == p.xp and d.skills == p.skills and d.skill_points() == p.skill_points(), "Erfahrung und Skills gespeichert")
var bad := Profile.from_dict({"version": Profile.SAVE_VERSION, "xp": 100, "skills": {"gibt_es_nicht": 2, "packglueck": 9}})
check(bad.skill("packglueck") == 3 and bad.skills.size() == 1 and bad.player_level() == 1, "unbekannte Skills fliegen raus, Stufen gedeckelt")
check(Skills.pack_weights(0) == PackedFloat32Array([45, 25, 15, 10, 5]) and Skills.pack_weights(3) == PackedFloat32Array([21, 34, 21, 16, 8]), "Packglück 3: Chancen 21/34/21/16/8")
check(Skills.effect("packglueck", 1) == "Chancen 37/28/17/12/6 %" and Skills.effect("packpreis", 2) == "Neutral 80, Element 160 Münzen" and Skills.effect("startmana", 1) == "erster Zug: 3 Mana" and Skills.effect("gratisziehen", 0) == "eine Karte je Zug gratis", "Wirkungstexte in Zahlen")
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])