Lava abgeschwächt, Archetypen aus Datei mit CI-Messlauf, Kartentext folgt Schriftgröße, Gamepad in Menüs, Anleitung 2 mit sicherem Angriff, Übersetzungsprüfung, Lint und gestaffelte CI

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
Team3
2026-09-16 17:02:59 +02:00
parent f68ac07550
commit 77d1b318d6
19 changed files with 285 additions and 36 deletions

29
tests/lint.gd Normal file
View File

@@ -0,0 +1,29 @@
extends SceneTree
## Statische Prüfung: lädt jedes Skript; Parse- oder Kompilierfehler lassen den Lauf scheitern.
## godot --headless --path . -s tests/lint.gd
func scan(dir: String, out: Array) -> void:
var d := DirAccess.open(dir)
if d == null:
return
for sub in d.get_directories():
if not sub.begins_with("."):
scan(dir.path_join(sub), out)
for f in d.get_files():
if f.ends_with(".gd"):
out.append(dir.path_join(f))
func _initialize() -> void:
var files := []
for dir in ["res://scripts", "res://tests"]:
scan(dir, files)
var bad := 0
for path in files:
var script = load(path)
if script == null or not (script is GDScript):
printerr("FEHLER: %s lädt nicht" % path)
bad += 1
print("%d Skripte geprüft, %d fehlerhaft" % [files.size(), bad])
quit(1 if bad > 0 else 0)

1
tests/lint.gd.uid Normal file
View File

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

View File

@@ -1,8 +1,10 @@
#!/usr/bin/env bash
# Führt Import und alle Headless-Prüfungen aus. Aufruf: tests/run_headless.sh [godot-binary]
# Führt Import, statische Prüfung 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/lint.gd 2>&1 | grep -E "FEHLER|Skripte" || true
"$GODOT" --headless --path . -s tests/lint.gd > /dev/null 2>&1
"$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

View File

@@ -34,4 +34,8 @@ func _initialize() -> void:
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

@@ -111,6 +111,7 @@ func _initialize() -> void:
test_kurve_statistik_vorschlag_verbesserung()
test_wasser_antworten()
test_ki_zeitbudget()
test_uebersetzung_vollstaendig()
print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed])
quit(1 if failed > 0 else 0)
@@ -532,15 +533,16 @@ func test_feldkombos() -> void:
g.play_card(h, C("w_reiter"))
check(strong.frozen and not weak.frozen, "Dampf friert das stärkste Wesen")
check(g.log.any(func(l): return l.begins_with("Feldkombo Dampf")), "Feldkombo im Verlauf")
# Lava: 1 Schaden an alle gegnerischen Wesen
# Lava: 1 Schaden an das stärkste gegnerische Wesen
s = setup(["e_golem"])
g = s[0]
h = s[1]
o = s[2][0]
put(h, "f_wolf")
var k := put(o, "f_kobold")
var big := put(o, "e_troll")
g.play_card(h, C("e_golem"))
check(not (k in o.board), "Lava tötet den 2/1 Kobold")
check(k in o.board and big.health == 5, "Lava trifft nur das stärkste Wesen")
# Blitz: 2 Schaden an den schwächsten Gegner
s = setup(["l_wind"], [], 2)
g = s[0]
@@ -1119,7 +1121,7 @@ func test_neue_fallen() -> void:
g.end_turn()
g.play_card(o, C("e_troll"))
check(o.board.size() == 1 and o.board[0].frozen and h.traps.size() == 1, "Sandfalle friert ein, eine Falle je Auslöser")
g.play_card(o, C("w_reiter")) # Wasser neben Erde: Schlamm heilt nur, der Windgeist überlebt
g.play_card(o, C("w_reiter")) # Wasser neben Erde: Schlamm heilt nur
check(h.board.size() == 1 and h.board[0].name == "Windgeist", "Spiegelfalle beschwört Windgeist beim zweiten Wesen")
check(h.traps.is_empty(), "beide Fallen verbraucht")
# Nebelfalle bricht ab und zieht, Dornenfalle verletzt und heilt
@@ -1240,7 +1242,7 @@ func test_deckstaerke_und_schwierigkeit() -> void:
func test_starter_gegen_passende_gegner() -> void:
check(Game.archetypes("leicht").size() == 5 and Game.archetypes("schwer").size() == 5 and Game.archetypes().size() == 10, "Archetypen geteilt")
var table: Dictionary = DeckGen.ARCHETYPE_STRENGTH
var table: Dictionary = DeckGen.strength()
var keys: Array = table.keys()
keys.sort_custom(func(a, b): return table[a] < table[b])
check(keys[0] in Game.archetypes("leicht") and keys[-1] in Game.archetypes("schwer"), "schwächster Archetyp in Leicht, stärkster in Schwer")
@@ -1358,3 +1360,37 @@ func test_ki_zeitbudget() -> void:
check(Cards.by_id("wl_gischt").keywords == ["Flink"] and Cards.by_id("wl_gischt").combo() == "Sturm", "Gischtläufer für Sturm")
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(Profile.starter().to_dict())))
check(v.settings.has("big_font") and not v.tutorial2_done and v.records.margins is Array, "Version 5 mit neuen Feldern")
## Jeder tr("...")-Text in der Oberfläche und jeder Anleitungsschritt hat einen englischen Eintrag.
func test_uebersetzung_vollstaendig() -> void:
var keys := {}
var csv := FileAccess.get_file_as_string("res://translations/ui.csv")
for line in csv.split("\n"):
if line.is_empty() or line.begins_with("keys,"):
continue
var key := line
if line.begins_with("\""):
key = line.substr(1, line.find("\",", 1) - 1)
else:
key = line.substr(0, line.find(","))
keys[key] = true
var missing := []
var d := DirAccess.open("res://scripts/ui")
d.list_dir_begin()
var name := d.get_next()
while name != "":
if name.ends_with(".gd"):
var src := FileAccess.get_file_as_string("res://scripts/ui/" + name)
var rx := RegEx.new()
rx.compile("tr\\(\"([^\"]+)\"\\)")
for m in rx.search_all(src):
var k: String = m.get_string(1)
if not keys.has(k) and not (k in missing):
missing.append(k)
name = d.get_next()
for stepset in [BattleTutorial.STEPS1, BattleTutorial.STEPS2]:
for st in stepset:
if not keys.has(st[0]):
missing.append(st[0])
check(missing.is_empty(), "fehlende Übersetzungen: %s" % [missing])

View File

@@ -162,7 +162,7 @@ func _ready() -> void:
add_child(main)
await frames(3)
var fast := OS.get_environment("DINGER_FAST") != ""
var suite := [test_menu_und_regeln, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
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]
if not fast:
suite += [test_hotseat, test_netzwerk, test_kampf.bind(2, 3), test_kampf_abbrechen]
@@ -513,6 +513,34 @@ func test_werkstatt() -> void:
check(screen() is MenuScreen, "zurück im Menü")
func test_gamepad_menue() -> void:
check(get_viewport().gui_get_focus_owner() == null, "ohne Gamepad kein Fokus")
await pad(JOY_BUTTON_DPAD_DOWN)
var f := get_viewport().gui_get_focus_owner()
check(f is Button and main.pad_mode, "Gamepad setzt den Fokus auf einen Knopf")
var down := InputEventJoypadButton.new()
down.button_index = JOY_BUTTON_DPAD_DOWN
down.pressed = true
Input.parse_input_event(down)
await frames(2)
var f2 := get_viewport().gui_get_focus_owner()
check(f2 is Button and f2 != f, "Steuerkreuz wechselt den Knopf")
var target_text: String = f2.text
var accept := InputEventJoypadButton.new()
accept.button_index = JOY_BUTTON_A
accept.pressed = true
get_viewport().push_input(accept)
await frames(1)
var release := InputEventJoypadButton.new()
release.button_index = JOY_BUTTON_A
release.pressed = false
get_viewport().push_input(release)
await frames(3)
check(not (screen() is MenuScreen) or target_text == "Beenden", "A öffnet den fokussierten Bildschirm (%s)" % target_text)
await hover(main)
check(not main.pad_mode and get_viewport().gui_get_focus_owner() == null, "Maus nimmt den Fokus wieder")
func test_anleitung2() -> void:
check(main.profile.tutorial_done and not main.profile.tutorial2_done, "Teil 1 erledigt, Teil 2 offen")
check(find_button("Anleitung 2: Fallen") != null, "Menü bietet Teil 2 an")
@@ -535,6 +563,15 @@ func test_anleitung2() -> void:
check(idx >= 0 and idx < 9, "Falle auf der Hand")
await key(KEY_1 + idx)
check(g.human.traps.size() == 1 and scr.tutorial_step == 2, "Falle gelegt, Schritt 3")
# Mara läuft in die Falle: zwei Züge abwarten
for i in 2:
await clear_mulligan(scr)
if g.current() == g.human and g.winner == "":
await key(KEY_SPACE)
await wait_for_human(scr)
if scr.tutorial_step >= 3:
break
check(scr.tutorial_step >= 3 and g.log.any(func(l): return l.begins_with("Falle von Du")), "Mara löst die Frostfalle aus, Schritt 4")
await give_up()
check(main.profile.tutorial2_done and find_button("Anleitung") != null, "Teil 2 erledigt")