KI bewertet Kontrolle, Deckstärke mit Kurve und Statistik, Gegnervorschau, Deck-Vorschlag, Schwierigkeitstipp, DeckGen/NetState ausgelagert, Wasser gestärkt

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
Team3
2026-09-16 15:16:49 +02:00
parent 7ae6017f12
commit 0dbc98b456
18 changed files with 595 additions and 236 deletions

View File

@@ -43,7 +43,6 @@ static func make_rng() -> RandomNumberGenerator:
const DECK_SIZE := 20
const MAX_COPIES := 3
const MAX_DECK_ELEMENTS := 2
const OPPONENT_RARITY_WEIGHT := {"gewöhnlich": 6.0, "selten": 3.0, "episch": 2.0, "legendär": 1.0, "mythisch": 0.3}
class Creature extends RefCounted:
@@ -475,7 +474,12 @@ func summon(p: Player, card: Cards.Card) -> void:
update_resonance()
## Resonanz: +1 Angriff je anderem eigenen Wesen, das ein Element teilt (höchstens +4).
## Resonanz je Anzahl Partner mit gleichem Element: +1, +2, +3, +3.
static func resonance_for(partners: int) -> int:
return DeckGen.RESONANCE_STEPS[mini(partners, DeckGen.RESONANCE_STEPS.size() - 1)]
## Resonanz: abnehmender Angriffsbonus je Partner mit gleichem Element (siehe resonance_for).
func update_resonance() -> void:
for p in players:
for c in p.board:
@@ -483,7 +487,7 @@ func update_resonance() -> void:
for o in p.board:
if o != c and o.card.elements.any(func(e): return e in c.card.elements):
bonus += 1
c.resonance = mini(bonus, RESONANCE_MAX) * (2 if "resonanz_doppelt" in p.perks else 1)
c.resonance = resonance_for(bonus) * (2 if "resonanz_doppelt" in p.perks else 1)
func resolve(p: Player, fx: Cards.Effect, target, source: Creature = null, bonus: int = 0) -> void:
@@ -643,103 +647,14 @@ func check_end() -> void:
say("Alle Gegner sind besiegt!" if humans.size() == 1 else "%s gewinnt!" % winner_player.name)
# --- Zustand für das Netzwerk -------------------------------------------------
# --- Zustand für das Netzwerk (siehe NetState) ------------------------------------
## Serialisierter Zustand aus Sicht eines Spielers: fremde Hände, Decks und Fallen nur als Anzahl.
func snapshot(viewer: int) -> Dictionary:
var ps := []
for i in players.size():
var p: Player = players[i]
var mine := i == viewer
var board := []
for c in p.board:
board.append({"uid": c.uid, "card": c.card.id, "attack": c.attack, "resonance": c.resonance,
"health": c.health, "max_health": c.max_health, "shield": c.shield, "frozen": c.frozen,
"ready": c.ready, "attacks_left": c.attacks_left})
ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life,
"energy": p.energy, "max_energy": p.max_energy, "fatigue": p.fatigue, "start_bonus": p.start_bonus,
"order": p.order, "perks": p.perks, "turns": p.turns, "mulligan_done": p.mulligan_done,
"hand": p.hand.map(func(c): return c.id) if mine else p.hand.size(),
"deck": p.deck.size(), "traps": p.traps.map(func(c): return c.id) if mine else p.traps.size(),
"board": board})
var evs := []
for ev in events:
var e: Dictionary = ev.duplicate()
if e.has("card"):
e["card"] = e.card.id
evs.append(e)
return {"players": ps, "human": players.find(human), "turn": turn, "current_idx": current_idx,
"winner": winner, "winner_player": players.find(winner_player) if winner_player != null else -1,
"next_uid": next_uid, "log": log.slice(maxi(0, log.size() - 80)), "events": evs}
return NetState.snapshot(self, viewer)
## Baut einen Spielzustand aus einem Snapshot; verborgene Karten werden durch Platzhalter ersetzt.
static func from_snapshot(d: Dictionary) -> Game:
var placeholder: Cards.Card = Cards.all()[0]
var list := []
for pd in d.players:
var p := Player.new(pd.name, [], pd.is_human, int(pd.life), pd.title)
p.max_life = int(pd.max_life)
p.energy = int(pd.energy)
p.max_energy = int(pd.max_energy)
p.fatigue = int(pd.fatigue)
p.start_bonus = int(pd.start_bonus)
p.order = int(pd.order)
p.perks = Array(pd.perks)
p.turns = int(pd.turns)
p.mulligan_done = bool(pd.mulligan_done)
if pd.hand is Array:
for cid in pd.hand:
p.hand.append(Cards.by_id(cid))
else:
for i in int(pd.hand):
p.hand.append(placeholder)
for i in int(pd.deck):
p.deck.append(placeholder)
if pd.traps is Array:
for cid in pd.traps:
p.traps.append(Cards.by_id(cid))
else:
for i in int(pd.traps):
p.traps.append(placeholder)
for cd in pd.board:
var c := Creature.new(Cards.by_id(cd.card), p, int(cd.uid))
c.attack = int(cd.attack)
c.resonance = int(cd.resonance)
c.health = int(cd.health)
c.max_health = int(cd.max_health)
c.shield = bool(cd.shield)
c.frozen = bool(cd.frozen)
c.ready = bool(cd.ready)
c.attacks_left = int(cd.attacks_left)
p.board.append(c)
list.append(p)
var hi := int(d.human)
var g := Game.new(list[hi], list.slice(0, hi) + list.slice(hi + 1), RandomNumberGenerator.new())
g.players = list
g.turn = int(d.turn)
g.current_idx = int(d.current_idx)
g.winner = d.winner
g.winner_player = list[int(d.winner_player)] if int(d.winner_player) >= 0 else null
g.next_uid = int(d.next_uid)
for line in d.log:
g.log.append(str(line))
for ev in d.events:
var e: Dictionary = ev.duplicate()
if e.has("card"):
e["card"] = Cards.by_id(e.card)
for key in ["player", "uid", "count", "amount"]:
if e.has(key):
e[key] = int(e[key])
for key in ["target", "attacker", "source"]:
if e.has(key):
var r: Dictionary = e[key]
if r.has("uid"):
r["uid"] = int(r.uid)
if r.has("index"):
r["index"] = int(r.index)
g.events.append(e)
return g
return NetState.from_snapshot(d)
# --- Deckregeln und Gegner ---------------------------------------------------
@@ -778,106 +693,29 @@ static func validate_deck(cards: Array, owned = null) -> Array:
return [true, ""]
## Deckstärke: Durchschnitt der Kartenwerte (1 = nur gewöhnliche Karten, bis etwa 3,5).
static func deck_rating(cards: Array) -> float:
if cards.is_empty():
return 0.0
var sum := 0.0
for c in cards:
sum += Cards.card_value(c)
return sum / cards.size()
## Gegnerdeck auf eine Zielstärke: die Gewichtung wird angepasst, bis das Deck nahe am Ziel liegt.
static func build_opponent_deck_rated(r: RandomNumberGenerator, elements: Array, target: float, tolerance: float = 0.12) -> Array:
var bias := 1.0
var best := []
var best_diff := 99.0
for attempt in 24:
var deck := build_opponent_deck(r, elements, bias)
var diff := deck_rating(deck) - target
if absf(diff) < best_diff:
best = deck
best_diff = absf(diff)
if absf(diff) <= tolerance:
break
bias = clampf(bias * (1.35 if diff < 0 else 1.0 / 1.35), 0.05, 20.0)
return best
# Gemessene Stärke der Elementkombinationen (Siegquote gegen das Feld, KI gegen KI, tests/strength_probe.gd).
const ARCHETYPE_STRENGTH := {
"Feuer": 33, "Wasser": 20, "Erde": 53, "Luft": 46,
"Feuer+Wasser": 46, "Feuer+Erde": 60, "Feuer+Luft": 60,
"Wasser+Erde": 23, "Wasser+Luft": 36, "Erde+Luft": 66,
}
## Elementkombinationen einer Stufe: "leicht" die schwächere Hälfte, "schwer" die stärkere, sonst alle.
static func archetypes(tier: String = "") -> Array:
var keys: Array = ARCHETYPE_STRENGTH.keys()
keys.sort_custom(func(a, b): return ARCHETYPE_STRENGTH[a] < ARCHETYPE_STRENGTH[b])
var half: int = keys.size() / 2
match tier:
"leicht":
return keys.slice(0, half)
"schwer":
return keys.slice(half)
return keys
# Energiekurve der Gegnerdecks: Anzahl Karten je Kosten (6 = sechs und mehr).
const CURVE := {1: 3, 2: 5, 3: 4, 4: 3, 5: 2, 6: 3}
## Gegnerdeck aus den Elementen entlang der Energiekurve. bias > 1 bevorzugt seltene Karten, bias < 1 gewöhnliche.
static func build_opponent_deck(r: RandomNumberGenerator, elements: Array, bias: float = 1.0) -> Array:
var pool := Cards.all().filter(func(c): return c.elements.all(func(e): return e in elements))
var deck := []
var counts := {}
for bucket in CURVE:
for i in CURVE[bucket]:
var c = _pick_for_cost(r, pool, counts, bucket, bias)
if c != null:
counts[c.id] = counts.get(c.id, 0) + 1
deck.append(c)
while deck.size() < DECK_SIZE:
var c = _pick_for_cost(r, pool, counts, r.randi_range(1, 6), bias)
if c == null:
break
counts[c.id] = counts.get(c.id, 0) + 1
deck.append(c)
return deck
## Karte nahe den gewünschten Kosten, gewichtet nach Seltenheit (bias) und Kostenabstand.
## Bei extremer Seltenheitsvorgabe darf die Kurve nachgeben, sonst wäre das Ziel unerreichbar.
static func _pick_for_cost(r: RandomNumberGenerator, pool: Array, counts: Dictionary, cost: int, bias: float):
var candidates := []
var weights := PackedFloat32Array()
for c in pool:
if counts.get(c.id, 0) >= MAX_COPIES:
continue
var spread: int = absi(mini(c.cost, 6) - cost)
var w: float = OPPONENT_RARITY_WEIGHT[c.rarity]
w = w / bias if c.rarity == "gewöhnlich" else w * bias
w *= pow(0.3, spread)
candidates.append(c)
weights.append(w)
if candidates.is_empty():
return null
return candidates[r.rand_weighted(weights)]
## Zufälliger Gegner. target > 0: Deck auf diese Stärke gebaut. tier: Archetypen der Schwierigkeit.
static func random_opponent(r: RandomNumberGenerator, index: int, life: int = START_LIFE, target: float = 0.0, tier: String = "") -> Player:
var options := archetypes(tier)
var elements: Array = options[r.randi_range(0, options.size() - 1)].split("+")
var deck := build_opponent_deck_rated(r, elements, target) if target > 0.0 else build_opponent_deck(r, elements)
return Player.new("Gegner %d" % index, deck, false, life, Cards.combo_name(elements) + "-Magier")
## rules: {"human_perks": [...], "everyone_perk": String, "start_life": int, "target_rating": float, "tier": String}
# --- Deckstärke und Gegner (siehe DeckGen) --------------------------------------
static func deck_rating(cards: Array, stats: Dictionary = {}) -> float:
return DeckGen.deck_rating(cards, stats)
static func build_opponent_deck(r: RandomNumberGenerator, elements: Array, bias: float = 1.0) -> Array:
return DeckGen.build_opponent_deck(r, elements, bias)
static func build_opponent_deck_rated(r: RandomNumberGenerator, elements: Array, target: float, tolerance: float = 0.12) -> Array:
return DeckGen.build_opponent_deck_rated(r, elements, target, tolerance)
static func random_opponent(r: RandomNumberGenerator, index: int, life: int = START_LIFE, target: float = 0.0, tier: String = "") -> Player:
return DeckGen.random_opponent(r, index, life, target, tier)
static func archetypes(tier: String = "") -> Array:
return DeckGen.archetypes(tier)
static func new_battle(deck: Array, n_opp: int, r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
assert(n_opp >= 1 and n_opp <= MAX_OPPONENTS, "1 bis 5 Gegner")
if r == null: