update
This commit is contained in:
@@ -30,13 +30,21 @@ static func board_value(p: Game.Player) -> float:
|
||||
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.
|
||||
## Karten auf der Hand zählen nach ihren Werten: ein Drache ist mehr wert als ein Kobold.
|
||||
static func hand_value(p: Game.Player) -> float:
|
||||
var v := 0.0
|
||||
for c in p.hand:
|
||||
v += 0.15 * (c.attack + c.health)
|
||||
return v
|
||||
|
||||
|
||||
## Stellung aus Sicht von p: eigenes Leben, Feld, Hand und übriges Mana 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 score := p.life * 1.0 + board_value(p) * 1.0 + hand_value(p) + p.energy * 0.02
|
||||
var alive_enemies := 0
|
||||
var threat := 0.0
|
||||
var leader := 0.0
|
||||
@@ -117,6 +125,18 @@ static func _top(list: Array, rank: Callable, n: int) -> Array:
|
||||
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
|
||||
@@ -144,6 +164,14 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
|
||||
valid = valid_cache[key]
|
||||
for t in valid:
|
||||
out.append({"type": "play", "card": card, "target": t})
|
||||
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)
|
||||
for cr in p.board:
|
||||
if not cr.can_attack():
|
||||
continue
|
||||
@@ -165,10 +193,17 @@ static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void:
|
||||
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)
|
||||
match action.type:
|
||||
"play":
|
||||
g2.play_card(p2, action.card, target)
|
||||
"buy":
|
||||
g2.buy(p2, int(action.slot))
|
||||
"reroll":
|
||||
g2.reroll(p2)
|
||||
"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)
|
||||
|
||||
|
||||
static func map_target(g2: Game, game: Game, t):
|
||||
@@ -182,31 +217,26 @@ 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.has("other"):
|
||||
out["other"] = game.find_creature(action.other.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)
|
||||
var g2 := game.clone()
|
||||
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)
|
||||
@@ -223,7 +253,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
if count >= FOLLOW_UPS or Time.get_ticks_msec() - t0 > time_budget_ms:
|
||||
break
|
||||
count += 1
|
||||
var g3 := g2.clone_for(p2)
|
||||
var g3 := g2.clone()
|
||||
apply_on_clone(g3, g2, nxt)
|
||||
score = maxf(score, evaluate(g3, g3.players[game.current_idx]) - 0.001)
|
||||
if score > best_score:
|
||||
@@ -240,7 +270,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
# --- Gierige Vergleichs-KI -------------------------------------------------------
|
||||
|
||||
static func _kills(attacker, defender) -> bool:
|
||||
return not defender.shield and defender.health <= attacker.power()
|
||||
return not defender.shield and defender.health <= attacker.power() + Game.matchup_bonus(attacker, defender)
|
||||
|
||||
|
||||
static func _value(c) -> int:
|
||||
@@ -276,8 +306,15 @@ static func _weakest(heroes: Array):
|
||||
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)}
|
||||
# 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}
|
||||
var playable := p.hand.filter(func(c): return game.can_play(p, c)[0])
|
||||
if not playable.is_empty():
|
||||
Game.shuffle(playable, game.rng)
|
||||
@@ -360,10 +397,14 @@ 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)
|
||||
"attack":
|
||||
game.attack(action.creature, action.target)
|
||||
"mulligan":
|
||||
game.mulligan(p, action.cards)
|
||||
"fuse":
|
||||
game.fuse(p, action.creature, action.other)
|
||||
_:
|
||||
game.end_turn()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user