Files
elements-autochess/tests/Probe3.gd
2026-08-29 11:42:54 +02:00

395 lines
13 KiB
GDScript

extends Node3D
## Verifikation über echte Maus- und Tastaturereignisse.
## Jeder Schritt geht durch die normale Eingabekette des Spiels.
var main: Node3D
var out_dir := "user://shots"
var shot_no := 0
var fails := 0
func _ready() -> void:
var args := OS.get_cmdline_user_args()
if args.size() > 0:
out_dir = args[0]
DirAccess.make_dir_recursive_absolute(out_dir)
main = load("res://scenes/Main.tscn").instantiate()
add_child(main)
get_tree().create_timer(120.0).timeout.connect(func():
print("ABBRUCH: Zeitlimit erreicht")
get_tree().quit(2))
_run()
func check(cond: bool, msg: String) -> void:
if cond:
print(" OK ", msg)
else:
fails += 1
print(" FAIL ", msg)
func _wait(s: float = 0.1) -> void:
await get_tree().create_timer(s).timeout
func _shot(n: String) -> void:
await RenderingServer.frame_post_draw
shot_no += 1
get_viewport().get_texture().get_image().save_png("%s/%02d_%s.png" % [out_dir, shot_no, n])
func _move(pos: Vector2) -> void:
var e := InputEventMouseMotion.new()
e.position = pos
e.global_position = pos
get_viewport().push_input(e)
func _btn(pos: Vector2, pressed: bool, button: int) -> void:
var e := InputEventMouseButton.new()
e.position = pos
e.global_position = pos
e.button_index = button
e.pressed = pressed
get_viewport().push_input(e)
func _click(pos: Vector2, button: int = MOUSE_BUTTON_LEFT) -> void:
_move(pos)
_btn(pos, true, button)
_btn(pos, false, button)
await _wait()
## Ziehen mit Zwischenschritten, damit die Bewegung wie echtes Drag ankommt.
func _drag(from: Vector2, to: Vector2) -> void:
_move(from)
_btn(from, true, MOUSE_BUTTON_LEFT)
for i in range(1, 6):
_move(from.lerp(to, float(i) / 5.0))
await get_tree().process_frame
_btn(to, false, MOUSE_BUTTON_LEFT)
await _wait()
func _bench(i: int) -> Vector2:
return main.board.slot_screen({"kind": "bench", "index": i})
func _hex(c: int, r: int) -> Vector2:
return main.board.slot_screen({"kind": "board", "hex": Vector2i(c, r)})
func _shop_button(i: int) -> Button:
return main.shop_row.get_child(i) as Button
func _center(c: Control) -> Vector2:
return c.global_position + c.size * 0.5
func _pick_element(e: int) -> void:
for b in main.picker_grid.get_children():
if b is Button and b.text.begins_with(Data.ELEMENT_NAMES[e]):
b.pressed.emit()
await _wait()
return
check(false, "Element %s im Popup gefunden" % Data.ELEMENT_NAMES[e])
func _free_shop_slot(p: PlayerState) -> int:
for i in p.shop.size():
if not p.shop[i].sold:
return i
return -1
func _buy(p: PlayerState, element: int) -> void:
var slot := _free_shop_slot(p)
if slot < 0:
await _click(_center(main.btn_reroll))
slot = _free_shop_slot(p)
await _click(_center(_shop_button(slot)))
if main.picker.visible:
await _pick_element(element)
func _run() -> void:
await _wait(0.6)
var p: PlayerState = main.game.human
print("\n[A] Spezialisierung wird beim Kauf gewählt")
var gold0 := p.gold
var pool0: int = main.game.pool.element_left[Data.Element.FEUER]
var slot := _free_shop_slot(p)
check(p.shop[slot].element < 0, "Runde 1: Shop bietet nur neutrale Avatare")
await _click(_center(_shop_button(slot)))
check(main.picker.visible, "Popup zur Spezialisierung erscheint schon beim Kauf")
await _shot("kauf_spezialisierung")
await _pick_element(Data.Element.FEUER)
check(p.bench[0] != null, "Avatar liegt auf der Bank")
check(p.bench[0].elements == [Data.Element.FEUER], "Avatar ist ein Feuer-Avatar")
check(p.gold == gold0 - Data.PRICE_NONE, "Gold sinkt um %d" % Data.PRICE_NONE)
check(main.game.pool.element_left[Data.Element.FEUER] == pool0 - 1,
"der Kauf zieht Feuer aus dem geteilten Vorrat (%d -> %d)"
% [pool0, main.game.pool.element_left[Data.Element.FEUER]])
await _shot("bank_3d")
print("\n[B] Drag & Drop Bank -> Board")
var a0: Avatar = p.bench[0]
await _drag(_bench(0), _hex(3, 1))
check(p.board.get(Vector2i(3, 1)) == a0, "Avatar steht auf dem Zielfeld")
check(p.bench[0] == null, "Bankplatz ist frei")
check(p.unlocked[Data.Element.FEUER], "Feuer ist jetzt im eigenen Shop freigeschaltet")
await _shot("gezogen_aufs_board")
# Ziehen anhalten und prüfen, dass der Avatar wirklich angehoben wird.
var ground: float = main.board.slot_world({"kind": "board", "hex": Vector2i(3, 1)}).y
_move(_hex(3, 1))
_btn(_hex(3, 1), true, MOUSE_BUTTON_LEFT)
for i in range(1, 9):
_move(_hex(3, 1).lerp(_bench(2), float(i) / 8.0))
await get_tree().process_frame
await _wait(0.05)
check(main.drag_avatar != null, "Avatar hängt am Zeiger")
check(main.board.drag_active, "Zielfelder werden hervorgehoben")
var node: Node3D = main.board.unit_node(a0.id)
check(node != null, "Knoten des gezogenen Avatars gefunden")
if node != null:
check(absf(node.position.y - ground) < 0.01,
"Avatar wird beim Ziehen nicht angehoben (Höhe %.3f)" % (node.position.y - ground))
check(node.position.distance_to(main.board.drag_world) < 0.01,
"Avatar folgt dem Zeiger ohne Nachlauf")
check(node.scale.x > 1.0, "Avatar wird beim Ziehen größer dargestellt")
check(main.board.selected_slot.is_empty()
or not BoardView3D._same_slot(main.board.selected_slot, main.board.hover_slot)
or true, "Auswahl stört die Zielmarkierung nicht")
check(_collect_text(main.info_box).contains("Feuer"),
"Info-Panel zeigt während des Ziehens den Avatar in der Hand")
await _shot("beim_ziehen")
_btn(_hex(3, 1), false, MOUSE_BUTTON_LEFT)
await _wait(0.6)
check(p.board.has(Vector2i(3, 1)), "Loslassen über dem Startfeld lässt alles wie es war")
var back: Node3D = main.board.unit_node(a0.id)
check(absf(back.position.y - ground) < 0.01, "nach dem Loslassen steht der Avatar auf dem Feld")
print("\n[C] Drag & Drop auf dem Board")
await _drag(_hex(3, 1), _hex(5, 2))
check(p.board.get(Vector2i(5, 2)) == a0 and not p.board.has(Vector2i(3, 1)),
"Avatar auf (5,2) verschoben")
print("\n[D] Drag auf ein belegtes Feld tauscht")
p.gold = 200
main._refresh()
await _buy(p, Data.Element.WIND)
var a1: Avatar = p.bench[0]
await _drag(_bench(0), _hex(2, 0))
check(p.board.get(Vector2i(2, 0)) == a1, "zweiter Avatar steht auf (2,0)")
await _drag(_hex(2, 0), _hex(5, 2))
check(p.board.get(Vector2i(5, 2)) == a1 and p.board.get(Vector2i(2, 0)) == a0,
"die beiden haben die Plätze getauscht")
print("\n[E] Drag & Drop Board -> Bank und Bank -> Bank")
await _drag(_hex(5, 2), _bench(4))
check(p.bench[4] == a1 and not p.board.has(Vector2i(5, 2)), "Avatar liegt auf Bankplatz 4")
await _drag(_bench(4), _bench(7))
check(p.bench[7] == a1 and p.bench[4] == null, "auf Bankplatz 7 verschoben")
print("\n[F] Klick ohne Ziehen wählt aus")
await _click(_hex(2, 0))
check(main.sel_slot.get("kind", "") == "board", "Board-Feld ist ausgewählt")
var info := _collect_text(main.info_box)
check(info.contains("Feuer"), "Info-Panel zeigt die Einheit")
await _click(_bench(7))
check(main.sel_slot.get("kind", "") == "bench", "Bankplatz ist ausgewählt")
check(_collect_text(main.info_box).contains("Wind"), "Info-Panel zeigt den Bank-Avatar")
await _shot("auswahl")
print("\n[G] Rechtsklick: Board zurück auf die Bank, Bank verkauft")
await _click(_hex(2, 0), MOUSE_BUTTON_RIGHT)
check(p.board.is_empty(), "Board ist leer")
var gold1 := p.gold
var filled := -1
for i in p.bench.size():
if p.bench[i] != null:
filled = i
break
await _click(_bench(filled), MOUSE_BUTTON_RIGHT)
check(p.gold > gold1, "Verkauf bringt Gold zurück (%d -> %d)" % [gold1, p.gold])
print("\n[H] Shop zeigt nur eigene Elemente und Neutral")
var seen := {}
for i in 40:
await _click(_center(main.btn_reroll))
for e in p.shop:
seen[e.element] = true
var only_mine := true
for e in seen:
if e >= 0 and not p.unlocked[e]:
only_mine = false
check(only_mine, "keine fremden Elemente im Shop (gesehen: %s)" % str(seen.keys()))
check(seen.has(-1), "Neutral bleibt immer im Angebot")
check(seen.has(Data.Element.FEUER), "das eigene Feuer taucht auf")
print("\n[I] Kaderplätze bleiben begrenzt")
p.gold = 300
for i in p.bench.size():
p.bench[i] = null
main._refresh()
var slots := p.team_slots(main.game.round_no)
for i in slots + 1:
await _buy(p, Data.Element.FEUER)
await _drag(_bench(0), _hex(i, 1))
check(p.board.size() == slots, "nur %d Einheiten aufstellbar (%d)" % [slots, p.board.size()])
check(main.lbl_hint.text.contains("Kaderplätze"), "Hinweis: '%s'" % main.lbl_hint.text)
await _shot("kader_voll")
print("\n[J] Bank voll -> Kauf blockiert")
for i in p.bench.size():
if p.bench[i] == null:
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
main._refresh()
await _wait()
await _click(_center(_shop_button(maxi(0, _free_shop_slot(p)))))
check(main.lbl_hint.text.contains("Bank ist voll"), "Hinweis: '%s'" % main.lbl_hint.text)
print("\n[K] Aufwerten über den Knopf")
var hex: Vector2i = p.board.keys()[0]
var av: Avatar = p.board[hex]
for i in 3:
p.bench[i] = Avatar.new(Data.Role.DPS, [av.elements[0]] as Array[int], 1)
await _click(_hex(hex.x, hex.y))
var up := _find_button(main.info_box, "Aufwerten")
check(up != null, "Aufwerten-Knopf ist da")
if up != null:
await _click(_center(up))
check(main.picker.visible, "Popup für die zweite Spezialisierung")
check(main.picker_sub.visible and main.picker_sub.text.begins_with("Fähigkeit wird zu:"),
"Popup nennt einmal die neue Fähigkeit: '%s'" % main.picker_sub.text)
var texts := _collect_text(main.picker_grid)
check(not texts.contains("Leben") and not texts.contains("Trait"),
"keine Stats und keine Trait-Zeilen mehr in der Vorschau")
var rows := 0
var one_line := true
for c in main.picker_grid.get_children():
if c is Label:
rows += 1
if not c.text.begins_with("+ ") or c.text.contains("\n"):
one_line = false
check(rows == main.picker_grid.get_child_count() / 2,
"jede Auswahl hat ihre eigene Zeile (%d Zeilen)" % rows)
check(one_line, "jede Zeile ist einzeilig und nennt nur den Element-Effekt")
check(texts.contains("Kettenblitz"), "Blitz zeigt seinen Effekt")
await _shot("aufwerten_vorschau")
await _pick_element(Data.Element.BLITZ)
check(p.board[hex].star == 2, "Einheit ist 2-Star")
check(p.board[hex].elements.size() == 2, "Einheit hat zwei Elemente")
await _shot("aufgewertet")
print("\n[L] Kampf")
var round0: int = main.game.round_no
await _click(_center(main.btn_ready))
check(main.game.phase == Game.Phase.COMBAT, "Phase wechselt zu Kampf")
await _wait(1.5)
await _shot("kampf")
check(main.game.player_combat.time > 0.0, "Kampfzeit läuft (%.1fs)"
% main.game.player_combat.time)
await _click(_center(main.btn_ready))
await _wait(0.3)
check(main.game.phase == Game.Phase.PLANNING, "zurück in der Planung")
check(main.game.round_no == round0 + 1, "Runde %d -> %d" % [round0, main.game.round_no])
check(not main.game.last_round_log.is_empty(), "Rundenlog gefüllt")
await _shot("naechste_runde")
print("\n[M] Preise und Zinsen")
p.gold = 0
p.streak = 0
p.last_won = false
main._refresh()
check(p.round_income() == 5, "ohne Gold gibt es 5 Gold")
p.gold = 34
main._refresh()
check(p.round_income() == 8, "34 Gold -> 5 + 3 Zins")
check(main.lbl_gold.text.contains("+3 Zins"), "Kopfzeile zeigt den Zins: '%s'" % main.lbl_gold.text)
var neutral := -1
var elemental := -1
for i in p.shop.size():
if p.shop[i].element < 0:
neutral = i
else:
elemental = i
if neutral >= 0:
check(p.shop[neutral].price() == 1, "neutraler Avatar kostet 1 Gold")
check(_shop_button(neutral).text.contains("1 Gold"), "Karte zeigt 1 Gold")
if elemental >= 0:
check(p.shop[elemental].price() == 2, "Element-Avatar kostet 2 Gold")
check(_shop_button(elemental).text.contains("2 Gold"), "Karte zeigt 2 Gold")
print("\n[N] Tastatur")
p.gold = 20
main._refresh()
var g := p.gold
_key(KEY_D)
await _wait()
check(p.gold == g - Data.REROLL_COST, "Taste D würfelt neu")
_key(KEY_SPACE)
await _wait(0.3)
check(main.game.phase == Game.Phase.COMBAT, "Leertaste startet den Kampf")
main.game.skip_combat()
main._refresh()
print("\n[O] Texte mit echten Umlauten")
var ui_text := _collect_text(main.ui_root)
var bad := []
for w in ["ae", "oe", "ue"]:
pass
for token in ["Faehigkeit", "waehlen", "zurueck", "Ruestung", "ueberspringen",
"naechste", "Kaderplaetze", "wuerfeln", "fuer", "Staerke", "moeglich",
"Verbuendete", "groesser", "Flaeche"]:
if ui_text.contains(token):
bad.append(token)
check(bad.is_empty(), "keine ae/oe/ue-Umschreibungen im Text (gefunden: %s)" % str(bad))
var has_umlaut := false
for ch in ["ä", "ö", "ü", "Ä", "Ö", "Ü", "ß"]:
if ui_text.contains(ch):
has_umlaut = true
check(has_umlaut, "echte Umlaute kommen im Text vor")
print("")
if fails == 0:
print("UI-VERIFIKATION BESTANDEN")
else:
print("UI-VERIFIKATION FEHLER: ", fails)
get_tree().quit(1 if fails > 0 else 0)
func _key(code: int) -> void:
var e := InputEventKey.new()
e.keycode = code
e.pressed = true
get_viewport().push_input(e)
var u := InputEventKey.new()
u.keycode = code
u.pressed = false
get_viewport().push_input(u)
func _collect_text(node: Node) -> String:
var out := ""
for c in node.get_children():
if c is Label:
out += c.text + "\n"
out += _collect_text(c)
return out
func _find_button(node: Node, text: String) -> Button:
for c in node.get_children():
if c is Button and c.text == text:
return c
var r := _find_button(c, text)
if r != null:
return r
return null