Files
cards/tests/ui_test.gd
team3 5bc8569717
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled
update
2026-10-06 18:16:17 +02:00

1010 lines
45 KiB
GDScript

extends Node
## Oberflächentest mit echtem Fenster und Screenshots:
## DINGER_SHOTS=/pfad/zum/ordner godot --path . tests/ui_test.tscn
const SAVE := "user://test_ui_spielstand.json"
var main: Control
var shots := ""
var passed := 0
var failed := 0
func check(cond: bool, msg: String) -> void:
if cond:
passed += 1
else:
failed += 1
printerr("FEHLER: " + msg)
func frames(n: int = 2) -> void:
for i in n:
await get_tree().process_frame
func shot(name: String) -> void:
if shots == "":
return
await frames(10) # Einblenden und Austeilen abwarten
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png(shots.path_join(name + ".png"))
func click_at(pos: Vector2) -> void:
var down := InputEventMouseButton.new()
down.position = pos
down.global_position = pos
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
get_viewport().push_input(down)
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
get_viewport().push_input(up)
await frames(2)
func click(node: Control) -> void:
check(node != null, "Klickziel vorhanden")
if node != null:
await click_at(node.get_global_rect().get_center())
func right_click() -> void:
await right_click_at(Vector2(5, 5))
func right_click_at(pos: Vector2, button: int = MOUSE_BUTTON_RIGHT) -> void:
var down := InputEventMouseButton.new()
down.position = pos
down.global_position = pos
down.button_index = button
down.pressed = true
get_viewport().push_input(down)
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
get_viewport().push_input(up)
await frames(2)
func key(code: int) -> void:
var down := InputEventKey.new()
down.keycode = code
down.pressed = true
get_viewport().push_input(down)
var up := InputEventKey.new()
up.keycode = code
up.pressed = false
get_viewport().push_input(up)
await frames(2)
func pad(button: int) -> void:
var ev := InputEventJoypadButton.new()
ev.button_index = button
ev.pressed = true
get_viewport().push_input(ev)
await frames(2)
## Mulligan-Maske wegklicken (Hand behalten), falls sie offen ist.
func hover(node: Control) -> void:
var ev := InputEventMouseMotion.new()
ev.position = node.get_global_rect().get_center()
ev.global_position = ev.position
get_viewport().push_input(ev)
await frames(2)
func screen():
return main.screen
## Jeder Abschnitt startet im Menü, auch wenn ein vorheriger abgebrochen ist.
func ensure_menu() -> void:
if main.net.connected:
main.net.leave()
if TranslationServer.get_locale().begins_with("en"):
main.set_language("de")
if not (screen() is MenuScreen):
main.show_screen(MenuScreen.new())
await frames(2)
func find_button(text: String, root: Node = null) -> Button:
if root == null:
root = screen()
for c in root.get_children():
if c is Button and (c.text == text or c.tr(c.text) == text) and c.is_visible_in_tree():
return c
var found := find_button(text, c)
if found != null:
return found
return null
## Wartet, bis keine Kachel mehr animiert wird (Angriffe verschieben Kacheln kurz).
func settle(scr) -> void:
for i in 40:
if not scr.tiles.values().any(func(t): return t.busy):
return
await frames(1)
func wait_for_human(scr) -> void:
var g: Game = scr.game
var wait := 0
while g.current() != g.human and g.winner == "" and wait < 600:
await frames(1)
wait += 1
await frames(1)
func _ready() -> void:
Game.test_seed = 4242 # wiederholbare Partien und Booster
shots = OS.get_environment("DINGER_SHOTS")
var abs := ProjectSettings.globalize_path(SAVE)
for suffix in ["", ".bak", ".corrupt"]:
if FileAccess.file_exists(SAVE + suffix):
DirAccess.remove_absolute(abs + suffix)
main = load("res://scenes/main.tscn").instantiate()
main.save_path = SAVE
add_child(main)
await frames(3)
var fast := OS.get_environment("DINGER_FAST") != ""
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen]
if not fast:
suite += [test_kampf.bind(3), test_kampf_abbrechen]
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
# Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY).
if OS.get_environment("DINGER_NETZ") != "":
suite += [test_netzwerk, test_host_wechsel, test_zuschauer]
suite.append(test_profil_zuruecksetzen)
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_host_wechsel,test_zuschauer
if only != "":
suite = suite.filter(func(t: Callable): return t.get_method() in only.split(","))
for t in suite:
await ensure_menu()
await t.call()
for suffix in ["", ".bak", ".corrupt"]:
if FileAccess.file_exists(SAVE + suffix):
DirAccess.remove_absolute(abs + suffix)
print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed])
get_tree().quit(1 if failed > 0 else 0)
func test_menu() -> void:
check(screen() is MenuScreen, "Menü sichtbar")
check(find_button("Einzelspieler") != null and find_button("Beenden") != null, "Menüknöpfe vorhanden")
await shot("01_menu")
func test_booster() -> void:
await click(find_button("Booster öffnen"))
var scr = screen()
check(scr is BoosterScreen, "Boosterbildschirm")
var coins: int = main.profile.coins
await click(scr.b_buy)
check(main.profile.coins == coins - Profile.BOOSTER_COST, "Booster bezahlt")
check(scr.cards.size() == 5 and scr.revealed.is_empty(), "5 verdeckte Karten")
await shot("03_booster_zu")
var backs: Array = scr.slots.get_children().filter(func(c): return c is BoosterScreen.CardBack)
await click(backs[0])
check(scr.revealed.size() == 1, "eine Karte aufgedeckt")
await click(scr.b_all)
check(scr.revealed.size() == 5, "alle aufgedeckt")
await shot("04_booster_offen")
check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Kauf gespeichert")
for res in scr.cards:
if res.upgraded:
check(main.profile.level(res.card.id) == Cards.RARITIES.find(res.rarity), "aufgewertete Karte trägt ihre Stufe")
else:
check(res.dust > 0, "sonst gibt es Staub")
check(scr.b_buy.disabled == (main.profile.coins < Profile.BOOSTER_COST), "Kaufknopf folgt dem Münzstand")
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
func target_view(scr, valid: Array) -> Control:
for v in scr.enemy_views:
if v.creature in valid:
return v
for v in scr.own_views:
if v.creature in valid:
return v
for p in scr.hero_panels:
if p in valid:
return scr.hero_panels[p]
return null
## Zieht eine Karte oder Kachel auf eine andere und hält sie dort, bis die beiden verschmelzen.
func drag_fuse(scr, from: Control, to: Control) -> void:
await frames(20) # Kacheln sind noch unterwegs (Einfliegen, Nachrücken)
await settle(scr)
var a := from.get_global_rect().get_center()
var b := to.get_global_rect().get_center()
var down := InputEventMouseButton.new()
down.position = a
down.global_position = a
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
get_viewport().push_input(down)
await frames(1)
for i in range(1, 6):
var m := InputEventMouseMotion.new()
m.position = a.lerp(b, i / 5.0)
m.global_position = m.position
m.button_mask = MOUSE_BUTTON_MASK_LEFT
get_viewport().push_input(m)
await frames(1)
check(scr.drag_ghost != null and scr.drag_target != null, "Karte schwebt über einem Verschmelzungsziel")
var waited := 0
while scr.drag_ghost != null and waited < 240:
var still := InputEventMouseMotion.new() # die Maus bleibt auf dem Ziel, auch wenn die echte zuckt
still.position = b
still.global_position = b
still.button_mask = MOUSE_BUTTON_MASK_LEFT
get_viewport().push_input(still)
await frames(1)
waited += 1
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
up.position = b
up.global_position = b
get_viewport().push_input(up)
await frames(2)
## Legt, was bezahlbar ist.
func play_hand(scr) -> void:
var g: Game = scr.game
for i in 12:
var playable: Array = scr.hand_views.filter(func(v): return g.can_play(g.human, v.card)[0])
if playable.is_empty():
break
var card: Cards.Card = playable[0].card
var board_before: int = g.human.board.size()
await click(playable[0])
check(g.human.board.size() > board_before, "Karte gespielt: " + card.name)
func human_turn(scr) -> void:
var g: Game = scr.game
# erst legen, was bezahlbar ist, dann mit dem Rest ziehen (und wieder legen)
for round in 2:
await play_hand(scr)
for i in 6:
if g.winner != "" or not g.can_draw(g.human)[0]:
break
var mana: int = g.human.energy
var hand: int = g.human.hand.size()
await click(scr.deck)
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand + 1, "gezogen: eine Karte")
for i in 12:
var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack())
if ready.is_empty() or g.winner != "":
break
var cr: Game.Creature = ready[0].creature
await settle(scr)
await hover(ready[0])
var under = get_viewport().gui_get_hovered_control()
await click(ready[0])
if scr.selected != cr:
print("DEBUG Auswahl fehlgeschlagen: rect=", ready[0].get_global_rect(), " scale=", ready[0].scale,
" busy=", ready[0].busy, " unter Maus=", under, " Meldung='", scr.msg_label.text,
"' Zug Mensch=", g.current().is_human, " kann=", cr.can_attack(), " selected=", scr.selected)
check(scr.selected == cr, "Wesen gewählt")
var targets := []
for o in [g.opponent]:
if o.alive() and g.can_attack(cr, o)[0]:
targets.append(o)
for c in o.board:
if g.can_attack(cr, c)[0]:
targets.append(c)
await settle(scr)
await click(target_view(scr, targets))
check(scr.selected == null and cr.attacks_done > 0, "Angriff ausgeführt")
if g.winner == "":
await click(scr.b_end)
func start_battle() -> void:
await click(find_button("Einzelspieler"))
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
await click(find_button("Starten"))
func test_gegnerauswahl() -> void:
await click(find_button("Einzelspieler"))
var sel = screen()
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
check(find_button("Neu würfeln") == null and find_button("Feuer") == null and find_button("Starten") != null, "nur Schwierigkeit und Starten")
await click(find_button("Schwer"))
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
check(str(Profile.DIFFICULTIES.schwer.life) in sel.info_label.text and str(int(Profile.REWARD_WIN * 1.5)) in sel.info_label.text, "Leben des Gegners und Belohnung stehen dabei")
await shot("07_gegnerauswahl")
await click(find_button("Starten"))
var hard = screen()
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
var opp: Game.Player = hard.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA), "alle fünf sind im ersten Zug spielbar")
await shot("05_starthand")
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht oben in der Mitte")
await shot("11_kampf")
await give_up()
await click(find_button("Einzelspieler"))
await click(find_button("Leicht"))
await click(find_button("Starten"))
var easy = screen()
opp = easy.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life)
check(easy.game.players[0].max_energy == Game.START_MANA and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
await give_up()
await click(find_button("Einzelspieler"))
await click(find_button("Normal"))
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
## Kampf verlassen: nach Spielende über den Ergebnisdialog, sonst per Aufgeben.
func leave_battle() -> void:
var scr = screen()
if scr is BattleScreen and scr.game != null and scr.game.winner != "":
await frames(2)
await click(find_button("Zurück zum Menü"))
check(screen() is MenuScreen, "Ergebnisdialog führt ins Menü")
else:
await give_up()
func give_up() -> void:
var scr = screen()
await click(find_button("Aufgeben"))
check(scr.confirm.visible, "Aufgeben fragt nach")
await click(find_button("Weiterspielen"))
check(not scr.confirm.visible and screen() == scr, "Weiterspielen bleibt im Kampf")
await click(find_button("Aufgeben"))
await click(find_button("Ja, aufgeben"))
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
func test_einstellungen() -> void:
await click(find_button("Einstellungen"))
var scr = screen()
check(scr is SettingsScreen, "Einstellungen geöffnet")
scr.volume.value = 0.3
scr.speed.value = 2.0
scr.anim.button_pressed = false
await frames(1)
check(is_equal_approx(main.profile.settings.volume, 0.3) and is_equal_approx(main.profile.settings.ai_speed, 2.0), "Regler gespeichert")
check(main.profile.settings.animations == false, "Animationen abschaltbar")
check(is_equal_approx(Profile.load_or_new(SAVE).settings.volume, 0.3), "Einstellungen auf Platte")
scr.anim.button_pressed = true
scr.big_font.button_pressed = true
await frames(2)
check(main.profile.settings.big_font and Style.font_scale > 1.0 and screen() is SettingsScreen, "große Schrift aktiv")
screen().big_font.button_pressed = false
await frames(2)
check(is_equal_approx(Style.font_scale, 1.0), "normale Schrift")
var scr_b = screen()
await shot("13_einstellungen")
scr = scr_b
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
func test_sammlung() -> void:
await click(find_button("Sammlung"))
var scr = screen()
check(scr is CollectionScreen, "Sammlung geöffnet")
var p: Profile = main.profile
check(scr.grid.views().size() == Cards.all().size(), "alle Karten gehören dir")
p.dust = 5000
scr.search_box.text = "Phönix"
scr.search_box.text_changed.emit("Phönix")
await frames(1)
var phoenix = scr.grid.views().filter(func(v): return v.card.id == "ff_phoenix")[0]
await click(phoenix)
check(scr.selected.id == "ff_phoenix" and p.rarity("ff_phoenix") == "Common", "Karte gewählt, noch Common")
await click(scr.b_upgrade)
check(p.level("ff_phoenix") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["Rare"], "auf Rare aufgewertet")
scr.search_box.text = ""
scr.search_box.text_changed.emit("")
await frames(1)
var front: Array = scr.grid.views().slice(0, p.upgraded_count())
check(front.all(func(v): return v.rarity != "Common") and front.any(func(v): return v.card.id == "ff_phoenix" and v.rarity == "Rare"), "aufgewertete Karten stehen vorn in der Sammlung, der Phönix als Rare dabei")
scr.search_box.text = "Phönix"
scr.search_box.text_changed.emit("Phönix")
await frames(1)
check(Profile.load_or_new(SAVE).level("ff_phoenix") == 1, "Sammlung gespeichert")
var dust: int = p.dust
await click(scr.b_dismantle)
check(p.level("ff_phoenix") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt")
await shot("20_sammlung")
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
func test_gamepad_menue() -> void:
check(get_viewport().gui_get_focus_owner() == null, "ohne Gamepad kein Fokus")
await pad(JOY_BUTTON_DPAD_DOWN)
var f := get_viewport().gui_get_focus_owner()
check(f is Button and main.pad_mode, "Gamepad setzt den Fokus auf einen Knopf")
var down := InputEventJoypadButton.new()
down.button_index = JOY_BUTTON_DPAD_DOWN
down.pressed = true
Input.parse_input_event(down)
await frames(2)
var f2 := get_viewport().gui_get_focus_owner()
check(f2 is Button and f2 != f, "Steuerkreuz wechselt den Knopf")
var target_text: String = f2.text
check(InputMap.action_get_events("ui_accept").any(func(e): return e is InputEventJoypadButton), "A ist auf ui_accept gemappt")
var accept := InputEventJoypadButton.new()
accept.button_index = JOY_BUTTON_A
accept.pressed = true
get_viewport().push_input(accept)
await frames(1)
var release := InputEventJoypadButton.new()
release.button_index = JOY_BUTTON_A
release.pressed = false
get_viewport().push_input(release)
await frames(3)
check(not (screen() is MenuScreen) or target_text == "Beenden", "A öffnet den fokussierten Bildschirm (%s)" % target_text)
await hover(main)
check(not main.pad_mode and get_viewport().gui_get_focus_owner() == null, "Maus nimmt den Fokus wieder")
func test_profil_zuruecksetzen() -> void:
await click(find_button("Einstellungen"))
await click(find_button("Profil zurücksetzen"))
var scr = screen()
check(scr.confirm.visible, "Rückfrage")
await click(find_button("Abbrechen"))
check(not scr.confirm.visible, "abgebrochen")
await click(find_button("Profil zurücksetzen"))
await click(find_button("Ja, zurücksetzen"))
check(screen() is MenuScreen and main.profile.coins == Profile.START_COINS and main.profile.collection.is_empty(), "Profil zurückgesetzt")
check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert")
func test_sprache() -> void:
await click(find_button("Einstellungen"))
var scr = screen()
scr.language.select(1)
scr.language.item_selected.emit(1)
await frames(2)
check(TranslationServer.get_locale().begins_with("en") and main.profile.settings.language == "en", "Sprache auf Englisch")
check(TranslationServer.translate("Zurück") == "Back" and TranslationServer.translate("Einzelspieler") == "Single player", "Übersetzung geladen")
check(TranslationServer.translate("Zug %d · Du bist dran") == "Turn %d · Your turn" and TranslationServer.translate("Nicht genug Mana") == "Not enough mana", "dynamische Texte übersetzt")
check(find_button("Back") != null, "Knöpfe übersetzt")
await shot("26_englisch")
await click(find_button("Back"))
check(find_button("Single player") != null and find_button("Settings") != null, "Menü auf Englisch")
check(screen().coins_label.text.begins_with("Coins:"), "zusammengesetzte Texte übersetzt")
await click(find_button("Settings"))
var scr2 = screen()
scr2.language.select(0)
scr2.language.item_selected.emit(0)
await frames(2)
check(find_button("Zurück") != null, "zurück auf Deutsch")
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
## Steuerung im Kampf: Tastatur, Rückgängig, Gamepad, Überspringen, Esc.
func test_kampf_steuerung() -> void:
await start_battle()
var scr = screen()
check(scr is BattleScreen, "Kampf gestartet")
scr.ai_delay = 0.0
await wait_for_human(scr)
var g: Game = scr.game
g.human.energy = 20 # die Starthand ist zufällig, hier soll nur die Steuerung geprüft werden
g.human.hand = [Cards.by_id("f_wolf").copy()]
scr.refresh()
check(scr.hand_views.size() == 1 and scr.deck.enabled, "eine Handkarte, Stapel bereit")
# Ziehen: 2 Mana weg, eine Karte landet auf der Hand
var mana: int = g.human.energy
await click(scr.deck)
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == 2 and scr.hand_views.size() == 2, "gezogen: eine Karte auf der Hand")
check(g.human.hand[1].cost <= g.human.max_energy, "nur bezahlbare Karten")
# Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an
var board_before: int = g.human.board.size()
mana = g.human.energy
await key(KEY_1)
check(g.human.board.size() == board_before + 1 and g.human.energy == mana - Cards.by_id("f_wolf").cost, "Wesen per Taste gespielt, Kosten bezahlt")
check(not g.human.board.back().can_attack(), "frisch gelegt greift es noch nicht an")
# Rückgängig: Wesen zurück auf die Hand, Mana zurück
check(scr.b_undo.visible, "Rückgängig angeboten")
await click(scr.b_undo)
g = scr.game
check(g.human.board.size() == board_before and g.human.hand.any(func(c): return c.id == "f_wolf") and g.human.energy == mana, "Rückgängig stellt Hand und Mana her")
check(not scr.b_undo.visible and g.log.back() == "Rückgängig.", "Rückgängig verbraucht")
await key(KEY_1)
check(g.human.board.size() == board_before + 1, "erneut gespielt")
# Ziehen per Taste
mana = g.human.energy
var hand_before: int = g.human.hand.size()
await key(KEY_Z)
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand_before + 1, "Z zieht")
await shot("22_kampf_steuerung")
# Gamepad: A auf dem Stapel zieht
await pad(JOY_BUTTON_DPAD_LEFT)
check(scr.gp_zone == "deck" and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf dem Stapel")
hand_before = g.human.hand.size()
await pad(JOY_BUTTON_A)
check(g.human.hand.size() == hand_before + 1, "Gamepad A zieht")
scr.ai_delay = 0.75
await key(KEY_SPACE)
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
await frames(2)
check(scr.b_skip.visible and not scr.b_end.visible, "Überspringen-Knopf während des Gegnerzugs")
await click(scr.b_skip)
var w := 0
while g.current() != g.human and g.winner == "" and w < 120:
await frames(1)
w += 1
check(g.current() == g.human and w < 60, "Gegnerzug übersprungen (%d Bilder)" % w)
scr.ai_delay = 0.0
await key(KEY_ESCAPE)
check(scr.confirm.visible, "Esc öffnet Aufgeben")
await key(KEY_ESCAPE)
check(not scr.confirm.visible, "Esc schließt den Dialog")
await give_up()
## Grenzen im Kampf: volle Hand, volles Feld, kein Mana.
func test_kampf_grenzen() -> void:
await start_battle()
var scr = screen()
scr.ai_delay = 0.0
await wait_for_human(scr)
var g: Game = scr.game
g.human.energy = 100
scr.refresh()
await frames(1)
# Hand voll ziehen
var guard := 0
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
guard += 1
await click(scr.deck)
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
check(not scr.deck.enabled, "bei voller Hand lässt sich nichts ziehen")
await click(scr.deck)
check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
# Feld voll spielen
guard = 0
while g.human.board.size() < Game.MAX_BOARD and guard < 30:
guard += 1
if g.human.hand.is_empty():
await click(scr.deck)
await key(KEY_1)
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
if g.human.hand.is_empty():
await click(scr.deck)
await key(KEY_1)
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
await shot("25_kampf_voll")
# Verschmelzen per Ziehen macht wieder Platz
var before: int = g.human.board.size()
scr.hold_time = 0.3
await drag_fuse(scr, scr.own_views[0], scr.own_views[1])
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
# Ohne Mana kein Ziehen
g.human.energy = 0
scr.refresh()
await frames(1)
check(not scr.deck.enabled, "ohne Mana kein Ziehen")
await give_up()
## Elementboni, verdeckt legen, Kartenmenü und Verschmelzen (per Ziehen und per Menü) im Kampf.
func test_kampf_elemente_und_verschmelzen() -> void:
await start_battle()
var scr = screen()
check(scr is BattleScreen, "Kampf gestartet")
scr.ai_delay = 0.0
scr.hold_time = 0.3
await wait_for_human(scr)
var g: Game = scr.game
g.human.board.clear()
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler
var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature:
var cr := Game.Creature.new(Cards.by_id(id).copy(), p, g.next_uid)
g.next_uid += 1
cr.ready = true
cr.hidden = hidden
p.board.append(cr)
return cr
put.call(g.human, "f_wolf")
put.call(g.human, "f_asche")
put.call(g.human, "w_reiter")
var falke: Game.Creature = put.call(scr.opponent(), "l_falke")
var secret: Game.Creature = put.call(scr.opponent(), "f_kobold", true)
g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()]
g.human.energy = 20
g.human.looks = {"w_reiter": 3}
g.apply_auras()
scr.refresh()
await frames(2)
check(scr.element_view.text() == "Feuer 2 (Doppelschlag) · Wasser 1 · Erde 0 · Luft 1", "Zähler ohne das verdeckte Wesen, Feuerpaar: " + scr.element_view.text())
check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 1, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken; der Kobold würde den Stapel voll machen")
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen, ohne Verschmelzen
await hover(scr.hand_views[0])
await right_click_at(scr.hand_views[0].get_global_rect().get_center())
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü")
check(find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
await shot("23_kartenmenue")
await right_click()
check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü")
# Mittlere Maustaste legt die Handkarte verdeckt
var mana: int = g.human.energy
await right_click_at(scr.hand_views[0].get_global_rect().get_center(), MOUSE_BUTTON_MIDDLE)
var mine_hidden: Game.Creature = g.human.board[3]
check(mine_hidden.hidden and g.human.energy == mana - 1 and scr.element_view.text().begins_with("Feuer 2"), "mittlere Maustaste legt verdeckt, zählt nicht")
# Rechtsklick auf ein eigenes offenes Wesen: nichts zu wählen, also kein Menü
await right_click_at(scr.own_views[0].get_global_rect().get_center())
check(not scr.menu_layer.visible, "kein Menü ohne Einträge")
var wolf: Game.Creature = g.human.board[0]
while scr.own_views[3].busy:
await frames(1)
await click(scr.own_views[3])
check(mine_hidden.hidden and scr.msg_label.text == "Feuerkobold kann erst im nächsten Zug aufgedeckt werden", "im Legezug nicht aufzudecken: " + scr.msg_label.text)
mine_hidden.ready = true
await click(scr.own_views[3])
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3 (+1/+1)"), "aufgedeckt: drei Feuer, +1/+1: " + scr.element_view.text())
check(wolf.power() == 4 and falke.power() == 3 and mine_hidden.can_attack(), "Feuerwesen +1, kein Malus für Luft, aufgedeckt greift sofort an")
check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
await shot("23b_drei_feuer")
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt
await click(scr.own_views[0])
check(scr.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen Luft, nichts gegen Verdeckte")
check(scr.tiles[falke.uid].shown_stats() == [falke.power() - 1, falke.health], "der Falke zeigt seinen Konter gegen den Wolf (1 weniger) und sein echtes Leben")
await hover(scr.tiles[falke.uid])
check(scr.tiles[falke.uid].doomed and not scr.own_views[0].doomed and scr.own_views[0].shown_stats()[0] == wolf.power() + 1, "über dem Falken: er stirbt, der Wolf nicht, der Wolf schlägt mit +1 zu")
await shot("23c_angreifer_gewaehlt")
check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen")
await click(scr.tiles[secret.uid])
check(not secret.hidden and not (secret in scr.opponent().board), "Angriff deckt auf und tötet den Kobold")
check(wolf.attacks_left() == 1, "Doppelschlag: ein Angriff bleibt")
await right_click()
# Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen und kurz halten
mana = g.human.energy
var before: int = g.human.board.size()
await drag_fuse(scr, scr.own_views[1], scr.own_views[2])
check(g.human.board.size() == before - 1 and g.human.board.any(func(c): return c.name == "Dampf") and g.human.energy == mana - 1, "zu Dampf verschmolzen, 1 Mana bezahlt")
var dampf: Game.Creature = g.human.board.filter(func(c): return c.name == "Dampf")[0]
check(scr.tiles[dampf.uid].rarity() == "Ultra Rare" and dampf.card.look == 3, "die Fusion trägt die höhere Stufe (Wellenreiter war Ultra Rare)")
await shot("24_verschmelzen")
# Verschmelzen auf der Hand per Ziehen
g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()]
scr.refresh()
await frames(1)
await drag_fuse(scr, scr.hand_views[0], scr.hand_views[1])
check(g.human.hand.size() == 2 and g.human.hand[0].name == "Inferno" and g.human.hand[0].attack == 3 and g.human.hand[0].health == 2, "zwei Kobolde auf der Hand zu Inferno 3/2")
# Gamepad-Menü (Y) bietet Verdeckt legen und Verschmelzen mit …
scr.open_menu(g.human.hand[0], Rect2(scr.hand_views[0].position, scr.hand_views[0].size), true)
check(find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and g.human.hand[0].cost == 2, "Gamepad-Menü: Verdeckt legen, Inferno kostet 2")
await click(find_button("Verschmelzen mit … ◆ 2", scr.menu_layer))
check(scr.fuse_first == g.human.hand[0], "Partner wählen")
await click(scr.hand_views[1])
check(g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz", "Inferno und Windgeist zu Blitz")
await give_up()
func test_netzwerk() -> void:
await click(find_button("Netzwerkspiel"))
var scr = screen()
check(scr is LobbyScreen, "Lobby geöffnet")
await click(find_button("Spiel eröffnen"))
check(main.net.is_host and main.net.connected, "Spiel eröffnet")
if FileAccess.file_exists("user://bot_quit"):
DirAccess.remove_absolute(ProjectSettings.globalize_path("user://bot_quit"))
var args := ["--headless", "--path", ProjectSettings.globalize_path("res://"), "tests/net_bot.tscn", "--", "--ip=127.0.0.1", "--name=Bot"]
var pid := OS.create_process(OS.get_executable_path(), args)
check(pid > 0, "Bot-Prozess gestartet")
var wait := 0
while main.net.names.size() < 2 and wait < 600:
await frames(1)
wait += 1
if main.net.names.size() < 2:
print("HINWEIS: Bot kam nicht, zweiter Versuch")
OS.kill(pid)
pid = OS.create_process(OS.get_executable_path(), args)
while main.net.names.size() < 2 and wait < 1500:
await frames(1)
wait += 1
check(main.net.names.size() == 2, "Bot ist beigetreten (Pfad %s)" % main.net.get_path())
await shot("23_lobby")
await click(find_button("Start"))
var b = screen()
check(b is BattleScreen and b.is_online() and not b.is_client(), "Netzwerkkampf als Host")
b.ai_delay = 0.0
var rounds := 0
while b.game != null and b.game.winner == "" and rounds < 2:
if b.game.current() == b.perspective:
if b.game.current() == b.perspective:
await click(b.b_end)
var w2 := 0
while b.game.current() != b.perspective and b.game.winner == "" and w2 < 900:
await frames(1)
w2 += 1
rounds += 1
if b.game.winner != "":
print("DEBUG Netz: Sieger=", b.game.winner, " Verlauf=", b.game.log.slice(maxi(0, b.game.log.size() - 12)))
check(b.game.turn >= 2 and b.game.winner == "", "Runden gespielt, Partie läuft (Zug %d)" % b.game.turn)
check(b.game.log.any(func(l): return l.begins_with("Bot kauft") or l.begins_with("Bot spielt")), "Bot hat gehandelt")
await shot("24_netzwerk_kampf")
# Bot verlässt geordnet: Platz bleibt frei, Züge werden übersprungen
var quit_file := "user://bot_quit"
var f := FileAccess.open(quit_file, FileAccess.WRITE)
f.store_string("x")
f.close()
var w3 := 0
while main.net.waiting.is_empty() and w3 < 600:
await frames(1)
w3 += 1
check(main.net.waiting.has("Bot") and b.game.log.any(func(l): return "Verbindung verloren" in l), "Platz wird freigehalten")
var turn_before: int = b.game.turn
for i in 2:
if b.game.current() == b.perspective and b.game.winner == "":
await click(b.b_end)
var w4 := 0
while b.game.current() != b.perspective and b.game.winner == "" and w4 < 300:
await frames(1)
w4 += 1
check(b.game.turn >= turn_before + 1 and b.game.log.any(func(l): return "übersprungen" in l), "Züge des fehlenden Spielers werden übersprungen")
DirAccess.remove_absolute(ProjectSettings.globalize_path(quit_file))
await frames(5)
var pid2 := OS.create_process(OS.get_executable_path(), args)
var w5 := 0
while not main.net.waiting.is_empty() and w5 < 900:
await frames(1)
w5 += 1
check(main.net.waiting.is_empty() and main.net.seats.size() == 2 and b.game.log.any(func(l): return l == "Bot ist zurück."), "Bot wieder verbunden, Platz zurück")
var turn2: int = b.game.turn
for i in 2:
if b.game.current() == b.perspective and b.game.winner == "":
await click(b.b_end)
var w6 := 0
while b.game.current() != b.perspective and b.game.winner == "" and w6 < 900:
await frames(1)
w6 += 1
check(b.game.turn >= turn2 + 1, "Partie läuft nach der Rückkehr weiter")
OS.kill(pid2)
await leave_battle()
check(screen() is MenuScreen and not main.net.connected, "Netzwerk getrennt, zurück im Menü")
func _bot_args(extra: Array) -> Array:
return ["--headless", "--path", ProjectSettings.globalize_path("res://"), "tests/net_bot.tscn", "--"] + extra
func _bot_quit(on: bool) -> void:
var path := "user://bot_quit"
if on:
var f := FileAccess.open(path, FileAccess.WRITE)
f.store_string("x")
f.close()
elif FileAccess.file_exists(path):
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
## Verbindet sich mit dem Bot-Host (mehrere Versuche, bis er lauscht).
func _join_bot_host(watch: bool) -> void:
await click(find_button("Netzwerkspiel"))
var lobby = screen()
lobby.ip_box.text = "127.0.0.1"
for attempt in 8:
await click(find_button("Zuschauen" if watch else "Beitreten"))
var w := 0
while not main.net.connected and w < 180:
await frames(1)
w += 1
if main.net.connected:
return
main.net.leave()
await frames(30)
## Der Host (ein Bot) verschwindet: der einzige Mitspieler übernimmt und spielt zu Ende.
func test_host_wechsel() -> void:
_bot_quit(false)
var pid := OS.create_process(OS.get_executable_path(), _bot_args(["--host", "--name=BotHost"]))
check(pid > 0, "Bot-Host gestartet")
await frames(30)
await _join_bot_host(false)
var w := 0
while not (screen() is BattleScreen) and w < 900:
await frames(1)
w += 1
var b = screen()
check(b is BattleScreen and b.is_client(), "als Mitspieler beim Bot-Host im Kampf")
w = 0
while (b.game == null or main.net.backup.is_empty()) and w < 600:
await frames(1)
w += 1
check(b.game != null and main.net.i_am_successor and not main.net.backup.is_empty(), "Nachfolger kennt den vollen Spielstand")
main.net.rejoin_seconds = 3.0
_bot_quit(true)
w = 0
while not main.net.is_host and w < 900:
await frames(1)
w += 1
check(main.net.is_host and b.game == main.net.game and screen() == b and b.game.log.any(func(l): return "übernimmt als Host" in l),
"Host-Wechsel: Mitspieler führt das Spiel weiter")
check(main.net.waiting.has("BotHost"), "Platz des alten Hosts bleibt zunächst frei")
await shot("34_host_wechsel")
# Der alte Host kehrt nicht zurück: ein Zug wird übersprungen, nach der Frist scheidet er aus
b.ai_delay = 0.0
if b.game.current() == b.perspective:
await click(b.b_end)
w = 0
while b.game.winner == "" and w < 900:
await frames(1)
w += 1
check(b.game.log.any(func(l): return "übersprungen" in l), "Zug des fehlenden Hosts übersprungen")
check(b.game.winner != "" and b.game.winner_player == b.perspective and b.game.log.any(func(l): return "kehrt nicht zurück" in l),
"nach der Frist scheidet der alte Host aus, Partie gewonnen (Sieger '%s', Zug %d)" % [b.game.winner, b.game.turn])
await leave_battle()
_bot_quit(false)
check(screen() is MenuScreen and not main.net.connected, "nach dem Host-Wechsel zurück im Menü")
## Zuschauer: sieht die Partie zweier Bots ohne Hände und ohne eigene Züge.
func test_zuschauer() -> void:
_bot_quit(false)
var pid_h := OS.create_process(OS.get_executable_path(), _bot_args(["--host", "--name=BotHost"]))
await frames(30)
var pid_c := OS.create_process(OS.get_executable_path(), _bot_args(["--ip=127.0.0.1", "--name=Bot"]))
check(pid_h > 0 and pid_c > 0, "zwei Bots gestartet")
await _join_bot_host(true)
var w := 0
while not (screen() is BattleScreen) and w < 1200:
await frames(1)
w += 1
var b = screen()
check(b is BattleScreen and b.spectating(), "als Zuschauer im Kampf")
w = 0
while (b.game == null or b.game.turn < 2) and w < 900:
await frames(1)
w += 1
check(b.game != null and b.game.turn >= 2 and b.hand_views.is_empty() and not b.b_end.visible,
"Zuschauer sieht die Partie ohne Hände und ohne Züge (Zug %d)" % (b.game.turn if b.game != null else 0))
await shot("35_zuschauer")
check(not b.b_end.visible and not b.b_undo.visible, "Zuschauer hat keine Zugknöpfe")
if screen() is BattleScreen: # die Bots spielen schnell, die Partie kann schon vorbei sein
await leave_battle()
else:
var w2 := 0
while not (screen() is MenuScreen) and w2 < 300:
await frames(1)
w2 += 1
check(screen() is MenuScreen and not main.net.connected, "Zuschauer zurück im Menü")
_bot_quit(true)
await frames(60)
_bot_quit(false)
func test_kampagne() -> void:
await click(find_button("Kampagne"))
var scr = screen()
check(scr is CampaignScreen, "Kampagne geöffnet")
await shot("15_kampagne")
check(scr.node_buttons.filter(func(b): return b.disabled).size() == Campaign.STAGES.size() - 1 and find_button("Kämpfen") != null, "neun Stationen gesperrt, die erste offen")
await click(scr.node_buttons[3])
check(scr.selected == 0, "gesperrte Station lässt sich nicht wählen")
check(find_button("Kapitel 2").disabled and scr.perk_box == null, "Kapitel 2 und Merkmal noch gesperrt")
await click(find_button("Kämpfen"))
var battle = screen()
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
check(battle.game.opponent.name == "Fenn", "Fenn ist der Gegner")
check(battle.game.opponent.perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
await shot("16_kampagne_kampf")
await give_up()
# Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl
main.profile.campaign_stage = Campaign.STAGES.size()
await click(find_button("Kampagne"))
var scr2 = screen()
check(scr2.chapter == 2 and scr2.perk_box != null, "Kapitel 2 offen, Merkmal wählbar")
scr2.perk_box.select(1)
scr2.perk_box.item_selected.emit(1)
check(main.profile.perk == Game.PERKS.keys()[0], "Merkmal gewählt")
await shot("27_kampagne_kapitel2")
await click(find_button("Kämpfen"))
var b2 = screen()
check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponent.perks.size() == 2 and b2.game.opponent.life == 22, "Kapitel-2-Boss mit zwei Merkmalen und 22 Leben")
check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf")
await give_up()
main.profile.campaign_stage = 0
main.profile.perk = ""
main.save()
func test_kampf(shot_turn: int) -> void:
await start_battle()
var scr = screen()
check(scr is BattleScreen, "Kampfbildschirm")
scr.ai_delay = 0.0
var g: Game = scr.game
check(g.players.size() == 2 and g.log[0].begins_with("Zugreihenfolge"), "Spiel gestartet, Reihenfolge ausgelost")
await wait_for_human(scr)
check(g.current() == g.human, "Spieler ist dran")
check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar")
await shot("07_kampf_start")
var coins: int = main.profile.coins
var guard := 0
while g.winner == "" and guard < 60:
guard += 1
var turn_before := g.turn
await human_turn(scr)
if g.winner != "":
break
check(g.current() != g.human or g.turn > turn_before, "Zug beendet")
var wait := 0
while g.current() != g.human and g.winner == "" and wait < 600:
await frames(1)
wait += 1
if guard == shot_turn:
await shot("08_kampf_zug%d" % (shot_turn + 1))
check(g.winner != "", "Spiel beendet")
await frames(3)
await shot("09_kampf_ende")
check(scr.reward > 0 and main.profile.coins >= coins + scr.reward, "Belohnung gutgeschrieben")
check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Belohnung gespeichert")
await click(find_button("Zurück zum Menü"))
check(screen() is MenuScreen, "zurück im Menü")
func test_kampf_abbrechen() -> void:
await start_battle()
var scr = screen()
scr.ai_delay = 0.0
var g: Game = scr.game
await wait_for_human(scr)
check(g.opponent != null and g.human.max_energy == Game.START_MANA, "Duell läuft")
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
g.human.energy = 10
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
wolf.ready = true
g.human.board.append(wolf)
g.human.hand.append(Cards.by_id("w_reiter"))
g.human.looks["w_reiter"] = Cards.RARITIES.find("Super Rare")
scr.refresh()
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
check(reiter_view.rarity == "Super Rare", "Handkarte trägt die eigene Stufe")
await hover(scr.element_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
await hover(reiter_view)
var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView)
check(previews.size() == 1 and previews[0].rarity == "Super Rare", "Vorschau ebenfalls")
await shot("21_kampf_vorschau")
if not scr.own_views.is_empty():
await click(scr.own_views[0])
await right_click()
check(scr.selected == null, "Rechtsklick bricht ab")
await hover(scr.hand_views[0])
if scr.preview_holder.get_child_count() == 0:
print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(),
" dran=", g.current().name, " Sieger=", g.winner, " hovered=", get_viewport().gui_get_hovered_control())
check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover")
await click(scr.b_log)
check(scr.log_overlay.visible and scr.any_overlay(), "Verlauf öffnet als Überlagerung")
await key(KEY_ESCAPE)
check(not scr.log_overlay.visible, "Esc schließt den Verlauf")
# Runden laufen lassen, damit die Gegner ihre Züge machen
for round in 4:
await click(scr.b_end)
var wait := 0
while g.current() != g.human and g.winner == "" and wait < 600:
await frames(1)
wait += 1
var ai_attacked: bool = g.log.any(func(l): return l.begins_with("Gegner") and " spielt " in l) \
and g.log.any(func(l): return " greift " in l)
check(ai_attacked, "Gegner spielen Karten und greifen an")
check(scr.tiles.size() == g.players.reduce(func(acc, p): return acc + p.board.size(), 0), "eine Kachel je Wesen")
await shot("10_kampf_runden")