update
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled

This commit is contained in:
team3
2026-10-03 01:17:27 +02:00
parent 24a03ed1ee
commit 5f9442497a
27 changed files with 1040 additions and 871 deletions

View File

@@ -10,10 +10,10 @@ class_name AI
static func creature_value(c: Game.Creature) -> float:
var v := float(c.power() + c.health)
if c.shield:
v += 1.0
if c.attacks_per_turn() == 2:
v += c.power() * 0.5
if c.has("Regeneration"):
v += 1.0
return v
@@ -61,26 +61,25 @@ static func evaluate(g: Game, p: Game.Player) -> float:
else:
score -= threat * 0.3
else:
score -= threat * 0.12 # Durchschlag: auch hinter Wesen tut Angriffskraft weh
score -= threat * 0.12 # auch hinter Wesen tut gegnerische Angriffskraft weh
return score
# --- Vorausschau ---------------------------------------------------------------
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
## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, dann der Rest; Durchschlag zählt mit.
## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, 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) + _overflow(cr, t)
return (100.0 if not _kills(t, cr) else 60.0) + _value(t)
return -float(t.health)
@@ -98,22 +97,20 @@ 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 := []
for card in p.hand:
if not game.can_play(p, card)[0]:
continue
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})
if game.can_play(p, card)[0]:
out.append({"type": "play", "card": card, "face_down": false})
var has_target := not game.enemies_of(p).is_empty()
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:
if cr.hidden:
if game.can_reveal(p, cr)[0]:
out.append({"type": "reveal", "creature": cr})
elif allow_upgrade and has_target:
# 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]:
# ein Wesen, das gleich angreift; Flink für eines, das sonst erst nächsten Zug dürfte.
if cr.can_attack() 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"})
if not cr.ready and not cr.has("Flink") and cr.power() > 0 and game.can_upgrade(p, cr, "Flink")[0]:
out.append({"type": "upgrade", "creature": cr, "keyword": "Flink"})
var pairs := []
for i in p.board.size():
for j in range(i + 1, p.board.size()):
@@ -178,8 +175,6 @@ static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary
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()
@@ -228,18 +223,9 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
static func _kills(attacker, defender) -> bool:
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:
if c is Game.Player:
return 1000
@@ -257,40 +243,11 @@ static func _best(list: Array, key: Callable):
return best
## 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
## 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"}
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.
# Gierig: erst das teuerste Spielbare legen (bei vollem Feld die zwei schwächsten Wesen
# verschmelzen), dann aufdecken und angreifen, dann Doppelschlag kaufen, 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)
@@ -299,6 +256,11 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
if c.cost > card.cost:
card = c
return {"type": "play", "card": card, "face_down": false}
if p.board.size() >= Game.MAX_BOARD and p.hand.any(func(c): return c.cost <= p.energy - 1):
var weakest: Array = p.board.duplicate()
weakest.sort_custom(func(a, b): return _value(a) < _value(b))
if game.can_fuse(p, weakest[0], weakest[1])[0]:
return {"type": "fuse", "creature": weakest[0], "other": weakest[1]}
for cr in p.board:
if cr.hidden and game.can_reveal(p, cr)[0]:
return {"type": "reveal", "creature": cr}
@@ -307,6 +269,10 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
var t = pick_attack_target(game, p, cr)
if t != null:
return {"type": "attack", "creature": cr, "target": t}
if not game.enemies_of(p).is_empty():
var strongest = _best(p.board.filter(func(c): return not c.hidden and game.can_upgrade(p, c, "Doppelschlag")[0]), func(c): return c.power())
if strongest != null:
return {"type": "upgrade", "creature": strongest, "keyword": "Doppelschlag"}
if game.can_draw(p)[0]:
return {"type": "draw"}
return {"type": "end"}
@@ -337,8 +303,6 @@ static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void
match action.type:
"play":
game.play_card(p, action.card, bool(action.get("face_down", false)))
"pick":
game.pick(p, int(action.index))
"draw":
game.draw(p)
"reveal":