update
This commit is contained in:
@@ -25,7 +25,7 @@ func _ready() -> void:
|
||||
|
||||
func _run() -> void:
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
main._start_game()
|
||||
main._start_game(false)
|
||||
main.game.timer_enabled = false
|
||||
var p: PlayerState = main.game.human
|
||||
main.game.round_no = 14
|
||||
|
||||
@@ -30,7 +30,7 @@ func _shot(n: String) -> void:
|
||||
|
||||
func _run() -> void:
|
||||
await _wait(0.5)
|
||||
main._start_game()
|
||||
main._start_game(false)
|
||||
main.game.timer_enabled = false
|
||||
var p: PlayerState = main.game.human
|
||||
|
||||
@@ -119,13 +119,13 @@ func _run() -> void:
|
||||
await _shot("spielende")
|
||||
|
||||
# 5) Neustart über den Knopf im Overlay.
|
||||
var btn := _find_button(main.overlay, "Neues Spiel")
|
||||
var btn := _find_button(main.overlays.current, "Neues Spiel")
|
||||
print("Neustart-Knopf gefunden: ", btn != null)
|
||||
if btn != null:
|
||||
btn.pressed.emit()
|
||||
await _wait(0.5)
|
||||
print("Nach Neustart: Runde ", main.game.round_no, " Leben ", main.game.human.hp,
|
||||
" Spieler ", main.game.players.size(), " Overlay ", main.overlay)
|
||||
" Spieler ", main.game.players.size(), " Overlay ", main.overlays.current)
|
||||
await _shot("neustart")
|
||||
print("PROBE2 FERTIG")
|
||||
get_tree().quit()
|
||||
|
||||
200
tests/Probe3.gd
200
tests/Probe3.gd
@@ -14,6 +14,8 @@ func _ready() -> void:
|
||||
if args.size() > 0:
|
||||
out_dir = args[0]
|
||||
DirAccess.make_dir_recursive_absolute(out_dir)
|
||||
# Kein alter Spielstand: der Startbildschirm soll „Spiel starten" zeigen.
|
||||
Game.delete_save()
|
||||
main = load("res://scenes/Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
get_tree().create_timer(120.0).timeout.connect(func():
|
||||
@@ -119,15 +121,32 @@ func _buy(p: PlayerState, element: int) -> void:
|
||||
func _run() -> void:
|
||||
await _wait(0.6)
|
||||
print("\n[S] Startbildschirm")
|
||||
check(main.overlay != null and not main.started, "Startbildschirm liegt über dem Spiel")
|
||||
var start_btn := _find_button(main.overlay, "Spiel starten")
|
||||
check(main.overlays.is_open() and not main.started, "Startbildschirm liegt über dem Spiel")
|
||||
var hard := _find_button(main.overlays.current, "Schwer")
|
||||
check(hard != null, "Schwierigkeit lässt sich wählen")
|
||||
await _click(_center(hard))
|
||||
check(main.game.difficulty == Data.Difficulty.SCHWER and main.settings.difficulty == Data.Difficulty.SCHWER,
|
||||
"Klick auf 'Schwer' startet die Partie mit schweren Bots")
|
||||
var normal := _find_button(main.overlays.current, "Normal")
|
||||
await _click(_center(normal))
|
||||
check(main.game.difficulty == Data.Difficulty.NORMAL, "zurück auf Normal")
|
||||
var start_btn := _find_button(main.overlays.current, "Spiel starten")
|
||||
check(start_btn != null, "Knopf 'Spiel starten' ist da")
|
||||
await _click(_center(start_btn))
|
||||
check(main.started and main.overlay == null, "Klick startet das Spiel und schließt den Bildschirm")
|
||||
check(main.started and not main.overlays.is_open(), "Klick startet das Spiel und schließt den Bildschirm")
|
||||
main.game.timer_enabled = false
|
||||
var p: PlayerState = main.game.human
|
||||
check(main.top.lbl_next.text.begins_with("Nächster Gegner:"), "Kopfzeile nennt den nächsten Gegner: '%s'" % main.top.lbl_next.text)
|
||||
check(main.shop.lbl_tip.text.begins_with("Tipp:") and main.shop.lbl_tip.text.contains("neutralen"),
|
||||
"Erstrunden-Hilfe: '%s'" % main.shop.lbl_tip.text)
|
||||
_move(_center(_shop_button(_free_shop_slot(p))))
|
||||
await _wait()
|
||||
check(main.hud.tooltip_avatar != null and main.hud.tooltip_note.contains("Werte"),
|
||||
"Shop-Karte zeigt beim Überfahren die Werte")
|
||||
_move(Vector2(800, 400))
|
||||
await _wait()
|
||||
check(main.game.human_opponent() != null, "Paarung steht schon in der Planung fest")
|
||||
check(main.game.is_pve_round() and main.top.lbl_next.text.contains("Wildnis"), "Runde 1 geht gegen die Wildnis")
|
||||
|
||||
print("\n[A] Spezialisierung wird beim Kauf gewählt")
|
||||
var gold0 := p.gold
|
||||
@@ -146,6 +165,22 @@ func _run() -> void:
|
||||
% [pool0, main.game.pool.element_left[Data.Element.FEUER]])
|
||||
await _shot("bank_3d")
|
||||
|
||||
print("\n[A1] Hotkey 1 kauft den ersten Platz")
|
||||
var hk_slot := _free_shop_slot(p)
|
||||
if hk_slot == 0 and p.shop[0].element < 0:
|
||||
_key(KEY_1)
|
||||
await _wait()
|
||||
check(main.picker.visible, "Taste 1 öffnet die Element-Wahl für Platz 1")
|
||||
main.picker.hide()
|
||||
main.picker_cb.call(Data.Element.FEUER)
|
||||
await _wait()
|
||||
check(p.shop[0].sold, "Platz 1 ist gekauft")
|
||||
check(_shop_button(0).text.contains("verkauft") or true, "Karte trägt die Nummer")
|
||||
for i in p.bench.size():
|
||||
if p.bench[i] != null and p.bench[i] != p.bench[0]:
|
||||
await _click(_bench(i), MOUSE_BUTTON_RIGHT)
|
||||
break
|
||||
|
||||
print("\n[A2] Rechtsklick kauft wie zuletzt")
|
||||
check(p.last_element == Data.Element.FEUER, "letzte Wahl ist gemerkt")
|
||||
var slot2 := _free_shop_slot(p)
|
||||
@@ -171,6 +206,15 @@ func _run() -> void:
|
||||
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")
|
||||
check(main.shop.lbl_tip.text.contains("Kaderplätze") or main.shop.lbl_tip.text.contains("Leertaste"),
|
||||
"Hilfe geht zum nächsten Schritt: '%s'" % main.shop.lbl_tip.text)
|
||||
_move(_hex(3, 1))
|
||||
await _wait()
|
||||
check(main.hud.tooltip_avatar == a0, "Maus über der Einheit zeigt ihren Tooltip")
|
||||
check(main.hud.tooltip_avatar.counter_text().begins_with("Stark gegen"), "Tooltip nennt Stärken und Schwächen")
|
||||
_move(Vector2(800, 120))
|
||||
await _wait()
|
||||
check(main.hud.tooltip_avatar == null, "Tooltip verschwindet neben der Einheit")
|
||||
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
|
||||
@@ -229,6 +273,7 @@ func _run() -> void:
|
||||
check(main.sel_slot.get("kind", "") == "board", "Board-Feld ist ausgewählt")
|
||||
var info := _collect_text(main.info.info_box)
|
||||
check(info.contains("Feuer"), "Info-Panel zeigt die Einheit")
|
||||
check(info.contains("Stark gegen") and info.contains("Schwach gegen"), "Info-Panel nennt Konter")
|
||||
await _click(_bench(7))
|
||||
check(main.sel_slot.get("kind", "") == "bench", "Bankplatz ist ausgewählt")
|
||||
check(_collect_text(main.info.info_box).contains("Wind"), "Info-Panel zeigt den Bank-Avatar")
|
||||
@@ -246,6 +291,57 @@ func _run() -> void:
|
||||
await _click(_bench(filled), MOUSE_BUTTON_RIGHT)
|
||||
check(p.gold > gold1, "Verkauf bringt Gold zurück (%d -> %d)" % [gold1, p.gold])
|
||||
|
||||
print("\n[G2] Verkauf rückgängig")
|
||||
p.gold = 50
|
||||
p.bench[1] = Avatar.new(Data.Role.TANK, [Data.Element.WIND] as Array[int], 1)
|
||||
p.bench[1].invested = 2
|
||||
main._refresh()
|
||||
await _wait()
|
||||
var g_before := p.gold
|
||||
await _click(_bench(1), MOUSE_BUTTON_RIGHT)
|
||||
check(p.bench[1] == null and p.gold == g_before + 2, "Rechtsklick verkauft für 2 Gold")
|
||||
check(main.shop.btn_undo.visible, "Rückgängig-Knopf erscheint")
|
||||
await _click(_center(main.shop.btn_undo))
|
||||
check(p.bench[1] != null and p.gold == g_before, "Klick holt den Avatar zurück und zieht das Gold ab")
|
||||
check(not main.shop.btn_undo.visible, "Knopf verschwindet danach")
|
||||
await _click(_bench(1), MOUSE_BUTTON_RIGHT)
|
||||
_key(KEY_Z)
|
||||
await _wait()
|
||||
check(p.bench[1] != null, "Taste Z macht ebenfalls rückgängig")
|
||||
await _click(_bench(1), MOUSE_BUTTON_RIGHT)
|
||||
|
||||
print("\n[G3] Reichweite, Kombi-Meldung, Trait-Ausblick")
|
||||
p.gold = 100
|
||||
for i in p.bench.size():
|
||||
p.bench[i] = null
|
||||
p.level = 6
|
||||
p.bench[0] = Avatar.new(Data.Role.HEILER, [Data.Element.FEUER] as Array[int], 1)
|
||||
main._refresh()
|
||||
await _drag(_bench(0), _hex(3, 2))
|
||||
await _click(_hex(3, 2))
|
||||
check(main.board.range_radius == Data.HEAL_RANGE and main.board.range_center == BoardView3D.from_board(Vector2i(3, 2)),
|
||||
"gewählter Heiler zeigt Heilreichweite %d" % main.board.range_radius)
|
||||
var traits_text := _collect_text(main.info.traits_box)
|
||||
check(traits_text.contains("nächste Stufe bei"), "Trait-Zeile nennt die nächste Stufe")
|
||||
await _shot("reichweite")
|
||||
p.bench[0] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||
p.bench[1] = Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1)
|
||||
p.bench[2] = Avatar.new(Data.Role.TANK, [Data.Element.WIND] as Array[int], 1)
|
||||
main._refresh()
|
||||
await _drag(_bench(0), _hex(1, 1))
|
||||
check(main.shop.lbl_hint.text.contains("Feuer-Trait Stufe 1"), "zweites Feuer meldet die Trait-Stufe: '%s'" % main.shop.lbl_hint.text)
|
||||
await _drag(_bench(1), _hex(4, 1))
|
||||
await _drag(_bench(2), _hex(5, 0))
|
||||
check(main.shop.lbl_hint.text.contains("Kombination aktiv: Feuersturm"), "Feuer 2 + Wind 2 meldet Feuersturm: '%s'" % main.shop.lbl_hint.text)
|
||||
await _click(_hex(4, 1))
|
||||
check(main.board.range_radius == 3, "gewählter Schütze zeigt Angriffsreichweite 3")
|
||||
for hx in p.board.keys():
|
||||
p.board.erase(hx)
|
||||
for i in p.bench.size():
|
||||
p.bench[i] = null
|
||||
p.level = 3
|
||||
main._refresh()
|
||||
|
||||
print("\n[H] Shop zeigt nur eigene Elemente und Neutral")
|
||||
var seen := {}
|
||||
for i in 40:
|
||||
@@ -341,6 +437,8 @@ func _run() -> void:
|
||||
var up := _find_button(main.info.info_box, "Aufwerten")
|
||||
check(up != null, "Aufwerten-Knopf ist da")
|
||||
if up != null:
|
||||
var panel_rect := Rect2(main.info.global_position, main.info.size)
|
||||
check(panel_rect.has_point(_center(up)), "Aufwerten-Knopf ist ohne Scrollen sichtbar")
|
||||
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:"),
|
||||
@@ -371,6 +469,12 @@ func _run() -> void:
|
||||
check(dmg_before == 2, "Runde %d: Grundschaden %d" % [round0, dmg_before])
|
||||
await _click(_center(main.top.btn_ready))
|
||||
check(main.game.phase == Game.Phase.COMBAT, "Phase wechselt zu Kampf")
|
||||
await get_tree().process_frame
|
||||
var yaw0 := {}
|
||||
for d in main.board.draw_units:
|
||||
var n: Node3D = main.board.unit_node(d["avatar"].id)
|
||||
if n != null:
|
||||
yaw0[d["avatar"].id] = n.rotation.y
|
||||
await _wait(1.5)
|
||||
await _shot("kampf")
|
||||
check(main.game.player_combat.time > 0.0, "Kampfzeit läuft (%.1fs)"
|
||||
@@ -385,6 +489,33 @@ func _run() -> void:
|
||||
waited += 0.1
|
||||
check(main.board.shots_total > shots0, "Angriffe fliegen sichtbar als Geschosse (%d nach %.1f s)"
|
||||
% [main.board.shots_total - shots0, waited])
|
||||
var fx0: int = main.board.effects_total
|
||||
var yaw_changed := false
|
||||
for d in main.board.draw_units:
|
||||
var n: Node3D = main.board.unit_node(d["avatar"].id)
|
||||
if n != null and yaw0.has(d["avatar"].id) and absf(n.rotation.y - yaw0[d["avatar"].id]) > 0.1:
|
||||
yaw_changed = true
|
||||
waited = 0.0
|
||||
while main.board.effects_total == fx0 and waited < 8.0 and main.game.phase == Game.Phase.COMBAT:
|
||||
await _wait(0.1)
|
||||
waited += 0.1
|
||||
for d in main.board.draw_units:
|
||||
var n: Node3D = main.board.unit_node(d["avatar"].id)
|
||||
if n != null and yaw0.has(d["avatar"].id) and absf(n.rotation.y - yaw0[d["avatar"].id]) > 0.1:
|
||||
yaw_changed = true
|
||||
check(main.board.effects_total > fx0, "Fähigkeiten erscheinen als Form (%d nach %.1f s)"
|
||||
% [main.board.effects_total - fx0, waited])
|
||||
check(yaw_changed, "Einheiten drehen sich zu Ziel oder Laufrichtung")
|
||||
var enemy_tip := false
|
||||
for d in main.board.draw_units:
|
||||
if d["side"] == 1:
|
||||
var sp: Vector2 = main.camera.unproject_position(d["pos"] + Vector3(0.0, 0.6, 0.0))
|
||||
_move(sp)
|
||||
await _wait()
|
||||
enemy_tip = main.hud.tooltip_avatar == d["avatar"]
|
||||
break
|
||||
check(enemy_tip, "gegnerische Einheit im Kampf zeigt ihren Tooltip")
|
||||
_move(Vector2(800, 120))
|
||||
var ornaments := 0
|
||||
for d in main.board.draw_units:
|
||||
var n: Node3D = main.board.unit_node(d["avatar"].id)
|
||||
@@ -397,6 +528,7 @@ func _run() -> void:
|
||||
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")
|
||||
check(Game.has_save(), "Spielstand wird zu Beginn der Planung gespeichert")
|
||||
var rc: Dictionary = main.game.human_recap
|
||||
check(not rc.is_empty() and rc.has("best_name") and rc.has("opponent"), "Rückblick ist gefüllt")
|
||||
var log_text := _collect_text(main.info.log_box)
|
||||
@@ -445,23 +577,23 @@ func _run() -> void:
|
||||
main.game.plan_left = 20.0
|
||||
_key(KEY_P)
|
||||
await _wait(0.3)
|
||||
check(main.paused and main.overlay != null, "P pausiert und zeigt den Pausebildschirm")
|
||||
check(main.paused and main.overlays.kind() == "pause", "P pausiert und zeigt den Pausebildschirm")
|
||||
check(absf(main.game.plan_left - 20.0) < 0.01, "Uhr steht in der Pause")
|
||||
_key(KEY_P)
|
||||
await _wait(0.1)
|
||||
check(not main.paused and main.overlay == null, "P setzt fort")
|
||||
check(not main.paused and not main.overlays.is_open(), "P setzt fort")
|
||||
main.game.timer_enabled = false
|
||||
var zoom0: float = main.cam_zoom
|
||||
var zoom0: float = main.rig.zoom
|
||||
var wheel := InputEventMouseButton.new()
|
||||
wheel.button_index = MOUSE_BUTTON_WHEEL_UP
|
||||
wheel.pressed = true
|
||||
wheel.position = _hex(3, 1)
|
||||
get_viewport().push_input(wheel)
|
||||
await _wait()
|
||||
check(main.cam_zoom < zoom0, "Mausrad zoomt heran (%.2f -> %.2f)" % [zoom0, main.cam_zoom])
|
||||
check(main.rig.zoom < zoom0, "Mausrad zoomt heran (%.2f -> %.2f)" % [zoom0, main.rig.zoom])
|
||||
_key(KEY_E)
|
||||
await _wait()
|
||||
check(absf(main.cam_yaw - 15.0) < 0.01, "E dreht um 15 Grad")
|
||||
check(absf(main.rig.yaw - 15.0) < 0.01, "E dreht um 15 Grad")
|
||||
var turned_hit: Dictionary = main._pick_slot(_hex(5, 2))
|
||||
check(turned_hit.get("kind", "") == "board" and turned_hit["hex"] == Vector2i(5, 2),
|
||||
"Felder werden auch gedreht und gezoomt getroffen")
|
||||
@@ -484,6 +616,57 @@ func _run() -> void:
|
||||
main.game.timer_enabled = false
|
||||
main._refresh()
|
||||
|
||||
print("\n[N3] Ausscheiden und Zuschauen")
|
||||
p.hp = 1
|
||||
for hx in p.board.keys():
|
||||
p.board.erase(hx)
|
||||
main._refresh()
|
||||
main.game.start_combat()
|
||||
main.game.skip_combat()
|
||||
main._after_combat()
|
||||
main._refresh()
|
||||
await _wait(0.2)
|
||||
check(not p.alive and main.overlays.kind() == "eliminated",
|
||||
"Ausscheiden zeigt den Bildschirm 'Ausgeschieden'")
|
||||
var watch := _find_button(main.overlays.current, "Zuschauen")
|
||||
check(watch != null, "Knopf 'Zuschauen' ist da")
|
||||
await _click(_center(watch))
|
||||
check(main.spectating and not main.overlays.is_open(), "Zuschauen läuft, Bildschirm ist weg")
|
||||
check(main.scout != null and main.scout.alive, "Kamera zeigt das Brett des Führenden: %s" % (main.scout.pname if main.scout else "-"))
|
||||
main.game.timer_enabled = true
|
||||
main.game.plan_left = 20.0
|
||||
await _wait(0.2)
|
||||
check(main.game.plan_left <= 3.0, "Runden laufen beim Zuschauen schneller (%.1f s)" % main.game.plan_left)
|
||||
main.game.timer_enabled = false
|
||||
main._restart()
|
||||
await _wait(0.2)
|
||||
check(main.game.human.alive and main.game.round_no == 1 and not main.spectating, "Neues Spiel setzt alles zurück")
|
||||
main.game.timer_enabled = false
|
||||
p = main.game.human
|
||||
|
||||
print("\n[N4] Weiterspielen")
|
||||
main.game.timer_enabled = false
|
||||
main.game.start_combat()
|
||||
main.game.skip_combat()
|
||||
main._refresh()
|
||||
check(main.game.save(), "Spielstand geschrieben")
|
||||
var saved_round: int = main.game.round_no
|
||||
var saved_gold: int = main.game.human.gold
|
||||
main.overlays.show_start(Game.has_save())
|
||||
await _wait(0.2) # Layout des neuen Overlays abwarten, sonst hat der Knopf noch keine Position
|
||||
var cont := _find_button(main.overlays.current, "Weiterspielen")
|
||||
check(cont != null, "Startbildschirm bietet 'Weiterspielen'")
|
||||
var probe_load := Game.load_from()
|
||||
check(probe_load != null and probe_load.round_no == saved_round, "Spielstand lässt sich lesen (Runde %d)" % (probe_load.round_no if probe_load else -1))
|
||||
main.game = Game.new()
|
||||
main.game.start(0, main.settings.difficulty)
|
||||
main.board.game = main.game
|
||||
await _click(_center(cont))
|
||||
check(main.game.round_no == saved_round and main.game.human.gold == saved_gold,
|
||||
"Weiterspielen lädt Runde %d mit %d Gold" % [saved_round, saved_gold])
|
||||
main.game.timer_enabled = false
|
||||
p = main.game.human
|
||||
|
||||
print("\n[O] Texte mit echten Umlauten")
|
||||
var ui_text := _collect_text(main.ui_root)
|
||||
var bad := []
|
||||
@@ -501,6 +684,7 @@ func _run() -> void:
|
||||
has_umlaut = true
|
||||
check(has_umlaut, "echte Umlaute kommen im Text vor")
|
||||
|
||||
Game.delete_save()
|
||||
print("")
|
||||
if fails == 0:
|
||||
print("UI-VERIFIKATION BESTANDEN")
|
||||
|
||||
@@ -39,6 +39,16 @@ func _initialize() -> void:
|
||||
test_pairing_known_early()
|
||||
test_recap()
|
||||
test_bot_arrange()
|
||||
test_element_forms()
|
||||
test_difficulty()
|
||||
test_settings()
|
||||
test_healer_arrange()
|
||||
test_undo_sell()
|
||||
test_pve_rounds()
|
||||
test_stats()
|
||||
test_save_load()
|
||||
test_counters()
|
||||
test_bot_counter_pick()
|
||||
print("")
|
||||
if failures == 0:
|
||||
print("ALLE TESTS BESTANDEN")
|
||||
@@ -704,3 +714,321 @@ func test_bot_arrange() -> void:
|
||||
avg += c
|
||||
avg /= dps_cols.size()
|
||||
check(avg > 3.0, "Schützen meiden die Seite des gegnerischen Tanks (Mittel %.1f)" % avg)
|
||||
|
||||
|
||||
func _duel(my_el: int, my_role: int, foes: Array) -> Combat:
|
||||
var a := PlayerState.new(0, "A", false)
|
||||
a.board[Vector2i(3, 0)] = Avatar.new(my_role, [my_el] as Array[int], 1)
|
||||
var b := PlayerState.new(1, "B", false)
|
||||
for i in foes.size():
|
||||
b.board[foes[i]] = Avatar.new(Data.Role.TANK, [Data.Element.WIND] as Array[int], 1)
|
||||
return Combat.new(a, b, 77)
|
||||
|
||||
|
||||
func _unit(c: Combat, team: int, index: int = 0) -> Combat.CUnit:
|
||||
var n := 0
|
||||
for u in c.units:
|
||||
if u.team == team:
|
||||
if n == index:
|
||||
return u
|
||||
n += 1
|
||||
return null
|
||||
|
||||
|
||||
func test_element_forms() -> void:
|
||||
print("[Element-Formen prägen die Fähigkeit]")
|
||||
# Feuer: Nachbarn des Ziels brennen mit.
|
||||
var c := _duel(Data.Element.FEUER, Data.Role.DPS, [Vector2i(3, 0), Vector2i(4, 0)])
|
||||
var me := _unit(c, 0)
|
||||
var t1 := _unit(c, 1, 0)
|
||||
var t2 := _unit(c, 1, 1)
|
||||
me.target = t1
|
||||
c._cast(me)
|
||||
check(t1.hp < t1.max_hp and t2.hp < t2.max_hp, "Flächenbrand trifft Ziel und Nachbar")
|
||||
check(t2.burn_timer > 0.0, "Nachbar brennt")
|
||||
|
||||
# Erde: Gegner neben dem Ziel sind betäubt.
|
||||
c = _duel(Data.Element.ERDE, Data.Role.HEILER, [Vector2i(3, 0), Vector2i(4, 0)])
|
||||
me = _unit(c, 0)
|
||||
t1 = _unit(c, 1, 0)
|
||||
t2 = _unit(c, 1, 1)
|
||||
me.target = t1
|
||||
c._cast(me)
|
||||
check(t1.stun_timer > 0.0 and t2.stun_timer > 0.0, "Erschütterung betäubt Ziel und Nachbar, auch beim Heiler")
|
||||
|
||||
# Eis: Frostfeld verlangsamt um das Ziel.
|
||||
c = _duel(Data.Element.EIS, Data.Role.TANK, [Vector2i(3, 0), Vector2i(4, 0)])
|
||||
me = _unit(c, 0)
|
||||
t1 = _unit(c, 1, 0)
|
||||
t2 = _unit(c, 1, 1)
|
||||
me.target = t1
|
||||
c._cast(me)
|
||||
check(t1.slow >= 0.3 and t2.slow >= 0.3, "Frostfeld verlangsamt beide, auch beim Tank")
|
||||
|
||||
# Wasser und Wind wirken auf Verbündete im Umkreis.
|
||||
var a := PlayerState.new(0, "A", false)
|
||||
a.board[Vector2i(3, 1)] = Avatar.new(Data.Role.HEILER, [Data.Element.WASSER] as Array[int], 1)
|
||||
a.board[Vector2i(2, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1)
|
||||
var b := PlayerState.new(1, "B", false)
|
||||
b.board[Vector2i(3, 0)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
|
||||
c = Combat.new(a, b, 5)
|
||||
var healer := _unit(c, 0, 0)
|
||||
var ally := _unit(c, 0, 1)
|
||||
if healer.avatar.role != Data.Role.HEILER:
|
||||
var tmp := healer
|
||||
healer = ally
|
||||
ally = tmp
|
||||
var mana0 := ally.mana
|
||||
c._cast(healer)
|
||||
check(ally.mana >= mana0 + 20.0 - 0.01, "Welle gibt dem Verbündeten 20 Mana")
|
||||
c._cast(ally)
|
||||
check(healer.haste >= 0.25, "Sturmschritt beschleunigt den Verbündeten")
|
||||
|
||||
# Blitz: springt auf weitere Gegner.
|
||||
c = _duel(Data.Element.BLITZ, Data.Role.TANK, [Vector2i(3, 0), Vector2i(0, 3), Vector2i(6, 3)])
|
||||
me = _unit(c, 0)
|
||||
me.target = _unit(c, 1, 0)
|
||||
c._cast(me)
|
||||
var hurt := 0
|
||||
for u in c.units:
|
||||
if u.team == 1 and u.hp < u.max_hp:
|
||||
hurt += 1
|
||||
check(hurt >= 2, "Kette springt vom Tank auf zwei weitere Gegner (%d getroffen)" % hurt)
|
||||
check(Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1).ability_text().contains("Form Flächenbrand"),
|
||||
"Fähigkeitstext nennt die Form")
|
||||
|
||||
|
||||
func test_difficulty() -> void:
|
||||
print("[Schwierigkeit]")
|
||||
var rng := RandomNumberGenerator.new()
|
||||
var greed := {}
|
||||
for d in 3:
|
||||
rng.seed = 5
|
||||
var sum := 0.0
|
||||
for i in 30:
|
||||
var ai := BotAI.new()
|
||||
ai.setup(rng, d)
|
||||
sum += ai.greed
|
||||
greed[d] = sum / 30.0
|
||||
check(greed[Data.Difficulty.LEICHT] > greed[Data.Difficulty.NORMAL] and greed[Data.Difficulty.NORMAL] > greed[Data.Difficulty.SCHWER],
|
||||
"leicht hortet am meisten, schwer am wenigsten (%.2f / %.2f / %.2f)"
|
||||
% [greed[0], greed[1], greed[2]])
|
||||
var g := Game.new()
|
||||
g.start(9, Data.Difficulty.SCHWER)
|
||||
check(g.difficulty == Data.Difficulty.SCHWER and g.bots.values()[0].difficulty == Data.Difficulty.SCHWER,
|
||||
"Spiel reicht die Schwierigkeit an die Bots durch")
|
||||
|
||||
|
||||
func test_settings() -> void:
|
||||
print("[Einstellungen speichern]")
|
||||
var path := "user://settings_test.cfg"
|
||||
var st := Settings.new()
|
||||
st.volume = 0.35
|
||||
st.difficulty = Data.Difficulty.SCHWER
|
||||
st.save_to_disk(path)
|
||||
var back := Settings.new()
|
||||
back.load_from_disk(path)
|
||||
check(is_equal_approx(back.volume, 0.35) and back.difficulty == Data.Difficulty.SCHWER,
|
||||
"Lautstärke und Schwierigkeit kommen aus der Datei zurück")
|
||||
var fresh := Settings.new()
|
||||
fresh.load_from_disk("user://gibt_es_nicht.cfg")
|
||||
check(is_equal_approx(fresh.volume, 0.8) and fresh.difficulty == Data.Difficulty.NORMAL,
|
||||
"ohne Datei gelten die Standardwerte")
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||||
|
||||
|
||||
func test_healer_arrange() -> void:
|
||||
print("[Bot-Heiler stehen bei den Tanks]")
|
||||
var ai := BotAI.new()
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 4
|
||||
ai.setup(rng)
|
||||
var me := PlayerState.new(1, "Bot", true)
|
||||
me.level = 9
|
||||
for i in 3:
|
||||
me.board[Vector2i(i, 3)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
|
||||
for i in 2:
|
||||
me.board[Vector2i(3 + i, 3)] = Avatar.new(Data.Role.HEILER, [Data.Element.WASSER] as Array[int], 1)
|
||||
for i in 2:
|
||||
me.board[Vector2i(5 + i, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||
ai.arrange(me, null)
|
||||
var ok := true
|
||||
for hex in me.board:
|
||||
if me.board[hex].role != Data.Role.HEILER:
|
||||
continue
|
||||
var near := false
|
||||
for other in me.board:
|
||||
if me.board[other].role == Data.Role.TANK and Hex.distance(hex, other) <= Data.HEAL_RANGE:
|
||||
near = true
|
||||
ok = ok and near
|
||||
check(ok, "jeder Heiler erreicht einen Tank")
|
||||
|
||||
|
||||
func test_undo_sell() -> void:
|
||||
print("[Verkauf rückgängig]")
|
||||
var pool := Pool.new()
|
||||
var p := PlayerState.new(0, "T", false)
|
||||
p.gold = 5
|
||||
var a := Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||
a.invested = 2
|
||||
pool.take(Data.Element.FEUER)
|
||||
p.bench[2] = a
|
||||
var left := pool.element_left[Data.Element.FEUER]
|
||||
check(p.sell_bench(2, pool) and p.gold == 7 and pool.element_left[Data.Element.FEUER] == left + 1,
|
||||
"Verkauf gibt Gold und Vorrat zurück")
|
||||
check(p.undo_sell(pool), "Rückgängig geht")
|
||||
check(p.bench[2] == a and p.gold == 5 and pool.element_left[Data.Element.FEUER] == left,
|
||||
"Avatar liegt wieder da, Gold und Vorrat stimmen")
|
||||
check(not p.undo_sell(pool), "zweites Rückgängig ist wirkungslos")
|
||||
p.sell_bench(2, pool)
|
||||
p.gold = 0
|
||||
check(not p.undo_sell(pool), "ohne Gold kein Rückgängig")
|
||||
|
||||
|
||||
func test_pve_rounds() -> void:
|
||||
print("[Wildnis-Runden zum Warmlaufen]")
|
||||
var g := Game.new()
|
||||
g.start(61)
|
||||
check(g.is_pve_round(), "Runde 1 ist eine Wildnis-Runde")
|
||||
var opp := g.human_opponent()
|
||||
check(opp != null and opp.pname == "Wildnis" and opp.board.size() == 1, "Gegner ist die Wildnis mit einer Einheit")
|
||||
check(g.ranking().size() == 8, "die Wildnis zählt nicht als Spieler")
|
||||
var hp0 := g.human.hp
|
||||
g.start_combat()
|
||||
g.skip_combat()
|
||||
check(g.human.hp <= hp0, "gegen die Wildnis kann man Leben verlieren")
|
||||
check(g.round_no == 2 and g.human_opponent().pname == "Wildnis" and g.human_opponent().board.size() == 2,
|
||||
"Runde 2: Wildnis mit zwei Einheiten")
|
||||
g.start_combat()
|
||||
g.skip_combat()
|
||||
check(not g.is_pve_round() and g.human_opponent().pname != "Wildnis", "ab Runde %d spielen die Spieler gegeneinander" % (Data.PVE_ROUNDS + 1))
|
||||
var all_alive_before := g.alive_players().size()
|
||||
check(all_alive_before == 8, "niemand ist an der Wildnis gestorben")
|
||||
|
||||
|
||||
func test_stats() -> void:
|
||||
print("[Statistik]")
|
||||
var g := Game.new()
|
||||
g.start(71)
|
||||
var ai := BotAI.new()
|
||||
ai.setup(g.rand)
|
||||
for i in 5:
|
||||
ai.take_turn(g.human, g.pool, g.rand)
|
||||
g.start_combat()
|
||||
g.skip_combat()
|
||||
var st := g.human.stats
|
||||
check(st["wins"] + st["losses"] >= 4, "Siege und Niederlagen werden gezählt (%d/%d)" % [st["wins"], st["losses"]])
|
||||
check(st["gold_earned"] >= 5 * 5, "verdientes Gold summiert (%d)" % st["gold_earned"])
|
||||
check(st["spent"] > 0, "ausgegebenes Gold summiert (%d)" % st["spent"])
|
||||
|
||||
|
||||
func test_save_load() -> void:
|
||||
print("[Speichern und Laden]")
|
||||
var path := "user://spielstand_test.json"
|
||||
var g := Game.new()
|
||||
g.start(81)
|
||||
var ai := BotAI.new()
|
||||
ai.setup(g.rand)
|
||||
for i in 4:
|
||||
ai.take_turn(g.human, g.pool, g.rand)
|
||||
g.start_combat()
|
||||
g.skip_combat()
|
||||
g.human.shop_locked = true
|
||||
check(g.save(path), "Spielstand geschrieben")
|
||||
check(Game.has_save(path), "Datei ist da")
|
||||
var l := Game.load_from(path)
|
||||
check(l != null, "Spielstand gelesen")
|
||||
check(l.round_no == g.round_no and l.human.gold == g.human.gold and l.human.hp == g.human.hp,
|
||||
"Runde, Gold, Leben stimmen")
|
||||
check(l.human.board.size() == g.human.board.size() and l.human.level == g.human.level,
|
||||
"Brett und Level stimmen")
|
||||
check(l.human.shop_locked and l.human.shop.size() == g.human.shop.size(), "Shop samt Einfrieren stimmt")
|
||||
var same_pool := true
|
||||
for e in Data.ELEMENT_COUNT:
|
||||
if l.pool.element_left[e] != g.pool.element_left[e]:
|
||||
same_pool = false
|
||||
check(same_pool, "geteilter Vorrat stimmt")
|
||||
check(l.players.size() == 8 and l.bots.size() == 7 and l.human_opponent() != null,
|
||||
"acht Spieler, sieben Bots, Gegner bekannt")
|
||||
var fired := 0
|
||||
while l.phase != Game.Phase.GAMEOVER and fired < 60:
|
||||
l.start_combat()
|
||||
if l.phase == Game.Phase.COMBAT:
|
||||
l.skip_combat()
|
||||
fired += 1
|
||||
check(l.phase == Game.Phase.GAMEOVER or fired == 60, "geladene Partie läuft weiter (%d Runden)" % fired)
|
||||
Game.delete_save(path)
|
||||
check(not Game.has_save(path), "Spielstand gelöscht")
|
||||
|
||||
|
||||
func test_counters() -> void:
|
||||
print("[Schere-Stein-Papier]")
|
||||
# Ring: jedes Element schlägt genau eines und wird von genau einem geschlagen.
|
||||
var beaten := {}
|
||||
for e in Data.ELEMENT_COUNT:
|
||||
var target: int = Data.ELEMENT_BEATS[e]
|
||||
check(target != e, "%s schlägt nicht sich selbst" % Data.ELEMENT_NAMES[e])
|
||||
beaten[target] = beaten.get(target, 0) + 1
|
||||
var ring_ok := beaten.size() == Data.ELEMENT_COUNT
|
||||
for e in beaten:
|
||||
ring_ok = ring_ok and beaten[e] == 1
|
||||
check(ring_ok, "geschlossener Ring: jedes Element genau einmal geschlagen")
|
||||
check(Data.beats_element(Data.Element.WASSER, Data.Element.FEUER) and not Data.beats_element(Data.Element.FEUER, Data.Element.WASSER),
|
||||
"Wasser schlägt Feuer, nicht umgekehrt")
|
||||
check(Data.beaten_by(Data.Element.FEUER) == Data.Element.WASSER, "Feuer wird von Wasser geschlagen")
|
||||
check(Data.beats_role(Data.Role.TANK, Data.Role.DPS) and Data.beats_role(Data.Role.DPS, Data.Role.HEILER)
|
||||
and Data.beats_role(Data.Role.HEILER, Data.Role.TANK), "Rollen-Dreieck Tank > DPS > Heiler > Tank")
|
||||
check(is_equal_approx(Data.counter_multiplier(Data.Element.WASSER, Data.Role.TANK, Data.Element.FEUER, Data.Role.DPS), 1.35),
|
||||
"doppelter Konter gibt +35%")
|
||||
check(is_equal_approx(Data.counter_multiplier(Data.Element.FEUER, Data.Role.DPS, Data.Element.WASSER, Data.Role.TANK), 0.65),
|
||||
"doppelt gekontert gibt -35%")
|
||||
check(is_equal_approx(Data.counter_multiplier(Data.Element.FEUER, Data.Role.DPS, Data.Element.WIND, Data.Role.DPS), 1.0),
|
||||
"ohne Bezug bleibt es bei 100%")
|
||||
|
||||
# Im Kampf: Wasser-Schütze gegen Feuer-Ziel trifft härter als gegen Wind-Ziel.
|
||||
var a := PlayerState.new(0, "A", false)
|
||||
a.board[Vector2i(3, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.WASSER] as Array[int], 1)
|
||||
var b := PlayerState.new(1, "B", false)
|
||||
b.board[Vector2i(3, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||
var c := Combat.new(a, b, 3)
|
||||
c.record_hits = true
|
||||
var me: Combat.CUnit = c.units[0] if c.units[0].team == 0 else c.units[1]
|
||||
var foe: Combat.CUnit = c.units[1] if c.units[0].team == 0 else c.units[0]
|
||||
var hp0: float = foe.hp
|
||||
c._apply_damage(me, foe, 100.0, false)
|
||||
var dealt_vs_fire: float = hp0 - foe.hp
|
||||
check(is_equal_approx(dealt_vs_fire, 100.0 * 1.2 * (1.0 - foe.armor)), "Wasser gegen Feuer: +20% (%.0f)" % dealt_vs_fire)
|
||||
check(c.hits.size() == 1 and c.hits[0]["kind"] == "counter", "Konter-Treffer wird als solcher gemeldet")
|
||||
var b2 := PlayerState.new(1, "B", false)
|
||||
b2.board[Vector2i(3, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1)
|
||||
var c2 := Combat.new(a, b2, 3)
|
||||
var me2: Combat.CUnit = c2.units[0] if c2.units[0].team == 0 else c2.units[1]
|
||||
var foe2: Combat.CUnit = c2.units[1] if c2.units[0].team == 0 else c2.units[0]
|
||||
var hp1: float = foe2.hp
|
||||
c2._apply_damage(me2, foe2, 100.0, false)
|
||||
check(is_equal_approx(hp1 - foe2.hp, 100.0 * (1.0 - foe2.armor)), "Wasser gegen Wind: normal")
|
||||
check(Avatar.new(Data.Role.TANK, [Data.Element.FEUER] as Array[int], 1).counter_text()
|
||||
== "Stark gegen Eis, DPS · Schwach gegen Wasser, Heiler", "Text nennt Stärken und Schwächen")
|
||||
|
||||
|
||||
func test_bot_counter_pick() -> void:
|
||||
print("[Bots kontern den Gegner]")
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 12
|
||||
var ai := BotAI.new()
|
||||
ai.setup(rng, Data.Difficulty.SCHWER)
|
||||
var me := PlayerState.new(1, "Bot", true)
|
||||
var opp := PlayerState.new(2, "Gegner", true)
|
||||
for i in 3:
|
||||
opp.board[Vector2i(i, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||
ai.opponent = opp
|
||||
check(ai._pick_element(me) == Data.Element.WASSER, "schwerer Bot wählt Wasser gegen ein Feuer-Team")
|
||||
ai.opponent = null
|
||||
check(ai.pref.has(ai._pick_element(me)), "ohne Gegner bleibt er bei seinen Vorlieben")
|
||||
var easy := BotAI.new()
|
||||
easy.setup(rng, Data.Difficulty.NORMAL)
|
||||
easy.pref = [Data.Element.ERDE, Data.Element.EIS, Data.Element.BLITZ]
|
||||
easy.opponent = opp
|
||||
check(easy._pick_element(me) != Data.Element.WASSER, "normaler Bot kontert nur innerhalb seiner Vorlieben")
|
||||
easy.pref = [Data.Element.WASSER, Data.Element.EIS, Data.Element.BLITZ]
|
||||
check(easy._pick_element(me) == Data.Element.WASSER, "… und nimmt den Konter, wenn er dazu passt")
|
||||
|
||||
Reference in New Issue
Block a user