update
This commit is contained in:
353
scripts/ai.gd
353
scripts/ai.gd
@@ -1,25 +1,19 @@
|
||||
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.
|
||||
## 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.
|
||||
## Verdeckte gegnerische Wesen kennt die KI nicht (Game.masked_clone). 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
|
||||
v += 1.0
|
||||
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
|
||||
v += c.power() * 0.5
|
||||
return v
|
||||
|
||||
|
||||
@@ -38,16 +32,14 @@ static func hand_value(p: Game.Player) -> float:
|
||||
return v
|
||||
|
||||
|
||||
## Stellung aus Sicht von p: eigenes Leben, Feld, Hand und übriges Mana zählen positiv,
|
||||
## die des Gegners negativ. Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung,
|
||||
## tödliche besonders.
|
||||
## Stellung aus Sicht von p: eigenes Leben, Brett und Hand zählen positiv, die des Gegners
|
||||
## negativ. Ohne eigene Wesen zielt gegnerische Angriffskraft aufs Leben, 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 + hand_value(p) + p.energy * 0.02
|
||||
var score := p.life * 1.0 + board_value(p) * 1.0 + hand_value(p) + p.energy * 0.01
|
||||
var alive_enemies := 0
|
||||
var threat := 0.0
|
||||
var leader := 0.0
|
||||
for e in g.players:
|
||||
if e == p:
|
||||
continue
|
||||
@@ -55,59 +47,40 @@ static func evaluate(g: Game, p: Game.Player) -> float:
|
||||
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)
|
||||
score -= e.life * 0.6 + board_value(e) * 0.9 + e.hand.size() * 0.2
|
||||
if e.life <= 6:
|
||||
score += 3.0 # fast besiegt: dranbleiben
|
||||
for c in e.board:
|
||||
if not c.frozen:
|
||||
if not c.hidden:
|
||||
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
|
||||
if p.board.is_empty():
|
||||
if threat >= p.life:
|
||||
score -= 25.0
|
||||
else:
|
||||
score -= threat * 0.3
|
||||
else:
|
||||
score -= threat * 0.3
|
||||
score -= threat * 0.12 # Durchschlag: auch hinter Wesen tut Angriffskraft weh
|
||||
return score
|
||||
|
||||
|
||||
# --- Vorausschau ---------------------------------------------------------------
|
||||
|
||||
const MAX_TARGETS := 4 # je Karte bzw. Angreifer nur die aussichtsreichsten Ziele prüfen
|
||||
const MAX_TARGETS := 4 # je Angreifer nur die aussichtsreichsten Ziele prüfen
|
||||
static var allow_hidden := true # zum Messen: verdeckt legen erwägen
|
||||
static var allow_upgrade := true # zum Messen: Verbessern erwägen
|
||||
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.
|
||||
## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, dann der Rest; Durchschlag zählt mit.
|
||||
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 (100.0 if not _kills(t, cr) else 60.0) + _value(t) + _overflow(cr, t)
|
||||
return -float(t.health)
|
||||
|
||||
|
||||
@@ -121,65 +94,44 @@ static func _top(list: Array, rank: Callable, n: int) -> Array:
|
||||
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.
|
||||
## Mögliche Aktionen; mit prune nur die MAX_TARGETS besten Ziele je Angreifer.
|
||||
static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -> Array:
|
||||
var out := []
|
||||
var valid_cache := {}
|
||||
var afford := false
|
||||
var seen := {}
|
||||
for i in p.shop.size():
|
||||
var offer = p.shop[i]
|
||||
if offer != null and p.energy >= offer.cost and p.hand.size() < Game.MAX_HAND:
|
||||
afford = true
|
||||
if not seen.has(offer.id): # dasselbe Angebot zweimal zu prüfen bringt nichts
|
||||
seen[offer.id] = true
|
||||
out.append({"type": "buy", "slot": i, "card": offer})
|
||||
# Neu würfeln lohnt nur, wenn ein Laden da ist und nichts Bezahlbares darin liegt.
|
||||
if not afford and not p.shop.is_empty() and p.energy >= Game.REROLL_COST and p.hand.size() < Game.MAX_HAND:
|
||||
out.append({"type": "reroll"})
|
||||
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})
|
||||
out.append({"type": "play", "card": card, "face_down": false})
|
||||
if allow_hidden and game.forecast(p, card) < 0:
|
||||
out.append({"type": "play", "card": card, "face_down": true})
|
||||
for cr in p.board:
|
||||
if cr.hidden and game.can_reveal(p, cr)[0]:
|
||||
out.append({"type": "reveal", "creature": cr})
|
||||
elif not cr.hidden and allow_upgrade:
|
||||
# Nur Verbesserungen, die sich noch in diesem Zug auszahlen können: Doppelschlag für
|
||||
# ein Wesen, das gleich angreift; Schild nur, wenn kein Wesen mehr spielbar ist.
|
||||
var has_target := not game.enemies_of(p).is_empty()
|
||||
if cr.attacks_left > 0 and has_target and game.can_upgrade(p, cr, "Doppelschlag")[0]:
|
||||
out.append({"type": "upgrade", "creature": cr, "keyword": "Doppelschlag"})
|
||||
if not p.hand.any(func(c): return game.can_play(p, c)[0]) and game.can_upgrade(p, cr, "Schild")[0]:
|
||||
out.append({"type": "upgrade", "creature": cr, "keyword": "Schild"})
|
||||
var pairs := []
|
||||
for i in p.board.size():
|
||||
for j in range(i + 1, p.board.size()):
|
||||
if game.can_fuse(p, p.board[i], p.board[j])[0]:
|
||||
pairs.append({"type": "fuse", "creature": p.board[i], "other": p.board[j]})
|
||||
if prune: # Verschmelzen lohnt vor allem bei schwachen Wesen
|
||||
pairs = _top(pairs, func(a): return -float(_value(a.creature) + _value(a.other)), MAX_TARGETS)
|
||||
out.append_array(pairs)
|
||||
pairs.append([p.board[i], p.board[j]])
|
||||
if prune and pairs.size() > MAX_TARGETS:
|
||||
pairs = _top(pairs, func(pr): return _value(pr[0]) + _value(pr[1]), MAX_TARGETS)
|
||||
for pr in pairs:
|
||||
out.append({"type": "fuse", "creature": pr[0], "other": pr[1]})
|
||||
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)
|
||||
targets.append_array(e.board)
|
||||
if e.board.is_empty():
|
||||
targets.append(e)
|
||||
targets = targets.filter(func(t): return game.can_attack(cr, t)[0])
|
||||
if prune:
|
||||
targets = _top(targets, func(t): return _attack_rank(cr, t), MAX_TARGETS)
|
||||
for t in targets:
|
||||
@@ -190,20 +142,17 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
|
||||
## 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)
|
||||
match action.type:
|
||||
"play":
|
||||
g2.play_card(p2, action.card, target)
|
||||
"buy":
|
||||
g2.buy(p2, int(action.slot))
|
||||
"reroll":
|
||||
g2.reroll(p2)
|
||||
g2.play_card(p2, action.card, bool(action.get("face_down", false)))
|
||||
"reveal":
|
||||
g2.reveal(p2, g2.find_creature(action.creature.uid))
|
||||
"upgrade":
|
||||
g2.upgrade(p2, g2.find_creature(action.creature.uid), action.keyword)
|
||||
"fuse":
|
||||
g2.fuse(p2, g2.find_creature(action.creature.uid), g2.find_creature(action.other.uid))
|
||||
_:
|
||||
g2.attack(g2.find_creature(action.creature.uid), target)
|
||||
g2.attack(g2.find_creature(action.creature.uid), map_target(g2, game, action.target))
|
||||
|
||||
|
||||
static func map_target(g2: Game, game: Game, t):
|
||||
@@ -224,21 +173,24 @@ static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary
|
||||
return out
|
||||
|
||||
|
||||
## 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.
|
||||
## Beste Aktion nach einer Vorausschau von zwei Schritten auf einer Kopie, die verdeckte
|
||||
## gegnerische Wesen nicht kennt. 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.offer.is_empty():
|
||||
return {"type": "pick", "index": pick_index(game, p)}
|
||||
var view := game.masked_clone(p)
|
||||
var me: Game.Player = view.players[game.players.find(p)]
|
||||
var t0 := Time.get_ticks_msec()
|
||||
var base := evaluate(game, p) + 0.01
|
||||
var base := evaluate(view, me) + 0.01
|
||||
var firsts := [] # {action, game, score}
|
||||
for action in candidate_actions(game, p, true):
|
||||
for action in candidate_actions(view, me, true):
|
||||
if not firsts.is_empty() and Time.get_ticks_msec() - t0 > time_budget_ms:
|
||||
break
|
||||
var g2 := game.clone()
|
||||
apply_on_clone(g2, game, action)
|
||||
firsts.append({"action": action, "game": g2, "score": evaluate(g2, g2.players[game.current_idx])})
|
||||
var g2 := view.clone()
|
||||
apply_on_clone(g2, view, action)
|
||||
firsts.append({"action": action, "game": g2, "score": evaluate(g2, g2.players[view.current_idx])})
|
||||
firsts.sort_custom(func(a, b): return a.score > b.score)
|
||||
var best := {"type": "end"}
|
||||
var best_score := base
|
||||
@@ -247,7 +199,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
var g2: Game = f.game
|
||||
var score: float = f.score
|
||||
if g2.winner == "":
|
||||
var p2: Game.Player = g2.players[game.current_idx]
|
||||
var p2: Game.Player = g2.players[view.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:
|
||||
@@ -255,7 +207,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
count += 1
|
||||
var g3 := g2.clone()
|
||||
apply_on_clone(g3, g2, nxt)
|
||||
score = maxf(score, evaluate(g3, g3.players[game.current_idx]) - 0.001)
|
||||
score = maxf(score, evaluate(g3, g3.players[view.current_idx]) - 0.001)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = f.action
|
||||
@@ -264,57 +216,81 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
if firsts[i].score > best_score:
|
||||
best_score = firsts[i].score
|
||||
best = firsts[i].action
|
||||
return best
|
||||
if best.type == "end":
|
||||
if game.can_draw(p)[0]:
|
||||
return {"type": "draw"} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
||||
return best
|
||||
return map_action(game, view, best)
|
||||
|
||||
|
||||
# --- Gierige Vergleichs-KI -------------------------------------------------------
|
||||
# --- Einfache Heuristiken ----------------------------------------------------------
|
||||
|
||||
static func _kills(attacker, defender) -> bool:
|
||||
return not defender.shield and defender.health <= attacker.power() + Game.matchup_bonus(attacker, defender)
|
||||
if defender is Game.Player:
|
||||
return attacker.power() >= defender.life
|
||||
if defender.shield:
|
||||
return false
|
||||
return attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) >= defender.health
|
||||
|
||||
|
||||
## Schaden, der beim Angriff auf das Wesen auf dessen Besitzer durchschlägt.
|
||||
static func _overflow(attacker, defender) -> int:
|
||||
if defender is Game.Player or defender.shield:
|
||||
return 0
|
||||
return maxi(0, attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) - defender.health)
|
||||
|
||||
|
||||
static func _value(c) -> int:
|
||||
return c.power() + c.health + (2 if c.taunt() else 0)
|
||||
if c is Game.Player:
|
||||
return 1000
|
||||
return c.power() + c.health
|
||||
|
||||
|
||||
static func _best(arr: Array, key: Callable):
|
||||
static func _best(list: 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
|
||||
var best_v := -INF
|
||||
for t in list:
|
||||
var v := float(key.call(t))
|
||||
if v > best_v:
|
||||
best_v = v
|
||||
best = t
|
||||
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
|
||||
## Wert einer angebotenen Karte für p: Werte je Mana, jetzt spielbar zählt extra, der
|
||||
## Elementbonus und ein Vorteil gegen das gegnerische Brett auch; viel zu teuer zählt ab.
|
||||
static func offer_score(game: Game, p: Game.Player, c: Cards.Card) -> float:
|
||||
var score := float(c.attack + c.health) / maxf(1.0, c.cost) * 2.0 + c.attack + c.health
|
||||
if c.cost <= p.energy:
|
||||
score += 3.0
|
||||
if c.cost > p.max_energy + 2:
|
||||
score -= 2.0 * (c.cost - p.max_energy - 2)
|
||||
score += 2.0 * game.forecast(p, c)
|
||||
for e in game.enemies_of(p):
|
||||
for o in e.board:
|
||||
if not o.hidden:
|
||||
score += 1.0 * Cards.matchup(c.elements, o.card.elements)
|
||||
return score
|
||||
|
||||
|
||||
static func _weakest(heroes: Array):
|
||||
var best = null
|
||||
for h in heroes:
|
||||
if best == null or _threat_score(h) < _threat_score(best):
|
||||
best = h
|
||||
## Welche der angebotenen Karten p nimmt.
|
||||
static func pick_index(game: Game, p: Game.Player) -> int:
|
||||
var best := 0
|
||||
var best_score := -INF
|
||||
for i in p.offer.size():
|
||||
var s := offer_score(game, p, p.offer[i])
|
||||
if s > best_score:
|
||||
best_score = s
|
||||
best = i
|
||||
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"}
|
||||
# Gierig: erst das teuerste Bezahlbare kaufen, dann ausspielen, dann angreifen.
|
||||
var best := -1
|
||||
for k in p.shop.size():
|
||||
var offer = p.shop[k]
|
||||
if offer != null and p.energy >= offer.cost and p.hand.size() < Game.MAX_HAND:
|
||||
if best < 0 or offer.cost > p.shop[best].cost:
|
||||
best = k
|
||||
if best >= 0:
|
||||
return {"type": "buy", "slot": best}
|
||||
if not p.offer.is_empty():
|
||||
return {"type": "pick", "index": pick_index(game, p)}
|
||||
# Gierig: erst das teuerste Spielbare legen, dann aufdecken und angreifen, dann ziehen.
|
||||
var playable := p.hand.filter(func(c): return game.can_play(p, c)[0])
|
||||
if not playable.is_empty():
|
||||
Game.shuffle(playable, game.rng)
|
||||
@@ -322,71 +298,35 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
||||
for c in playable:
|
||||
if c.cost > card.cost:
|
||||
card = c
|
||||
return {"type": "play", "card": card, "target": pick_target(game, p, card)}
|
||||
return {"type": "play", "card": card, "face_down": false}
|
||||
for cr in p.board:
|
||||
if cr.hidden and game.can_reveal(p, cr)[0]:
|
||||
return {"type": "reveal", "creature": cr}
|
||||
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}
|
||||
if game.can_draw(p)[0]:
|
||||
return {"type": "draw"}
|
||||
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]
|
||||
|
||||
|
||||
## Ziel eines Angriffs: sichere Abtausche, dann gute Abtausche, dann das schwächste Wesen;
|
||||
## der Held nur, wenn kein Wesen mehr steht.
|
||||
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():
|
||||
for e in game.enemies_of(p):
|
||||
if e.board.is_empty():
|
||||
return e if game.can_attack(cr, e)[0] else null
|
||||
var creatures: Array = e.board.filter(func(c): return game.can_attack(cr, c)[0])
|
||||
if creatures.is_empty():
|
||||
continue
|
||||
var good: Array = creatures.filter(func(c): return not c.hidden and _kills(cr, c) and not _kills(c, cr))
|
||||
if not good.is_empty():
|
||||
return _best(good, Callable(AI, "_value"))
|
||||
var trade: Array = creatures.filter(func(c): return not c.hidden and _kills(cr, c) and _value(c) > _value(cr))
|
||||
if not trade.is_empty():
|
||||
return _best(trade, Callable(AI, "_value"))
|
||||
return _best(creatures, func(c): return 1000 - c.health)
|
||||
return null
|
||||
|
||||
@@ -396,11 +336,15 @@ static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
|
||||
static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void:
|
||||
match action.type:
|
||||
"play":
|
||||
game.play_card(p, action.card, action.target)
|
||||
"buy":
|
||||
game.buy(p, int(action.slot))
|
||||
"reroll":
|
||||
game.reroll(p)
|
||||
game.play_card(p, action.card, bool(action.get("face_down", false)))
|
||||
"pick":
|
||||
game.pick(p, int(action.index))
|
||||
"draw":
|
||||
game.draw(p)
|
||||
"reveal":
|
||||
game.reveal(p, action.creature)
|
||||
"upgrade":
|
||||
game.upgrade(p, action.creature, action.keyword)
|
||||
"attack":
|
||||
game.attack(action.creature, action.target)
|
||||
"fuse":
|
||||
@@ -416,3 +360,4 @@ static func take_turn(game: Game, p: Game.Player, greedy: bool = false, limit: i
|
||||
apply_action(game, p, action)
|
||||
if action.type == "end" or game.winner != "":
|
||||
return
|
||||
game.end_turn()
|
||||
|
||||
Reference in New Issue
Block a user