update
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled

This commit is contained in:
Team3
2026-09-22 13:23:02 +02:00
parent 8944fdd017
commit dfcd3cb713
46 changed files with 1525 additions and 4141 deletions

View File

@@ -19,14 +19,11 @@ func _ready() -> void:
await get_tree().process_frame
await get_tree().process_frame
var h: Game.Player = scr.game.human
while h.hand.size() < 8 and not h.deck.is_empty():
scr.game.draw(h)
scr.game.buy(h, 0)
var t0 := Time.get_ticks_usec()
for i in 30:
scr.refresh()
print("refresh() mit 5 Gegnern und 8 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
scr.mulligan.visible = false
h.mulligan_done = true
scr.ai_delay = 0.2
measuring = true
var start := Time.get_ticks_msec()

View File

@@ -1,35 +0,0 @@
extends SceneTree
## Misst Deckarchetypen im Duell (beide gierig): wer beginnt, Mono gegen Paar.
func deck(r: RandomNumberGenerator, mono: bool) -> Array:
var pool: Array = Cards.ELEMENTS.duplicate()
Game.shuffle(pool, r)
return Game.build_opponent_deck(r, [pool[0]] if mono else [pool[0], pool[1]])
func duel(r: RandomNumberGenerator, first_mono: bool, second_mono: bool) -> bool:
var second := Game.Player.new("Gegner 1", deck(r, second_mono), false, Game.START_LIFE, "x")
var g := Game.new_battle_with(deck(r, first_mono), [second], r)
var guard := 0
while g.winner == "" and guard < 300:
AI.take_turn(g, g.current(), true)
guard += 1
return g.winner == "human"
func run(label: String, a_mono: bool, b_mono: bool, games: int, r: RandomNumberGenerator) -> void:
var wins := 0
for i in games:
if duel(r, a_mono, b_mono):
wins += 1
print("%-28s Startspieler gewinnt %3d%%" % [label, wins * 100 / games])
func _initialize() -> void:
var r := RandomNumberGenerator.new()
r.seed = 5
run("Paar gegen Paar", false, false, 200, r)
run("Mono gegen Mono", true, true, 200, r)
run("Paar (startet) gegen Mono", false, true, 200, r)
run("Mono (startet) gegen Paar", true, false, 200, r)
quit()

View File

@@ -1 +0,0 @@
uid://bu5hjcfuinhy8

View File

@@ -25,7 +25,7 @@ func _ready() -> void:
net.name = "Net"
get_tree().root.add_child.call_deferred(net)
net.my_name = bot_name
net.my_deck = Profile.starter().deck.duplicate()
net.my_pick = {"elements": ["Feuer", "Erde"], "looks": {}}
net.started.connect(func(s):
seat = s
if hosting:
@@ -73,16 +73,18 @@ func _act_host() -> void:
_decide(game.players[seat])
## Einfache Züge mit kurzer Bedenkzeit: Mulligan überspringen, erstes spielbares Wesen ohne Ziel,
## dann angreifen, sonst Zug beenden.
## Einfache Züge mit kurzer Bedenkzeit: erst das billigste bezahlbare Angebot kaufen, dann das
## erste spielbare 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
for i in me.shop.size():
var offer = me.shop[i]
if offer != null and me.energy >= offer.cost and me.hand.size() < Game.MAX_HAND:
net.send_action({"type": "buy", "slot": i})
return
for i in me.hand.size():
var card: Cards.Card = me.hand[i]
if game.can_play(me, card)[0] and card.target_type() == "":

View File

@@ -14,18 +14,16 @@ func _initialize() -> void:
print("Kartenpool laden: %.1f ms" % t)
var r := RandomNumberGenerator.new()
r.seed = 3
var starter := Profile.starter().deck_cards()
print("Gegnerdeck (Zielstärke): %.1f ms" % ms(func(): DeckGen.build_opponent_deck_rated(r, ["Feuer", "Erde"], 1.5), 10))
print("Ladenangebot: %.2f ms" % ms(func(): DeckGen.shop_cards(r, 5), 50))
print("Vorschau 1..5 Gegner: %.1f ms" % ms(func():
for n in range(1, 6):
for i in n:
Game.random_opponent(r, i + 1, Game.START_LIFE, 1.3, "")))
DeckGen.random_opponent(i + 1)))
for n in [1, 3, 5]:
var g := Game.new_battle(starter, n, r, {"target_rating": 1.5})
var g := Game.new_battle(n, r)
# ein volles Brett für alle, damit die Entscheidung teuer wird
var uid := 5000
for p in g.players:
p.mulligan_done = true
for k in 4:
var c := Game.Creature.new(Cards.by_id("f_wolf"), p, uid)
uid += 1
@@ -36,15 +34,14 @@ func _initialize() -> void:
h.energy = 10
h.max_energy = 10
for k in 6:
h.hand.append(Cards.by_id("f_funke"))
h.hand.append(Cards.by_id("f_ritter"))
AI.time_budget_ms = 100000
var t_full := ms(func(): AI.choose_action(g, h))
AI.time_budget_ms = 150
var t_budget := ms(func(): AI.choose_action(g, h))
var cands := AI.candidate_actions(g, h).size()
print("KI-Entscheidung, %d Gegner, %d Kandidaten: ohne Budget %.0f ms, mit Budget %.0f ms" % [n, cands, t_full, t_budget])
print("Spielkopie: %.3f ms" % ms(func(): Game.new_battle(starter, 5, r).clone(), 20))
print("Spielkopie: %.3f ms" % ms(func(): Game.new_battle(5, r).clone(), 20))
print("Musik erzeugen (läuft im Hintergrund-Thread): %.0f ms" % ms(func(): Sfx.new()._music()))
print("Deckstärke berechnen: %.3f ms" % ms(func(): Game.deck_rating(starter), 100))
print("Snapshot 6 Spieler: %.2f ms" % ms(func(): Game.new_battle(starter, 5, r).snapshot(0), 10))
print("Snapshot 6 Spieler: %.2f ms" % ms(func(): Game.new_battle(5, r).snapshot(0), 10))
quit()

View File

@@ -8,31 +8,26 @@ func _initialize() -> void:
var games := 100
var r := RandomNumberGenerator.new()
r.seed = 0
var starter := Profile.starter().deck_cards()
var random_deck := func(rr: RandomNumberGenerator) -> Array:
var pool: Array = Cards.ELEMENTS.duplicate()
Game.shuffle(pool, rr)
return Game.build_opponent_deck(rr, [pool[0], pool[1]])
var t0 := Time.get_ticks_msec()
print("Alle gierig (Erwartung etwa 1/(Gegner+1)):")
for n in range(1, Game.MAX_OPPONENTS + 1):
_run("Zufallsdeck", random_deck, n, games, r, true, true)
_run(n, games, r, true, true)
print("Du mit Vorausschau, Gegner gierig:")
for n in [1, 2, 3]:
_run("Zufallsdeck", random_deck, n, games / 2, r, false, true)
_run(n, games / 2, r, false, true)
print("Alle mit Vorausschau:")
_run("Starterdeck", func(_rr): return starter, 1, games / 2, r, false, false)
_run("Zufallsdeck", random_deck, 2, games / 4, r, false, false)
_run(1, games / 2, r, false, false)
_run(2, games / 4, r, false, false)
print("Dauer %.1f s" % ((Time.get_ticks_msec() - t0) / 1000.0))
quit()
func _run(label: String, deck_fn: Callable, n_opp: int, games: int, r: RandomNumberGenerator,
func _run(n_opp: int, games: int, r: RandomNumberGenerator,
human_greedy: bool, opp_greedy: bool) -> void:
var wins := 0
var turns := 0
for i in games:
var g := Game.new_battle(deck_fn.call(r), n_opp, r)
var g := Game.new_battle(n_opp, r)
while g.winner == "":
var p := g.current()
AI.take_turn(g, p, human_greedy if p.is_human else opp_greedy)
@@ -40,4 +35,4 @@ func _run(label: String, deck_fn: Callable, n_opp: int, games: int, r: RandomNum
if g.winner == "human":
wins += 1
turns += g.turn
print("%-26s vs %d: Siege %3d%% Ø Züge %.1f" % [label, n_opp, wins * 100 / games, float(turns) / games])
print("gegen %d: Siege %3d%% Ø Züge %.1f" % [n_opp, wins * 100 / games, float(turns) / games])

View File

@@ -1,41 +0,0 @@
extends SceneTree
## Archetyp-Stärke: jede Elementkombination gegen zufällige andere, gleiche Zielstärke, beide KI.
func archetypes() -> Array:
var out := []
for e in Cards.ELEMENTS:
out.append([e])
for i in Cards.ELEMENTS.size():
for j in range(i + 1, Cards.ELEMENTS.size()):
out.append([Cards.ELEMENTS[i], Cards.ELEMENTS[j]])
return out
func _initialize() -> void:
var r := RandomNumberGenerator.new()
r.seed = 21
var arts := archetypes()
var n := int(OS.get_environment("N")) if OS.get_environment("N") != "" else 60
var results := {}
for a in arts:
var wins := 0
for i in n:
var b: Array = arts[r.randi_range(0, arts.size() - 1)]
var deck_a := Game.build_opponent_deck_rated(r, a, 1.6)
var deck_b := Game.build_opponent_deck_rated(r, b, 1.6)
var opp := Game.Player.new("Gegner 1", deck_b, false, Game.START_LIFE, "x")
var g := Game.new_battle_with(deck_a, [opp], r)
var guard := 0
while g.winner == "" and guard < 300:
AI.take_turn(g, g.current())
guard += 1
if g.winner == "human":
wins += 1
results["+".join(a)] = wins * 100 / n
print("%-14s %3d%%" % ["+".join(a), wins * 100 / n])
var f := FileAccess.open(DeckGen.ARCHETYPE_PATH, FileAccess.WRITE)
f.store_string(JSON.stringify(results, "\t"))
f.close()
print("nach %s geschrieben" % DeckGen.ARCHETYPE_PATH)
quit()

View File

@@ -1 +0,0 @@
uid://d4m21t7od3kdm

File diff suppressed because it is too large Load Diff

View File

@@ -83,11 +83,6 @@ func pad(button: int) -> void:
## 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()
@@ -123,15 +118,6 @@ func find_button(text: String, root: Node = null) -> Button:
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:
@@ -147,7 +133,6 @@ func wait_for_human(scr) -> void:
await frames(1)
wait += 1
await frames(1)
await clear_mulligan(scr)
func _ready() -> void:
@@ -164,9 +149,9 @@ func _ready() -> void:
add_child(main)
await frames(3)
var fast := OS.get_environment("DINGER_FAST") != ""
var suite := [test_menu, test_gamepad_menue, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung,
test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster]
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_feld_und_verschmelzen, test_kampf_grenzen,
test_kampf_fortsetzen]
if not fast:
suite += [test_kampf.bind(2, 3), test_kampf_abbrechen]
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
@@ -209,108 +194,16 @@ func test_booster() -> void:
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")
for res in scr.cards:
if res.upgraded:
check(main.profile.level(res.card.id) == Cards.RARITIES.find(res.rarity), "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 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:
@@ -326,17 +219,26 @@ func target_view(scr, valid: Array) -> Control:
func human_turn(scr) -> void:
var g: Game = scr.game
await clear_mulligan(scr)
# erst kaufen, solange Mana und Handplatz reichen
for i in 12:
var offers: Array = scr.shop_views.filter(func(v): return g.human.energy >= v.card.cost and g.human.hand.size() < Game.MAX_HAND)
if offers.is_empty():
break
var offer: Cards.Card = offers[0].card
var mana: int = g.human.energy
await click(offers[0])
check(g.human.energy == mana - offer.cost, "gekauft: " + offer.name)
# dann ausspielen, das kostet nichts
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
var board_before: int = g.human.board.size()
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)
check(g.human.board.size() > board_before, "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 != "":
@@ -377,9 +279,8 @@ 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 and "Gegnerdecks" in sel.rating_label.text
and not ("Archetypen" in sel.rating_label.text or "KI" in sel.rating_label.text), "unter der Schwierigkeit nur die zwei Stärken")
check(find_button("Neu würfeln") == null and find_button("1 Gegner") == null and find_button("Starten") != null, "kein Würfeln und keine Gegnerknöpfe, nur Starten")
check(find_button("Feuer") == null and find_button("Luft") == null, "keine Elementwahl mehr")
check(sel.count_slider.min_value == 1 and sel.count_slider.max_value == Game.MAX_OPPONENTS and sel.count_slider.step == 1, "Gegnerzahl als Schieberegler von 1 bis %d" % Game.MAX_OPPONENTS)
check(sel.opponents() == 1 and sel.count_label.text == "1 Gegner" and "Duell" in sel.info_label.text, "Regler beginnt beim Duell")
var rect: Rect2 = sel.count_slider.get_global_rect()
@@ -400,17 +301,17 @@ func test_gegnerauswahl() -> void:
await click(find_button("Starten"))
var hard = screen()
check(hard is BattleScreen and hard.game.opponents.size() == 1, "Kampf mit einem Gegner gestartet")
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])
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.shop.size() == Game.SHOP_SIZE and hard.game.human.hand.is_empty(),
"Start mit vollem Laden und leerer Hand")
await give_up()
await click(find_button("Einzelspieler"))
await click(find_button("Leicht"))
await click(find_button("Starten"))
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")
check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life)
await give_up()
await click(find_button("Einzelspieler"))
await click(find_button("Normal"))
@@ -438,7 +339,6 @@ func leave_battle() -> void:
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"))
@@ -448,71 +348,6 @@ 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)
@@ -569,28 +404,20 @@ func test_sammlung() -> void:
var scr = screen()
check(scr is CollectionScreen, "Sammlung 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")
check(scr.grid.views().size() == Cards.all().size(), "alle Karten gehören dir")
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, "Sammlung gespeichert")
check(scr.selected.id == "ff_phoenix" and p.rarity("ff_phoenix") == "gewöhnlich", "Karte gewählt, noch gewöhnlich")
await click(scr.b_upgrade)
check(p.level("ff_phoenix") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["selten"], "auf selten aufgewertet")
check(Profile.load_or_new(SAVE).level("ff_phoenix") == 1, "Sammlung gespeichert")
var dust: int = p.dust
await click(scr.b_dismantle)
check(p.level("ff_phoenix") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["selten"], "wieder zerlegt")
await shot("20_sammlung")
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
@@ -634,7 +461,7 @@ func test_profil_zuruecksetzen() -> void:
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 main.profile.collection.size() > 0, "Profil zurückgesetzt")
check(screen() is MenuScreen and main.profile.coins == Profile.START_COINS and main.profile.collection.is_empty(), "Profil zurückgesetzt")
check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert")
@@ -646,7 +473,7 @@ func test_sprache() -> void:
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(TranslationServer.translate("Zug %d · Du bist dran") == "Turn %d · Your turn" 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"))
@@ -670,41 +497,39 @@ func test_kampf_steuerung() -> void:
scr.ai_delay = 0.0
await wait_for_human(scr)
var g: Game = scr.game
var 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]:
idx = i
break
check(idx >= 0, "spielbares Wesen auf der Hand")
if idx >= 0:
var played_id: String = g.human.hand[idx].id
var board_before: int = g.human.board.size()
await key(KEY_1 + idx)
check(g.human.board.size() == board_before + 1, "Wesen per Taste gespielt")
# 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.size() == board_before and g.human.hand.any(func(c): return c.id == played_id) and g.human.energy > energy, "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() == board_before + 1, "erneut gespielt")
g.human.energy = 20 # das Angebot ist zufällig, hier soll nur die Steuerung geprüft werden
scr.refresh()
check(scr.shop_views.size() == Game.SHOP_SIZE and g.human.hand.is_empty(), "voller Laden, leere Hand")
# Kaufen: Mana weg, Karte auf der Hand
var offer: Cards.Card = scr.shop_views[0].card
var mana: int = g.human.energy
await click(scr.shop_views[0])
check(g.human.hand.size() == 1 and g.human.hand[0] == offer and g.human.energy == mana - offer.cost, "Angebot gekauft")
# Ausspielen kostet nichts
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, "Wesen per Taste gespielt, ohne Mana")
# Rückgängig: Wesen zurück auf die Hand
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 == offer.id), "Rückgängig stellt die Hand 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")
# Neu würfeln kostet Mana
var shop_before: Array = g.human.shop.duplicate()
mana = g.human.energy
await click(scr.b_reroll)
check(g.human.energy == mana - Game.REROLL_COST and g.human.shop != shop_before, "Laden neu gewürfelt")
await shot("22_kampf_steuerung")
# Gamepad: Zeiger bewegen, Karte spielen
# Gamepad: Zeiger im Laden bewegen, A kauft
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_before2: int = g.human.board.size()
await pad(JOY_BUTTON_A)
check(g.human.board.size() == board_before2 + 1, "Gamepad A spielt das Wesen")
check(scr.gp_zone == "shop" and scr.gp_index == 1 and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf zweitem Angebot")
var hand_before: int = g.human.hand.size()
await pad(JOY_BUTTON_A)
check(g.human.hand.size() == hand_before + 1, "Gamepad A kauft das Angebot")
scr.ai_delay = 0.75
await key(KEY_SPACE)
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
@@ -717,7 +542,6 @@ func test_kampf_steuerung() -> void:
w += 1
check(g.current() == g.human and w < 60, "Gegnerzug übersprungen (%d Bilder)" % w)
scr.ai_delay = 0.0
await clear_mulligan(scr)
await key(KEY_ESCAPE)
check(scr.confirm.visible, "Esc öffnet Aufgeben")
await key(KEY_ESCAPE)
@@ -725,6 +549,92 @@ func test_kampf_steuerung() -> void:
await give_up()
## Grenzen im Kampf: volle Hand, volles Feld, kein Mana.
func test_kampf_grenzen() -> void:
await start_battle(1)
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 kaufen
var guard := 0
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
guard += 1
if scr.shop_views.is_empty():
await click(scr.b_reroll)
continue
await click(scr.shop_views[0])
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
check(scr.shop_views.all(func(v): return v.dim), "bei voller Hand ist nichts mehr kaufbar")
if not scr.shop_views.is_empty():
await click(scr.shop_views[0])
check(scr.msg_label.text == tr("Hand 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
await key(KEY_1)
if scr.pending != null: # Karte will ein Ziel
var valid: Array = g.valid_targets(g.human, scr.pending.target_type())
if valid.is_empty():
await right_click()
else:
await click(target_view(scr, valid))
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
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 macht wieder Platz
var before: int = g.human.board.size()
await click(scr.b_fuse)
await click(scr.own_views[0])
await click(scr.own_views[1])
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
# Ohne Mana kein Neuwürfeln
g.human.energy = 0
scr.refresh()
await frames(1)
check(scr.b_reroll.disabled, "ohne Mana kein Neuwürfeln")
await give_up()
## Feldanzeige und Verschmelzen im Kampf.
func test_kampf_feld_und_verschmelzen() -> void:
await start_battle(1)
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.board.clear()
for id in ["f_wolf", "w_reiter"]:
var cr := Game.Creature.new(Cards.by_id(id), g.human, g.next_uid)
g.next_uid += 1
g.human.board.append(cr)
g.human.energy = 10
for i in Cards.FIELD_CHARGE:
g.charge_field(Cards.by_id("f_kobold"))
scr.refresh()
await frames(2)
check(scr.field_label.text.begins_with("Vulkan"), "Feldanzeige nennt das Feld: " + scr.field_label.text)
check(g.human.board[0].field_bonus == 1 and g.human.board[0].power() == 4, "das Feuerwesen bekommt +1 Angriff")
check(scr.b_fuse.visible and scr.b_fuse.text == "Verschmelzen", "Verschmelzen wird angeboten")
await shot("23_feld")
await click(scr.b_fuse)
check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt")
await click(scr.own_views[0])
check(scr.fuse_first != null, "erstes Wesen gewählt")
await shot("24_verschmelzen")
await click(scr.own_views[1])
check(g.human.board.size() == 1 and g.human.board[0].name == "Dampf", "die beiden Wesen sind zu Dampf verschmolzen")
check(not scr.fusing and g.human.energy == 8, "Modus beendet und Mana bezahlt")
check(scr.own_views.size() == 1, "nur noch eine eigene Kachel")
await give_up()
func test_netzwerk() -> void:
await click(find_button("Netzwerkspiel"))
var scr = screen()
@@ -756,7 +666,6 @@ func test_netzwerk() -> void:
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
@@ -767,7 +676,7 @@ func test_netzwerk() -> void:
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")
check(b.game.log.any(func(l): return l.begins_with("Bot kauft") or l.begins_with("Bot spielt")), "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"
@@ -781,7 +690,6 @@ func test_netzwerk() -> void:
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
@@ -799,7 +707,6 @@ func test_netzwerk() -> void:
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
@@ -873,7 +780,6 @@ func test_host_wechsel() -> void:
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
@@ -906,7 +812,7 @@ func test_zuschauer() -> void:
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,
check(b.game != null and b.game.turn >= 2 and b.hand_views.is_empty() 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.b_end.visible and not b.b_undo.visible, "Zuschauer hat keine Zugknöpfe")
@@ -964,7 +870,7 @@ func test_kampf(n_opp: int, shot_turn: int) -> void:
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(g.current() == g.human, "Spieler ist dran")
check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar")
await shot("07_kampf_start")
var coins: int = main.profile.coins
@@ -998,35 +904,30 @@ func test_kampf_abbrechen() -> void:
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"))
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
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"))
g.human.looks["w_reiter"] = Cards.RARITIES.find("episch")
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]
check(reiter_view.rarity == "episch", "Handkarte trägt die eigene Stufe")
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])
var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView)
check(previews.size() == 1 and previews[0].rarity == "episch", "Vorschau ebenfalls")
await shot("21_kampf_vorschau")
var targeted: Array = scr.hand_views.filter(func(v): return v.card.target_type() != "" and g.can_play(g.human, v.card)[0])
if not targeted.is_empty():
var card: Cards.Card = targeted[0].card
await click(targeted[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(),
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 click(find_button("Lupe"))