Dinger: Kartenspiel in Godot 4.7
Elemente, Kombos, Fallen, KI mit Vorausschau, Kampagne, Hotseat und Netzwerk, Werkstatt, Erfolge, Tagesaufgaben, Tests und Exporte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
11
tests/dump_cards.gd
Normal file
11
tests/dump_cards.gd
Normal file
@@ -0,0 +1,11 @@
|
||||
extends SceneTree
|
||||
## Schreibt den Kartenpool als JSON: godot --headless --path . -s tests/dump_cards.gd
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var list := Cards.all().map(func(c): return c.to_dict())
|
||||
var f := FileAccess.open("res://data/karten.json", FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify({"karten": list}, "\t", false))
|
||||
f.close()
|
||||
print("%d Karten nach data/karten.json geschrieben" % list.size())
|
||||
quit()
|
||||
1
tests/dump_cards.gd.uid
Normal file
1
tests/dump_cards.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bdywlyx7noq6
|
||||
35
tests/matchup.gd
Normal file
35
tests/matchup.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
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()
|
||||
1
tests/matchup.gd.uid
Normal file
1
tests/matchup.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bu5hjcfuinhy8
|
||||
65
tests/net_bot.gd
Normal file
65
tests/net_bot.gd
Normal file
@@ -0,0 +1,65 @@
|
||||
extends Node
|
||||
## Test-Mitspieler: tritt einem Host bei und spielt einfache Züge.
|
||||
## godot --path . tests/net_bot.tscn -- --ip=127.0.0.1 --name=Bot
|
||||
|
||||
var net: Net
|
||||
var seat := -1
|
||||
var game: Game
|
||||
var t := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var ip := "127.0.0.1"
|
||||
var bot_name := "Bot"
|
||||
for a in OS.get_cmdline_user_args():
|
||||
if a.begins_with("--ip="):
|
||||
ip = a.substr(5)
|
||||
if a.begins_with("--name="):
|
||||
bot_name = a.substr(7)
|
||||
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.snapshot_received.connect(_on_snapshot)
|
||||
net.ended.connect(func(_r): get_tree().quit())
|
||||
await get_tree().process_frame
|
||||
net.join(ip)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
t += delta
|
||||
if t > 180.0 or FileAccess.file_exists("user://bot_quit"):
|
||||
net.leave()
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func _on_snapshot(snap: Dictionary) -> void:
|
||||
game = Game.from_snapshot(snap)
|
||||
if game.winner != "":
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
get_tree().quit()
|
||||
return
|
||||
if game.current_idx != seat:
|
||||
return
|
||||
var me: Game.Player = game.players[seat]
|
||||
if not me.mulligan_done and me.turns == 1:
|
||||
net.send_action({"type": "mulligan", "hand": []})
|
||||
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() == "":
|
||||
net.send_action({"type": "play", "hand": i, "target": null})
|
||||
return
|
||||
for cr in me.board:
|
||||
if cr.can_attack():
|
||||
for e in game.enemies_of(me):
|
||||
if game.can_attack(cr, e)[0]:
|
||||
net.send_action({"type": "attack", "uid": cr.uid, "target": {"kind": "player", "index": game.players.find(e)}})
|
||||
return
|
||||
for c in e.board:
|
||||
if game.can_attack(cr, c)[0]:
|
||||
net.send_action({"type": "attack", "uid": cr.uid, "target": {"kind": "creature", "uid": c.uid}})
|
||||
return
|
||||
net.send_action({"type": "end"})
|
||||
1
tests/net_bot.gd.uid
Normal file
1
tests/net_bot.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dcbqt8ybdkhrb
|
||||
6
tests/net_bot.tscn
Normal file
6
tests/net_bot.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/net_bot.gd" id="1"]
|
||||
|
||||
[node name="NetBot" type="Node"]
|
||||
script = ExtResource("1")
|
||||
8
tests/run_headless.sh
Executable file
8
tests/run_headless.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Führt Import und alle Headless-Prüfungen aus. Aufruf: tests/run_headless.sh [godot-binary]
|
||||
set -euo pipefail
|
||||
GODOT="${1:-godot}"
|
||||
cd "$(dirname "$0")/.."
|
||||
"$GODOT" --headless --path . --import > /dev/null 2>&1 || true
|
||||
"$GODOT" --headless --path . -s tests/test_runner.gd 2>&1 | tee /tmp/dinger_tests.log | grep -E "FEHLER|Prüfungen|SCRIPT ERROR" || true
|
||||
grep -q " 0 fehlgeschlagen" /tmp/dinger_tests.log
|
||||
43
tests/simulate.gd
Normal file
43
tests/simulate.gd
Normal file
@@ -0,0 +1,43 @@
|
||||
extends SceneTree
|
||||
## Balance-Simulation, die KI spielt auch den Menschen. Bei gleichen Regeln
|
||||
## ist etwa 1/(Gegner+1) Siege zu erwarten:
|
||||
## godot --headless --path . -s tests/simulate.gd
|
||||
|
||||
|
||||
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)
|
||||
print("Du mit Vorausschau, Gegner gierig:")
|
||||
for n in [1, 2, 3]:
|
||||
_run("Zufallsdeck", random_deck, 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)
|
||||
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,
|
||||
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)
|
||||
while g.winner == "":
|
||||
var p := g.current()
|
||||
AI.take_turn(g, p, human_greedy if p.is_human else opp_greedy)
|
||||
assert(g.turn < 300, "Spiel endet nicht")
|
||||
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])
|
||||
1
tests/simulate.gd.uid
Normal file
1
tests/simulate.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bnbiq4krauuyq
|
||||
1198
tests/test_runner.gd
Normal file
1198
tests/test_runner.gd
Normal file
File diff suppressed because it is too large
Load Diff
1
tests/test_runner.gd.uid
Normal file
1
tests/test_runner.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cgjm40u037dnl
|
||||
674
tests/ui_test.gd
Normal file
674
tests/ui_test.gd
Normal file
@@ -0,0 +1,674 @@
|
||||
extends Node
|
||||
## Oberflächentest mit echtem Fenster und Screenshots:
|
||||
## DINGER_SHOTS=/pfad/zum/ordner godot --path . tests/ui_test.tscn
|
||||
|
||||
const SAVE := "user://test_ui_spielstand.json"
|
||||
|
||||
var main: Control
|
||||
var shots := ""
|
||||
var passed := 0
|
||||
var failed := 0
|
||||
|
||||
|
||||
func check(cond: bool, msg: String) -> void:
|
||||
if cond:
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
printerr("FEHLER: " + msg)
|
||||
|
||||
|
||||
func frames(n: int = 2) -> void:
|
||||
for i in n:
|
||||
await get_tree().process_frame
|
||||
|
||||
|
||||
func shot(name: String) -> void:
|
||||
if shots == "":
|
||||
return
|
||||
await RenderingServer.frame_post_draw
|
||||
get_viewport().get_texture().get_image().save_png(shots.path_join(name + ".png"))
|
||||
|
||||
|
||||
func click_at(pos: Vector2) -> void:
|
||||
var down := InputEventMouseButton.new()
|
||||
down.position = pos
|
||||
down.global_position = pos
|
||||
down.button_index = MOUSE_BUTTON_LEFT
|
||||
down.pressed = true
|
||||
get_viewport().push_input(down)
|
||||
var up: InputEventMouseButton = down.duplicate()
|
||||
up.pressed = false
|
||||
get_viewport().push_input(up)
|
||||
await frames(2)
|
||||
|
||||
|
||||
func click(node: Control) -> void:
|
||||
check(node != null, "Klickziel vorhanden")
|
||||
if node != null:
|
||||
await click_at(node.get_global_rect().get_center())
|
||||
|
||||
|
||||
func right_click() -> void:
|
||||
var down := InputEventMouseButton.new()
|
||||
down.position = Vector2(5, 5)
|
||||
down.global_position = down.position
|
||||
down.button_index = MOUSE_BUTTON_RIGHT
|
||||
down.pressed = true
|
||||
get_viewport().push_input(down)
|
||||
var up: InputEventMouseButton = down.duplicate()
|
||||
up.pressed = false
|
||||
get_viewport().push_input(up)
|
||||
await frames(2)
|
||||
|
||||
|
||||
func key(code: int) -> void:
|
||||
var down := InputEventKey.new()
|
||||
down.keycode = code
|
||||
down.pressed = true
|
||||
get_viewport().push_input(down)
|
||||
var up := InputEventKey.new()
|
||||
up.keycode = code
|
||||
up.pressed = false
|
||||
get_viewport().push_input(up)
|
||||
await frames(2)
|
||||
|
||||
|
||||
## Mulligan-Maske wegklicken (Hand behalten), falls sie offen ist.
|
||||
func clear_mulligan(scr) -> void:
|
||||
if scr.mulligan_box.visible:
|
||||
await click(scr.b_keep)
|
||||
|
||||
|
||||
func hover(node: Control) -> void:
|
||||
var ev := InputEventMouseMotion.new()
|
||||
ev.position = node.get_global_rect().get_center()
|
||||
ev.global_position = ev.position
|
||||
get_viewport().push_input(ev)
|
||||
await frames(2)
|
||||
|
||||
|
||||
func screen():
|
||||
return main.screen
|
||||
|
||||
|
||||
## Jeder Abschnitt startet im Menü, auch wenn ein vorheriger abgebrochen ist.
|
||||
func ensure_menu() -> void:
|
||||
if main.net.connected:
|
||||
main.net.leave()
|
||||
if TranslationServer.get_locale().begins_with("en"):
|
||||
main.set_language("de")
|
||||
if not (screen() is MenuScreen):
|
||||
main.show_screen(MenuScreen.new())
|
||||
await frames(2)
|
||||
|
||||
|
||||
func find_button(text: String, root: Node = null) -> Button:
|
||||
if root == null:
|
||||
root = screen()
|
||||
for c in root.get_children():
|
||||
if c is Button and (c.text == text or c.tr(c.text) == text) and c.is_visible_in_tree():
|
||||
return c
|
||||
var found := find_button(text, c)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func card_views(root: Node) -> Array:
|
||||
var out := []
|
||||
for c in root.get_children():
|
||||
if c is CardView:
|
||||
out.append(c)
|
||||
out.append_array(card_views(c))
|
||||
return out
|
||||
|
||||
|
||||
## Wartet, bis keine Kachel mehr animiert wird (Angriffe verschieben Kacheln kurz).
|
||||
func settle(scr) -> void:
|
||||
for i in 40:
|
||||
if not scr.tiles.values().any(func(t): return t.busy):
|
||||
return
|
||||
await frames(1)
|
||||
|
||||
|
||||
func wait_for_human(scr) -> void:
|
||||
var g: Game = scr.game
|
||||
var wait := 0
|
||||
while g.current() != g.human and g.winner == "" and wait < 600:
|
||||
await frames(1)
|
||||
wait += 1
|
||||
await frames(1)
|
||||
await clear_mulligan(scr)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Game.test_seed = 4242 # wiederholbare Partien und Booster
|
||||
shots = OS.get_environment("DINGER_SHOTS")
|
||||
var abs := ProjectSettings.globalize_path(SAVE)
|
||||
for suffix in ["", ".bak", ".corrupt"]:
|
||||
if FileAccess.file_exists(SAVE + suffix):
|
||||
DirAccess.remove_absolute(abs + suffix)
|
||||
main = load("res://scenes/main.tscn").instantiate()
|
||||
main.save_path = SAVE
|
||||
add_child(main)
|
||||
await frames(3)
|
||||
for t in [test_menu_und_regeln, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
||||
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_hotseat,
|
||||
test_netzwerk, test_kampf.bind(2, 3), test_kampf_abbrechen]:
|
||||
await ensure_menu()
|
||||
await t.call()
|
||||
for suffix in ["", ".bak", ".corrupt"]:
|
||||
if FileAccess.file_exists(SAVE + suffix):
|
||||
DirAccess.remove_absolute(abs + suffix)
|
||||
print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed])
|
||||
get_tree().quit(1 if failed > 0 else 0)
|
||||
|
||||
|
||||
func test_menu_und_regeln() -> void:
|
||||
check(screen() is MenuScreen, "Menü sichtbar")
|
||||
await shot("01_menu")
|
||||
await click(find_button("Regeln"))
|
||||
check(screen() is RulesScreen, "Regeln geöffnet")
|
||||
await shot("02_regeln")
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_booster() -> void:
|
||||
await click(find_button("Booster öffnen"))
|
||||
var scr = screen()
|
||||
check(scr is BoosterScreen, "Boosterbildschirm")
|
||||
var coins: int = main.profile.coins
|
||||
var ach_before: int = main.profile.achievements.size()
|
||||
await click(scr.b_buy)
|
||||
var bonus := 0
|
||||
for a in Profile.ACHIEVEMENTS:
|
||||
if a.id in main.profile.achievements.slice(ach_before):
|
||||
bonus += a.reward
|
||||
check(main.profile.coins == coins - Profile.BOOSTER_COST + bonus, "Booster bezahlt (plus Erfolgsmünzen %d)" % bonus)
|
||||
check(scr.cards.size() == 5 and scr.revealed.is_empty(), "5 verdeckte Karten")
|
||||
await shot("03_booster_zu")
|
||||
var backs: Array = scr.slots.get_children().filter(func(c): return c is BoosterScreen.CardBack)
|
||||
await click(backs[0])
|
||||
check(scr.revealed.size() == 1, "eine Karte aufgedeckt")
|
||||
await click(scr.b_all)
|
||||
check(scr.revealed.size() == 5, "alle aufgedeckt")
|
||||
await shot("04_booster_offen")
|
||||
check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Kauf gespeichert")
|
||||
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")
|
||||
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(find_button("Zurück"))
|
||||
check(screen() is MenuScreen and main.profile.deck.size() == 20, "Änderungen verworfen")
|
||||
|
||||
|
||||
func target_view(scr, valid: Array) -> Control:
|
||||
for v in scr.enemy_views:
|
||||
if v.creature in valid:
|
||||
return v
|
||||
for v in scr.own_views:
|
||||
if v.creature in valid:
|
||||
return v
|
||||
for p in scr.hero_panels:
|
||||
if p in valid:
|
||||
return scr.hero_panels[p]
|
||||
return null
|
||||
|
||||
|
||||
func human_turn(scr) -> void:
|
||||
var g: Game = scr.game
|
||||
await clear_mulligan(scr)
|
||||
for i in 12:
|
||||
var playable: Array = scr.hand_views.filter(func(v): return g.can_play(g.human, v.card)[0])
|
||||
if playable.is_empty():
|
||||
break
|
||||
var card: Cards.Card = playable[0].card
|
||||
var energy: int = g.human.energy
|
||||
await click(playable[0])
|
||||
if scr.pending != null:
|
||||
await click(target_view(scr, g.valid_targets(g.human, card.target_type())))
|
||||
check(g.human.energy == energy - card.cost, "Karte gespielt: " + card.name)
|
||||
for i in 12:
|
||||
var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack())
|
||||
if ready.is_empty() or g.winner != "":
|
||||
break
|
||||
var cr: Game.Creature = ready[0].creature
|
||||
await settle(scr)
|
||||
await hover(ready[0])
|
||||
var under = get_viewport().gui_get_hovered_control()
|
||||
await click(ready[0])
|
||||
if scr.selected != cr:
|
||||
print("DEBUG Auswahl fehlgeschlagen: rect=", ready[0].get_global_rect(), " scale=", ready[0].scale,
|
||||
" busy=", ready[0].busy, " unter Maus=", under, " Meldung='", scr.msg_label.text,
|
||||
"' Zug Mensch=", g.current().is_human, " kann=", cr.can_attack(), " pending=", scr.pending, " selected=", scr.selected)
|
||||
check(scr.selected == cr, "Wesen gewählt")
|
||||
var targets := []
|
||||
for o in g.opponents:
|
||||
if o.alive() and g.can_attack(cr, o)[0]:
|
||||
targets.append(o)
|
||||
for c in o.board:
|
||||
if g.can_attack(cr, c)[0]:
|
||||
targets.append(c)
|
||||
await settle(scr)
|
||||
await click(target_view(scr, targets))
|
||||
check(scr.selected == null and cr.attacks_left < cr.attacks_per_turn(), "Angriff ausgeführt")
|
||||
if g.winner == "":
|
||||
await click(scr.b_end)
|
||||
|
||||
|
||||
func start_battle(n_opp: int) -> void:
|
||||
await click(find_button("Einzelspieler"))
|
||||
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
|
||||
await click(find_button("%d Gegner" % n_opp))
|
||||
|
||||
|
||||
func test_gegnerauswahl() -> void:
|
||||
await click(find_button("Einzelspieler"))
|
||||
check(screen() is OpponentSelectScreen, "Gegnerauswahl geöffnet")
|
||||
await shot("07_gegnerauswahl")
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
for n in [1, 5]:
|
||||
await start_battle(n)
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen and scr.game.opponents.size() == n, "Kampf gegen %d gestartet" % n)
|
||||
check(scr.game.players[0].max_energy == 1 and scr.game.opponents[0].life == Game.START_LIFE, "gleiche Regeln gegen %d" % n)
|
||||
await shot("11_kampf_%d" % n)
|
||||
await give_up()
|
||||
|
||||
|
||||
func give_up() -> void:
|
||||
var scr = screen()
|
||||
await clear_mulligan(scr)
|
||||
await click(find_button("Aufgeben"))
|
||||
check(scr.confirm.visible, "Aufgeben fragt nach")
|
||||
await click(find_button("Weiterspielen"))
|
||||
check(not scr.confirm.visible and screen() == scr, "Weiterspielen bleibt im Kampf")
|
||||
await click(find_button("Aufgeben"))
|
||||
await click(find_button("Ja, aufgeben"))
|
||||
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
|
||||
|
||||
|
||||
func test_einstellungen() -> void:
|
||||
await click(find_button("Einstellungen"))
|
||||
var scr = screen()
|
||||
check(scr is SettingsScreen, "Einstellungen geöffnet")
|
||||
scr.volume.value = 0.3
|
||||
scr.speed.value = 2.0
|
||||
scr.anim.button_pressed = false
|
||||
await frames(1)
|
||||
check(is_equal_approx(main.profile.settings.volume, 0.3) and is_equal_approx(main.profile.settings.ai_speed, 2.0), "Regler gespeichert")
|
||||
check(main.profile.settings.animations == false, "Animationen abschaltbar")
|
||||
check(is_equal_approx(Profile.load_or_new(SAVE).settings.volume, 0.3), "Einstellungen auf Platte")
|
||||
scr.anim.button_pressed = true
|
||||
await shot("13_einstellungen")
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_erfolge() -> void:
|
||||
await click(find_button("Erfolge"))
|
||||
check(screen() is AchievementsScreen, "Erfolge geöffnet")
|
||||
await shot("14_erfolge")
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_werkstatt() -> void:
|
||||
await click(find_button("Werkstatt"))
|
||||
var scr = screen()
|
||||
check(scr is WorkshopScreen, "Werkstatt geöffnet")
|
||||
var p: Profile = main.profile
|
||||
p.dust = 0
|
||||
var in_decks := {}
|
||||
for d in p.decks:
|
||||
for cid in d.cards:
|
||||
in_decks[cid] = true
|
||||
var free = scr.grid.views().filter(func(v): return p.collection.get(v.card.id, 0) > 0 and not in_decks.has(v.card.id))
|
||||
check(not free.is_empty(), "eine freie Karte zum Zerlegen")
|
||||
var card: Cards.Card = free[0].card
|
||||
await click(free[0])
|
||||
check(scr.selected == card, "Karte gewählt")
|
||||
var before: int = p.collection[card.id]
|
||||
await click(scr.b_dismantle)
|
||||
check(p.collection[card.id] == before - 1 and p.dust == Profile.DUST_DISMANTLE[card.rarity], "zerlegt, Staub gutgeschrieben")
|
||||
p.dust = 5000
|
||||
scr.search_box.text = "Phönix"
|
||||
scr.search_box.text_changed.emit("Phönix")
|
||||
await frames(1)
|
||||
var phoenix = scr.grid.views().filter(func(v): return v.card.id == "ff_phoenix")[0]
|
||||
await click(phoenix)
|
||||
await click(scr.b_craft)
|
||||
check(p.collection.get("ff_phoenix", 0) == 1 and p.dust == 5000 - Profile.DUST_CRAFT["legendär"], "Phönix hergestellt")
|
||||
check(Profile.load_or_new(SAVE).collection.get("ff_phoenix", 0) == 1, "Werkstatt gespeichert")
|
||||
await shot("20_werkstatt")
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_hotseat() -> void:
|
||||
await click(find_button("Lokal zu mehreren"))
|
||||
var scr = screen()
|
||||
check(scr is HotseatScreen, "Hotseat-Auswahl")
|
||||
await click(find_button("3 Spieler"))
|
||||
await click(find_button("1 KI"))
|
||||
check(scr.n_humans == 3 and scr.n_ai == 1, "3 Menschen, 1 KI")
|
||||
await shot("17_hotseat")
|
||||
await click(find_button("Start"))
|
||||
var b = screen()
|
||||
check(b is BattleScreen and b.is_hotseat() and b.game.players.size() == 4, "Hotseat-Kampf gestartet")
|
||||
b.ai_delay = 0.0
|
||||
var guard := 0
|
||||
while not b.handover.visible and b.game.winner == "" and guard < 400:
|
||||
if b.game.current() == b.perspective:
|
||||
await clear_mulligan(b)
|
||||
if b.game.current() == b.perspective:
|
||||
await click(b.b_end)
|
||||
await frames(1)
|
||||
guard += 1
|
||||
check(b.handover.visible, "Übergabe erscheint")
|
||||
var cur: Game.Player = b.game.current()
|
||||
check(cur.is_human and cur != b.perspective, "anderer Mensch ist dran")
|
||||
await shot("18_hotseat_uebergabe")
|
||||
await click(find_button("Weiter"))
|
||||
check(b.perspective == cur and not b.handover.visible, "Perspektive gewechselt")
|
||||
check(b.hand_views.size() == cur.hand.size() and b.name_label.text == cur.name, "Hand und Name des neuen Spielers")
|
||||
check(b.mulligan_box.visible, "neuer Spieler bekommt seine Mulligan-Maske")
|
||||
await clear_mulligan(b)
|
||||
check(b.hero_panels.size() == 3 and not b.hero_panels.has(cur), "drei andere am Tisch")
|
||||
await shot("19_hotseat_kampf")
|
||||
await give_up()
|
||||
|
||||
|
||||
func test_sprache() -> void:
|
||||
await click(find_button("Einstellungen"))
|
||||
var scr = screen()
|
||||
scr.language.select(1)
|
||||
scr.language.item_selected.emit(1)
|
||||
await frames(2)
|
||||
check(TranslationServer.get_locale().begins_with("en") and main.profile.settings.language == "en", "Sprache auf Englisch")
|
||||
check(TranslationServer.translate("Zurück") == "Back" and TranslationServer.translate("Einzelspieler") == "Single player", "Übersetzung geladen")
|
||||
check(find_button("Back") != null, "Knöpfe übersetzt")
|
||||
await shot("26_englisch")
|
||||
await click(find_button("Back"))
|
||||
check(find_button("Single player") != null and find_button("Settings") != null, "Menü auf Englisch")
|
||||
await click(find_button("Settings"))
|
||||
var scr2 = screen()
|
||||
scr2.language.select(0)
|
||||
scr2.language.item_selected.emit(0)
|
||||
await frames(2)
|
||||
check(find_button("Zurück") != null, "zurück auf Deutsch")
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_anleitung() -> void:
|
||||
check(not main.profile.tutorial_done, "Anleitung noch offen")
|
||||
await click(find_button("Anleitung"))
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen and scr.tutorial, "Anleitung gestartet")
|
||||
scr.ai_delay = 0.0
|
||||
var g: Game = scr.game
|
||||
check(g.players[0] == g.human and g.opponents[0].name == "Fenn" and g.opponents[0].perks.is_empty(), "Fenn ohne Merkmal, Mensch beginnt")
|
||||
await frames(2)
|
||||
check(scr.mulligan_box.visible, "Mulligan-Maske im ersten Zug")
|
||||
var views = scr.mulligan_layer.get_children()
|
||||
await click(views[0])
|
||||
check(scr.mulligan_selected == [0], "Karte zum Tausch gewählt")
|
||||
await shot("22_anleitung_mulligan")
|
||||
await click(scr.b_swap)
|
||||
check(not scr.mulligan_box.visible and g.human.mulligan_done, "getauscht")
|
||||
check(g.log.any(func(l): return "tauscht 1 Karten" in l), "Tausch im Verlauf")
|
||||
check(scr.tutorial_step == 0 and "Energie" in scr.tutorial_text.text, "Schritt 1 sichtbar")
|
||||
var idx := -1
|
||||
for i in g.human.hand.size():
|
||||
if g.human.hand[i].is_creature() and g.human.hand[i].cost == 1:
|
||||
idx = i
|
||||
break
|
||||
if idx >= 0:
|
||||
await key(KEY_1 + idx)
|
||||
check(g.human.board.size() == 1, "Wesen per Taste gespielt")
|
||||
check(scr.tutorial_step == 1, "Schritt 2 nach dem Ausspielen")
|
||||
await shot("22_anleitung")
|
||||
await key(KEY_SPACE)
|
||||
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
||||
await wait_for_human(scr)
|
||||
check(scr.tutorial_step >= 2 or idx < 0, "Schritt 3 im zweiten Zug")
|
||||
var step_before: int = scr.tutorial_step
|
||||
await click(scr.b_tut_next)
|
||||
check(scr.tutorial_step == step_before + 1, "Weiter-Knopf")
|
||||
await key(KEY_ESCAPE)
|
||||
check(scr.confirm.visible, "Esc öffnet Aufgeben")
|
||||
await key(KEY_ESCAPE)
|
||||
check(not scr.confirm.visible, "Esc schließt den Dialog")
|
||||
await give_up()
|
||||
check(main.profile.tutorial_done, "Anleitung als erledigt gespeichert")
|
||||
|
||||
|
||||
func test_netzwerk() -> void:
|
||||
await click(find_button("Netzwerkspiel"))
|
||||
var scr = screen()
|
||||
check(scr is LobbyScreen, "Lobby geöffnet")
|
||||
await click(find_button("Spiel eröffnen"))
|
||||
check(main.net.is_host and main.net.connected, "Spiel eröffnet")
|
||||
var args := ["--headless", "--path", ProjectSettings.globalize_path("res://"), "tests/net_bot.tscn", "--", "--ip=127.0.0.1", "--name=Bot"]
|
||||
var pid := OS.create_process(OS.get_executable_path(), args)
|
||||
check(pid > 0, "Bot-Prozess gestartet")
|
||||
var wait := 0
|
||||
while main.net.names.size() < 2 and wait < 900:
|
||||
await frames(1)
|
||||
wait += 1
|
||||
check(main.net.names.size() == 2, "Bot ist beigetreten (Pfad %s)" % main.net.get_path())
|
||||
await shot("23_lobby")
|
||||
await click(find_button("Start"))
|
||||
var b = screen()
|
||||
check(b is BattleScreen and b.is_online() and not b.is_client(), "Netzwerkkampf als Host")
|
||||
b.ai_delay = 0.0
|
||||
var rounds := 0
|
||||
while b.game != null and b.game.winner == "" and rounds < 4:
|
||||
if b.game.current() == b.perspective:
|
||||
await clear_mulligan(b)
|
||||
if b.game.current() == b.perspective:
|
||||
await click(b.b_end)
|
||||
var w2 := 0
|
||||
while b.game.current() != b.perspective and b.game.winner == "" and w2 < 900:
|
||||
await frames(1)
|
||||
w2 += 1
|
||||
rounds += 1
|
||||
check(b.game.turn >= 3 and b.game.winner == "", "mehrere Runden gespielt, Partie läuft (Zug %d)" % b.game.turn)
|
||||
check(b.game.log.any(func(l): return l.begins_with("Bot spielt") or l.begins_with("Bot behält") or l.begins_with("Bot tauscht")), "Bot hat gehandelt")
|
||||
await shot("24_netzwerk_kampf")
|
||||
OS.kill(pid)
|
||||
var w3 := 0
|
||||
while b.game.winner == "" and w3 < 1500:
|
||||
await frames(1)
|
||||
w3 += 1
|
||||
if b.game.winner != "":
|
||||
check(b.game.winner == "human" and b.game.log.any(func(l): return "verlassen" in l), "Trennung des Bots zählt als Aufgabe")
|
||||
await frames(2)
|
||||
check(b.overlay.visible, "Spielende angezeigt")
|
||||
await click(find_button("Zurück zum Menü"))
|
||||
else:
|
||||
print("HINWEIS: Bot-Trennung nach 25 s noch nicht erkannt, Aufgeben stattdessen")
|
||||
await give_up()
|
||||
check(screen() is MenuScreen and not main.net.connected, "Netzwerk getrennt, zurück im Menü")
|
||||
|
||||
|
||||
func test_kampagne() -> void:
|
||||
await click(find_button("Kampagne"))
|
||||
var scr = screen()
|
||||
check(scr is CampaignScreen, "Kampagne geöffnet")
|
||||
await shot("15_kampagne")
|
||||
check(find_button("Gesperrt") != null and find_button("Kämpfen") != null, "Stufen gesperrt und offen")
|
||||
await click(find_button("Kämpfen"))
|
||||
var battle = screen()
|
||||
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
|
||||
check(battle.game.opponents.size() == 1 and battle.game.opponents[0].name == "Fenn", "Fenn ist der Gegner")
|
||||
await shot("16_kampagne_kampf")
|
||||
await give_up()
|
||||
|
||||
|
||||
func test_kampf(n_opp: int, shot_turn: int) -> void:
|
||||
await start_battle(n_opp)
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen, "Kampfbildschirm")
|
||||
scr.ai_delay = 0.0
|
||||
var g: Game = scr.game
|
||||
check(g.opponents.size() == n_opp and g.log[0].begins_with("Zugreihenfolge"), "Spiel gestartet, Reihenfolge ausgelost")
|
||||
await wait_for_human(scr)
|
||||
check(g.current() == g.human and g.human.mulligan_done, "Spieler ist dran, Mulligan erledigt")
|
||||
check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar")
|
||||
await shot("07_kampf_start")
|
||||
var coins: int = main.profile.coins
|
||||
var guard := 0
|
||||
while g.winner == "" and guard < 60:
|
||||
guard += 1
|
||||
var turn_before := g.turn
|
||||
await human_turn(scr)
|
||||
if g.winner != "":
|
||||
break
|
||||
check(g.current() != g.human or g.turn > turn_before, "Zug beendet")
|
||||
var wait := 0
|
||||
while g.current() != g.human and g.winner == "" and wait < 600:
|
||||
await frames(1)
|
||||
wait += 1
|
||||
if guard == shot_turn:
|
||||
await shot("08_kampf_zug%d" % (shot_turn + 1))
|
||||
check(g.winner != "", "Spiel beendet")
|
||||
await frames(3)
|
||||
await shot("09_kampf_ende")
|
||||
check(scr.reward > 0 and main.profile.coins >= coins + scr.reward, "Belohnung gutgeschrieben")
|
||||
check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Belohnung gespeichert")
|
||||
await click(find_button("Zurück zum Menü"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_kampf_abbrechen() -> void:
|
||||
await start_battle(5)
|
||||
var scr = screen()
|
||||
scr.ai_delay = 0.0
|
||||
var g: Game = scr.game
|
||||
await wait_for_human(scr)
|
||||
check(g.opponents.size() == 5 and g.human.max_energy == 1, "5 Gegner")
|
||||
# Falle legen und Kombo-Hinweis prüfen
|
||||
g.human.hand.append(Cards.by_id("f_falle"))
|
||||
g.human.energy = 10
|
||||
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
|
||||
g.human.board.append(wolf)
|
||||
g.human.hand.append(Cards.by_id("w_reiter"))
|
||||
scr.refresh()
|
||||
var trap_view = scr.hand_views.filter(func(v): return v.card.id == "f_falle")[0]
|
||||
await click(trap_view)
|
||||
check(g.human.traps.size() == 1 and scr.trap_views.size() == 1, "Falle gelegt und sichtbar")
|
||||
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
|
||||
await hover(reiter_view)
|
||||
var hint_boxes = scr.preview_holder.get_children().filter(func(c): return c is RichTextLabel)
|
||||
check(hint_boxes.size() == 1 and "Feldkombo Dampf" in hint_boxes[0].text, "Kombo-Hinweis in der Vorschau")
|
||||
await shot("21_kampf_falle_hinweis")
|
||||
var spells: Array = scr.hand_views.filter(func(v): return v.card.target_type() != "" and g.can_play(g.human, v.card)[0])
|
||||
if not spells.is_empty():
|
||||
var card: Cards.Card = spells[0].card
|
||||
await click(spells[0])
|
||||
check(scr.pending == card, "Ziel wird erwartet")
|
||||
await right_click()
|
||||
check(scr.pending == null and card in g.human.hand, "Rechtsklick bricht ab")
|
||||
await hover(scr.hand_views[0])
|
||||
check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover")
|
||||
await click(find_button("Lupe"))
|
||||
check(scr.zoom_player == scr.others()[0], "Lupe öffnet die erste Spalte")
|
||||
await shot("12_lupe")
|
||||
await click(find_button("Schließen"))
|
||||
check(scr.zoom_player == null, "Lupe geschlossen")
|
||||
# Runden laufen lassen, damit die Gegner ihre Züge machen
|
||||
for round in 4:
|
||||
await click(scr.b_end)
|
||||
var wait := 0
|
||||
while g.current() != g.human and g.winner == "" and wait < 600:
|
||||
await frames(1)
|
||||
wait += 1
|
||||
var ai_attacked: bool = g.log.any(func(l): return l.begins_with("Gegner") and " spielt " in l) \
|
||||
and g.log.any(func(l): return " greift " in l)
|
||||
check(ai_attacked, "Gegner spielen Karten und greifen an")
|
||||
check(scr.tiles.size() == g.players.reduce(func(acc, p): return acc + p.board.size(), 0), "eine Kachel je Wesen")
|
||||
await shot("10_kampf_fuenf")
|
||||
1
tests/ui_test.gd.uid
Normal file
1
tests/ui_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ci5hglrkyevqi
|
||||
6
tests/ui_test.tscn
Normal file
6
tests/ui_test.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/ui_test.gd" id="1"]
|
||||
|
||||
[node name="UITest" type="Node"]
|
||||
script = ExtResource("1")
|
||||
Reference in New Issue
Block a user