1052 lines
56 KiB
GDScript
1052 lines
56 KiB
GDScript
extends SceneTree
|
||
## Regel-, KI- und Spielstand-Tests ohne Fenster:
|
||
## godot --headless --path . -s tests/test_runner.gd
|
||
|
||
var passed := 0
|
||
var failed := 0
|
||
|
||
|
||
func check(cond: bool, msg: String) -> void:
|
||
if cond:
|
||
passed += 1
|
||
else:
|
||
failed += 1
|
||
printerr("FEHLER: " + msg)
|
||
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])
|