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 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: var down := InputEventMouseButton.new() down.position = Vector2(5, 5) down.global_position = down.position down.button_index = MOUSE_BUTTON_RIGHT 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 clear_mulligan(scr) -> void: if scr.mulligan_box.visible: await click(scr.b_keep) 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 func card_views(root: Node) -> Array: var out := [] for c in root.get_children(): if c is CardView: out.append(c) out.append_array(card_views(c)) return out ## 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) await clear_mulligan(scr) 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_und_regeln, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen, test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2, test_herausforderung] if not fast: suite += [test_hotseat, test_netzwerk, test_kampf.bind(2, 3), test_kampf_abbrechen] suite.append(test_profil_zuruecksetzen) 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_und_regeln() -> void: check(screen() is MenuScreen, "Menü sichtbar") await shot("01_menu") await click(find_button("Regeln")) check(screen() is RulesScreen, "Regeln geöffnet") await shot("02_regeln") await click(find_button("Zurück")) check(screen() is MenuScreen, "zurück im Menü") func test_booster() -> void: await click(find_button("Booster öffnen")) var scr = screen() check(scr is BoosterScreen, "Boosterbildschirm") var coins: int = main.profile.coins var ach_before: int = main.profile.achievements.size() await click(scr.b_buy) var bonus := 0 for a in Profile.ACHIEVEMENTS: if a.id in main.profile.achievements.slice(ach_before): bonus += a.reward check(main.profile.coins == coins - Profile.BOOSTER_COST + bonus, "Booster bezahlt (plus Erfolgsmünzen %d)" % bonus) 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") var ids: Array = scr.cards.map(func(c): return c.id) var better: Array = main.profile.deck_suggestions(ids) check(scr.b_improve.visible == (not better.is_empty()), "Deck-Vorschlag nur bei passender Karte") if scr.b_improve.visible: var deck_before: Array = main.profile.deck.duplicate() await click(scr.b_improve) check(main.profile.deck != deck_before and main.profile.deck_valid()[0] and "im Deck" in scr.msg_label.text, "Karte ins Deck übernommen") 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 test_deckbau() -> void: await click(find_button("Deck bauen")) var scr = screen() check(scr is DeckScreen, "Deckbildschirm") check(scr.deck.size() == 20, "Deck geladen") check("Stärke" in scr.elems_label.text, "Deckstärke im Deckbau") await shot("05_deck") await click(find_button("Leeren")) check(scr.deck.is_empty(), "geleert") var first_id: String = card_views(scr.grid)[0].card.id var allowed: int = mini(main.profile.collection[first_id], Game.MAX_COPIES) await click(card_views(scr.grid)[0]) check(scr.deck == [first_id], "Karte hinzugefügt") for i in allowed: await click(card_views(scr.grid)[0]) check(scr.deck.count(first_id) == allowed and "Kopien" in scr.msg_label.text, "nur besessene Kopien (%d)" % allowed) await click(find_button("Speichern")) check(main.profile.deck.size() == 20 and scr.msg_label.text != "Deck gespeichert", "zu kleines Deck nicht gespeichert") await click(find_button("Wasser")) await click(card_views(scr.grid)[0]) await click(find_button("Erde")) await click(card_views(scr.grid)[0]) check("Elemente" in scr.msg_label.text, "drittes Element abgelehnt") await click(find_button("Alle")) # Suche, Sortierung, fehlende Karten var all_count := card_views(scr.grid).size() scr.search_box.text = "Wächter" scr.search_box.text_changed.emit("Wächter") await frames(1) var found := card_views(scr.grid) check(found.size() > 0 and found.size() < all_count, "Suche filtert") check(found.all(func(v): return "Wächter" in v.card.text() or "Wächter" in v.card.name), "Suche trifft Kartentext") scr.search_box.text = "" scr.search_box.text_changed.emit("") scr.sort_box.select(1) scr.sort_box.item_selected.emit(1) await frames(1) var names: Array = card_views(scr.grid).map(func(v): return v.card.name) var sorted_names := names.duplicate() sorted_names.sort() check(names == sorted_names, "Sortierung nach Name") scr.missing_box.button_pressed = true await frames(1) check(card_views(scr.grid).size() == Cards.all().size(), "Fehlende Karten eingeblendet") var missing = card_views(scr.grid).filter(func(v): return main.profile.collection.get(v.card.id, 0) == 0) check(not missing.is_empty(), "es gibt fehlende Karten") await click(missing[0]) check("fehlt" in scr.msg_label.text, "fehlende Karte nicht ins Deck") scr.missing_box.button_pressed = false scr.sort_box.select(0) scr.sort_box.item_selected.emit(0) await frames(1) var n: int = scr.deck.size() await click(scr.deck_list.get_children()[0]) check(scr.deck.size() == n - 1, "Zeile entfernt Karte") await hover(card_views(scr.grid)[0]) check(scr.preview.visible, "Vorschau bei Hover") await shot("06_deck_bearbeitet") # Slots: Deck 2 aus Deck 1 kopieren, speichern, aktiv setzen await click(scr.slot_buttons[1]) if scr.slot != 1 or not scr.deck.is_empty(): print("DEBUG Slot: slot=", scr.slot, " deck=", scr.deck.size(), " rect=", scr.slot_buttons[1].get_global_rect(), " hovered=", get_viewport().gui_get_hovered_control(), " decks1=", main.profile.decks[1].cards.size()) check(scr.slot == 1 and scr.deck.is_empty(), "Slot 2 leer") scr.deck = main.profile.decks[0].cards.duplicate() scr.refresh() await click(find_button("Speichern")) check(main.profile.decks[1].cards.size() == 20 and "gespeichert" in scr.msg_label.text, "Slot 2 gespeichert") await click(find_button("Aktiv setzen")) check(main.profile.active_deck == 1 and main.profile.deck_name() == "Deck 2", "Slot 2 aktiv") check(Profile.load_or_new(SAVE).active_deck == 1, "aktiver Slot gespeichert") scr.name_box.text = "Testdeck" scr.name_box.text_submitted.emit("Testdeck") check(main.profile.deck_name(1) == "Testdeck" and Profile.load_or_new(SAVE).deck_name(1) == "Testdeck", "Deck umbenannt") await click(scr.slot_buttons[2]) check(scr.slot == 2 and scr.deck.is_empty(), "Slot 3 leer") await click(find_button("Auto-Ausfüllen")) check(scr.deck.size() == 20 and main.profile.deck_valid(2)[0], "Slot 3 automatisch gefüllt und spielbar") await shot("25_deck_slots") await click(scr.deck_list.get_children()[0]) # ungespeicherte Änderung: eine Karte entfernt check(scr.dirty(), "Deck geändert") await click(find_button("Zurück")) check(scr.confirm.visible and screen() == scr, "Warnung bei ungespeicherten Änderungen") await click(find_button("Weiter bearbeiten")) check(not scr.confirm.visible, "weiter bearbeiten") await click(find_button("Zurück")) await click(find_button("Verwerfen")) check(screen() is MenuScreen and main.profile.deck.size() == 20, "Änderungen verworfen") 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 func human_turn(scr) -> void: var g: Game = scr.game await clear_mulligan(scr) 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 energy: int = g.human.energy await click(playable[0]) if scr.pending != null: await click(target_view(scr, g.valid_targets(g.human, card.target_type()))) check(g.human.energy == energy - card.cost, "Karte gespielt: " + card.name) 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(), " pending=", scr.pending, " selected=", scr.selected) check(scr.selected == cr, "Wesen gewählt") var targets := [] for o in g.opponents: 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_left < cr.attacks_per_turn(), "Angriff ausgeführt") if g.winner == "": await click(scr.b_end) func start_battle(n_opp: int) -> void: await click(find_button("Einzelspieler")) check(screen() is OpponentSelectScreen, "Gegnerauswahl") await click(find_button("%d Gegner" % n_opp)) func test_gegnerauswahl() -> void: await click(find_button("Einzelspieler")) var sel = screen() check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet") check("Deckstärke" in sel.rating_label.text, "Deckstärke angezeigt") check(sel.preview_labels[3].text.count("(") == 3 and sel.previews[3].size() == 3, "Vorschau zeigt drei Gegner mit Stärke") for i in 3: await click(sel.b_reroll) check(sel.rerolls_left == 0 and sel.b_reroll.disabled, "höchstens dreimal neu würfeln") 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") var previewed: Array = sel.previews[1].map(func(o): return o.title) await shot("07_gegnerauswahl") await click(find_button("1 Gegner")) var hard = screen() check(hard.game.opponents.map(func(o): return o.title) == previewed, "Kampf nutzt die vorgeschauten Gegner") var own := Game.deck_rating(main.profile.deck_cards()) var opp: Game.Player = hard.game.opponents[0] var opp_rating := Game.deck_rating(opp.deck + opp.hand) check(opp_rating >= own * 1.05 - 0.05 and hard.smart_ai, "Schwer: Gegnerdeck stärker (%.2f zu %.2f), Vorausschau-KI" % [opp_rating, own]) await give_up() await click(find_button("Einzelspieler")) await click(find_button("Leicht")) await click(find_button("1 Gegner")) var easy = screen() opp = easy.game.opponents[0] check(Game.deck_rating(opp.deck + opp.hand) <= own * 1.0 + 0.15 and not easy.smart_ai, "Leicht: schwächeres Deck, einfache KI") await give_up() await click(find_button("Einzelspieler")) await click(find_button("Normal")) main.profile.records.streak = 3 main.profile.records.margins = [20, 22, 18] screen().refresh() check(screen().b_tip.visible and "Schwer" in screen().tip_label.text, "Vorschlag nach drei deutlichen Siegen") await click(find_button("Übernehmen")) check(main.profile.settings.difficulty == "schwer", "Vorschlag übernommen") main.profile.records.streak = 0 main.profile.records.margins = [] await click(find_button("Normal")) await click(find_button("Zurück")) check(screen() is MenuScreen, "zurück im Menü") for n in [1, 5]: await start_battle(n) var scr = screen() check(scr is BattleScreen and scr.game.opponents.size() == n, "Kampf gegen %d gestartet" % n) check(scr.game.players[0].max_energy == 1 and scr.game.opponents[0].life == Game.START_LIFE, "gleiche Regeln gegen %d" % n) await shot("11_kampf_%d" % n) await give_up() ## 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 clear_mulligan(scr) 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_erfolge() -> void: await click(find_button("Erfolge")) check(screen() is AchievementsScreen, "Erfolge geöffnet") var csv := "user://statistik.csv" if FileAccess.file_exists(csv): DirAccess.remove_absolute(ProjectSettings.globalize_path(csv)) await click(find_button("Statistik exportieren")) check(FileAccess.file_exists(csv) and "Gespeichert" in screen().export_label.text, "Statistik exportiert") await shot("14_erfolge") await click(find_button("Zurück")) check(screen() is MenuScreen, "zurück im Menü") func test_werkstatt() -> void: await click(find_button("Werkstatt")) var scr = screen() check(scr is WorkshopScreen, "Werkstatt geöffnet") var p: Profile = main.profile p.dust = 0 var in_decks := {} for d in p.decks: for cid in d.cards: in_decks[cid] = true var free = scr.grid.views().filter(func(v): return p.collection.get(v.card.id, 0) > 0 and not in_decks.has(v.card.id)) check(not free.is_empty(), "eine freie Karte zum Zerlegen") var card: Cards.Card = free[0].card await click(free[0]) check(scr.selected == card, "Karte gewählt") var before: int = p.collection[card.id] await click(scr.b_dismantle) check(p.collection[card.id] == before - 1 and p.dust == Profile.DUST_DISMANTLE[card.rarity], "zerlegt, Staub gutgeschrieben") 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) await click(scr.b_craft) check(p.collection.get("ff_phoenix", 0) == 1 and p.dust == 5000 - Profile.DUST_CRAFT["legendär"], "Phönix hergestellt") check(Profile.load_or_new(SAVE).collection.get("ff_phoenix", 0) == 1, "Werkstatt gespeichert") await shot("20_werkstatt") await click(find_button("Zurück")) check(screen() is MenuScreen, "zurück im Menü") func test_anleitung2() -> void: check(main.profile.tutorial_done and not main.profile.tutorial2_done, "Teil 1 erledigt, Teil 2 offen") check(find_button("Anleitung 2: Fallen") != null, "Menü bietet Teil 2 an") await click(find_button("Anleitung 2: Fallen")) var scr = screen() check(scr is BattleScreen and scr.tutorial and scr.tutorial_part == 2, "Anleitung 2 gestartet") scr.ai_delay = 0.0 var g: Game = scr.game check(g.opponents[0].name == "Mara" and g.human.hand.any(func(c): return c.id == "w_falle") and g.human.hand.any(func(c): return c.id == "w_reiter"), "Mara, Frostfalle und Wellenreiter auf der Hand") await frames(2) check(scr.mulligan_box.visible and scr.tutorial_step == 0, "Schritt 1: Mulligan-Maske") await click(scr.b_keep) check(scr.tutorial_step == 1 and "Frostfalle" in scr.tutorial_text.text, "Schritt 2: Falle legen") await shot("29_anleitung2") # Zug 1 hat nur 1 Energie: Zug beenden, dann Falle legen await key(KEY_SPACE) await wait_for_human(scr) check(scr.highlight_layer.get_child_count() >= 1, "Falle hervorgehoben") var idx: int = g.human.hand.map(func(c): return c.id).find("w_falle") check(idx >= 0 and idx < 9, "Falle auf der Hand") await key(KEY_1 + idx) check(g.human.traps.size() == 1 and scr.tutorial_step == 2, "Falle gelegt, Schritt 3") await give_up() check(main.profile.tutorial2_done and find_button("Anleitung") != null, "Teil 2 erledigt") 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 not main.profile.tutorial_done, "Profil zurückgesetzt") check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert") func test_herausforderung() -> void: await click(find_button("Herausforderung")) var scr = screen() check(scr is ChallengeScreen, "Herausforderung geöffnet") var ch := Challenge.current() var allowed := Challenge.deck_allowed(ch, main.profile.deck_cards()) == "" check(find_button("Antreten").disabled == not allowed, "Antreten je nach Deck") await shot("28_herausforderung") if allowed: await click(find_button("Antreten")) var b = screen() check(b is BattleScreen and b.challenge == ch and b.game.opponents.size() == ch.n_opp, "Herausforderungskampf") var everyone: String = ch.rules.get("everyone_perk", "") check(everyone == "" or b.game.players.all(func(p): return everyone in p.perks), "Regel gilt für alle") await give_up() else: await click(find_button("Zurück")) check(screen() is MenuScreen, "zurück im Menü") func test_hotseat() -> void: await click(find_button("Lokal zu mehreren")) var scr = screen() check(scr is HotseatScreen, "Hotseat-Auswahl") await click(find_button("3 Spieler")) await click(find_button("1 KI")) check(scr.n_humans == 3 and scr.n_ai == 1, "3 Menschen, 1 KI") await shot("17_hotseat") await click(find_button("Start")) var b = screen() check(b is BattleScreen and b.is_hotseat() and b.game.players.size() == 4, "Hotseat-Kampf gestartet") b.ai_delay = 0.0 var guard := 0 while not b.handover.visible and b.game.winner == "" and guard < 400: if b.game.current() == b.perspective: await clear_mulligan(b) if b.game.current() == b.perspective: await click(b.b_end) await frames(1) guard += 1 check(b.handover.visible, "Übergabe erscheint") var cur: Game.Player = b.game.current() check(cur.is_human and cur != b.perspective, "anderer Mensch ist dran") await shot("18_hotseat_uebergabe") await click(find_button("Weiter")) check(b.perspective == cur and not b.handover.visible, "Perspektive gewechselt") check(b.hand_views.size() == cur.hand.size() and b.name_label.text == cur.name, "Hand und Name des neuen Spielers") check(b.mulligan_box.visible, "neuer Spieler bekommt seine Mulligan-Maske") await clear_mulligan(b) check(b.hero_panels.size() == 3 and not b.hero_panels.has(cur), "drei andere am Tisch") await shot("19_hotseat_kampf") await leave_battle() 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 Energie") == "Not enough energy", "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ü") func test_anleitung() -> void: check(not main.profile.tutorial_done, "Anleitung noch offen") await click(find_button("Anleitung")) var scr = screen() check(scr is BattleScreen and scr.tutorial, "Anleitung gestartet") scr.ai_delay = 0.0 var g: Game = scr.game check(g.players[0] == g.human and g.opponents[0].name == "Fenn" and g.opponents[0].perks.is_empty(), "Fenn ohne Merkmal, Mensch beginnt") await frames(2) check(scr.mulligan_box.visible, "Mulligan-Maske im ersten Zug") var views = scr.mulligan_layer.get_children() await click(views[0]) check(scr.mulligan_selected == [0], "Karte zum Tausch gewählt") await shot("22_anleitung_mulligan") await click(scr.b_swap) check(not scr.mulligan_box.visible and g.human.mulligan_done, "getauscht") check(g.log.any(func(l): return "tauscht 1 Karten" in l), "Tausch im Verlauf") check(scr.tutorial_step == 0 and "Energie" in scr.tutorial_text.text, "Schritt 1 sichtbar") var idx := -1 for i in g.human.hand.size(): if g.human.hand[i].is_creature() and g.human.hand[i].cost == 1: idx = i break check(idx < 0 or scr.highlight_layer.get_child_count() >= 1, "spielbare Wesen hervorgehoben") if idx >= 0: var played_id: String = g.human.hand[idx].id await key(KEY_1 + idx) check(g.human.board.size() == 1, "Wesen per Taste gespielt") check(scr.tutorial_step == 1, "Schritt 2 nach dem Ausspielen") check(scr.highlight_layer.get_child_count() == 1 and scr.highlight_layer.get_child(0).get_rect().intersects(scr.b_end.get_rect()), "Zug-beenden-Knopf hervorgehoben") # Rückgängig: Wesen zurück auf die Hand check(scr.b_undo.visible, "Rückgängig angeboten") var energy: int = g.human.energy await click(scr.b_undo) g = scr.game check(g.human.board.is_empty() and g.human.hand.any(func(c): return c.id == played_id) and g.human.energy == energy + 1, "Rückgängig stellt Hand und Energie her") check(not scr.b_undo.visible and g.log.back() == "Rückgängig.", "Rückgängig verbraucht") await key(KEY_1 + idx) check(g.human.board.size() == 1, "erneut gespielt") await shot("22_anleitung") # Gamepad: Zeiger bewegen, Karte spielen await pad(JOY_BUTTON_DPAD_RIGHT) check(scr.gp_zone == "hand" and scr.gp_index == 1 and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf zweiter Handkarte") var pad_idx := -1 for i in g.human.hand.size(): if g.human.hand[i].is_creature() and g.can_play(g.human, g.human.hand[i])[0]: pad_idx = i break if pad_idx >= 0: while scr.gp_index != pad_idx: await pad(JOY_BUTTON_DPAD_RIGHT) var board_before: int = g.human.board.size() await pad(JOY_BUTTON_A) check(g.human.board.size() == board_before + 1, "Gamepad A spielt das Wesen") 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 clear_mulligan(scr) check(scr.tutorial_step >= 2 or idx < 0, "Schritt 3 im zweiten Zug") var step_before: int = scr.tutorial_step await click(scr.b_tut_next) check(scr.tutorial_step == step_before + 1, "Weiter-Knopf") 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() check(main.profile.tutorial_done, "Anleitung als erledigt gespeichert") 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: await clear_mulligan(b) 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 spielt") or l.begins_with("Bot behält") or l.begins_with("Bot tauscht")), "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: await clear_mulligan(b) 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: await clear_mulligan(b) 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 test_kampagne() -> void: await click(find_button("Kampagne")) var scr = screen() check(scr is CampaignScreen, "Kampagne geöffnet") await shot("15_kampagne") check(find_button("Gesperrt") != null and find_button("Kämpfen") != null, "Stufen gesperrt und offen") 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.opponents.size() == 1 and battle.game.opponents[0].name == "Fenn", "Fenn ist der Gegner") check(battle.game.opponents[0].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.opponents[0].perks.size() == 2, "Kapitel-2-Boss mit zwei Merkmalen") 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(n_opp: int, shot_turn: int) -> void: await start_battle(n_opp) var scr = screen() check(scr is BattleScreen, "Kampfbildschirm") scr.ai_delay = 0.0 var g: Game = scr.game check(g.opponents.size() == n_opp and g.log[0].begins_with("Zugreihenfolge"), "Spiel gestartet, Reihenfolge ausgelost") await wait_for_human(scr) check(g.current() == g.human and g.human.mulligan_done, "Spieler ist dran, Mulligan erledigt") 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(5) var scr = screen() scr.ai_delay = 0.0 var g: Game = scr.game await wait_for_human(scr) check(g.opponents.size() == 5 and g.human.max_energy == 1, "5 Gegner") # Falle legen und Kombo-Hinweis prüfen g.human.hand.append(Cards.by_id("f_falle")) g.human.energy = 10 var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000) g.human.board.append(wolf) g.human.hand.append(Cards.by_id("w_reiter")) scr.refresh() var trap_view = scr.hand_views.filter(func(v): return v.card.id == "f_falle")[0] await click(trap_view) check(g.human.traps.size() == 1 and scr.trap_views.size() == 1, "Falle gelegt und sichtbar") var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0] await hover(scr.log_box) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird await hover(reiter_view) var hint_boxes = scr.preview_holder.get_children().filter(func(c): return c is RichTextLabel) if hint_boxes.size() != 1: print("DEBUG Hinweis: hints=", scr.combo_hints(Cards.by_id("w_reiter")), " kinder=", scr.preview_holder.get_children().map(func(c): return c.get_class()), " brett=", g.human.board.map(func(c): return c.name), " hovered=", get_viewport().gui_get_hovered_control(), " reiter_pos=", reiter_view.get_global_rect()) check(hint_boxes.size() == 1 and "Feldkombo Dampf" in hint_boxes[0].text, "Kombo-Hinweis in der Vorschau") await shot("21_kampf_falle_hinweis") var spells: Array = scr.hand_views.filter(func(v): return v.card.target_type() != "" and g.can_play(g.human, v.card)[0]) if not spells.is_empty(): var card: Cards.Card = spells[0].card await click(spells[0]) check(scr.pending == card, "Ziel wird erwartet") await right_click() check(scr.pending == null and card in g.human.hand, "Rechtsklick bricht ab") await hover(scr.hand_views[0]) if scr.preview_holder.get_child_count() == 0: print("DEBUG Hover: mulligan=", scr.mulligan_box.visible, " 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(find_button("Lupe")) check(scr.zoom_player == scr.others()[0], "Lupe öffnet die erste Spalte") await shot("12_lupe") await click(find_button("Schließen")) check(scr.zoom_player == null, "Lupe geschlossen") # 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_fuenf")