From b6d5916a8d304a80d374e50731f2d2de0bcb050e Mon Sep 17 00:00:00 2001 From: Team3 Date: Wed, 16 Sep 2026 17:53:27 +0200 Subject: [PATCH] Spiel untersucht und optimiert: KI-Suche, Hintergrund-Threads, Dialog-Helfer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx --- README.md | 3 + data/archetypen.json | 8 +- scripts/ai.gd | 134 ++++++++++++++++++++++++++------- scripts/deckgen.gd | 6 +- scripts/ui/battle_screen.gd | 54 ++++++++----- scripts/ui/deck_screen.gd | 16 +--- scripts/ui/settings_screen.gd | 16 +--- scripts/ui/sfx.gd | 22 +++++- scripts/ui/style.gd | 25 ++++++ tests/frame_probe.gd | 49 ++++++++++++ tests/frame_probe.gd.uid | 1 + tests/frame_probe.tscn | 6 ++ tests/perf_probe.gd | 50 ++++++++++++ tests/perf_probe.gd.uid | 1 + translations/ui.csv | 2 + translations/ui.en.translation | Bin 6480 -> 6536 bytes 16 files changed, 307 insertions(+), 86 deletions(-) create mode 100644 tests/frame_probe.gd create mode 100644 tests/frame_probe.gd.uid create mode 100644 tests/frame_probe.tscn create mode 100644 tests/perf_probe.gd create mode 100644 tests/perf_probe.gd.uid diff --git a/README.md b/README.md index a7ccc3e..8dd8641 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/data/archetypen.json b/data/archetypen.json index c9514d9..0f1fe1f 100644 --- a/data/archetypen.json +++ b/data/archetypen.json @@ -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 } \ No newline at end of file diff --git a/scripts/ai.gd b/scripts/ai.gd index 69b8b0c..5e06578 100644 --- a/scripts/ai.gd +++ b/scripts/ai.gd @@ -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 diff --git a/scripts/deckgen.gd b/scripts/deckgen.gd index 73f7bf8..d41de9c 100644 --- a/scripts/deckgen.gd +++ b/scripts/deckgen.gd @@ -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} diff --git a/scripts/ui/battle_screen.gd b/scripts/ui/battle_screen.gd index 6e19e38..f64bf3d 100644 --- a/scripts/ui/battle_screen.gd +++ b/scripts/ui/battle_screen.gd @@ -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(): diff --git a/scripts/ui/deck_screen.gd b/scripts/ui/deck_screen.gd index fc52c2d..e41c4e4 100644 --- a/scripts/ui/deck_screen.gd +++ b/scripts/ui/deck_screen.gd @@ -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 diff --git a/scripts/ui/settings_screen.gd b/scripts/ui/settings_screen.gd index 55d50a8..11c07b4 100644 --- a/scripts/ui/settings_screen.gd +++ b/scripts/ui/settings_screen.gd @@ -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() diff --git a/scripts/ui/sfx.gd b/scripts/ui/sfx.gd index 067a088..59833c9 100644 --- a/scripts/ui/sfx.gd +++ b/scripts/ui/sfx.gd @@ -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() diff --git a/scripts/ui/style.gd b/scripts/ui/style.gd index fb4093a..680c1f2 100644 --- a/scripts/ui/style.gd +++ b/scripts/ui/style.gd @@ -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) diff --git a/tests/frame_probe.gd b/tests/frame_probe.gd new file mode 100644 index 0000000..6876c69 --- /dev/null +++ b/tests/frame_probe.gd @@ -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) diff --git a/tests/frame_probe.gd.uid b/tests/frame_probe.gd.uid new file mode 100644 index 0000000..eeeb89d --- /dev/null +++ b/tests/frame_probe.gd.uid @@ -0,0 +1 @@ +uid://botawgwpha3ai diff --git a/tests/frame_probe.tscn b/tests/frame_probe.tscn new file mode 100644 index 0000000..d6f7938 --- /dev/null +++ b/tests/frame_probe.tscn @@ -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") diff --git a/tests/perf_probe.gd b/tests/perf_probe.gd new file mode 100644 index 0000000..15ac8c9 --- /dev/null +++ b/tests/perf_probe.gd @@ -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() diff --git a/tests/perf_probe.gd.uid b/tests/perf_probe.gd.uid new file mode 100644 index 0000000..8e0fabd --- /dev/null +++ b/tests/perf_probe.gd.uid @@ -0,0 +1 @@ +uid://xrm3o6vloxh0 diff --git a/translations/ui.csv b/translations/ui.csv index 473779b..ddd3a26 100644 --- a/translations/ui.csv +++ b/translations/ui.csv @@ -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" diff --git a/translations/ui.en.translation b/translations/ui.en.translation index a24c325385fff65c725ea3362af5631f87187cd5..75023f7c36f1073842be9832298fe517f1f1e8e2 100644 GIT binary patch delta 602 zcmYk4J4nM&6oyZ(nzTspL8T5_6f!x87<3Q^p;T}YsiG(awc;YBh=UG3A{C^d2%1jf z=+r3?aS(BHP&$Z+gE;8Yse^Rt)b_vj6mGa5|Kr>n5|X#ktnDR)b4NrEpa4F>52*1~ zgfDR}vsR!&BMN}#zNU6_*9(oXw^)auBkU(wajxtC>=cSwumId&E!4RJvj(=nHkZyW zjNE?AD*Y+^Ik-}~8}@mqr}T3F6X$}`zr%kkS?((;xzzA6+1JVl6=f(pT@Zyp1juYb zhruM6=1%r=@OZtj`=_-zi#$j`lOV-;hgBXpV4q>lDl<91K+b_{a0~Ll1NYz&yn+v) z+#!V?cACJ}nY3>EB delta 570 zcmYk3Jt#$C7{}l9c3mI2zVdOVo53KHTp3JCWRT%7NG9DuCYeOtBP7Y7ZW&Al8Dx-S zU^Cs2awdc6TJCg}?yQdA<38nmJHP%P&-2zfox@x_avb7Mz*P_sK_a0%vdzmv9ZY@CYxE=7;q|jOQ`D sf3%l+wo$%uINd{%#-yHF-gYs)ct3i`wGp0#bg85+b7bJTBlmGw#