952 lines
46 KiB
GDScript
952 lines
46 KiB
GDScript
extends SceneTree
|
|
## Regel-, KI- und Spielstand-Tests ohne Fenster:
|
|
## godot --headless --path . -s tests/test_runner.gd
|
|
|
|
var passed := 0
|
|
var failed := 0
|
|
|
|
|
|
func check(cond: bool, msg: String) -> void:
|
|
if cond:
|
|
passed += 1
|
|
else:
|
|
failed += 1
|
|
printerr("FEHLER: " + msg)
|
|
|
|
|
|
func C(id: String) -> Cards.Card:
|
|
return Cards.by_id(id).copy()
|
|
|
|
|
|
## Duell mit 10 Mana; der Gegner hat 15 Leben, kein Stapel nötig. 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)
|
|
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_ziehen()
|
|
test_wesen_und_angriff()
|
|
test_elementvorteil()
|
|
test_verdeckt()
|
|
test_elementboni()
|
|
test_spott_und_regeneration()
|
|
test_verbessern()
|
|
test_verschmelzen()
|
|
test_limits()
|
|
test_gleiche_regeln()
|
|
test_zugreihenfolge()
|
|
test_sieg_niederlage()
|
|
test_booster()
|
|
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_snapshot()
|
|
test_kapitel_zwei_und_merkmalwahl()
|
|
test_ki_zeitbudget()
|
|
test_englische_karten()
|
|
test_kampf_speichern()
|
|
test_uebersetzung_vollstaendig()
|
|
print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed])
|
|
quit(1 if failed > 0 else 0)
|
|
|
|
|
|
func test_kartenpool() -> void:
|
|
check(Cards.all().size() == 81, "81 Karten")
|
|
check(Cards.all().all(func(c): return c.elements.size() == 1 and c.elements[0] in Cards.ELEMENTS), "jede Karte hat genau ein Element")
|
|
for e in Cards.ELEMENTS:
|
|
check(Cards.all().any(func(c): return c.elements == [e]), "Element vorhanden: " + e)
|
|
for c in Cards.all():
|
|
check(c.health > 0 and c.cost == floori((2 * c.attack + c.health) / 3.0), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id)
|
|
check(c.keywords.is_empty() and c.text() == "", "Karten kommen ohne Eigenschaften: " + c.id)
|
|
check(C("f_funke").cost == 0 and C("f_kobold").cost == 1 and C("f_wolf").cost == 2 and C("ff_phoenix").cost == 7 and C("e_wall").cost == 1 and C("f_drache").cost == 5, "Kostenbeispiele, 0/2 kostet nichts")
|
|
var a := C("f_wolf")
|
|
var b := C("f_wolf")
|
|
a.keywords.append("Spott")
|
|
check(b.keywords.is_empty() and Cards.by_id("f_wolf").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("ff_phoenix").is_combo() and Cards.fusion_name(["Feuer"]) == "Inferno", "Kombos gibt es nur beim Verschmelzen")
|
|
check(Cards.combo_name(["Feuer", "Wasser", "Erde"]) == "Chimäre", "drei Elemente sind eine Chimäre")
|
|
|
|
|
|
## 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_wolf", "f_drache"])
|
|
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_wolf").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) == 5 and Cards.fusion_value(8, 1) == 9 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 4 and Cards.fusion_value(8, 8) == 11, "Fusionswert: besserer Wert plus ein Drittel des schwächeren")
|
|
|
|
|
|
## Start: fünf im ersten Zug bezahlbare Karten, keine Wahl. Jeder Zug beginnt mit einer Karte;
|
|
## Ziehen: 2 Mana, eine weitere bezahlbare Karte vom Stapel.
|
|
func test_ziehen() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 1
|
|
var g := Game.new_battle(r)
|
|
check(g.deck.size() == Cards.all().size() * DeckGen.COPIES - 2 * Game.START_HAND, "Stapel: jede Karte dreimal, zehn schon ausgegeben")
|
|
for p in g.players:
|
|
check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
|
|
check(p.hand.all(func(c): return c.cost <= Game.START_MANA), "nur Karten, die im ersten Zug spielbar sind")
|
|
check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare")
|
|
var first: Game.Player = g.current()
|
|
check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
|
|
first.hand.pop_back()
|
|
check(g.draw(first) == "Nicht genug Mana", "im ersten Zug reicht das Mana nicht zum Ziehen")
|
|
first.energy = 3
|
|
first.max_energy = 3
|
|
var mana := first.energy
|
|
var deck_before := g.deck.size()
|
|
check(g.draw(first) == "" and first.energy == mana - Game.DRAW_COST and first.hand.size() == Game.START_HAND, "gezogen: 2 Mana, eine Karte")
|
|
check(first.hand.back().cost <= first.max_energy and g.deck.size() == deck_before - 1, "bezahlbar; zu teure liegen wieder unten im Stapel")
|
|
first.energy = 0
|
|
first.hand.pop_back()
|
|
check(g.draw(first) == "Nicht genug Mana", "ohne Mana kein Ziehen")
|
|
var second: Game.Player = g.enemy_of(first)
|
|
check(g.draw(second) == "Nicht dein Zug", "nur im eigenen Zug")
|
|
# Leerer Stapel wird neu gemischt
|
|
g.deck = []
|
|
first.energy = 3
|
|
check(g.draw(first) == "" and g.deck.size() > 0 and g.log.any(func(l): return l.begins_with("Der Stapel wird neu gemischt")), "leerer Stapel: neu gemischt")
|
|
# Mit viel Mana tauchen auch teure Karten auf
|
|
first.max_energy = 20
|
|
first.energy = 20
|
|
var seen_expensive := false
|
|
for i in 12:
|
|
first.hand = []
|
|
first.energy = 20
|
|
g.draw(first)
|
|
if first.hand[0].cost > 5:
|
|
seen_expensive = true
|
|
check(seen_expensive, "mit 20 Mana kommen auch teure Karten")
|
|
# Zugbeginn: eine Karte gratis, solange die Hand nicht voll ist
|
|
var g5 := Game.new_battle(r)
|
|
var p1: Game.Player = g5.current()
|
|
var p2: Game.Player = g5.enemy_of(p1)
|
|
g5.end_turn()
|
|
check(p2.hand.size() == Game.START_HAND, "volle Hand: zu Zugbeginn kommt nichts")
|
|
p2.hand.pop_back()
|
|
p2.hand.pop_back()
|
|
g5.end_turn()
|
|
p1.hand.pop_back()
|
|
g5.end_turn()
|
|
check(p2.hand.size() == Game.START_HAND - 1 and p2.hand.back().cost <= p2.max_energy, "Zugbeginn: eine bezahlbare Karte gezogen")
|
|
g5.end_turn()
|
|
check(p1.hand.size() == Game.START_HAND, "auch der Startspieler zieht zu Zugbeginn")
|
|
|
|
|
|
## 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_wolf"])
|
|
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] == "Flammenwolf 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_wall") # 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 == "Steinwall 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_troll") # 3/6
|
|
g.begin_turn(h)
|
|
g.attack(wolf, troll)
|
|
check(troll.health == 3 and wolf.health <= 0 and not (wolf in h.board), "Konter tötet den Wolf")
|
|
g.begin_turn(o)
|
|
check(troll.health == 3, "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")
|
|
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_tiefen") # Wasser 4/6
|
|
var wolf := put(o, "f_wolf") # Feuer 3/2
|
|
var life: int = o.life
|
|
g.attack(tiefen, wolf)
|
|
check(wolf.health <= 0 and o.life == life and tiefen.health == 6 - 2, "Wasser gegen Feuer: +1 beim Zuschlagen, der Konter macht 1 weniger, nichts schlägt durch")
|
|
var s3 := setup()
|
|
var w3 := put(s3[1], "f_wolf")
|
|
var t3 := put(s3[2], "w_tiefen")
|
|
s3[0].attack(w3, t3)
|
|
check(t3.health == 6 - 2 and w3.health == 2 - 5, "Feuer gegen Wasser: 1 weniger Schaden, der Konter macht 1 mehr")
|
|
var s2 := setup()
|
|
var g2: Game = s2[0]
|
|
var geist := put(s2[1], "w_geist") # Wasser 2/2
|
|
g2.attack(geist, s2[2])
|
|
check(s2[2].life == 13, "gegen Helden gibt es keinen 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_wolf", "f_kobold"])
|
|
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_wolf").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) == "Flammenwolf 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] == "Flammenwolf 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_kobold", 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 == "Feuerkobold wird aufgedeckt."), "Aufdecken steht im Verlauf")
|
|
# Zuschauer und Gegner sehen die Karte nicht
|
|
var hid := put(h, "f_asche", true, true)
|
|
var oi := g.players.find(o)
|
|
var snap := g.snapshot(oi)
|
|
var mine: Dictionary = snap.players[g.players.find(h)]
|
|
var cd: Dictionary = mine.board.filter(func(c): return int(c.uid) == hid.uid)[0]
|
|
check(cd.hidden and cd.card is Dictionary and cd.card.id == "?" and int(cd.attack) == 0, "verdecktes Wesen reist ohne Karte und Werte")
|
|
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")
|
|
var own: Dictionary = g.snapshot(g.players.find(h)).players[g.players.find(h)].board.filter(func(c): return int(c.uid) == hid.uid)[0]
|
|
check(own.card == "f_asche", "der Besitzer sieht seine verdeckte Karte")
|
|
|
|
|
|
## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
|
|
## Flink, Wasser Regeneration, Feuer Doppelschlag); drei auf dem Tisch geben dem Element +1/+1.
|
|
func test_elementboni() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2]
|
|
var fire := [put(h, "f_wolf"), put(h, "f_kobold")]
|
|
var bird := put(o, "l_wind") # Luft 1/1
|
|
g.apply_auras()
|
|
check(fire[0].aura == 0 and fire[1].aura == 0, "zwei reichen nicht für +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")
|
|
var hidden_fire := put(o, "f_asche", true, true)
|
|
g.apply_auras()
|
|
check(fire[0].aura == 0 and g.element_counts()["Feuer"] == 2 and not hidden_fire.has("Doppelschlag"), "verdeckte zählen nicht und bekommen nichts")
|
|
var third := put(o, "f_ritter") # Feuer beim Gegner zählt für den Tisch
|
|
g.apply_auras()
|
|
check(fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and third.aura == 1, "drei Feuer auf dem Tisch: +1/+1 für alle Feuerwesen")
|
|
check(bird.aura == 0 and bird.health == 1, "kein Malus für das geschlagene Element")
|
|
check(not third.has("Doppelschlag"), "der Gegner hat nur ein offenes Feuer: kein Sondereffekt")
|
|
g._flip(hidden_fire)
|
|
check(hidden_fire.aura == 1 and third.has("Doppelschlag") and hidden_fire.has("Doppelschlag"), "aufgedeckt zählt es sofort: Bonus und Paar")
|
|
fire[0].health = 1
|
|
o.board.erase(third)
|
|
o.board.erase(hidden_fire)
|
|
g.apply_auras()
|
|
check(fire[0].aura == 0 and fire[0].max_health == 2 and fire[0].health == 1 and fire[1].health == 1, "Bonus geht wieder: nur das Höchstleben sinkt, Leben wird höchstens gekappt")
|
|
# Vorschau
|
|
check(g.forecast(h, C("f_asche")) == 1 and g.forecast(h, C("l_wind")) == 0, "Vorschau: Feuer würde den Stapel voll machen")
|
|
# Merkmal: doppelt
|
|
h.perks = ["element_doppelt"]
|
|
put(h, "f_asche")
|
|
g.apply_auras()
|
|
check(fire[0].aura == 2 and fire[0].power() == 5, "Elementbonus zählt doppelt")
|
|
h.perks = []
|
|
# Die anderen Paare
|
|
var walls := [put(h, "e_wall"), put(h, "e_wall")]
|
|
check(walls[0].has("Spott") and not fire[0].has("Spott"), "zwei Erde: Spott nur für Erdwesen")
|
|
var winds := [put(h, "l_wind", false), put(h, "l_wind", false)]
|
|
check(winds[0].has("Flink") and winds[0].can_attack(), "zwei Luft: Flink, frisch gelegt greift an")
|
|
var geister := [put(h, "w_geist"), put(h, "w_geist")]
|
|
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_wolf")
|
|
dampf.card = Cards.fused(C("f_wolf"), C("w_geist"))
|
|
var fw := put(h2, "f_kobold")
|
|
var ww := put(h2, "w_geist")
|
|
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")
|
|
put(s2[2], "f_asche")
|
|
put(s2[2], "w_reiter")
|
|
g2.apply_auras()
|
|
check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "drei Feuer und drei Wasser: Dampf bekommt +2/+2")
|
|
|
|
|
|
## 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_wolf")
|
|
var wall := put(o, "e_wall")
|
|
var kobold := put(o, "f_kobold")
|
|
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_troll", 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_tiefen")
|
|
reg.card.keywords.append("Regeneration")
|
|
var plain := put(h2, "e_troll")
|
|
var enemy := put(s2[2], "w_reiter")
|
|
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 == "Tiefenwächter 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")
|
|
|
|
|
|
## Verbessern: Eigenschaften für Mana, je Exemplar, auf Hand und Brett.
|
|
func test_verbessern() -> void:
|
|
var s := setup(["f_wolf"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2]
|
|
h.energy = 20
|
|
var card: Cards.Card = h.hand[0]
|
|
check(g.upgrade(h, card, "Spott") == "" and h.energy == 20 - 2 and "Spott" in h.hand[0].keywords, "Spott für die Handkarte gekauft")
|
|
check(h.hand[0] != card and Cards.by_id("f_wolf").keywords.is_empty(), "eigenes Exemplar, der Pool bleibt sauber")
|
|
check(g.upgrade(h, h.hand[0], "Spott") != "", "nicht doppelt")
|
|
g.play_card(h, h.hand[0])
|
|
var wolf: Game.Creature = h.board[0]
|
|
var after_play := h.energy
|
|
check(wolf.has("Spott") and not wolf.can_attack(), "mit Spott gelegt, greift noch nicht an")
|
|
check(g.upgrade(h, wolf, "Flink") == "" and wolf.can_attack() and h.energy == after_play - 2, "Flink: greift sofort an")
|
|
check(g.upgrade(h, wolf, "Doppelschlag") == "" and wolf.attacks_per_turn() == 2 and wolf.attacks_left() == 2 and h.energy == after_play - 6, "Doppelschlag: zwei Angriffe")
|
|
check(g.upgrade(h, wolf, "Regeneration") == "" and h.energy == after_play - 9, "Regeneration kostet 3")
|
|
check(wolf.card.text() == "Spott, Flink, Regeneration, Doppelschlag.", "Kartentext zeigt die Eigenschaften")
|
|
check(g.upgrade(h, wolf, "Doppelschlag") != "" and g.upgrade(h, put(o, "e_wall"), "Spott") != "" and g.upgrade(h, wolf, "Schild") != "", "nicht doppelt, nicht fremd, nicht unbekannt")
|
|
h.energy = 1
|
|
var k2 := put(h, "f_kobold")
|
|
check(g.upgrade(h, k2, "Spott") == "Nicht genug Mana", "zu teuer")
|
|
h.perks = ["verbessern_billig"]
|
|
check(g.upgrade(h, k2, "Spott") == "" and h.energy == 0, "Merkmal: Eigenschaften kosten 1 weniger")
|
|
|
|
|
|
## Verschmelzen: besserer Wert plus ein Drittel des schwächeren, Eigenschaften beider, Kosten 1, 2, 4, 8 …; auch auf der Hand.
|
|
func test_verschmelzen() -> void:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var a := put(h, "f_wolf") # 3/2
|
|
var b := put(h, "w_tiefen") # 4/6
|
|
b.health = 4
|
|
a.card.keywords.append("Spott")
|
|
check(Game.fusion_cost(a, b) == 1, "erste Verschmelzung kostet 1")
|
|
check(g.fuse(h, a, b) == "" and h.board.size() == 1 and h.energy == 9, "verschmolzen")
|
|
var f: Game.Creature = h.board[0]
|
|
check(f.name == "Dampf" and f.attack == 5 and f.max_health == 7 and f.health == 5, "Dampf: 4+1 Angriff, 6+1 Leben, aktuell 4+1")
|
|
check("Spott" in f.card.keywords and f.card.fusions == 1 and f.card.cost == 5, "Eigenschaften geerbt, einmal verschmolzen, Kosten 5")
|
|
check(f.card.elements == ["Feuer", "Wasser"] and f.ready, "beide Elemente, beide waren bereit")
|
|
var c := put(h, "e_wall", false)
|
|
check(Game.fusion_cost(f, c) == 2, "zweite Verschmelzung kostet 2")
|
|
g.fuse(h, f, c)
|
|
var f2: Game.Creature = h.board[0]
|
|
check(f2.name == "Chimäre" and f2.card.fusions == 2 and h.energy == 7 and not f2.ready, "Chimäre aus drei Elementen, nicht bereit, weil der Wall es nicht war")
|
|
var d := put(h, "l_wind")
|
|
check(Game.fusion_cost(f2, d) == 4, "dritte kostet 4")
|
|
h.energy = 3
|
|
check(g.fuse(h, f2, d) == "Nicht genug Mana", "zu teuer")
|
|
check(g.can_fuse(h, f2, f2)[1] == "Zwei verschiedene Karten wählen", "nicht mit sich selbst")
|
|
# Verdeckte verschmelzen offen
|
|
var s2 := setup()
|
|
var x := put(s2[1], "f_wolf", true, true)
|
|
var y := put(s2[1], "f_kobold", true, true)
|
|
s2[0].fuse(s2[1], x, y)
|
|
check(not s2[1].board[0].hidden and s2[1].board[0].name == "Inferno", "das Ergebnis liegt offen")
|
|
# Elementbonus der Eltern zählt nicht doppelt; Dampf bekommt die Boni beider Elemente
|
|
var s3 := setup()
|
|
var g3: Game = s3[0]
|
|
var h3: Game.Player = s3[1]
|
|
var fa := put(h3, "f_wolf")
|
|
var fb := put(h3, "w_tiefen")
|
|
for i in 2:
|
|
put(h3, "f_kobold")
|
|
g3.apply_auras()
|
|
check(fa.aura == 1 and fa.max_health == 3, "drei Feuer vor dem Verschmelzen")
|
|
g3.fuse(h3, fa, fb)
|
|
var ff: Game.Creature = h3.board[0]
|
|
check(ff.aura == 1 and ff.max_health == 8 and ff.health == 8 and ff.power() == 6, "nach dem Verschmelzen zählt der Bonus nur einmal (7 + 1)")
|
|
for i in 2:
|
|
put(h3, "w_geist")
|
|
g3.apply_auras()
|
|
check(g3.element_counts()["Wasser"] == 3 and ff.aura == 2 and ff.max_health == 9, "Feuer und Wasser voll: Dampf bekommt +2/+2")
|
|
# Auf der Hand
|
|
var s4 := setup(["f_wolf", "w_tiefen", "l_wind"])
|
|
var g4: Game = s4[0]
|
|
var h4: Game.Player = s4[1]
|
|
var hand_a: Cards.Card = h4.hand[0]
|
|
var hand_b: Cards.Card = h4.hand[1]
|
|
check(g4.can_fuse(h4, hand_a, put(h4, "e_wall"))[1] == "Nur Hand mit Hand oder Brett mit Brett", "Hand und Brett mischen sich nicht")
|
|
check(g4.fuse(h4, hand_a, hand_b) == "" and h4.hand.size() == 2 and h4.energy == 9, "zwei Handkarten verschmolzen, 1 Mana")
|
|
var hc: Cards.Card = h4.hand[0]
|
|
check(hc.name == "Dampf" and hc.attack == 5 and hc.health == 7 and hc.cost == 5 and hc.fusions == 1, "Dampf auf der Hand mit 5/7 und Kosten 5")
|
|
var lava := Cards.fused(C("f_asche"), C("f_kobold"), 4, 8)
|
|
check(Cards.fused(lava, C("f_kobold")).attack == 5 and Cards.fused(lava, C("f_kobold")).health == 9, "ein Kobold macht Lava 4/8 zu 5/9, nie schlechter")
|
|
check(g4.play_card(h4, hc) == "" and h4.energy == 9 - 5 and h4.board.back().name == "Dampf", "Ausspielen kostet die neuen Kosten")
|
|
check(g4.log.any(func(l): return l == "Flammenwolf und Tiefenwächter verschmelzen auf der Hand zu Dampf (5/7)."), "Verlauf")
|
|
|
|
|
|
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_kobold")
|
|
h.hand = [C("f_kobold")]
|
|
check(g.can_play(h, h.hand[0])[1] == "Dein Feld ist voll", "Feld voll")
|
|
h.hand = []
|
|
for i in Game.MAX_HAND:
|
|
h.hand.append(C("f_kobold"))
|
|
check(g.draw(h) == "Deine Hand ist voll" and h.hand.size() == Game.MAX_HAND, "volle Hand zieht nicht")
|
|
check(g.play_card(h, C("f_kobold")) == "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(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")
|
|
|
|
|
|
## Booster geben Seltenheitsstufen: höhere werten auf, der Rest wird zu Staub.
|
|
func test_booster() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 3
|
|
for i in 50:
|
|
var rolled := Profile.roll_booster(r)
|
|
check(rolled.size() == 5, "5 Karten")
|
|
check(rolled.any(func(x): return x.rarity != "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)
|
|
var upgraded := got.filter(func(x): return x.upgraded)
|
|
check(upgraded.all(func(x): return p.rarity(x.card.id) == x.rarity), "aufgewertete Karten tragen ihre Stufe")
|
|
check(p.dust == got.reduce(func(a, x): return a + int(x.dust), 0), "der Rest wird zu Staub")
|
|
check(p.open_booster(r).is_empty(), "ohne Münzen kein Booster")
|
|
p.dust = 100000
|
|
check(p.upgrade("f_wolf") == "" and p.level("f_wolf") >= 1, "aufgewertet")
|
|
var lvl := p.level("f_wolf")
|
|
check(p.dismantle("f_wolf") == "" and p.level("f_wolf") == lvl - 1, "eine Stufe abgegeben")
|
|
p.collection["f_wolf"] = Cards.RARITIES.size() - 1
|
|
check(p.upgrade("f_wolf") != "", "über die höchste Stufe hinaus geht nicht")
|
|
p.collection.erase("f_wolf")
|
|
check(p.dismantle("f_wolf") != "" and p.rarity("f_wolf") == "Common", "Common-Karten gehören allen")
|
|
check(Profile.RARITY_WEIGHTS.values().reduce(func(a, b): return a + b, 0.0) == 100.0, "Chancen ergeben 100 %")
|
|
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_wolf", "w_tiefen"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
h.looks = {"w_tiefen": 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_kobold")
|
|
g.play_card(h, h.hand[0])
|
|
var fused: Game.Creature = h.board[1]
|
|
g.fuse(h, fused, kobold)
|
|
check(h.board[0].card.look == 3 and h.board[0].card.rarity_in(h.looks) == "Ultra Rare", "auch auf dem Brett und über zwei Fusionen")
|
|
check(Cards.Card.from_dict(h.board[0].card.to_dict()).look == 3, "die Stufe reist mit der Karte")
|
|
|
|
|
|
func test_spielstand() -> void:
|
|
var path := "user://test_spielstand.json"
|
|
var abs := ProjectSettings.globalize_path(path)
|
|
for suffix in ["", ".bak"]:
|
|
if FileAccess.file_exists(path + suffix):
|
|
DirAccess.remove_absolute(abs + suffix)
|
|
var p := Profile.load_or_new(path)
|
|
check(p.coins == Profile.START_COINS and p.collection.is_empty(), "Starterprofil: alle Karten Common")
|
|
check(FileAccess.file_exists(path), "Datei angelegt")
|
|
p.coins = 999
|
|
p.collection["f_wolf"] = 2
|
|
p.save(path)
|
|
var p2 := Profile.load_or_new(path)
|
|
check(p2.coins == 999 and p2.level("f_wolf") == 2, "geladen")
|
|
for suffix in ["", ".bak"]:
|
|
if FileAccess.file_exists(path + suffix):
|
|
DirAccess.remove_absolute(abs + suffix)
|
|
|
|
|
|
func test_kopie() -> void:
|
|
var s := setup(["f_wolf"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2]
|
|
var a := put(h, "f_asche")
|
|
var g2 := g.clone()
|
|
var a2 := g2.find_creature(a.uid)
|
|
check(a2 != null and a2 != a and a2.owner() == g2.players[0], "Wesen kopiert mit neuem Besitzer")
|
|
g2.attack(a2, g2.players[1])
|
|
check(g2.players[1].life == 11 and o.life == 15, "Kopie verändert das Original nicht")
|
|
g2.play_card(g2.players[0], g2.players[0].hand[0])
|
|
check(h.hand.size() == 1 and g2.players[0].hand.is_empty(), "Hand der Kopie unabhängig")
|
|
g2.upgrade(g2.players[0], a2, "Spott")
|
|
check(not a.has("Spott") and a.card.keywords.is_empty() and a2.has("Spott"), "Verbessern in der Kopie lässt das Original unberührt")
|
|
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_asche")
|
|
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_kobold", "ww_leviathan"]:
|
|
var s := setup()
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2]
|
|
put(o, "f_wolf")
|
|
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 Kobold 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; die alte Sammlung entfällt.
|
|
func test_spielstand_migration() -> void:
|
|
var alt := {"version": 5, "coins": 300, "dust": 70, "campaign_stage": 2,
|
|
"collection": {"f_kobold": 2}, "decks": [], "settings": {"volume": 0.3}, "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.collection.is_empty() and p.perk == "", "Sammlung auf Standard, unbekanntes Merkmal verworfen")
|
|
check(is_equal_approx(p.settings.volume, 0.3) and p.settings.animations, "Einstellungen übernommen")
|
|
p.collection["f_wolf"] = 3
|
|
var d := p.to_dict()
|
|
check(d.version == Profile.SAVE_VERSION, "Version gespeichert")
|
|
var p2 := Profile.from_dict(JSON.parse_string(JSON.stringify(d)))
|
|
check(p2.level("f_wolf") == 3 and p2.campaign_stage == 2, "neuer Stand gelesen")
|
|
|
|
|
|
func test_kampagne() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 4
|
|
check(Campaign.STAGES.size() == 10, "zehn Stufen")
|
|
var boss := Campaign.opponent(4)
|
|
check(boss.name == "Vulkar" and boss.title == "Herr der Lava" and boss.life == 24, "Stufe 5: Vulkar mit 24 Leben")
|
|
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)
|
|
var guard := 0
|
|
while sim.winner == "" and guard < 300:
|
|
AI.take_turn(sim, sim.current(), true)
|
|
guard += 1
|
|
check(sim.winner != "", "Kampagnenduell endet")
|
|
|
|
|
|
func test_zufallssamen() -> void:
|
|
Game.test_seed = 77
|
|
var a := Game.make_rng().randi()
|
|
Game.test_seed = 77
|
|
var b := Game.make_rng().randi()
|
|
Game.test_seed = 0
|
|
check(a == b, "fester Samen wiederholbar")
|
|
var c := Game.make_rng().randi()
|
|
var d := Game.make_rng().randi()
|
|
check(c != d, "ohne Samen echter Zufall")
|
|
|
|
|
|
func test_spielstand_sicherung() -> void:
|
|
var path := "user://test_sicherung.json"
|
|
var abs := ProjectSettings.globalize_path(path)
|
|
for suffix in ["", ".bak", ".corrupt"]:
|
|
if FileAccess.file_exists(path + suffix):
|
|
DirAccess.remove_absolute(abs + suffix)
|
|
var p := Profile.load_or_new(path)
|
|
p.coins = 123
|
|
p.save(path)
|
|
p.coins = 456
|
|
p.save(path)
|
|
check(FileAccess.file_exists(path + ".bak"), "Sicherung angelegt")
|
|
check(Profile.load_or_new(path).coins == 456 and Profile.load_or_new(path + ".bak").coins == 123, "Sicherung ist der Vorgänger")
|
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
|
f.store_string("{kaputt")
|
|
f.close()
|
|
check(Profile.load_or_new(path).coins == 123, "kaputter Stand: Sicherung greift")
|
|
for suffix in ["", ".bak", ".corrupt"]:
|
|
if FileAccess.file_exists(path + suffix):
|
|
DirAccess.remove_absolute(abs + suffix)
|
|
|
|
|
|
func test_merkmale() -> void:
|
|
var s := setup(["f_wolf", "l_falke"])
|
|
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")
|
|
g.deck = DeckGen.deck(g.rng)
|
|
var mana := h.energy
|
|
check(g.draw(h) == "" and h.energy == mana and h.hand.size() == 1, "Merkmal: Ziehen kostet nichts")
|
|
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).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 kein Wesen; der Wüstenkönig (5 Angriff) liegt frisch auf dem Brett:
|
|
# erst Flink kaufen, dann mit ihm 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, "el_koenig", false)
|
|
put(h, "f_wolf", false)
|
|
var act := AI.choose_action(g, h)
|
|
check(act.type == "upgrade" and act.creature == king and act.keyword == "Flink", "KI kauft zuerst Flink für den König")
|
|
AI.apply_action(g, h, act)
|
|
var act2 := AI.choose_action(g, h)
|
|
check(act2.type == "attack" and act2.target == o, "dann tödlicher Angriff")
|
|
AI.apply_action(g, h, act2)
|
|
check(g.winner == "human", "Zwei-Schritt-Plan gewinnt")
|
|
|
|
|
|
func test_snapshot() -> void:
|
|
var s := setup(["f_wolf"], ["w_frost"])
|
|
var g: Game = s[0]
|
|
var h: Game.Player = s[1]
|
|
var o: Game.Player = s[2]
|
|
g.upgrade(h, h.hand[0], "Spott")
|
|
g.play_card(h, h.hand[0])
|
|
var troll := put(o, "e_troll")
|
|
troll.attacks_done = 1
|
|
var hi := g.players.find(h)
|
|
var oi := g.players.find(o)
|
|
h.energy = 5
|
|
o.energy = 5
|
|
g.deck = DeckGen.deck(g.rng)
|
|
g.draw(h)
|
|
var snap := g.snapshot(hi)
|
|
var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
|
|
var g2 := Game.from_snapshot(json)
|
|
check(g2.players.size() == 2 and g2.turn == g.turn and g2.current_idx == g.current_idx and g2.deck.size() == g.deck.size(), "Grunddaten und Stapelgröße")
|
|
check(g2.players[hi].hand.size() == 1 and g2.players[hi].hand[0].id == h.hand[0].id, "eigene Hand sichtbar")
|
|
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0].id != "w_frost", "fremde Hand nur als Anzahl")
|
|
var cr2 := g2.find_creature(h.board[0].uid)
|
|
check(cr2 != null and cr2.card.id == "f_wolf" and cr2.owner() == g2.players[hi] and cr2.has("Spott") and not cr2.ready, "Wesen mit Besitzer, Zustand und gekaufter Eigenschaft")
|
|
var troll2 := g2.find_creature(troll.uid)
|
|
check(troll2.ready and troll2.attacks_done == 1, "Bereitschaft und Angriffe reisen mit")
|
|
check(g2.events.size() == g.events.size() and g2.events.back().type == g.events.back().type, "Ereignisse übertragen")
|
|
check(g2.draw(g2.players[oi]) == "Nicht dein Zug", "Kopie kennt die Regeln")
|
|
var snap_o := g.snapshot(oi)
|
|
check(snap_o.players[hi].hand is int and snap_o.players[oi].hand is Array, "Sicht des Gegners")
|
|
|
|
|
|
func test_kapitel_zwei_und_merkmalwahl() -> void:
|
|
check(Campaign.total() == 30 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(29) == 3, "dreißig Stufen in drei Kapiteln")
|
|
check(Campaign.stage(25).perks.size() == 3 and Campaign.stage(25).name == "Nyx", "Kapitel 3 mit drei Merkmalen")
|
|
for 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() == 2 and archi2.life == 28, "letzte Stufe von Kapitel 2: Archimagier mit zwei Merkmalen und 28 Leben")
|
|
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, "Version 5 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 Game.MAX_HAND:
|
|
h.hand.append(C("f_ritter"))
|
|
for p in g.players:
|
|
for i in 3:
|
|
put(p, "e_troll")
|
|
AI.time_budget_ms = 1
|
|
var t0 := Time.get_ticks_msec()
|
|
var act := AI.choose_action(g, h)
|
|
var ms := Time.get_ticks_msec() - t0
|
|
AI.time_budget_ms = 150
|
|
check(act.type in ["play", "attack", "upgrade", "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, gekaufte Eigenschaften werden übersetzt.
|
|
func test_englische_karten() -> void:
|
|
check(Cards.all().all(func(c): return c.name_en != ""), "alle Karten mit englischem Namen")
|
|
var ritter := C("f_ritter")
|
|
ritter.keywords = ["Spott", "Doppelschlag"]
|
|
TranslationServer.set_locale("en")
|
|
check(ritter.title() == "Blazing Knight" and ritter.text() == "Taunt, Double Strike.", "Ritter auf Englisch: %s / %s" % [ritter.title(), ritter.text()])
|
|
TranslationServer.set_locale("de")
|
|
check(ritter.title() == "Lodernder Ritter" and ritter.text() == "Spott, Doppelschlag.", "zurück auf Deutsch")
|
|
|
|
|
|
## Voller Spielstand (viewer -1) stellt Hände und Bretter wieder her; Zuschauer (-2) sehen nichts Verborgenes.
|
|
func test_kampf_speichern() -> void:
|
|
var r := RandomNumberGenerator.new()
|
|
r.seed = 7
|
|
var g := Game.new_battle(r)
|
|
AI.take_turn(g, g.current())
|
|
var d: Dictionary = JSON.parse_string(JSON.stringify(g.snapshot(-1)))
|
|
var g2 := Game.from_snapshot(d)
|
|
var same := g2.players.size() == g.players.size()
|
|
for i in g.players.size():
|
|
var a: Game.Player = g.players[i]
|
|
var b: Game.Player = g2.players[i]
|
|
var ids := func(cs: Array): return cs.map(func(c): return c.id)
|
|
same = same and ids.call(a.hand) == ids.call(b.hand) \
|
|
and a.life == b.life and a.board.size() == b.board.size() and a.is_human == b.is_human
|
|
check(same and g2.current_idx == g.current_idx and g2.turn == g.turn and g2.human.is_human, "voller Spielstand: Hände und Bretter erhalten")
|
|
g2.deck = DeckGen.deck(r) # der Stapel reist nur als Größe, zum Weiterspielen braucht er Karten
|
|
var guard := 0
|
|
while g2.winner == "" and guard < 300:
|
|
AI.take_turn(g2, g2.current())
|
|
guard += 1
|
|
check(g2.winner != "", "der volle Spielstand ist bis zum Ende spielbar")
|
|
var s2: Dictionary = g.snapshot(-2)
|
|
check(s2.players.all(func(p): return not (p.hand is Array)), "Zuschauer-Sicht ohne Hände")
|
|
|
|
|
|
## 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])
|