Je 200 Partien pro Archetyp; Spanne 40–64 % statt 36–78 %. Feuer+Erde bleibt mit 64 % die Spitze, Feuer mit 40 % das Schlusslicht. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
143 lines
5.8 KiB
GDScript
143 lines
5.8 KiB
GDScript
class_name DeckGen
|
|
## Deckstärke, Gegnerdecks und Archetypen (ausgelagert aus den Spielregeln).
|
|
|
|
const OPPONENT_RARITY_WEIGHT := {"gewöhnlich": 6.0, "selten": 3.0, "episch": 2.0, "legendär": 1.0, "mythisch": 0.3}
|
|
# Abnehmende Resonanz je Anzahl gleichelementiger Partner (Index = Partner).
|
|
const RESONANCE_STEPS := [0, 1, 2, 3, 3]
|
|
|
|
# Gemessene Stärke der Elementkombinationen (Siegquote gegen das Feld, KI gegen KI, 60 Partien je Archetyp, tests/strength_probe.gd).
|
|
const ARCHETYPE_STRENGTH := {"Erde": 50, "Erde+Luft": 56, "Feuer": 40, "Feuer+Erde": 64, "Feuer+Luft": 42, "Feuer+Wasser": 57, "Luft": 45, "Wasser": 43, "Wasser+Erde": 50, "Wasser+Luft": 41}
|
|
|
|
|
|
|
|
const ARCHETYPE_PATH := "res://data/archetypen.json"
|
|
static var _strength: Dictionary = {}
|
|
|
|
|
|
## Gemessene Werte aus data/archetypen.json (vom Messlauf geschrieben), sonst die eingebaute Tabelle.
|
|
static func strength() -> Dictionary:
|
|
if _strength.is_empty():
|
|
_strength = ARCHETYPE_STRENGTH.duplicate()
|
|
if FileAccess.file_exists(ARCHETYPE_PATH):
|
|
var json := JSON.new()
|
|
if json.parse(FileAccess.get_file_as_string(ARCHETYPE_PATH)) == OK and json.data is Dictionary:
|
|
for k in json.data:
|
|
if _strength.has(k):
|
|
_strength[k] = int(json.data[k])
|
|
return _strength
|
|
|
|
|
|
## Elementkombinationen einer Stufe: "leicht" die schwächere Hälfte, "schwer" die stärkere, sonst alle.
|
|
static func archetypes(tier: String = "") -> Array:
|
|
var table := strength()
|
|
var keys: Array = table.keys()
|
|
keys.sort_custom(func(a, b): return table[a] < table[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() < Game.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) >= Game.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)]
|
|
|
|
|
|
## 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
|
|
|
|
|
|
## 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 = Game.START_LIFE, target: float = 0.0, tier: String = "") -> Game.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 Game.Player.new("Gegner %d" % index, deck, false, life, Cards.combo_name(elements) + "-Magier")
|
|
|
|
|
|
## Kartenwert, bei genug Partien mit der Siegquote der Karte gewichtet (Statistik aus Spieltests).
|
|
static func card_value(c: Cards.Card, stats: Dictionary = {}) -> float:
|
|
var v := Cards.card_value(c)
|
|
if stats.has(c.id):
|
|
var st: Dictionary = stats[c.id]
|
|
if int(st.games) >= 10:
|
|
v *= 0.5 + float(st.wins) / float(st.games)
|
|
return v
|
|
|
|
|
|
## Abzug für schlechte Energiekurven: zu wenig günstige oder zu viele teure Karten.
|
|
static func curve_penalty(cards: Array) -> float:
|
|
var cheap := 0
|
|
var expensive := 0
|
|
for c in cards:
|
|
if c.cost <= 2:
|
|
cheap += 1
|
|
if c.cost >= 5:
|
|
expensive += 1
|
|
return minf(0.05 * maxi(0, 6 - cheap) + 0.05 * maxi(0, expensive - 5), 0.4)
|
|
|
|
|
|
## Deckstärke: Durchschnitt der Kartenwerte minus Kurvenabzug (1 = nur gewöhnliche Karten, bis etwa 3,5).
|
|
static func deck_rating(cards: Array, stats: Dictionary = {}) -> float:
|
|
if cards.is_empty():
|
|
return 0.0
|
|
var sum := 0.0
|
|
for c in cards:
|
|
sum += card_value(c, stats)
|
|
return sum / cards.size() - curve_penalty(cards)
|