class_name AI ## 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 ------------------------------------------------------------------- static func creature_value(c: Game.Creature) -> float: var v := float(c.power() + c.health) if c.taunt(): v += 2.0 if c.shield: v += 1.5 if c.attacks_per_turn() == 2: v += c.power() if c.frozen: v -= c.power() # setzt den nächsten Angriff aus: der Angriffswert zählt nicht if not c.ready: v -= c.power() * 0.25 return v static func board_value(p: Game.Player) -> float: var v := 0.0 for c in p.board: v += creature_value(c) return v ## Stellung aus Sicht von p: eigenes Leben, Feld und Hand zählen positiv, die der Gegner ## negativ, der stärkste Gegner etwas mehr (jeder gegen jeden: den Führenden bremsen). ## Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung, tödliche besonders. static func evaluate(g: Game, p: Game.Player) -> float: if not p.alive(): return -10000.0 var score := p.life * 1.0 + board_value(p) * 1.0 + p.hand.size() * 0.8 + p.traps.size() * 2.5 var alive_enemies := 0 var threat := 0.0 var leader := 0.0 for e in g.players: if e == p: continue if not e.alive(): score += 40.0 continue alive_enemies += 1 var ev: float = e.life * 0.6 + board_value(e) * 0.9 + e.hand.size() * 0.2 score -= ev leader = maxf(leader, ev) if e.life <= 6: score += 3.0 # fast besiegt: dranbleiben for c in e.board: if not c.frozen: threat += c.power() * c.attacks_per_turn() if alive_enemies == 0: return 10000.0 score -= leader * 0.25 var guarded := false for c in p.board: if c.taunt(): guarded = true break if guarded: score -= threat * 0.1 elif threat >= p.life: score -= 25.0 else: score -= threat * 0.3 return score # --- Vorausschau --------------------------------------------------------------- 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}) 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]: 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 ## Führt eine Aktion des Originals auf der Kopie aus. static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void: var p2: Game.Player = g2.players[game.current_idx] var target = null if action.get("target") != null: target = map_target(g2, game, action.target) if action.type == "play": g2.play_card(p2, action.card, target) else: g2.attack(g2.find_creature(action.creature.uid), target) static func map_target(g2: Game, game: Game, t): if t is Game.Creature: return g2.find_creature(t.uid) return g2.players[game.players.find(t)] ## Ü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. 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: 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 t0 := Time.get_ticks_msec() 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 var g2 := game.clone_for(p) apply_on_clone(g2, game, action) 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 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, nxt) score = maxf(score, evaluate(g3, g3.players[game.current_idx]) - 0.001) if score > best_score: best_score = score 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 # --- Gierige Vergleichs-KI ------------------------------------------------------- static func _kills(attacker, defender) -> bool: return not defender.shield and defender.health <= attacker.power() static func _value(c) -> int: return c.power() + c.health + (2 if c.taunt() else 0) static func _best(arr: Array, key: Callable): var best = null var best_v := 0 for x in arr: var v: int = key.call(x) if best == null or v > best_v: best = x best_v = v return best static func _threat_score(h) -> int: var board_attack := 0 for c in h.board: board_attack += c.power() return h.life - board_attack static func _weakest(heroes: Array): var best = null for h in heroes: if best == null or _threat_score(h) < _threat_score(best): best = h return best static func choose_action_greedy(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 playable := p.hand.filter(func(c): return game.can_play(p, c)[0]) if not playable.is_empty(): Game.shuffle(playable, game.rng) var card = playable[0] for c in playable: if c.cost > card.cost: card = c return {"type": "play", "card": card, "target": pick_target(game, p, card)} for cr in p.board: if cr.can_attack(): var t = pick_attack_target(game, p, cr) if t != null: return {"type": "attack", "creature": cr, "target": t} return {"type": "end"} static func pick_target(game: Game, p: Game.Player, card: Cards.Card): var tt := card.target_type() if tt == "": return null var valid := game.valid_targets(p, tt) if valid.is_empty(): return null var fx: Cards.Effect = null for f in card.effects: if f.target == tt: fx = f break var creatures := valid.filter(func(t): return t is Game.Creature) var heroes := valid.filter(func(t): return t is Game.Player) match fx.kind: "damage": var killable := creatures.filter(func(c): return not c.shield and c.health <= fx.amount) if not killable.is_empty(): return _best(killable, Callable(AI, "_value")) if not heroes.is_empty(): return _weakest(heroes) return _best(creatures, Callable(AI, "_value")) "freeze", "bounce": return _best(creatures, func(c): return c.card.cost * 100 + c.power()) "buff", "shield": return _best(creatures, func(c): return (100 if c.can_attack() else 0) + c.power()) return valid[0] static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature): var enemies := game.enemies_of(p) var total := 0 for c in p.board: if c.can_attack(): total += c.power() * c.attacks_left var options := [] for e in enemies: var taunts: Array = e.board.filter(func(c): return c.taunt()) if not taunts.is_empty(): options.append_array(taunts) else: if total >= e.life: return e options.append_array(e.board) options.append(e) var creatures := options.filter(func(t): return t is Game.Creature) var heroes := options.filter(func(t): return t is Game.Player) var good := creatures.filter(func(c): return _kills(cr, c) and not _kills(c, cr)) if not good.is_empty(): return _best(good, Callable(AI, "_value")) var trade := creatures.filter(func(c): return _kills(cr, c) and _value(c) > _value(cr)) if not trade.is_empty(): return _best(trade, Callable(AI, "_value")) if not heroes.is_empty(): return _weakest(heroes) if not creatures.is_empty(): return _best(creatures, func(c): return 1000 - c.health) return null # --- Ausführung ------------------------------------------------------------------- static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void: match action.type: "play": game.play_card(p, action.card, action.target) "attack": game.attack(action.creature, action.target) "mulligan": game.mulligan(p, action.cards) _: game.end_turn() ## Spielt den ganzen Zug (für Simulationen und Tests). greedy: gierige KI statt Vorausschau. static func take_turn(game: Game, p: Game.Player, greedy: bool = false, limit: int = 100) -> void: for i in limit: var action := choose_action_greedy(game, p) if greedy else choose_action(game, p) apply_action(game, p, action) if action.type == "end" or game.winner != "": return