update
Some checks are pending
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Waiting to run
Tests / balance (push) Waiting to run

This commit is contained in:
team3
2026-10-13 15:39:55 +02:00
parent b91f475a6e
commit f2c609a08b
25 changed files with 1338 additions and 776 deletions

View File

@@ -101,14 +101,7 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
for cr in p.board:
if cr.hidden and game.can_reveal(p, cr)[0]:
out.append({"type": "reveal", "creature": cr})
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([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:
for pr in fusion_pairs(game, p, prune):
out.append({"type": "fuse", "creature": pr[0], "other": pr[1]})
for cr in p.board:
if not cr.can_attack():
@@ -126,6 +119,39 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
return out
## Verschmelzungen, die sich lohnen könnten: Paare eigener Wesen (mit prune die schwächsten, die
## Plätze frei machen), jedes angeschlagene Wesen mit der billigsten Handkarte, die zwei
## billigsten Handkarten. Das Ergebnis kommt auf die Hand, die Vorausschau legt es im Folgeschritt.
static func fusion_pairs(game: Game, p: Game.Player, prune: bool) -> Array:
var pairs := []
for i in p.board.size():
for j in range(i + 1, p.board.size()):
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)
var cheap := _cheapest(p)
if not cheap.is_empty():
for cr in p.board:
if cr.health < cr.max_health:
pairs.append([cr, cheap[0]])
if cheap.size() >= 2:
pairs.append([cheap[0], cheap[1]])
return pairs.filter(func(pr): return game.can_fuse(p, pr[0], pr[1])[0])
## Handkarten nach Kosten aufsteigend.
static func _cheapest(p: Game.Player) -> Array:
var cheap: Array = p.hand.duplicate()
cheap.sort_custom(func(a, b): return a.cost < b.cost)
return cheap
## Teil einer Verschmelzung in einem anderen Spiel: Handkarten sind in Kopien dieselben Objekte,
## Wesen werden über ihre uid gefunden.
static func _part(g: Game, x):
return x if x is Cards.Card else g.find_creature(x.uid)
## 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]
@@ -135,7 +161,7 @@ static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void:
"reveal":
g2.reveal(p2, g2.find_creature(action.creature.uid))
"fuse":
g2.fuse(p2, g2.find_creature(action.creature.uid), g2.find_creature(action.other.uid))
g2.fuse(p2, _part(g2, action.creature), _part(g2, action.other))
_:
g2.attack(g2.find_creature(action.creature.uid), map_target(g2, game, action.target))
@@ -150,9 +176,9 @@ static func map_target(g2: Game, game: Game, t):
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)
out["creature"] = _part(game, action.creature)
if action.has("other"):
out["other"] = game.find_creature(action.other.uid)
out["other"] = _part(game, action.other)
if action.get("target") != null:
out["target"] = map_target(game, copy, action.target)
return out
@@ -211,7 +237,7 @@ 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
return attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) >= defender.health
return attacker.power() + Game.matchup_bonus(attacker, defender) >= defender.health
static func _value(c) -> int:
@@ -234,8 +260,14 @@ static func _best(list: Array, key: Callable):
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 Spielbare legen (bei vollem Feld die zwei schwächsten Wesen
# verschmelzen), dann aufdecken und angreifen, dann Doppelschlag kaufen, dann ziehen.
# Gierig: ein Wesen mit höchstens halbem Leben heilen (mit der billigsten Handkarte
# verschmelzen, wenn das Ergebnis bezahlbar bleibt), dann das teuerste Spielbare legen (bei
# vollem Feld die zwei schwächsten Wesen verschmelzen), dann aufdecken und angreifen, dann ziehen.
var cheap := _cheapest(p)
if not cheap.is_empty():
for cr in p.board:
if cr.health * 2 <= cr.max_health and game.can_fuse(p, cr, cheap[0])[0] and Game.fusion_result(p, cr, cheap[0]).cost <= p.energy:
return {"type": "fuse", "creature": cr, "other": cheap[0]}
var playable := p.hand.filter(func(c): return game.can_play(p, c)[0])
if not playable.is_empty():
Game.shuffle(playable, game.rng)
@@ -244,7 +276,7 @@ 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):
if p.board.size() >= Game.MAX_BOARD and p.hand.any(func(c): return c.cost <= p.energy):
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]: