Kampf fortsetzen, Gamepad im Deckbau, Deck-Codes, Kapitel 3, Elementbooster, englische Karten, Host-Wechsel und Zuschauer
- KI-Bewertung mit Bedrohung und Führenden-Bremse (62:38 gegen die alte im Duell, gleichauf zu viert) - Balance: Sand +1/+1 statt Schild, Erde+Luft und Erde geschwächt, Wasser angehoben, Messlauf mit N=200 möglich - Einzelspieler-Kämpfe werden laufend gespeichert (voller Spielstand, viewer -1) und im Menü fortgesetzt - Karten, Deckzeilen und Kartenrücken fokussierbar: Gamepad in Deckbau, Werkstatt und Booster - Kartennamen (name_en) und Kartentexte übersetzbar, Übersetzungstest deckt cards.gd mit ab - Deck-Code DINGER1:id*anzahl mit Zwischenablage; acht neue Karten; Kampagne Kapitel 3; Elementbooster 120 Münzen - Netzwerk: Nachfolge-Host mit vollem Spielstand, Neuverbindung der anderen, Zuschauer (Sicht -2); Bot kann hosten - Große Schrift: Kartennamen, Kartentext, Knopfleisten, Gegnervorschau, Dialogtext - Anleitung 1 garantiert ein 1-Energie-Wesen; Tests: DINGER_ONLY für einzelne Oberflächentests - Oberflächentest auch auf Xephyr mit llvmpipe (wie Xvfb in der CI) grün Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
216
tests/ui_test.gd
216
tests/ui_test.gd
@@ -157,16 +157,22 @@ func _ready() -> void:
|
||||
for suffix in ["", ".bak", ".corrupt"]:
|
||||
if FileAccess.file_exists(SAVE + suffix):
|
||||
DirAccess.remove_absolute(abs + suffix)
|
||||
if FileAccess.file_exists(SAVE.get_basename() + "_kampf.json"):
|
||||
DirAccess.remove_absolute(abs.get_basename() + "_kampf.json")
|
||||
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_gamepad_menue, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
||||
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2, test_herausforderung]
|
||||
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2, test_herausforderung,
|
||||
test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster]
|
||||
if not fast:
|
||||
suite += [test_hotseat, test_netzwerk, test_kampf.bind(2, 3), test_kampf_abbrechen]
|
||||
suite += [test_hotseat, test_netzwerk, test_host_wechsel, test_zuschauer, test_kampf.bind(2, 3), test_kampf_abbrechen]
|
||||
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()
|
||||
@@ -443,6 +449,97 @@ func give_up() -> void:
|
||||
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
|
||||
|
||||
|
||||
## Gamepad im Deckbau: Karten und Deckzeilen sind fokussierbar, A wirkt wie ein Klick, B geht zurück.
|
||||
func test_gamepad_deckbau() -> void:
|
||||
await click(find_button("Deck bauen"))
|
||||
var scr = screen()
|
||||
var before: int = scr.deck.size()
|
||||
await pad(JOY_BUTTON_DPAD_DOWN)
|
||||
check(get_viewport().gui_get_focus_owner() != null and main.pad_mode, "Gamepad setzt Fokus im Deckbau")
|
||||
var row: Control = scr.deck_list.get_child(0)
|
||||
row.grab_focus()
|
||||
await frames(1)
|
||||
check(row.has_focus() and scr.preview.visible, "Deckzeile fokussierbar, Vorschau folgt")
|
||||
await pad(JOY_BUTTON_A)
|
||||
check(scr.deck.size() == before - 1, "A entfernt die fokussierte Karte aus dem Deck")
|
||||
var elems: Array = Game.deck_elements(scr.deck_cards())
|
||||
var target: CardView = null
|
||||
for v in scr.grid.views():
|
||||
if not v.dim and v.card.elements.all(func(e): return e in elems):
|
||||
target = v
|
||||
break
|
||||
target.grab_focus()
|
||||
await frames(1)
|
||||
check(target.has_focus() and scr.preview.visible and scr.preview.card == target.card, "Karte im Raster fokussierbar mit Vorschau")
|
||||
var target_id: String = target.card.id
|
||||
await pad(JOY_BUTTON_A) # das Raster wird danach neu aufgebaut, target ist dann freigegeben
|
||||
check(scr.deck.size() == before and target_id in scr.deck, "A nimmt die fokussierte Karte ins Deck (%d Karten, Meldung '%s')" % [scr.deck.size(), scr.msg_label.text])
|
||||
await shot("31_gamepad_deckbau")
|
||||
await pad(JOY_BUTTON_B)
|
||||
check(scr.confirm.visible, "B fragt bei ungespeicherten Änderungen nach")
|
||||
await click(find_button("Verwerfen"))
|
||||
check(screen() is MenuScreen, "Verwerfen führt ins Menü")
|
||||
await hover(main)
|
||||
|
||||
|
||||
## Deck-Code kopieren, einfügen und übernehmen.
|
||||
func test_deckcode() -> void:
|
||||
await click(find_button("Deck bauen"))
|
||||
var scr = screen()
|
||||
await click(find_button("Code"))
|
||||
check(scr.code_dialog.visible and scr.code_box.text.begins_with(Profile.CODE_PREFIX), "Code-Dialog zeigt den Deck-Code")
|
||||
await click(find_button("In Zwischenablage"))
|
||||
check(DisplayServer.clipboard_get() == scr.code_box.text, "Code in der Zwischenablage")
|
||||
await shot("32_deckcode")
|
||||
scr.code_box.text = "DINGER1:f_funke*2,e_wall*1,x_nix*1"
|
||||
await click(find_button("Übernehmen"))
|
||||
check(scr.deck.size() == Game.DECK_SIZE and "Ungültiger Code" in scr.msg_label.text, "ungültiger Code wird abgelehnt")
|
||||
scr.code_box.text = "DINGER1:f_funke*2,e_wall*1,fff_sonne*1"
|
||||
await click(find_button("Übernehmen"))
|
||||
check(scr.deck == ["f_funke", "f_funke", "e_wall"] and "Kern der Sonne" in scr.msg_label.text and not scr.code_dialog.visible, "Code übernommen, fehlende Karte gemeldet")
|
||||
await click(find_button("Zurück"))
|
||||
await click(find_button("Verwerfen"))
|
||||
check(screen() is MenuScreen, "Deckbau verlassen")
|
||||
|
||||
|
||||
## Elementbooster liefert nur Karten des Elements.
|
||||
func test_elementbooster() -> void:
|
||||
main.profile.coins = 300
|
||||
await click(find_button("Booster öffnen"))
|
||||
var scr = screen()
|
||||
await click(find_button("Feuer"))
|
||||
check(scr.cards.size() == 5 and scr.cards.all(func(c): return "Feuer" in c.elements) and main.profile.coins == 300 - Profile.ELEMENT_BOOSTER_COST, "Feuer-Booster: fünf Feuerkarten")
|
||||
await click(find_button("Alle aufdecken"))
|
||||
await shot("33_elementbooster")
|
||||
await click(find_button("Zurück"))
|
||||
|
||||
|
||||
## Kampf speichern, im Menü fortsetzen, Aufgeben löscht die Datei.
|
||||
func test_kampf_fortsetzen() -> void:
|
||||
await start_battle(1)
|
||||
var scr = screen()
|
||||
scr.ai_delay = 0.0
|
||||
await wait_for_human(scr)
|
||||
var kampf: String = main.battle_save_path()
|
||||
check(FileAccess.file_exists(kampf), "Kampf wird laufend gespeichert")
|
||||
await click(scr.b_end)
|
||||
await wait_for_human(scr)
|
||||
var turn: int = scr.game.turn
|
||||
var life: int = scr.game.human.life
|
||||
var hand: int = scr.game.human.hand.size()
|
||||
await click(find_button("Aufgeben"))
|
||||
await click(find_button("Speichern und zurück"))
|
||||
check(screen() is MenuScreen and find_button("Kampf fortsetzen") != null, "Speichern führt ins Menü mit Fortsetzen-Knopf")
|
||||
await click(find_button("Kampf fortsetzen"))
|
||||
var scr2 = screen()
|
||||
check(scr2 is BattleScreen and scr2.game.turn == turn and scr2.game.human.life == life and scr2.game.human.hand.size() == hand
|
||||
and scr2.game.current() == scr2.game.human, "Kampf fortgesetzt mit gleichem Zug, Leben und Hand")
|
||||
scr2.ai_delay = 0.0
|
||||
await shot("30_kampf_fortgesetzt")
|
||||
await give_up()
|
||||
check(not FileAccess.file_exists(kampf) and find_button("Kampf fortsetzen") == null, "Aufgeben löscht den gespeicherten Kampf")
|
||||
|
||||
|
||||
func test_einstellungen() -> void:
|
||||
await click(find_button("Einstellungen"))
|
||||
var scr = screen()
|
||||
@@ -684,7 +781,7 @@ func test_anleitung() -> void:
|
||||
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(g.log.any(func(l): return "tauscht 1 Karte" 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():
|
||||
@@ -738,7 +835,7 @@ func test_anleitung() -> void:
|
||||
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")
|
||||
check(scr.tutorial_step >= step_before + 1, "Weiter-Knopf (Schritt %d)" % step_before)
|
||||
await key(KEY_ESCAPE)
|
||||
check(scr.confirm.visible, "Esc öffnet Aufgeben")
|
||||
await key(KEY_ESCAPE)
|
||||
@@ -834,6 +931,117 @@ func test_netzwerk() -> void:
|
||||
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
|
||||
await clear_mulligan(b)
|
||||
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.mulligan_box.visible 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.handover.visible and not b.is_hotseat(), "Zuschauer bekommt keine Hotseat-Übergabe")
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user