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:
Team3
2026-09-16 18:50:09 +02:00
parent b6d5916a8d
commit 4c6bf96eeb
32 changed files with 1355 additions and 174 deletions

View File

@@ -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()