Files
cards/scripts/deckgen.gd

147 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 := {
"Feuer": 36, "Wasser": 40, "Erde": 55, "Luft": 55,
"Feuer+Wasser": 50, "Feuer+Erde": 65, "Feuer+Luft": 46,
"Wasser+Erde": 51, "Wasser+Luft": 45, "Erde+Luft": 78,
}
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)