first commit
This commit is contained in:
68
tests/CamCal.gd
Normal file
68
tests/CamCal.gd
Normal file
@@ -0,0 +1,68 @@
|
||||
extends Node3D
|
||||
|
||||
## Rendert dieselbe Aufstellung mit verschiedenen Kameraeinstellungen.
|
||||
|
||||
var main: Node3D
|
||||
var out_dir := "user://shots"
|
||||
|
||||
const SETS := [
|
||||
{"pos": Vector3(0, 17.0, 15.5), "fov": 42.0, "h": 0.0, "v": 0.0},
|
||||
{"pos": Vector3(0, 17.0, 15.5), "fov": 42.0, "h": 0.4, "v": -1.0},
|
||||
{"pos": Vector3(0, 17.0, 15.5), "fov": 42.0, "h": 0.4, "v": 1.0},
|
||||
{"pos": Vector3(0, 19.0, 17.0), "fov": 40.0, "h": 0.4, "v": 1.4},
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
_run()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
var p: PlayerState = main.game.human
|
||||
main.game.round_no = 14
|
||||
var el := [Data.Element.FEUER, Data.Element.ERDE, Data.Element.WASSER]
|
||||
var i := 0
|
||||
for hex in [Vector2i(1, 0), Vector2i(5, 0), Vector2i(3, 1), Vector2i(0, 3), Vector2i(6, 3)]:
|
||||
p.board[hex] = Avatar.new(i % 3, [el[i % 3]] as Array[int], 1)
|
||||
i += 1
|
||||
main._refresh()
|
||||
# Volles Board: alle 4 Reihen belegen, damit die Einpassung sichtbar wird.
|
||||
p.board.clear()
|
||||
main.game.round_no = 20
|
||||
var k := 0
|
||||
for r in Data.BOARD_ROWS:
|
||||
for c in [0, 3, 6]:
|
||||
p.board[Vector2i(c, r)] = Avatar.new(k % 3, [el[k % 3]] as Array[int], 1)
|
||||
k += 1
|
||||
main._refresh()
|
||||
await get_tree().create_timer(0.4).timeout
|
||||
await RenderingServer.frame_post_draw
|
||||
get_viewport().get_texture().get_image().save_png("%s/fit.png" % out_dir)
|
||||
print("Kamera: ", main.camera.global_position, " blickt auf ", main.camera.global_position - main.camera.global_transform.basis.z * 10.0)
|
||||
# Trefferprüfung: jedes Feld muss unter seiner eigenen Bildschirmposition liegen.
|
||||
var bad := 0
|
||||
for r in Data.BOARD_ROWS:
|
||||
for c in Data.BOARD_COLS:
|
||||
var bh := Vector2i(c, r)
|
||||
var sp: Vector2 = main.board.slot_screen({"kind": "board", "hex": bh})
|
||||
var hit: Dictionary = main._pick_slot(sp)
|
||||
if hit.get("kind", "") != "board" or hit["hex"] != bh:
|
||||
bad += 1
|
||||
print(" Treffer daneben bei ", bh, " -> ", hit, " (Bildschirm ", sp, ")")
|
||||
print("Trefferprüfung Board: ", 28 - bad, "/28 Felder korrekt")
|
||||
var bad2 := 0
|
||||
for b in Data.BENCH_SLOTS:
|
||||
var sp2: Vector2 = main.board.slot_screen({"kind": "bench", "index": b})
|
||||
var hit2: Dictionary = main._pick_slot(sp2)
|
||||
if hit2.get("kind", "") != "bench" or hit2["index"] != b:
|
||||
bad2 += 1
|
||||
print(" Bankplatz daneben: ", b, " -> ", hit2)
|
||||
print("Trefferprüfung Bank: ", Data.BENCH_SLOTS - bad2, "/", Data.BENCH_SLOTS, " Plätze korrekt")
|
||||
get_tree().quit()
|
||||
141
tests/Probe2.gd
Normal file
141
tests/Probe2.gd
Normal file
@@ -0,0 +1,141 @@
|
||||
extends Node3D
|
||||
|
||||
## Zweiter Durchlauf: Element-Auswahl, 3-Star, grosser Kampf, Spielende.
|
||||
|
||||
var main: Node3D
|
||||
var out_dir := "user://shots"
|
||||
var shot_no := 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)
|
||||
_run()
|
||||
|
||||
|
||||
func _wait(s: float) -> 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])
|
||||
print("SHOT ", n)
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
await _wait(0.5)
|
||||
var p: PlayerState = main.game.human
|
||||
|
||||
# 1) Spezialisierung beim Kauf.
|
||||
p.gold = 200
|
||||
main._on_buy(0)
|
||||
await _wait(0.4)
|
||||
print("Picker sichtbar: ", main.picker.visible)
|
||||
await _shot("kauf_spezialisierung")
|
||||
main.picker.hide()
|
||||
main.picker_cb.call(Data.Element.FEUER)
|
||||
await _wait(0.2)
|
||||
print("Gekauft: ", p.bench[0].display_name())
|
||||
p.move_slot({"kind": "bench", "index": 0}, {"kind": "board", "hex": Vector2i(3, 1)},
|
||||
main.game.round_no)
|
||||
main._refresh()
|
||||
await _wait(0.2)
|
||||
print("Aufgestellt: ", p.board.get(Vector2i(3, 1)).display_name())
|
||||
|
||||
# 2) Bis 3-Star aufwerten.
|
||||
for i in 2:
|
||||
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||
p.upgrade(Vector2i(3, 1), Data.Element.WIND, main.game.pool)
|
||||
for i in 3:
|
||||
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||
for i in range(3, 6):
|
||||
p.bench[i] = Avatar.new(Data.Role.TANK, [Data.Element.WIND] as Array[int], 1)
|
||||
main.sel_slot = {"kind": "board", "hex": Vector2i(3, 1)}
|
||||
main._refresh()
|
||||
await _wait(0.3)
|
||||
print("3-Star möglich: ", p.can_upgrade(Vector2i(3, 1)))
|
||||
await _shot("dreistern_bereit")
|
||||
p.upgrade(Vector2i(3, 1), Data.Element.BLITZ, main.game.pool)
|
||||
main._refresh()
|
||||
var a: Avatar = p.board[Vector2i(3, 1)]
|
||||
print("3-Star: ", a.display_name(), " star=", a.star, " hp=", int(a.base_stats()["hp"]))
|
||||
await _wait(0.3)
|
||||
await _shot("dreistern")
|
||||
|
||||
# 3) Grosses Team für einen sichtbaren Kampf.
|
||||
main.game.round_no = 14
|
||||
var setup := [
|
||||
[Vector2i(2, 0), Data.Role.TANK, Data.Element.ERDE],
|
||||
[Vector2i(4, 0), Data.Role.TANK, Data.Element.ERDE],
|
||||
[Vector2i(2, 1), Data.Role.DPS, Data.Element.FEUER],
|
||||
[Vector2i(4, 1), Data.Role.DPS, Data.Element.FEUER],
|
||||
[Vector2i(3, 3), Data.Role.HEILER, Data.Element.WASSER],
|
||||
[Vector2i(2, 3), Data.Role.HEILER, Data.Element.EIS],
|
||||
[Vector2i(5, 2), Data.Role.DPS, Data.Element.WIND],
|
||||
]
|
||||
for e in setup:
|
||||
var av := Avatar.new(e[1], [e[2]] as Array[int], 1)
|
||||
p.board[e[0]] = av
|
||||
main.game.human.unlock(e[2])
|
||||
main._refresh()
|
||||
await _wait(0.3)
|
||||
print("Traits: ", p.trait_counts(), " Kombis: ", p.active_combos().size())
|
||||
await _shot("volles_team")
|
||||
|
||||
main._on_ready()
|
||||
await _wait(1.5)
|
||||
await _shot("grosser_kampf")
|
||||
main.combat_speed = 4.0
|
||||
await _wait(3.0)
|
||||
await _shot("kampf_spaeter")
|
||||
if main.game.phase == Game.Phase.COMBAT:
|
||||
main._on_ready()
|
||||
await _wait(0.4)
|
||||
print("Ergebnis: ", main.game.last_round_log)
|
||||
|
||||
# 4) Bis zum Spielende durchlaufen.
|
||||
var guard := 0
|
||||
while main.game.phase != Game.Phase.GAMEOVER and guard < 120:
|
||||
var ai := BotAI.new()
|
||||
ai.setup(main.game.rand)
|
||||
if main.game.human.alive:
|
||||
ai.take_turn(main.game.human, main.game.pool, main.game.rand, main.game.round_no)
|
||||
main._on_ready()
|
||||
if main.game.phase == Game.Phase.COMBAT:
|
||||
main.game.skip_combat()
|
||||
guard += 1
|
||||
main._refresh()
|
||||
await _wait(0.5)
|
||||
print("Spielende nach Runde ", main.game.round_no,
|
||||
" Platz des Spielers: ", main.game.human.placement)
|
||||
await _shot("spielende")
|
||||
|
||||
# 5) Neustart über den Knopf im Overlay.
|
||||
var btn := _find_button(main.overlay, "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)
|
||||
await _shot("neustart")
|
||||
print("PROBE2 FERTIG")
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func _find_button(node: Node, text: String) -> Button:
|
||||
if node == null:
|
||||
return null
|
||||
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
|
||||
1
tests/Probe2.gd.uid
Normal file
1
tests/Probe2.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://djhq2cshy63mj
|
||||
394
tests/Probe3.gd
Normal file
394
tests/Probe3.gd
Normal file
@@ -0,0 +1,394 @@
|
||||
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
|
||||
1
tests/Probe3.gd.uid
Normal file
1
tests/Probe3.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cf5y2v8d1k5pb
|
||||
6
tests/camcal.tscn
Normal file
6
tests/camcal.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/CamCal.gd" id="1"]
|
||||
|
||||
[node name="CamCal" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
7
tests/prev_check.gd
Normal file
7
tests/prev_check.gd
Normal file
@@ -0,0 +1,7 @@
|
||||
extends SceneTree
|
||||
func _initialize() -> void:
|
||||
var a := Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||
var b := Avatar.new(a.role, a.elements + [Data.Element.WIND] as Array[int], a.star + 1)
|
||||
print("vorher: ", a.elements, " star ", a.star, " hp ", int(a.base_stats()["hp"]))
|
||||
print("nachher: ", b.elements, " star ", b.star, " hp ", int(b.base_stats()["hp"]))
|
||||
quit()
|
||||
6
tests/probe2.tscn
Normal file
6
tests/probe2.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/Probe2.gd" id="1"]
|
||||
|
||||
[node name="Probe2" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
6
tests/probe3.tscn
Normal file
6
tests/probe3.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/Probe3.gd" id="1"]
|
||||
|
||||
[node name="Probe3" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
442
tests/sim_test.gd
Normal file
442
tests/sim_test.gd
Normal file
@@ -0,0 +1,442 @@
|
||||
extends SceneTree
|
||||
|
||||
## Kopflose Prüfläufe für die Kernlogik.
|
||||
|
||||
var failures := 0
|
||||
|
||||
|
||||
func check(cond: bool, msg: String) -> void:
|
||||
if cond:
|
||||
print(" OK ", msg)
|
||||
else:
|
||||
failures += 1
|
||||
print(" FAIL ", msg)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
test_hex()
|
||||
test_upgrade_chain()
|
||||
test_pool_shared()
|
||||
test_combat_runs()
|
||||
test_full_game()
|
||||
test_placements()
|
||||
test_trait_effect()
|
||||
test_buy_specialisation()
|
||||
test_own_shop()
|
||||
test_move_slot()
|
||||
test_interest()
|
||||
test_prices()
|
||||
test_pool_pressure()
|
||||
test_neutral_chance()
|
||||
print("")
|
||||
if failures == 0:
|
||||
print("ALLE TESTS BESTANDEN")
|
||||
else:
|
||||
print("FEHLER: ", failures)
|
||||
quit(1 if failures > 0 else 0)
|
||||
|
||||
|
||||
func test_hex() -> void:
|
||||
print("[Hex]")
|
||||
check(Hex.distance(Vector2i(0, 0), Vector2i(0, 0)) == 0, "Distanz zu sich selbst = 0")
|
||||
check(Hex.distance(Vector2i(0, 0), Vector2i(1, 0)) == 1, "Nachbar rechts = 1")
|
||||
for n in Hex.neighbors(Vector2i(2, 2)):
|
||||
if Hex.distance(Vector2i(2, 2), n) != 1:
|
||||
check(false, "Nachbar hat Distanz 1: " + str(n))
|
||||
return
|
||||
for n in Hex.neighbors(Vector2i(2, 3)):
|
||||
if Hex.distance(Vector2i(2, 3), n) != 1:
|
||||
check(false, "Nachbar (ungerade Zeile) hat Distanz 1: " + str(n))
|
||||
return
|
||||
check(true, "alle 6 Nachbarn haben Distanz 1 (gerade und ungerade Zeile)")
|
||||
|
||||
|
||||
func test_upgrade_chain() -> void:
|
||||
print("[Aufwerten]")
|
||||
var pool := Pool.new()
|
||||
var p := PlayerState.new(0, "Test", false)
|
||||
p.gold = 100
|
||||
# Beim Kauf gewählter Feuer-Avatar wird aufgestellt.
|
||||
p.bench[0] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||
var ok := p.place(0, Vector2i(3, 1), 10)
|
||||
check(ok, "Feuer-Avatar von der Bank aufstellen")
|
||||
check(p.unlocked[Data.Element.FEUER], "Feuer erscheint danach im eigenen Shop")
|
||||
|
||||
var hex := Vector2i(3, 1)
|
||||
check(not p.can_upgrade(hex), "ohne Futter kein Upgrade")
|
||||
check(p.upgrade_missing(hex).get(Data.Element.FEUER, 0) == 2, "es fehlen 2 Feuer-Avatare")
|
||||
|
||||
p.bench[0] = Avatar.new(Data.Role.TANK, [Data.Element.FEUER], 1)
|
||||
p.bench[1] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
||||
check(p.can_upgrade(hex), "mit 2 Feuer-Avataren ist 2-Star möglich")
|
||||
check(p.upgrade(hex, Data.Element.WIND, pool), "Upgrade auf 2-Star mit Wind als 2. Element")
|
||||
var a: Avatar = p.board[hex]
|
||||
check(a.star == 2 and a.elements == [Data.Element.FEUER, Data.Element.WIND], "2-Star Feuer/Wind")
|
||||
check(p.bench[0] == null and p.bench[1] == null, "Futter wurde verbraucht")
|
||||
|
||||
var miss := p.upgrade_missing(hex)
|
||||
check(miss.get(Data.Element.FEUER, 0) == 3 and miss.get(Data.Element.WIND, 0) == 3,
|
||||
"3-Star braucht 3 Feuer und 3 Wind")
|
||||
for i in 3:
|
||||
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
||||
for i in range(3, 6):
|
||||
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.WIND], 1)
|
||||
check(p.can_upgrade(hex), "mit 3+3 Futter ist 3-Star möglich")
|
||||
check(p.upgrade(hex, Data.Element.BLITZ, pool), "Upgrade auf 3-Star mit Blitz")
|
||||
a = p.board[hex]
|
||||
check(a.star == 3 and a.elements.size() == 3, "3-Star mit drei Elementen")
|
||||
check(p.upgrade_cost().is_empty() if false else a.upgrade_cost().is_empty(), "3-Star ist Maximum")
|
||||
var s1 := Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1).base_stats()
|
||||
check(a.base_stats()["hp"] > s1["hp"] * 3.0, "Stats steigen deutlich pro Stern")
|
||||
|
||||
|
||||
func test_pool_shared() -> void:
|
||||
print("[Geteilter Pool]")
|
||||
var pool := Pool.new()
|
||||
var p := PlayerState.new(0, "Test", false)
|
||||
p.unlock(Data.Element.FEUER)
|
||||
var taken := 0
|
||||
while pool.take(Data.Element.FEUER):
|
||||
taken += 1
|
||||
check(taken == Data.POOL_PER_ELEMENT, "Pro Element nur %d Avatare insgesamt" % Data.POOL_PER_ELEMENT)
|
||||
check(p.shop_elements(pool).is_empty(), "leergekauftes Element verschwindet aus dem Shop")
|
||||
|
||||
|
||||
func test_combat_runs() -> void:
|
||||
print("[Kampf]")
|
||||
var pool := Pool.new()
|
||||
var a := PlayerState.new(0, "A", false)
|
||||
var b := PlayerState.new(1, "B", true)
|
||||
for i in 4:
|
||||
a.bench[i] = Avatar.new(i % 3, [Data.Element.FEUER] as Array[int], 1)
|
||||
a.place(i, Vector2i(2 + i, i % 2), 20)
|
||||
for i in 4:
|
||||
b.bench[i] = Avatar.new(i % 3, [Data.Element.EIS] as Array[int], 1)
|
||||
b.place(i, Vector2i(2 + i, i % 2), 20)
|
||||
var c := Combat.new(a, b, 12345)
|
||||
check(c.units.size() == 8, "8 Einheiten in der Arena")
|
||||
var team0 := 0
|
||||
for u in c.units:
|
||||
if u.team == 0:
|
||||
team0 += 1
|
||||
check(team0 == 4, "4 Einheiten pro Team")
|
||||
c.run_to_end()
|
||||
check(c.finished, "Kampf endet")
|
||||
check(c.winner in [-1, 0, 1], "Ergebnis ist gültig")
|
||||
print(" Sieger: ", c.winner, " Überlebende: ", c.survivors, " Dauer: %.1fs" % c.time)
|
||||
|
||||
# Trait-Wirkung: Feuer 4 muss den Angriff erhöhen.
|
||||
var counts := a.trait_counts()
|
||||
check(counts.get(Data.Element.FEUER, 0) == 4, "4 Feuer-Einheiten aufgestellt")
|
||||
check(Data.trait_tier(Data.Element.FEUER, 4) == 2, "Feuer-Trait auf Stufe 2")
|
||||
|
||||
|
||||
func test_full_game() -> void:
|
||||
print("[Kompletter Loop]")
|
||||
var g := Game.new()
|
||||
g.start(999)
|
||||
check(g.players.size() == 8, "8 Spieler (du + 7 Bots)")
|
||||
check(g.human.hp == 100 and g.human.gold == Data.START_GOLD, "Start: 100 Leben, %d Gold" % Data.START_GOLD)
|
||||
check(g.human.shop.size() == Data.SHOP_SLOTS, "Shop hat %d Plätze" % Data.SHOP_SLOTS)
|
||||
var only_neutral := true
|
||||
for e in g.human.shop:
|
||||
if e.element >= 0:
|
||||
only_neutral = false
|
||||
check(true, "Shop-Angebot Runde 1 (nur neutral: %s)" % str(only_neutral))
|
||||
|
||||
# Menschlichen Spieler wie einen Bot spielen lassen, damit der Loop läuft.
|
||||
var ai := BotAI.new()
|
||||
ai.setup(g.rand)
|
||||
var guard := 0
|
||||
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
||||
ai.take_turn(g.human, g.pool, g.rand, g.round_no)
|
||||
g.start_combat()
|
||||
if g.phase == Game.Phase.COMBAT:
|
||||
g.skip_combat()
|
||||
guard += 1
|
||||
check(g.phase == Game.Phase.GAMEOVER, "Spiel endet (Runden gespielt: %d)" % g.round_no)
|
||||
var alive := g.alive_players()
|
||||
check(alive.size() == 1, "genau ein Gewinner")
|
||||
if alive.size() == 1:
|
||||
print(" Sieger: ", alive[0].pname, " mit ", alive[0].hp, " Leben")
|
||||
var sum_hp := 0
|
||||
for p in g.players:
|
||||
sum_hp += p.hp
|
||||
check(sum_hp < 8 * Data.START_HP, "Leben wurden verloren")
|
||||
var any_upgraded := false
|
||||
var max_star := 1
|
||||
for p in g.players:
|
||||
for u in p.board_units():
|
||||
max_star = maxi(max_star, u.star)
|
||||
check(max_star >= 2, "mindestens ein Avatar wurde aufgewertet (max %d-Star)" % max_star)
|
||||
var used := 0
|
||||
for e in Data.ELEMENT_COUNT:
|
||||
used += Data.POOL_PER_ELEMENT - g.pool.element_left[e]
|
||||
check(used > 0, "Bots verbrauchen den geteilten Pool (%d Avatare gekauft)" % used)
|
||||
|
||||
|
||||
func test_placements() -> void:
|
||||
print("[Platzierungen]")
|
||||
var g := Game.new()
|
||||
g.start(4242)
|
||||
var ai := BotAI.new()
|
||||
ai.setup(g.rand)
|
||||
var guard := 0
|
||||
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
||||
if g.human.alive:
|
||||
ai.take_turn(g.human, g.pool, g.rand, g.round_no)
|
||||
g.start_combat()
|
||||
if g.phase == Game.Phase.COMBAT:
|
||||
g.skip_combat()
|
||||
guard += 1
|
||||
var seen := {}
|
||||
var ok := true
|
||||
for p in g.players:
|
||||
if p.placement < 1 or p.placement > 8 or seen.has(p.placement):
|
||||
ok = false
|
||||
print(" ", p.pname, " Platz ", p.placement, " hp ", p.hp)
|
||||
seen[p.placement] = true
|
||||
check(ok, "jeder Spieler hat genau einen Platz 1..8")
|
||||
check(seen.size() == 8, "acht verschiedene Plätze vergeben")
|
||||
|
||||
|
||||
func test_trait_effect() -> void:
|
||||
print("[Trait-Wirkung]")
|
||||
var pool := Pool.new()
|
||||
# Team mit 4x Feuer -> Trait Stufe 2 (+26% Angriff).
|
||||
var strong := PlayerState.new(0, "Feuer4", false)
|
||||
for i in 4:
|
||||
strong.board[Vector2i(i, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
||||
# Team mit nur 1x Feuer -> kein Trait.
|
||||
var weak := PlayerState.new(1, "Gemischt", false)
|
||||
weak.board[Vector2i(0, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
||||
for i in range(1, 4):
|
||||
weak.board[Vector2i(i, 1)] = Avatar.new(Data.Role.DPS, [i], 1)
|
||||
|
||||
var c := Combat.new(strong, weak, 777)
|
||||
var atk_strong := 0.0
|
||||
var atk_weak := 0.0
|
||||
for u in c.units:
|
||||
if u.avatar.has_element(Data.Element.FEUER):
|
||||
if u.team == 0:
|
||||
atk_strong = u.atk
|
||||
else:
|
||||
atk_weak = u.atk
|
||||
check(atk_strong > atk_weak * 1.2,
|
||||
"Feuer-Trait erhöht den Angriff (%d statt %d)" % [int(atk_strong), int(atk_weak)])
|
||||
|
||||
# Kombi-Trait: Feuer 2 + Wind 2 muss Feuersturm auslösen.
|
||||
var combo := PlayerState.new(2, "Kombi", false)
|
||||
for i in 2:
|
||||
combo.board[Vector2i(i, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER], 1)
|
||||
combo.board[Vector2i(i, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.WIND], 1)
|
||||
var keys := []
|
||||
for x in combo.active_combos():
|
||||
keys.append(x["key"])
|
||||
check(keys.has("feuersturm"), "Feuer 2 + Wind 2 aktiviert Feuersturm")
|
||||
check(not keys.has("frost"), "Wasser/Eis bleibt ohne die Elemente inaktiv")
|
||||
|
||||
# Erde-Trait muss die Lebenspunkte im Kampf erhöhen.
|
||||
var earth := PlayerState.new(3, "Erde4", false)
|
||||
for i in 4:
|
||||
earth.board[Vector2i(i, 1)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE], 1)
|
||||
var c2 := Combat.new(earth, weak, 778)
|
||||
var base_hp: float = Avatar.new(Data.Role.TANK, [Data.Element.ERDE], 1).base_stats()["hp"]
|
||||
var got := 0.0
|
||||
for u in c2.units:
|
||||
if u.team == 0:
|
||||
got = u.max_hp
|
||||
check(got > base_hp * 1.2, "Erde-Trait erhöht Leben (%d statt %d)" % [int(got), int(base_hp)])
|
||||
|
||||
|
||||
func test_buy_specialisation() -> void:
|
||||
print("[Spezialisierung beim Kauf]")
|
||||
var pool := Pool.new()
|
||||
var p := PlayerState.new(0, "Test", false)
|
||||
p.gold = 20
|
||||
p.shop = [ShopEntry.new(Data.Role.TANK, -1)]
|
||||
check(p.shop[0].element < 0, "Shop bietet einen neutralen Avatar an")
|
||||
check(not p.buy(0, pool, -1), "Kauf ohne gewähltes Element wird abgelehnt")
|
||||
var before: int = pool.element_left[Data.Element.BLITZ]
|
||||
check(p.buy(0, pool, Data.Element.BLITZ), "Kauf mit Blitz als Spezialisierung")
|
||||
var a: Avatar = p.bench[0]
|
||||
check(a != null and a.elements == [Data.Element.BLITZ], "Avatar liegt als Blitz auf der Bank")
|
||||
check(pool.element_left[Data.Element.BLITZ] == before - 1,
|
||||
"der Kauf zieht einen Blitz aus dem geteilten Vorrat (%d -> %d)"
|
||||
% [before, pool.element_left[Data.Element.BLITZ]])
|
||||
check(not p.unlocked[Data.Element.BLITZ], "gekauft allein schaltet den Shop noch nicht frei")
|
||||
p.place(0, Vector2i(3, 1), 10)
|
||||
check(p.unlocked[Data.Element.BLITZ], "erst das Aufstellen schaltet Blitz im Shop frei")
|
||||
|
||||
|
||||
func test_own_shop() -> void:
|
||||
print("[Shop zeigt nur eigene Elemente]")
|
||||
var pool := Pool.new()
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 7
|
||||
var me := PlayerState.new(0, "Ich", false)
|
||||
var other := PlayerState.new(1, "Bot", true)
|
||||
other.unlock(Data.Element.EIS)
|
||||
me.unlock(Data.Element.FEUER)
|
||||
var mine := me.shop_elements(pool)
|
||||
var theirs := other.shop_elements(pool)
|
||||
check(mine.size() == 1 and mine[0] == Data.Element.FEUER, "eigener Shop kennt nur Feuer")
|
||||
check(theirs.size() == 1 and theirs[0] == Data.Element.EIS, "der Bot sieht nur sein eigenes Eis")
|
||||
var seen := {}
|
||||
for i in 200:
|
||||
me.roll_shop(pool, rng)
|
||||
for e in me.shop:
|
||||
seen[e.element] = true
|
||||
check(not seen.has(Data.Element.EIS), "fremdes Eis taucht im eigenen Shop nicht auf")
|
||||
check(seen.has(Data.Element.FEUER) and seen.has(-1), "eigener Shop zeigt Feuer und Neutral")
|
||||
|
||||
|
||||
func test_move_slot() -> void:
|
||||
print("[Verschieben zwischen Bank und Board]")
|
||||
var p := PlayerState.new(0, "Test", false)
|
||||
var f := Avatar.new(Data.Role.TANK, [Data.Element.FEUER] as Array[int], 1)
|
||||
var w := Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1)
|
||||
p.bench[0] = f
|
||||
p.bench[1] = w
|
||||
var b0 := {"kind": "bench", "index": 0}
|
||||
var b1 := {"kind": "bench", "index": 1}
|
||||
var b3 := {"kind": "bench", "index": 3}
|
||||
var h1 := {"kind": "board", "hex": Vector2i(2, 1)}
|
||||
var h2 := {"kind": "board", "hex": Vector2i(4, 2)}
|
||||
|
||||
check(p.move_slot(b0, h1, 10), "Bank -> Board")
|
||||
check(p.board.get(Vector2i(2, 1)) == f and p.bench[0] == null, "Feuer steht auf (2,1)")
|
||||
check(p.unlocked[Data.Element.FEUER], "Aufstellen schaltet Feuer frei")
|
||||
|
||||
check(p.move_slot(h1, h2, 10), "Board -> Board")
|
||||
check(p.board.get(Vector2i(4, 2)) == f and not p.board.has(Vector2i(2, 1)), "Feuer steht auf (4,2)")
|
||||
|
||||
check(p.move_slot(b1, h2, 10), "Bank auf ein belegtes Feld = Tausch")
|
||||
check(p.board.get(Vector2i(4, 2)) == w and p.bench[1] == f, "Wind steht, Feuer liegt auf der Bank")
|
||||
|
||||
check(p.move_slot(h2, b3, 10), "Board -> Bank")
|
||||
check(p.bench[3] == w and p.board.is_empty(), "Wind liegt wieder auf der Bank")
|
||||
|
||||
check(p.move_slot(b3, b1, 10), "Bank -> Bank ist ein Tausch")
|
||||
check(p.bench[1] == w and p.bench[3] == f, "beide haben die Plätze getauscht")
|
||||
|
||||
# Kaderplätze bleiben begrenzt.
|
||||
for i in 5:
|
||||
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.EIS] as Array[int], 1)
|
||||
var placed := 0
|
||||
for i in 5:
|
||||
if p.move_slot({"kind": "bench", "index": i}, {"kind": "board", "hex": Vector2i(i, 0)}, 1):
|
||||
placed += 1
|
||||
check(placed == 3 and p.board.size() == 3, "Runde 1 lässt nur 3 Einheiten zu (%d)" % placed)
|
||||
check(not p.move_slot({"kind": "board", "hex": Vector2i(9, 9)}, b0, 10),
|
||||
"Feld ausserhalb des Boards wird abgelehnt")
|
||||
|
||||
|
||||
func test_interest() -> void:
|
||||
print("[Zinsen]")
|
||||
check(Data.interest(0) == 0, "0 Gold -> 0 Zins")
|
||||
check(Data.interest(9) == 0, "9 Gold -> 0 Zins")
|
||||
check(Data.interest(10) == 1, "10 Gold -> 1 Zins")
|
||||
check(Data.interest(35) == 3, "35 Gold -> 3 Zins")
|
||||
check(Data.interest(49) == 4, "49 Gold -> 4 Zins")
|
||||
check(Data.interest(50) == 5, "50 Gold -> 5 Zins")
|
||||
check(Data.interest(200) == 5, "über 50 Gold bleibt es bei 5 Zins")
|
||||
|
||||
var p := PlayerState.new(0, "Test", false)
|
||||
p.gold = 0
|
||||
check(p.round_income() == Data.BASE_INCOME, "ohne Gold nur die 5 Grundgold")
|
||||
p.gold = 30
|
||||
check(p.round_income() == Data.BASE_INCOME + 3, "30 Gold -> 5 + 3 Zins")
|
||||
p.gold = 50
|
||||
p.register_result(true)
|
||||
check(p.round_income() == Data.BASE_INCOME + Data.WIN_GOLD + 5,
|
||||
"50 Gold mit Sieg -> 5 + 1 + 5 Zins")
|
||||
for i in 4:
|
||||
p.register_result(true)
|
||||
check(p.round_income() == Data.BASE_INCOME + Data.WIN_GOLD + 5 + Data.streak_gold(5),
|
||||
"Serie kommt oben drauf")
|
||||
|
||||
|
||||
func test_prices() -> void:
|
||||
print("[Preise]")
|
||||
check(Data.PRICE_NONE == 1, "neutraler Avatar kostet 1 Gold")
|
||||
check(Data.PRICE_ELEMENT == 2, "Element-Avatar kostet 2 Gold")
|
||||
var pool := Pool.new()
|
||||
var p := PlayerState.new(0, "Test", false)
|
||||
p.gold = 10
|
||||
p.shop = [ShopEntry.new(Data.Role.TANK, -1), ShopEntry.new(Data.Role.DPS, Data.Element.EIS)]
|
||||
check(p.shop[0].price() == 1 and p.shop[1].price() == 2, "Shop rechnet mit 1 und 2 Gold")
|
||||
p.buy(0, pool, Data.Element.FEUER)
|
||||
check(p.gold == 9, "neutraler Kauf zieht 1 Gold ab")
|
||||
p.buy(1, pool, -1)
|
||||
check(p.gold == 7, "Element-Kauf zieht 2 Gold ab")
|
||||
p.sell_bench(0, pool)
|
||||
check(p.gold == 8, "Verkauf gibt den Kaufpreis zurück")
|
||||
|
||||
|
||||
func test_pool_pressure() -> void:
|
||||
print("[Vorrat hält eine ganze Partie]")
|
||||
var lowest := Data.POOL_PER_ELEMENT
|
||||
var early_dry := 0
|
||||
for run in 4:
|
||||
var g := Game.new()
|
||||
g.start(4000 + run)
|
||||
var ai := BotAI.new()
|
||||
ai.setup(g.rand)
|
||||
var guard := 0
|
||||
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
||||
if g.human.alive:
|
||||
ai.take_turn(g.human, g.pool, g.rand, g.round_no)
|
||||
g.start_combat()
|
||||
if g.phase == Game.Phase.COMBAT:
|
||||
g.skip_combat()
|
||||
for e in Data.ELEMENT_COUNT:
|
||||
lowest = mini(lowest, g.pool.element_left[e])
|
||||
if g.round_no <= 15:
|
||||
for e in Data.ELEMENT_COUNT:
|
||||
if g.pool.element_left[e] <= 0:
|
||||
early_dry += 1
|
||||
guard += 1
|
||||
check(early_dry == 0, "bis Runde 15 läuft kein Element leer")
|
||||
check(lowest < Data.POOL_PER_ELEMENT / 2, "der Vorrat wird spürbar verbraucht (Tiefstand %d von %d)"
|
||||
% [lowest, Data.POOL_PER_ELEMENT])
|
||||
|
||||
|
||||
func test_neutral_chance() -> void:
|
||||
print("[Neutral-Chance sinkt je Element]")
|
||||
check(is_equal_approx(Data.neutral_chance(0), 1.0), "ohne eigene Elemente nur Neutral")
|
||||
check(is_equal_approx(Data.neutral_chance(1), 0.75), "1 Element -> 75% Neutral")
|
||||
check(is_equal_approx(Data.neutral_chance(2), 0.5625), "2 Elemente -> 56%")
|
||||
check(Data.neutral_chance(6) > 0.0, "Neutral verschwindet nie ganz (%.1f%%)"
|
||||
% (Data.neutral_chance(6) * 100.0))
|
||||
|
||||
var pool := Pool.new()
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 99
|
||||
var quotes := []
|
||||
for count in range(0, 5):
|
||||
var p := PlayerState.new(0, "Test", false)
|
||||
for e in count:
|
||||
p.unlock(e)
|
||||
var neutral := 0
|
||||
var total := 0
|
||||
for i in 400:
|
||||
p.roll_shop(pool, rng)
|
||||
for entry in p.shop:
|
||||
total += 1
|
||||
if entry.element < 0:
|
||||
neutral += 1
|
||||
quotes.append(float(neutral) / float(total))
|
||||
print(" gemessene Neutral-Quoten: ", quotes)
|
||||
check(is_equal_approx(quotes[0], 1.0), "0 Elemente: alles neutral")
|
||||
var falling := true
|
||||
for i in range(1, quotes.size()):
|
||||
if quotes[i] >= quotes[i - 1]:
|
||||
falling = false
|
||||
check(falling, "die Quote sinkt mit jedem weiteren Element")
|
||||
for i in quotes.size():
|
||||
if absf(quotes[i] - Data.neutral_chance(i)) > 0.05:
|
||||
check(false, "Quote bei %d Elementen weicht ab (%.2f statt %.2f)"
|
||||
% [i, quotes[i], Data.neutral_chance(i)])
|
||||
return
|
||||
check(true, "jede Quote trifft den erwarteten Wert auf 5 Prozentpunkte genau")
|
||||
1
tests/sim_test.gd.uid
Normal file
1
tests/sim_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bpluiwuwx5rtr
|
||||
Reference in New Issue
Block a user