Spiel untersucht und optimiert: KI-Suche, Hintergrund-Threads, Dialog-Helfer

- KI-Vorausschau in zwei Stufen mit Zielvorauswahl (höchstens 4 Ziele je Karte
  und Angreifer, die 8 besten Erstschritte vertieft): 5 Gegner 2063 ms -> 101 ms,
  1 Gegner 160 ms -> 36 ms, alle Kandidaten im Budget; 52:48 gegen die alte Suche
- KI rechnet im WorkerThreadPool auf einer Spielkopie, schon während der Wartezeit;
  längste Bildzeit im Kampf mit 5 Gegnern 108 ms -> 38 ms
- Musik wird im Hintergrund erzeugt (Start 185 ms schneller)
- Style.overlay und Style.confirm ersetzen vier kopierte Dialoge
- Übersetzungen für Abbrechen und Zurücksetzen, Archetypen neu gemessen
- tests/perf_probe.gd (Laufzeiten) und tests/frame_probe.tscn (Bildzeiten)

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:53:27 +02:00
parent e1e6211ef8
commit b6d5916a8d
16 changed files with 307 additions and 86 deletions

View File

@@ -40,6 +40,7 @@ Der Spielstand liegt unter `user://spielstand.json`
- KI mit Zeitbudget je Entscheidung (150 ms), Warnung bei ungespeichertem Deck
- Lava trifft nur noch das stärkste gegnerische Wesen; Archetypen-Werte kommen aus `data/archetypen.json` (Messlauf schreibt sie)
- Kartentext passt sich der Schriftgröße an, Gamepad auch in Menüs (Fokusrahmen), Anleitung 2 erzwingt Maras Angriff in die Falle
- KI-Vorausschau in zwei Stufen (erst alle Züge mit den besten Zielen, dann die acht besten vertieft): bei 5 Gegnern 0,1 s statt 2 s, und sie rechnet im Hintergrund-Thread, Musik ebenso
- Übersetzungsprüfung: jeder tr()-Text und jeder Anleitungsschritt braucht einen englischen Eintrag
## Tests
@@ -56,6 +57,8 @@ godot --headless --path . -s tests/simulate.gd # Balance: KI spielt gegen K
godot --headless --path . -s tests/matchup.gd # Startvorteil und Mono gegen Paar
godot --headless --path . -s tests/strength_probe.gd # Stärke der Elementkombinationen messen (Werte in DeckGen.ARCHETYPE_STRENGTH)
DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fenster und Screenshots
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Deckerzeugung, Musik
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf mit 5 Gegnern
```
## Export

View File

@@ -1,12 +1,12 @@
{
"Erde": 55,
"Erde": 61,
"Erde+Luft": 78,
"Feuer": 36,
"Feuer+Erde": 65,
"Feuer": 41,
"Feuer+Erde": 66,
"Feuer+Luft": 46,
"Feuer+Wasser": 50,
"Luft": 55,
"Wasser": 40,
"Wasser+Erde": 51,
"Wasser+Erde": 43,
"Wasser+Luft": 45
}

View File

@@ -1,7 +1,9 @@
class_name AI
## KI mit einfacher Vorausschau: jede mögliche Aktion wird auf einer Kopie des
## Spiels ausgeführt und die Stellung danach bewertet. Die gierige Variante
## (choose_action_greedy) dient als Vergleich.
## KI mit Vorausschau: jede mögliche Aktion wird auf einer Kopie des Spiels
## ausgeführt und die Stellung danach bewertet; die besten Erstschritte werden um
## einen Folgeschritt vertieft. Die gierige Variante (choose_action_greedy) dient
## als Vergleich und für die Schwierigkeit Leicht. Alles hier ist zustandslos und
## darf in einem Hintergrund-Thread auf einer Spielkopie laufen.
# --- Bewertung -------------------------------------------------------------------
@@ -52,27 +54,88 @@ static func evaluate(g: Game, p: Game.Player) -> float:
# --- Vorausschau ---------------------------------------------------------------
static func candidate_actions(game: Game, p: Game.Player) -> Array:
const MAX_TARGETS := 4 # je Karte bzw. Angreifer nur die aussichtsreichsten Ziele prüfen
const DEEPEN := 8 # so viele beste Erstschritte bekommen Folgeschritte
const FOLLOW_UPS := 10 # Folgeschritte je vertieftem Kandidat
static var time_budget_ms := 150 # Zeitbudget je Entscheidung; danach der beste bisherige Zug
## Grobe Rangfolge eines Ziels für einen Karteneffekt (nur zum Vorsortieren).
static func _target_rank(kind: String, amount: int, t) -> float:
if t is Game.Player:
return 50.0 - t.life if kind == "damage" else 0.0
match kind:
"damage":
return 100.0 + _value(t) if not t.shield and t.health <= amount else float(_value(t))
"freeze", "bounce":
return t.card.cost * 10.0 + t.power()
"buff", "shield":
return (100.0 if t.can_attack() else 0.0) + t.power()
return float(_value(t))
## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, dann Helden, dann der Rest.
static func _attack_rank(cr: Game.Creature, t) -> float:
if t is Game.Player:
return 50.0 - t.life
if _kills(cr, t):
return (100.0 if not _kills(t, cr) else 60.0) + _value(t)
return -float(t.health)
static func _top(list: Array, rank: Callable, n: int) -> Array:
if list.size() <= n:
return list
var ranked := []
for t in list:
ranked.append([rank.call(t), t])
ranked.sort_custom(func(a, b): return a[0] > b[0])
return ranked.slice(0, n).map(func(pair): return pair[1])
## Mögliche Aktionen; mit prune nur die MAX_TARGETS besten Ziele je Karte und Angreifer.
static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -> Array:
var out := []
var valid_cache := {}
for card in p.hand:
if not game.can_play(p, card)[0]:
continue
var tt: String = card.target_type()
if tt == "":
out.append({"type": "play", "card": card, "target": null})
else:
var valid := game.valid_targets(p, tt)
if valid.is_empty():
out.append({"type": "play", "card": card, "target": null})
for t in valid:
out.append({"type": "play", "card": card, "target": t})
continue
if not valid_cache.has(tt):
valid_cache[tt] = game.valid_targets(p, tt)
var valid: Array = valid_cache[tt]
if valid.is_empty():
out.append({"type": "play", "card": card, "target": null})
continue
if prune:
var fx: Cards.Effect = null
for f in card.effects:
if f.target == tt:
fx = f
break
var kind: String = fx.kind if fx != null else ""
var amount: int = fx.amount if fx != null else 0
var key := "%s/%s/%d" % [tt, kind, amount]
if not valid_cache.has(key):
valid_cache[key] = _top(valid, func(t): return _target_rank(kind, amount, t), MAX_TARGETS)
valid = valid_cache[key]
for t in valid:
out.append({"type": "play", "card": card, "target": t})
for cr in p.board:
if not cr.can_attack():
continue
var targets := []
for e in game.enemies_of(p):
for t in e.board + [e]:
if game.can_attack(cr, t)[0]:
out.append({"type": "attack", "creature": cr, "target": t})
targets.append(t)
if prune:
targets = _top(targets, func(t): return _attack_rank(cr, t), MAX_TARGETS)
for t in targets:
out.append({"type": "attack", "creature": cr, "target": t})
return out
@@ -94,8 +157,14 @@ static func map_target(g2: Game, game: Game, t):
return g2.players[game.players.find(t)]
const FOLLOW_UPS := 10 # Folgeschritte je Kandidat in der zweiten Suchstufe
static var time_budget_ms := 150 # Zeitbudget je Entscheidung; danach der beste bisherige Zug
## Überträgt eine auf der Kopie gewählte Aktion zurück auf das Original.
static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary:
var out := action.duplicate()
if action.has("creature"):
out["creature"] = game.find_creature(action.creature.uid)
if action.get("target") != null:
out["target"] = map_target(game, copy, action.target)
return out
## Mulligan der KI: teure Karten im ersten Zug zurücklegen.
@@ -103,37 +172,48 @@ static func mulligan_cards(p: Game.Player) -> Array:
return p.hand.filter(func(c): return c.cost >= 5)
## Beste Aktion nach einer Vorausschau von zwei Schritten: jede Aktion wird auf einer
## Kopie ausgeführt und mit ihrem besten Folgeschritt bewertet (z. B. verstärken, dann angreifen).
## Beste Aktion nach einer Vorausschau von zwei Schritten: erst wird jede Aktion auf einer
## Kopie ausgeführt und bewertet, dann bekommen die DEEPEN besten Folgeschritte
## (z. B. verstärken, dann angreifen). Bei Zeitmangel gilt der beste bisherige Zug.
static func choose_action(game: Game, p: Game.Player) -> Dictionary:
if game.winner != "" or game.enemies_of(p).is_empty():
return {"type": "end"}
if not p.mulligan_done and p.turns == 1 and p == game.current():
return {"type": "mulligan", "cards": mulligan_cards(p)}
var best := {"type": "end"}
var best_score := evaluate(game, p) + 0.01
var t0 := Time.get_ticks_msec()
var examined := 0
for action in candidate_actions(game, p):
if examined > 0 and Time.get_ticks_msec() - t0 > time_budget_ms:
var base := evaluate(game, p) + 0.01
var firsts := [] # {action, game, score}
for action in candidate_actions(game, p, true):
if not firsts.is_empty() and Time.get_ticks_msec() - t0 > time_budget_ms:
break
examined += 1
var g2 := game.clone_for(p)
apply_on_clone(g2, game, action)
var p2: Game.Player = g2.players[game.current_idx]
var score := evaluate(g2, p2)
firsts.append({"action": action, "game": g2, "score": evaluate(g2, g2.players[game.current_idx])})
firsts.sort_custom(func(a, b): return a.score > b.score)
var best := {"type": "end"}
var best_score := base
for i in mini(firsts.size(), DEEPEN):
var f: Dictionary = firsts[i]
var g2: Game = f.game
var score: float = f.score
if g2.winner == "":
var p2: Game.Player = g2.players[game.current_idx]
var count := 0
for f in candidate_actions(g2, p2):
for nxt in candidate_actions(g2, p2, true):
if count >= FOLLOW_UPS or Time.get_ticks_msec() - t0 > time_budget_ms:
break
count += 1
var g3 := g2.clone_for(p2)
apply_on_clone(g3, g2, f)
score = maxf(score, evaluate(g3, g3.players[g2.current_idx]) - 0.001)
apply_on_clone(g3, g2, nxt)
score = maxf(score, evaluate(g3, g3.players[game.current_idx]) - 0.001)
if score > best_score:
best_score = score
best = action
best = f.action
# Erstschritte ohne Vertiefung zählen mit ihrer Einzelbewertung
for i in range(DEEPEN, firsts.size()):
if firsts[i].score > best_score:
best_score = firsts[i].score
best = firsts[i].action
return best

View File

@@ -6,11 +6,7 @@ const OPPONENT_RARITY_WEIGHT := {"gewöhnlich": 6.0, "selten": 3.0, "episch": 2.
const RESONANCE_STEPS := [0, 1, 2, 3, 3]
# Gemessene Stärke der Elementkombinationen (Siegquote gegen das Feld, KI gegen KI, 60 Partien je Archetyp, tests/strength_probe.gd).
const ARCHETYPE_STRENGTH := {
"Feuer": 36, "Wasser": 40, "Erde": 55, "Luft": 55,
"Feuer+Wasser": 50, "Feuer+Erde": 65, "Feuer+Luft": 46,
"Wasser+Erde": 51, "Wasser+Luft": 45, "Erde+Luft": 78,
}
const ARCHETYPE_STRENGTH := {"Erde": 61, "Erde+Luft": 78, "Feuer": 41, "Feuer+Erde": 66, "Feuer+Luft": 46, "Feuer+Wasser": 50, "Luft": 55, "Wasser": 40, "Wasser+Erde": 43, "Wasser+Luft": 45}

View File

@@ -19,6 +19,9 @@ var pending = null # Cards.Card, wartet auf Ziel
var msg_t := 0.0
var ai_delay := 0.75
var ai_timer := 0.0
var ai_task := -1 # WorkerThreadPool-Aufgabe der laufenden KI-Berechnung
var ai_copy: Game # Spielkopie, auf der die KI rechnet
var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt)
var reward := -1
var ended := false
var zoom_player = null
@@ -301,12 +304,7 @@ func _build_static() -> void:
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
# Aufgeben bestätigen
confirm = _overlay()
Style.panel(confirm, Rect2(390, 300, 500, 180), Style.PANEL, 10, Style.MUTED, 2)
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(390, 320, 500, 40), "center", true)
Style.label(confirm, "Der Kampf endet ohne Münzen.", 16, Style.MUTED, Rect2(390, 365, 500, 24), "center")
Style.button(confirm, "Ja, aufgeben", Rect2(420, 410, 200, 46), leave, Color("5a2828"))
Style.button(confirm, "Weiterspielen", Rect2(660, 410, 200, 46), func(): confirm.visible = false)
confirm = Style.confirm(self, "Wirklich aufgeben?", "Der Kampf endet ohne Münzen.", "Ja, aufgeben", leave, "Weiterspielen")
# Übergabe zwischen Menschen (Hotseat)
handover = _overlay()
Style.panel(handover, Rect2(340, 280, 600, 220), Style.PANEL, 10, Style.GOLD, 2)
@@ -323,16 +321,7 @@ func _layer() -> Control:
func _overlay() -> Control:
var o := Control.new()
o.set_anchors_preset(Control.PRESET_FULL_RECT)
o.visible = false
var dark := ColorRect.new()
dark.color = Color(0, 0, 0, 0.67)
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
dark.mouse_filter = Control.MOUSE_FILTER_STOP
o.add_child(dark)
add_child(o)
return o
return Style.overlay(self)
func leave() -> void:
@@ -520,9 +509,22 @@ func _process(delta: float) -> void:
ai_timer = 0.0
else:
ai_timer -= delta
if ai_timer > 0:
return
var action := AI.choose_action(game, cur) if smart_ai else AI.choose_action_greedy(game, cur)
var action: Dictionary
if smart_ai:
# Die Vorausschau rechnet im Hintergrund auf einer Kopie, schon während der Wartezeit,
# damit Animationen nicht stocken.
if ai_task < 0:
_start_ai(cur)
if ai_timer > 0 or not WorkerThreadPool.is_task_completed(ai_task):
return
WorkerThreadPool.wait_for_task_completion(ai_task)
ai_task = -1
action = AI.map_action(game, ai_copy, ai_box.action)
ai_copy = null
else:
if ai_timer > 0:
return
action = AI.choose_action_greedy(game, cur)
AI.apply_action(game, cur, action)
ai_timer = ai_delay * (0.6 if action.type == "end" else 1.0)
if game.current().is_human:
@@ -530,6 +532,20 @@ func _process(delta: float) -> void:
after_action()
func _start_ai(cur: Game.Player) -> void:
ai_copy = game.clone_for(cur)
var copy := ai_copy
var idx := game.current_idx
var box := ai_box
ai_task = WorkerThreadPool.add_task(func(): box["action"] = AI.choose_action(copy, copy.players[idx]))
func _exit_tree() -> void:
if ai_task >= 0:
WorkerThreadPool.wait_for_task_completion(ai_task)
ai_task = -1
func _finish() -> void:
var won := game.winner == "human"
if counts_for_profile():

View File

@@ -153,20 +153,8 @@ func _ready() -> void:
Style.button(self, "Speichern", Rect2(860, 740, 180, 44), save_deck, Color("285a3c"))
Style.button(self, "Leeren", Rect2(1050, 740, 90, 44), clear_deck)
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), leave)
confirm = Control.new()
confirm.set_anchors_preset(Control.PRESET_FULL_RECT)
confirm.visible = false
var dark := ColorRect.new()
dark.color = Color(0, 0, 0, 0.67)
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
dark.mouse_filter = Control.MOUSE_FILTER_STOP
confirm.add_child(dark)
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.GOLD, 2)
Style.label(confirm, "Ungespeicherte Änderungen", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true)
Style.label(confirm, "Das Deck wurde geändert, aber nicht gespeichert.", 16, Style.MUTED, Rect2(340, 360, 600, 24), "center")
Style.button(confirm, "Verwerfen", Rect2(380, 420, 240, 46), func(): app.show_screen(MenuScreen.new()), Color("5a2828"))
Style.button(confirm, "Weiter bearbeiten", Rect2(660, 420, 240, 46), func(): confirm.visible = false)
add_child(confirm)
confirm = Style.confirm(self, "Ungespeicherte Änderungen", "Das Deck wurde geändert, aber nicht gespeichert.",
"Verwerfen", func(): app.show_screen(MenuScreen.new()), "Weiter bearbeiten", Style.GOLD)
preview = CardView.new(Cards.all()[0], Vector2(200, 280))
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview.visible = false

View File

@@ -82,20 +82,8 @@ func _ready() -> void:
r.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(r, "Profil zurücksetzen", Vector2(200, 44), func(): confirm.visible = true, Color("5a2828"))
Style.cbutton(r, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
confirm = Control.new()
confirm.set_anchors_preset(Control.PRESET_FULL_RECT)
confirm.visible = false
var dark := ColorRect.new()
dark.color = Color(0, 0, 0, 0.67)
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
dark.mouse_filter = Control.MOUSE_FILTER_STOP
confirm.add_child(dark)
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.RED, 2)
Style.label(confirm, "Profil wirklich zurücksetzen?", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true)
Style.label(confirm, "Sammlung, Decks, Münzen und Fortschritt gehen verloren.", 16, Style.MUTED, Rect2(340, 360, 600, 24), "center")
Style.button(confirm, "Ja, zurücksetzen", Rect2(380, 420, 240, 46), _reset, Color("5a2828"))
Style.button(confirm, "Abbrechen", Rect2(660, 420, 240, 46), func(): confirm.visible = false)
add_child(confirm)
confirm = Style.confirm(self, "Profil wirklich zurücksetzen?", "Sammlung, Decks, Münzen und Fortschritt gehen verloren.",
"Ja, zurücksetzen", _reset, "Abbrechen", Style.RED)
_labels()

View File

@@ -9,6 +9,8 @@ var players: Array[AudioStreamPlayer] = []
var volume := 0.8
var music_player: AudioStreamPlayer
var music_volume := 0.5
var music_task := -1 # die Musik wird im Hintergrund berechnet (spart ~150 ms beim Start)
var music_box := {}
func _ready() -> void:
@@ -26,9 +28,23 @@ func _ready() -> void:
add_child(p)
players.append(p)
music_player = AudioStreamPlayer.new()
music_player.stream = _music()
add_child(music_player)
set_music_volume(music_volume)
var box := music_box
music_task = WorkerThreadPool.add_task(func(): box["wav"] = _music())
func _process(_delta: float) -> void:
if music_task >= 0 and WorkerThreadPool.is_task_completed(music_task):
WorkerThreadPool.wait_for_task_completion(music_task)
music_task = -1
music_player.stream = music_box.wav
set_music_volume(music_volume)
func _exit_tree() -> void:
if music_task >= 0:
WorkerThreadPool.wait_for_task_completion(music_task)
music_task = -1
## Leise Hintergrundmusik: Arpeggio über vier Akkorde, als Schleife erzeugt.
@@ -67,7 +83,7 @@ func set_music_volume(v: float) -> void:
music_player.stop()
else:
music_player.volume_db = linear_to_db(music_volume * 0.6)
if not music_player.playing:
if not music_player.playing and music_player.stream != null:
music_player.play()

View File

@@ -165,6 +165,31 @@ static func spacer(parent: Node, h: float) -> Control:
return c
## Unsichtbare Vollbild-Ebene mit abgedunkeltem Hintergrund, die Klicks schluckt.
static func overlay(parent: Node) -> Control:
var o := Control.new()
o.set_anchors_preset(Control.PRESET_FULL_RECT)
o.visible = false
var dark := ColorRect.new()
dark.color = Color(0, 0, 0, 0.67)
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
dark.mouse_filter = Control.MOUSE_FILTER_STOP
o.add_child(dark)
parent.add_child(o)
return o
## Bestätigungsdialog (unsichtbar): Titel, Hinweis, Ja-Knopf mit Aktion und ein Abbrechen-Knopf.
static func confirm(parent: Node, title: String, text: String, yes: String, on_yes: Callable, no: String = "Abbrechen", color: Color = MUTED) -> Control:
var o := overlay(parent)
panel(o, Rect2(340, 290, 600, 200), PANEL, 10, color, 2)
label(o, title, 26, TEXT, Rect2(340, 312, 600, 40), "center", true)
label(o, text, 16, MUTED, Rect2(340, 360, 600, 24), "center")
button(o, yes, Rect2(380, 420, 240, 46), on_yes, Color("5a2828"))
button(o, no, Rect2(660, 420, 240, 46), func(): o.visible = false)
return o
static func clear(node: Node) -> void:
for c in node.get_children():
node.remove_child(c)

49
tests/frame_probe.gd Normal file
View File

@@ -0,0 +1,49 @@
extends Node
## Fensterprobe: Kosten von refresh() und längste Bildzeit während KI-Zügen (5 Gegner).
var main: Node
var max_dt := 0.0
var frames_n := 0
var measuring := false
func _ready() -> void:
Game.test_seed = 4242
main = load("res://scenes/main.tscn").instantiate()
main.save_path = "user://_probe.json"
add_child(main)
await get_tree().process_frame
await get_tree().process_frame
var scr: BattleScreen = BattleScreen.new(5)
main.show_screen(scr)
await get_tree().process_frame
await get_tree().process_frame
var h: Game.Player = scr.game.human
while h.hand.size() < 8 and not h.deck.is_empty():
scr.game.draw(h)
var t0 := Time.get_ticks_usec()
for i in 30:
scr.refresh()
print("refresh() mit 5 Gegnern und 8 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
scr.mulligan.visible = false
h.mulligan_done = true
scr.ai_delay = 0.2
measuring = true
var start := Time.get_ticks_msec()
var actions := 0
var last_turn := scr.game.turn
while Time.get_ticks_msec() - start < 12000 and scr.game.winner == "":
if scr.game.current().is_human and scr.game.current() == h:
scr.game.end_turn()
scr.after_action()
await get_tree().process_frame
measuring = false
print("längste Bildzeit in 12 s KI-Spiel: %.0f ms (%d Bilder, Zug %d)" % [max_dt * 1000.0, frames_n, scr.game.turn])
DirAccess.remove_absolute(ProjectSettings.globalize_path("user://_probe.json"))
get_tree().quit()
func _process(delta: float) -> void:
if measuring:
frames_n += 1
max_dt = maxf(max_dt, delta)

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

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

6
tests/frame_probe.tscn Normal file
View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/frame_probe.gd" id="1"]
[node name="FrameProbe" type="Node"]
script = ExtResource("1")

50
tests/perf_probe.gd Normal file
View File

@@ -0,0 +1,50 @@
extends SceneTree
## Laufzeitmessung: Start, KI-Entscheidungen, Deckerzeugung, Vorschau, Musik.
func ms(f: Callable, n: int = 1) -> float:
var t0 := Time.get_ticks_usec()
for i in n:
f.call()
return (Time.get_ticks_usec() - t0) / 1000.0 / n
func _initialize() -> void:
var t := ms(func(): Cards.all())
print("Kartenpool laden: %.1f ms" % t)
var r := RandomNumberGenerator.new()
r.seed = 3
var starter := Profile.starter().deck_cards()
print("Gegnerdeck (Zielstärke): %.1f ms" % ms(func(): DeckGen.build_opponent_deck_rated(r, ["Feuer", "Erde"], 1.5), 10))
print("Vorschau 1..5 Gegner: %.1f ms" % ms(func():
for n in range(1, 6):
for i in n:
Game.random_opponent(r, i + 1, Game.START_LIFE, 1.3, "")))
for n in [1, 3, 5]:
var g := Game.new_battle(starter, n, r, {"target_rating": 1.5})
# ein volles Brett für alle, damit die Entscheidung teuer wird
var uid := 5000
for p in g.players:
p.mulligan_done = true
for k in 4:
var c := Game.Creature.new(Cards.by_id("f_wolf"), p, uid)
uid += 1
c.ready = true
c.attacks_left = 1
p.board.append(c)
var h: Game.Player = g.players[0]
h.energy = 10
h.max_energy = 10
for k in 6:
h.hand.append(Cards.by_id("f_funke"))
AI.time_budget_ms = 100000
var t_full := ms(func(): AI.choose_action(g, h))
AI.time_budget_ms = 150
var t_budget := ms(func(): AI.choose_action(g, h))
var cands := AI.candidate_actions(g, h).size()
print("KI-Entscheidung, %d Gegner, %d Kandidaten: ohne Budget %.0f ms, mit Budget %.0f ms" % [n, cands, t_full, t_budget])
print("Spielkopie: %.3f ms" % ms(func(): Game.new_battle(starter, 5, r).clone(), 20))
print("Musik erzeugen (läuft im Hintergrund-Thread): %.0f ms" % ms(func(): Sfx.new()._music()))
print("Deckstärke berechnen: %.3f ms" % ms(func(): Game.deck_rating(starter), 100))
print("Snapshot 6 Spieler: %.2f ms" % ms(func(): Game.new_battle(starter, 5, r).snapshot(0), 10))
quit()

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

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

View File

@@ -133,3 +133,5 @@ Duell,Duel
%d Spieler am Tisch,%d players at the table
%d Münzen,%d coins
Stärke %.1f · %d Münzen,Strength %.1f · %d coins
Abbrechen,Cancel
"Ja, zurücksetzen","Yes, reset"
1 keys en
133 %d Spieler am Tisch %d players at the table
134 %d Münzen %d coins
135 Stärke %.1f · %d Münzen Strength %.1f · %d coins
136 Abbrechen Cancel
137 Ja, zurücksetzen Yes, reset

Binary file not shown.