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:
@@ -1,11 +1,14 @@
|
||||
extends Node
|
||||
## Test-Mitspieler: tritt einem Host bei und spielt einfache Züge.
|
||||
## Test-Mitspieler: tritt einem Host bei und spielt einfache Züge; mit --host eröffnet er
|
||||
## selbst und startet, sobald ein zweiter Mensch am Tisch sitzt (ohne KI).
|
||||
## godot --path . tests/net_bot.tscn -- --ip=127.0.0.1 --name=Bot
|
||||
## godot --path . tests/net_bot.tscn -- --host --name=BotHost
|
||||
|
||||
var net: Net
|
||||
var seat := -1
|
||||
var game: Game
|
||||
var t := 0.0
|
||||
var hosting := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -16,20 +19,37 @@ func _ready() -> void:
|
||||
ip = a.substr(5)
|
||||
if a.begins_with("--name="):
|
||||
bot_name = a.substr(7)
|
||||
if a == "--host":
|
||||
hosting = true
|
||||
net = Net.new()
|
||||
net.name = "Net"
|
||||
get_tree().root.add_child.call_deferred(net)
|
||||
net.my_name = bot_name
|
||||
net.my_deck = Profile.starter().deck.duplicate()
|
||||
net.started.connect(func(s): seat = s)
|
||||
net.started.connect(func(s):
|
||||
seat = s
|
||||
if hosting:
|
||||
net.broadcast() # erster Zustand für alle, wie es sonst der Kampfbildschirm tut
|
||||
_act_host.call_deferred())
|
||||
net.snapshot_received.connect(_on_snapshot)
|
||||
net.ended.connect(func(_r): get_tree().quit())
|
||||
await get_tree().process_frame
|
||||
net.join(ip)
|
||||
if hosting:
|
||||
net.host()
|
||||
net.lobby_changed.connect(func():
|
||||
if net.game == null and net.names.size() >= 2:
|
||||
net.start_battle(0))
|
||||
net.action_applied.connect(func():
|
||||
net.broadcast()
|
||||
_act_host.call_deferred())
|
||||
else:
|
||||
net.join(ip)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
t += delta
|
||||
if hosting and net.game != null:
|
||||
net.tick()
|
||||
if t > 180.0 or FileAccess.file_exists("user://bot_quit"):
|
||||
net.leave()
|
||||
get_tree().quit()
|
||||
@@ -43,7 +63,23 @@ func _on_snapshot(snap: Dictionary) -> void:
|
||||
return
|
||||
if game.current_idx != seat:
|
||||
return
|
||||
var me: Game.Player = game.players[seat]
|
||||
_decide(game.players[seat])
|
||||
|
||||
|
||||
func _act_host() -> void:
|
||||
if not hosting or net.game == null or net.game.winner != "" or seat < 0 or net.game.current_idx != seat:
|
||||
return
|
||||
game = net.game
|
||||
_decide(game.players[seat])
|
||||
|
||||
|
||||
## Einfache Züge mit kurzer Bedenkzeit: Mulligan überspringen, erstes spielbares Wesen ohne Ziel,
|
||||
## dann angreifen, sonst Zug beenden.
|
||||
func _decide(me: Game.Player) -> void:
|
||||
var g := game
|
||||
await get_tree().create_timer(0.25).timeout
|
||||
if game != g or (hosting and (net.game == null or net.game.current_idx != seat)):
|
||||
return # inzwischen ein neuer Zustand
|
||||
if not me.mulligan_done and me.turns == 1:
|
||||
net.send_action({"type": "mulligan", "hand": []})
|
||||
return
|
||||
|
||||
@@ -16,7 +16,7 @@ func _initialize() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 21
|
||||
var arts := archetypes()
|
||||
var n := 60
|
||||
var n := int(OS.get_environment("N")) if OS.get_environment("N") != "" else 60
|
||||
var results := {}
|
||||
for a in arts:
|
||||
var wins := 0
|
||||
|
||||
@@ -55,7 +55,11 @@ func put(p: Game.Player, cid: String, ready: bool = true) -> Game.Creature:
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
TranslationServer.set_locale("de") # das Spiel setzt die Sprache aus den Einstellungen; hier fehlt der Einstieg
|
||||
test_kartenpool()
|
||||
test_kampf_speichern()
|
||||
test_elementbooster_und_deckcode()
|
||||
test_englische_karten()
|
||||
test_kombos()
|
||||
test_wesen_spielen()
|
||||
test_flink()
|
||||
@@ -117,7 +121,7 @@ func _initialize() -> void:
|
||||
|
||||
|
||||
func test_kartenpool() -> void:
|
||||
check(Cards.all().size() == 110, "110 Karten")
|
||||
check(Cards.all().size() == 118, "118 Karten")
|
||||
for key in Cards.COMBOS:
|
||||
var found: bool = Cards.all().any(func(c): return c.is_combo() and Cards.combo_key(c.elements) == key)
|
||||
check(found, "Kombo vorhanden: " + key)
|
||||
@@ -462,7 +466,7 @@ func test_spielstand() -> void:
|
||||
p.coins = 999
|
||||
p.save(path)
|
||||
var p2 := Profile.load_or_new(path)
|
||||
check(p2.coins == 999 and p2.deck.size() == 20 and p2.collection.size() == 42, "geladen")
|
||||
check(p2.coins == 999 and p2.deck.size() == 20 and p2.collection.size() == Cards.of_rarity("gewöhnlich").size() + 2, "geladen")
|
||||
for suffix in ["", ".bak"]:
|
||||
if FileAccess.file_exists(path + suffix):
|
||||
DirAccess.remove_absolute(abs + suffix)
|
||||
@@ -551,7 +555,7 @@ func test_feldkombos() -> void:
|
||||
s[2][1].life = 5
|
||||
g.play_card(h, C("l_wind"))
|
||||
check(s[2][1].life == 3 and s[2][0].life == 15, "Blitz trifft den schwächsten Gegner")
|
||||
# Schlamm heilt, Sturm zieht, Sand gibt Schild
|
||||
# Schlamm heilt, Sturm zieht, Sand gibt +1/+1
|
||||
s = setup(["e_wall", "l_wind", "w_reiter"])
|
||||
g = s[0]
|
||||
h = s[1]
|
||||
@@ -562,13 +566,13 @@ func test_feldkombos() -> void:
|
||||
var hand_before := h.hand.size()
|
||||
g.play_card(h, C("l_wind"))
|
||||
check(h.hand.size() == hand_before, "Sturm zieht 1 (eine gespielt, eine gezogen)")
|
||||
check(not h.board[2].shield, "nur eine Feldkombo je Ausspielen")
|
||||
check(h.board[2].attack == 1, "nur eine Feldkombo je Ausspielen")
|
||||
s = setup(["l_wind"])
|
||||
g = s[0]
|
||||
h = s[1]
|
||||
put(h, "e_wall")
|
||||
g.play_card(h, C("l_wind"))
|
||||
check(h.board[1].shield, "Sand: neues Wesen hat Schild")
|
||||
check(h.board[1].attack == 2 and h.board[1].health == 2, "Sand: neues Wesen erhält +1/+1")
|
||||
# Gleiche Elemente lösen keine Feldkombo aus
|
||||
s = setup(["f_kobold"])
|
||||
g = s[0]
|
||||
@@ -736,7 +740,7 @@ func test_fallen() -> void:
|
||||
g.end_turn()
|
||||
var a := put(o, "e_troll")
|
||||
check(g.attack(a, h) == "", "Angriff in die Feuerfalle")
|
||||
check(h.traps.is_empty() and a.health == 3 and h.life == 26, "Feuerfalle: 3 Schaden, Angriff geht durch")
|
||||
check(h.traps.is_empty() and a.health == 3 and h.life == 27, "Feuerfalle: 3 Schaden, Angriff geht durch")
|
||||
# Frostfalle bricht den Angriff ab
|
||||
s = setup(["w_falle"])
|
||||
g = s[0]
|
||||
@@ -1089,7 +1093,7 @@ func test_deck_umbenennen_und_auffuellen() -> void:
|
||||
|
||||
|
||||
func test_neue_fallen() -> void:
|
||||
check(Cards.all().filter(func(c): return c.is_trap()).size() == 10, "zehn Fallen")
|
||||
check(Cards.all().filter(func(c): return c.is_trap()).size() == 12, "zwölf Fallen")
|
||||
check(C("e_sandfalle").text() == "Falle. Wenn ein Gegner ein Wesen ausspielt: Es wird eingefroren.", "Fallentext je Auslöser")
|
||||
check(C("f_aschefalle").text().ends_with("3 Schaden an dieses Wesen."), "Aschefalle-Text")
|
||||
# Böenfalle: Angreifer zurück auf die Hand, Angriff verpufft
|
||||
@@ -1144,7 +1148,11 @@ func test_neue_fallen() -> void:
|
||||
|
||||
|
||||
func test_kapitel_zwei_und_merkmalwahl() -> void:
|
||||
check(Campaign.total() == 20 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2, "zwanzig Stufen in zwei Kapiteln")
|
||||
check(Campaign.total() == 30 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(29) == 3, "dreißig Stufen in drei Kapiteln")
|
||||
check(Campaign.stage(25).perks.size() == 3 and Campaign.stage(25).name == "Nyx" and Campaign.target_rating(29, 1.0) == 4.4, "Kapitel 3 mit drei Merkmalen und höherer Zielstärke")
|
||||
for st in Campaign.STAGES3:
|
||||
for pk in st.perks:
|
||||
check(Game.PERKS.has(pk), "Merkmal %s bekannt" % pk)
|
||||
for i in Campaign.total():
|
||||
var st := Campaign.stage(i)
|
||||
check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1))
|
||||
@@ -1362,6 +1370,73 @@ func test_ki_zeitbudget() -> void:
|
||||
check(v.settings.has("big_font") and not v.tutorial2_done and v.records.margins is Array, "Version 5 mit neuen Feldern")
|
||||
|
||||
|
||||
## Elementbooster enthalten nur Karten des Elements; Deck-Codes sind umkehrbar und robust.
|
||||
## Englisch: jede Karte hat einen englischen Namen, Kartentexte und Schlüsselwörter werden übersetzt.
|
||||
func test_englische_karten() -> void:
|
||||
check(Cards.all().all(func(c): return c.name_en != ""), "alle Karten mit englischem Namen")
|
||||
var de: String = Cards.by_id("f_funke").text()
|
||||
TranslationServer.set_locale("en")
|
||||
var funke := Cards.by_id("f_funke")
|
||||
var falle := Cards.by_id("w_falle")
|
||||
var wall := Cards.by_id("e_wall")
|
||||
check(funke.title() == "Spark" and funke.text() == "2 damage to a target.", "Funke auf Englisch: %s / %s" % [funke.title(), funke.text()])
|
||||
check(falle.text().begins_with("Trap. When an enemy creature attacks") and "The attack fizzles" in falle.text(), "Fallentext auf Englisch: %s" % falle.text())
|
||||
check(wall.text() == "Guardian." and Cards.by_id("l_spiegel").text().ends_with("Summon a Wind Spirit."), "Schlüsselwort und Beschwörung auf Englisch")
|
||||
TranslationServer.set_locale("de")
|
||||
check(funke.title() == "Funke" and funke.text() == de, "zurück auf Deutsch")
|
||||
|
||||
|
||||
func test_elementbooster_und_deckcode() -> void:
|
||||
var p := Profile.starter()
|
||||
p.coins = 500
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 5
|
||||
var got: Array = p.open_booster(r, "Wasser")
|
||||
check(got.size() == 5 and got.all(func(c): return "Wasser" in c.elements) and p.coins == 500 - Profile.ELEMENT_BOOSTER_COST, "Wasser-Booster: fünf Wasserkarten, %d Münzen" % Profile.ELEMENT_BOOSTER_COST)
|
||||
p.coins = 100
|
||||
check(p.open_booster(r, "Feuer").is_empty() and p.coins == 100, "Elementbooster braucht %d Münzen" % Profile.ELEMENT_BOOSTER_COST)
|
||||
for e in Cards.ELEMENTS:
|
||||
for rar in Profile.RARITY_WEIGHTS:
|
||||
check(not Profile.booster_pool(rar, e).is_empty(), "%s hat %s Karten" % [e, rar])
|
||||
var deck: Array = Profile.starter().deck
|
||||
var code := Profile.deck_code(deck)
|
||||
check(code.begins_with(Profile.CODE_PREFIX) and Profile.parse_deck_code(code).size() == 20, "Deck-Code hin und zurück")
|
||||
var back: Array = Profile.parse_deck_code(code)
|
||||
back.sort()
|
||||
var orig: Array = deck.duplicate()
|
||||
orig.sort()
|
||||
check(back == orig, "Deck-Code enthält genau die Karten")
|
||||
check(Profile.parse_deck_code("kaputt").is_empty() and Profile.parse_deck_code("DINGER1:x_nix*2").is_empty()
|
||||
and Profile.parse_deck_code("DINGER1:f_funke*9").size() == Game.MAX_COPIES, "ungültige Codes leer, Kopien gedeckelt")
|
||||
check(Cards.by_id("w_flutfalle").is_trap() and Cards.by_id("fl_sturmfalke").combo() == "Blitz" and Cards.by_id("el_sandsturm").is_combo(), "neue Karten im Pool")
|
||||
|
||||
|
||||
## Voller Spielstand (viewer -1) stellt Hände, Decks und Bretter wieder her; Zuschauer (-2) sehen nichts Verborgenes.
|
||||
func test_kampf_speichern() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 7
|
||||
var g := Game.new_battle(Profile.starter().deck_cards(), 2, r)
|
||||
g.start()
|
||||
AI.take_turn(g, g.current())
|
||||
var d: Dictionary = JSON.parse_string(JSON.stringify(g.snapshot(-1)))
|
||||
var g2 := Game.from_snapshot(d)
|
||||
var same := g2.players.size() == g.players.size()
|
||||
for i in g.players.size():
|
||||
var a: Game.Player = g.players[i]
|
||||
var b: Game.Player = g2.players[i]
|
||||
var ids := func(cs: Array): return cs.map(func(c): return c.id)
|
||||
same = same and ids.call(a.hand) == ids.call(b.hand) and ids.call(a.deck) == ids.call(b.deck) \
|
||||
and a.life == b.life and a.board.size() == b.board.size() and a.is_human == b.is_human
|
||||
check(same and g2.current_idx == g.current_idx and g2.turn == g.turn and g2.human.is_human, "voller Spielstand: Hände, Decks und Bretter erhalten")
|
||||
var guard := 0
|
||||
while g2.winner == "" and guard < 300:
|
||||
AI.take_turn(g2, g2.current())
|
||||
guard += 1
|
||||
check(g2.winner != "", "fortgesetzte Partie ist bis zum Ende spielbar")
|
||||
var s2: Dictionary = g.snapshot(-2)
|
||||
check(s2.players.all(func(p): return not (p.hand is Array) and not (p.traps is Array) and not (p.deck is Array)), "Zuschauer-Sicht ohne Hände, Decks und Fallen")
|
||||
|
||||
|
||||
## Jeder tr("...")-Text in der Oberfläche und jeder Anleitungsschritt hat einen englischen Eintrag.
|
||||
func test_uebersetzung_vollstaendig() -> void:
|
||||
var keys := {}
|
||||
@@ -1376,6 +1451,9 @@ func test_uebersetzung_vollstaendig() -> void:
|
||||
key = line.substr(0, line.find(","))
|
||||
keys[key] = true
|
||||
var missing := []
|
||||
for k in Cards.COMBOS.values() + Cards.ELEMENTS + Cards.KEYWORDS + Cards.RARITIES + ["Wesen", "Zauber", "Falle"] + Cards.TRIGGER_TEXT.values():
|
||||
if not keys.has(k):
|
||||
missing.append(k)
|
||||
var d := DirAccess.open("res://scripts/ui")
|
||||
d.list_dir_begin()
|
||||
var name := d.get_next()
|
||||
@@ -1389,6 +1467,11 @@ func test_uebersetzung_vollstaendig() -> void:
|
||||
if not keys.has(k) and not (k in missing):
|
||||
missing.append(k)
|
||||
name = d.get_next()
|
||||
var rx_cards := RegEx.new()
|
||||
rx_cards.compile("tr\\(\"([^\"]+)\"\\)")
|
||||
for m in rx_cards.search_all(FileAccess.get_file_as_string("res://scripts/cards.gd")):
|
||||
if not keys.has(m.get_string(1)) and not (m.get_string(1) in missing):
|
||||
missing.append(m.get_string(1))
|
||||
for stepset in [BattleTutorial.STEPS1, BattleTutorial.STEPS2]:
|
||||
for st in stepset:
|
||||
if not keys.has(st[0]):
|
||||
|
||||
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