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) print("%d Prüfungen bestanden, Abbruch beim ersten Fehler" % passed) get_tree().quit(1) 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 double_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 down.double_click = true get_viewport().push_input(down) var up: InputEventMouseButton = down.duplicate() up.pressed = false up.double_click = false get_viewport().push_input(up) await frames(2) 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; ohne Startdeck wird ## Feuer gewählt. func ensure_menu() -> void: if screen() is StartDeckScreen: await click(find_button("Feuer wählen")) 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) main.sfx.muted = true # Tests bleiben still await frames(3) var suite := [test_start, test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen, test_kampagne, test_sammlung, test_deck, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen] suite += [test_kampf.bind(3), test_kampf_abbrechen] suite.append(test_profil_zuruecksetzen) var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_sammlung,test_kampf 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) ## Erster Start: eines von vier Startdecks wählen (ensure_menu hat schon eins gewählt, darum frisch). func test_start() -> void: main.reset_profile() await frames(2) check(screen() is StartDeckScreen and not main.profile.started(), "neues Profil: Startdeck wählen") await shot("00_startdeck") await click(find_button("Feuer wählen")) var p: Profile = main.profile check(screen() is MenuScreen and p.started() and p.deck.size() == Game.DECK_MIN and p.deck.filter(func(id): return Cards.by_id(id).elements[0] == "Feuer").size() == 25, "Feuer gewählt: 40 Karten, 25 davon Feuer") check(Profile.load_or_new(SAVE).started(), "gespeichert") 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.new or res.upgraded: check(main.profile.owns(res.card.id) and main.profile.level(res.card.id) == Cards.RARITIES.find(res.rarity), "neue oder 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) if scr.drag_ghost == null or scr.drag_target == null: var fo = from.card if from is CardView else from.creature var to_o = to.card if to is CardView else to.creature print("DEBUG drag: ghost=", scr.drag_ghost, " target=", scr.drag_target, " press=", scr.press_target, " dran=", scr.my_turn(), " overlay=", scr.any_overlay(), " mana=", scr.game.human.energy, " kann=", scr.game.can_fuse(scr.game.human, fo, to_o), " from=", from.get_global_rect(), " to=", to.get_global_rect()) 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() var cost: int = g.draw_cost(g.human) await click(scr.deck) check(g.human.energy == mana - cost and g.human.hand.size() == hand + 1, "gezogen: eine Karte") var tried := [] # Wesen ohne lohnendes Ziel (0 Angriff ohne Vorteil) nicht wieder versuchen for i in 12: var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack() and not (v.creature in tried)) if ready.is_empty() or g.winner != "": break var cr: Game.Creature = ready[0].creature var any_target: bool = g.can_attack(cr, g.opponent)[0] or g.opponent.board.any(func(c): return g.can_attack(cr, c)[0]) if not any_target: tried.append(cr) continue 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 key(KEY_SPACE) 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 links oben") 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 key(KEY_ESCAPE) check(scr.options.visible, "Esc öffnet die Optionen") await click(find_button("Aufgeben", scr.options)) check(scr.confirm.visible and not scr.options.visible, "Aufgeben fragt nach") await click(find_button("Weiterspielen")) check(not scr.confirm.visible and screen() == scr, "Weiterspielen bleibt im Kampf") await key(KEY_ESCAPE) await click(find_button("Aufgeben", scr.options)) 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ü") ## Deck: eigene Karten rein und raus, Auffüllen nach Filter, Speichern. Beginnt mit dem frischen Startdeck. func test_deck() -> void: main.profile.choose_start("Feuer") main.save() await click(find_button("Deck")) var scr = screen() check(scr is DeckScreen, "Deck geöffnet") var p: Profile = main.profile check(scr.grid.views().size() == p.collection.size() and scr.count_label.text == "Stapel: %d Karten (%d bis %d)" % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX], "alle eigenen Karten, Zähler stimmt") var views: Array = scr.grid.views() check(views.all(func(v): return not v.dim) and p.deck.size() == Game.DECK_MIN, "nach dem Start sind alle eigenen Karten im Stapel") var first_id: String = views[0].card.id await click(views[0]) check(not (first_id in p.deck) and p.deck.size() == Game.DECK_MIN - 1 and scr.grid.views().any(func(v): return v.card.id == first_id and v.dim), "Klick nimmt die Karte aus dem Stapel, sie wird dunkel") var back_view = scr.grid.views().filter(func(v): return v.card.id == first_id)[0] scr.grid.ensure_control_visible(back_view) # sie liegt jetzt hinter den Stapelkarten, außerhalb des Sichtbereichs await frames(2) await click(back_view) check(first_id in p.deck and p.deck.size() == Game.DECK_MIN, "Klick nimmt sie wieder hinein") await shot("19_deck") await click(find_button("Leeren")) check(p.deck.is_empty(), "geleert") await click(find_button("Wasser")) await click(find_button("Auffüllen")) check(p.deck.size() == 5 and p.deck.all(func(id): return Cards.by_id(id).elements[0] == "Wasser"), "Auffüllen hält sich an den Filter: nur die fünf Wasserkarten") await click(find_button("Alle")) await click(find_button("Auffüllen")) check(p.deck.size() == Game.DECK_MIN and Profile.load_or_new(SAVE).deck == p.deck, "ohne Filter alles, gespeichert") 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 des Spiels sind zu sehen") check(scr.grid.views().slice(0, p.collection.size()).all(func(v): return not v.dim) and scr.grid.views().back().dim, "eigene Karten vorn, fehlende abgedunkelt") p.dust = 5000 scr.search_box.text = "Lodernder Funke" scr.search_box.text_changed.emit("Lodernder Funke") await frames(1) var funke = scr.grid.views().filter(func(v): return v.card.id == "f_2_1")[0] await click(funke) check(scr.selected.id == "f_2_1" and p.rarity("f_2_1") == "Common" and scr.b_upgrade.visible, "eigene Karte gewählt, noch Common") await click(scr.b_upgrade) check(p.level("f_2_1") == 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 = scr.grid.views().filter(func(v): return v.card.id == "f_2_1")[0] check(front.rarity == "Rare" and front.card.attack == 3 and front.card.health == 2 and scr.grid.views()[0].rarity != "Common", "die Rare zeigt +1/+1, aufgewertete stehen vorn: %s %d/%d" % [front.rarity, front.card.attack, front.card.health]) check(Profile.load_or_new(SAVE).level("f_2_1") == 1, "Sammlung gespeichert") scr.search_box.text = "Lodernder Funke" scr.search_box.text_changed.emit("Lodernder Funke") await frames(1) var dust: int = p.dust await click(scr.b_dismantle) check(p.level("f_2_1") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt") # Herstellen einer fehlenden Karte scr.search_box.text = "Infernalischer Drache" scr.search_box.text_changed.emit("Infernalischer Drache") await frames(1) await click(scr.grid.views().filter(func(v): return v.card.id == "f_5_5")[0]) check(scr.b_craft.visible and not scr.b_upgrade.visible, "fehlende Karte: nur Herstellen") dust = p.dust await click(scr.b_craft) check(p.owns("f_5_5") and p.dust == dust - Profile.CRAFT_COST, "hergestellt") 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 StartDeckScreen and main.profile.coins == Profile.START_COINS and not main.profile.started(), "Profil zurückgesetzt: Startdeck neu wählen") await click(find_button("Luft wählen")) check(screen() is MenuScreen and main.profile.deck.filter(func(id): return Cards.by_id(id).elements[0] == "Luft").size() == 25, "Luft gewählt") 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("Abwerfen +%d ◆") == "Discard +%d ◆" 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_3_2").copy()] scr.refresh() check(scr.hand_views.size() == 1 and scr.deck.enabled, "eine Handkarte, Stapel bereit") # Ziehen: eine Karte vom eigenen Stapel, das erste Ziehen ist frei var mana: int = g.human.energy var deck_before: int = g.human.deck.size() await click(scr.deck) check(g.human.energy == mana and g.human.hand.size() == 2 and scr.hand_views.size() == 2 and g.human.deck.size() == deck_before - 1, "gezogen: eine Karte, frei") check(g.draw_cost(g.human) == 1 and scr.deck.cost == 1, "das nächste Ziehen kostet 1") # 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_3_2").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_3_2") 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 - 1 and g.human.hand.size() == hand_before + 1, "Z zieht, das zweite Ziehen für 1") 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, "Ü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.options.visible and find_button("Hauptmenü", scr.options) != null and find_button("Beenden", scr.options) != null, "Esc öffnet die Optionen") await key(KEY_ESCAPE) check(not scr.options.visible, "Esc schließt die Optionen") # Einstellungen aus dem Kampf und zurück await key(KEY_ESCAPE) await click(find_button("Einstellungen", scr.options)) check(screen() is SettingsScreen and is_instance_valid(scr), "Einstellungen geöffnet, der Kampf bleibt erhalten") await click(find_button("Zurück")) check(screen() == scr and scr.game == g and not scr.options.visible, "zurück im selben Kampf") 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 g.human.draws = 1 scr.refresh() await frames(1) check(not scr.deck.enabled, "ohne Mana kein zweites 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 for e in Cards.ELEMENTS: scr.opponent().played[e] = 0 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 var wolf_cr: Game.Creature = put.call(g.human, "f_3_2") wolf_cr.health = 4 # übersteht die Konter von Falke und Funke, damit der Test ihn weiter nutzen kann wolf_cr.max_health = 4 put.call(g.human, "f_4_3") put.call(g.human, "w_3_3") var falke: Game.Creature = put.call(scr.opponent(), "l_3_1") var secret: Game.Creature = put.call(scr.opponent(), "f_2_1", true) g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").copy()] g.human.energy = 20 g.human.looks = {"w_3_3": 3} g.apply_auras() scr.refresh() await frames(2) check(scr.element_view.text() == "Feuer 2/0 (Doppelschlag) · Wasser 1/0", "Zähler: nur Elemente mit Wesen oder gespielten Karten, Feuerpaar: " + scr.element_view.text()) check(scr.opp_element_view.text() == "Luft 1/0", "die Liste des Gegners zählt nur seine offenen Wesen: " + scr.opp_element_view.text()) check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 0, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken; noch kein Bonus in Sicht") # 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 and find_button("Abwerfen +1 ◆", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü mit Verdeckt legen und Abwerfen") check(find_button("Verschmelzen mit …", 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ü") await right_click_at(Vector2(1150, 300)) # leere Fläche check(scr.menu_layer.visible and find_button("Zug beenden", scr.menu_layer) != null, "Rechtsklick ins Leere bietet Zug beenden an") await right_click() # 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/1"), "mittlere Maustaste legt verdeckt: zählt als ausgespielt, nicht als offen") # 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 == "Lodernder Funke 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 (Doppelschlag)"), "aufgedeckt: drei offene Feuer, eine ausgespielt: " + scr.element_view.text()) check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "unter fünf Karten kein Bonus, aufgedeckt greift sofort an") g.human.played["Feuer"] = 5 g.apply_auras() scr.refresh() await frames(1) check(scr.element_view.text().begins_with("Feuer 3/5 (Doppelschlag) (+1/+1)") and wolf.power() == 4 and falke.power() == 3, "fünf Feuerkarten: +1/+1 für die eigenen Feuerwesen: " + scr.element_view.text()) check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus") await shot("23b_vier_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 double_click_at(scr.own_views[0].get_global_rect().get_center()) check(scr.msg_label.text == "Erst die Wesen" and scr.opponent().life == 30, "Doppelklick zielt auf das Leben, hier stehen noch Wesen: " + scr.msg_label.text) # 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 - 3, "zu Dampf verschmolzen, 3 Mana bezahlt (halbe Manasumme)") 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 (der Reiter war Ultra Rare)") await shot("24_verschmelzen") # Verschmelzen auf der Hand per Ziehen g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").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 Funken 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 …", 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_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") # Kurze Partie: einfache KI, Gegner fast tot, drei bereite Drachen räumen auf scr.smart_ai = false g.opponent.life = 1 for i in 3: var drache := Game.Creature.new(Cards.by_id("f_5_5").copy(), g.human, 9100 + i) drache.ready = true g.human.board.append(drache) scr.refresh() 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_3_2"), g.human, 9000) wolf.ready = true g.human.board.append(wolf) g.human.hand.append(Cards.by_id("w_3_3")) g.human.looks["w_3_3"] = Cards.RARITIES.find("Super Rare") scr.refresh() var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_3_3")[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 key(KEY_ESCAPE) await click(find_button("Verlauf", scr.options)) 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 key(KEY_SPACE) 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")