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:
@@ -15,7 +15,9 @@ static func creature_value(c: Game.Creature) -> float:
|
||||
if c.attacks_per_turn() == 2:
|
||||
v += c.power()
|
||||
if c.frozen:
|
||||
v -= c.power() * 0.5
|
||||
v -= c.power() # setzt den nächsten Angriff aus: der Angriffswert zählt nicht
|
||||
if not c.ready:
|
||||
v -= c.power() * 0.25
|
||||
return v
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ const FIELD_COMBO_TEXT := {
|
||||
"Sturm": "Ziehe 1 Karte",
|
||||
"Sand": "Das neue Wesen erhält Schild",
|
||||
}
|
||||
const RESONANCE_TEXT := "Resonanz: Ein Wesen hat +1 Angriff je anderem eigenen Wesen, das ein Element mit ihm teilt (höchstens +4)."
|
||||
const RESONANCE_TEXT := "Resonanz: Ein Wesen bekommt Angriff für eigene Wesen mit gleichem Element: +1, +2, +3, +3 bei ein bis vier Partnern."
|
||||
const RARITIES: Array[String] = ["gewöhnlich", "selten", "episch", "legendär", "mythisch"]
|
||||
const RARITY_COLORS := {
|
||||
"gewöhnlich": Color("aaaaaa"),
|
||||
|
||||
128
scripts/deckgen.gd
Normal file
128
scripts/deckgen.gd
Normal file
@@ -0,0 +1,128 @@
|
||||
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, tests/strength_probe.gd).
|
||||
const ARCHETYPE_STRENGTH := {
|
||||
"Feuer": 36, "Wasser": 30, "Erde": 63, "Luft": 50,
|
||||
"Feuer+Wasser": 46, "Feuer+Erde": 70, "Feuer+Luft": 40,
|
||||
"Wasser+Erde": 46, "Wasser+Luft": 20, "Erde+Luft": 46,
|
||||
}
|
||||
|
||||
|
||||
|
||||
## 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() < 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)
|
||||
1
scripts/deckgen.gd.uid
Normal file
1
scripts/deckgen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ev8t5ar278vl
|
||||
226
scripts/game.gd
226
scripts/game.gd
@@ -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:
|
||||
|
||||
99
scripts/netstate.gd
Normal file
99
scripts/netstate.gd
Normal file
@@ -0,0 +1,99 @@
|
||||
class_name NetState
|
||||
## Serialisierter Spielzustand für das Netzwerk (ausgelagert aus den Spielregeln).
|
||||
|
||||
|
||||
## Serialisierter Zustand aus Sicht eines Spielers: fremde Hände, Decks und Fallen nur als Anzahl.
|
||||
static func snapshot(g: Game, viewer: int) -> Dictionary:
|
||||
var ps := []
|
||||
for i in g.players.size():
|
||||
var p: Game.Player = g.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 g.events:
|
||||
var e: Dictionary = ev.duplicate()
|
||||
if e.has("card"):
|
||||
e["card"] = e.card.id
|
||||
evs.append(e)
|
||||
return {"players": ps, "human": g.players.find(g.human), "turn": g.turn, "current_idx": g.current_idx,
|
||||
"winner": g.winner, "winner_player": g.players.find(g.winner_player) if g.winner_player != null else -1,
|
||||
"next_uid": g.next_uid, "log": g.log.slice(maxi(0, g.log.size() - 80)), "events": evs}
|
||||
|
||||
|
||||
## 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 := Game.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 := Game.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
|
||||
1
scripts/netstate.gd.uid
Normal file
1
scripts/netstate.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c3ao1mpgbrter
|
||||
@@ -70,7 +70,7 @@ var card_stats := {} # id -> {"played", "games", "wins"}
|
||||
var booster_since_mythic := 0
|
||||
var tutorial_done := false
|
||||
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
||||
var records := {"streak": 0, "best_streak": 0, "fastest_win": 0}
|
||||
var records := {"streak": 0, "best_streak": 0, "fastest_win": 0, "loss_streak": 0}
|
||||
var challenge_week := -1 # Woche der zuletzt bestandenen Herausforderung
|
||||
|
||||
## Das aktive Deck (Kartenliste des gewählten Slots).
|
||||
@@ -174,7 +174,7 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
p.perk = ""
|
||||
var rc = d.get("records", {})
|
||||
p.records = {"streak": int(rc.get("streak", 0)), "best_streak": int(rc.get("best_streak", 0)),
|
||||
"fastest_win": int(rc.get("fastest_win", 0))}
|
||||
"fastest_win": int(rc.get("fastest_win", 0)), "loss_streak": int(rc.get("loss_streak", 0))}
|
||||
p.challenge_week = int(d.get("challenge_week", -1))
|
||||
return p
|
||||
|
||||
@@ -380,11 +380,17 @@ func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, pla
|
||||
stats.games += 1
|
||||
if won:
|
||||
records.streak += 1
|
||||
records.loss_streak = 0
|
||||
records.best_streak = maxi(records.best_streak, records.streak)
|
||||
if turns > 0 and (records.fastest_win == 0 or turns < records.fastest_win):
|
||||
records.fastest_win = turns
|
||||
else:
|
||||
records.streak = 0
|
||||
records.loss_streak += 1
|
||||
var tip := suggest_difficulty()
|
||||
if tip != "" and stage < 0 and challenge.is_empty():
|
||||
notices.append("Tipp: %s. Schwierigkeit %s?" % [
|
||||
"%d Siege in Folge" % records.streak if won else "%d Niederlagen in Folge" % records.loss_streak, DIFFICULTIES[tip].name])
|
||||
for cid in played_ids:
|
||||
_stat(cid).played += 1
|
||||
var seen := {}
|
||||
@@ -477,10 +483,71 @@ func difficulty() -> Dictionary:
|
||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||
|
||||
|
||||
## Vorschlag: nach drei Siegen in Folge eine Stufe rauf, nach drei Niederlagen runter. "" = kein Vorschlag.
|
||||
func suggest_difficulty() -> String:
|
||||
var order := ["leicht", "normal", "schwer"]
|
||||
var i := order.find(str(settings.get("difficulty", "normal")))
|
||||
if records.streak >= 3 and i < order.size() - 1:
|
||||
return order[i + 1]
|
||||
if records.loss_streak >= 3 and i > 0:
|
||||
return order[i - 1]
|
||||
return ""
|
||||
|
||||
|
||||
## Eigene Deckstärke, mit Kartenstatistik gewichtet.
|
||||
func deck_rating(cards: Array = []) -> float:
|
||||
var deck_used: Array = cards if not cards.is_empty() else deck_cards()
|
||||
return Game.deck_rating(deck_used, card_stats)
|
||||
|
||||
|
||||
## Zielstärke der Gegnerdecks: eigene Deckstärke mal Schwierigkeitsfaktor.
|
||||
func target_rating(cards: Array = []) -> float:
|
||||
var deck_used: Array = cards if not cards.is_empty() else deck_cards()
|
||||
return Game.deck_rating(deck_used) * float(difficulty().factor)
|
||||
return deck_rating(cards) * float(difficulty().factor)
|
||||
|
||||
|
||||
## Tauscht die schwächste Karte des aktiven Decks gegen cid, wenn das Deck dadurch stärker und gültig bleibt.
|
||||
## dry_run: nur prüfen. Gibt "" zurück oder den Grund, warum nicht.
|
||||
func improve_deck_with(cid: String, dry_run: bool = false) -> String:
|
||||
if collection.get(cid, 0) <= 0:
|
||||
return "Karte nicht in der Sammlung"
|
||||
var card := Cards.by_id(cid)
|
||||
if deck.count(cid) >= mini(collection[cid], Game.MAX_COPIES):
|
||||
return "Schon so oft im Deck wie möglich"
|
||||
var elems := Game.deck_elements(deck_cards())
|
||||
for e in card.elements:
|
||||
if not (e in elems):
|
||||
elems.append(e)
|
||||
if elems.size() > Game.MAX_DECK_ELEMENTS:
|
||||
return "Passt nicht zu den Elementen des Decks"
|
||||
var weakest := ""
|
||||
var weakest_value := Cards.card_value(card)
|
||||
for other in deck:
|
||||
if other == cid:
|
||||
continue
|
||||
var v := Cards.card_value(Cards.by_id(other))
|
||||
if v < weakest_value:
|
||||
weakest_value = v
|
||||
weakest = other
|
||||
if weakest == "":
|
||||
return "Keine schwächere Karte im Deck"
|
||||
if dry_run:
|
||||
return ""
|
||||
var trial: Array = deck.duplicate()
|
||||
trial[trial.find(weakest)] = cid
|
||||
var ok := Game.validate_deck(trial.map(func(id): return Cards.by_id(id)), collection)
|
||||
if not ok[0]:
|
||||
return ok[1]
|
||||
deck = trial
|
||||
return ""
|
||||
|
||||
|
||||
## Karten aus new_ids, die das aktive Deck verbessern würden.
|
||||
func deck_suggestions(new_ids: Array) -> Array:
|
||||
var out := []
|
||||
for cid in new_ids:
|
||||
if not (cid in out) and improve_deck_with(cid, true) == "":
|
||||
out.append(cid)
|
||||
return out
|
||||
|
||||
|
||||
func perk_unlocked() -> bool:
|
||||
|
||||
@@ -27,6 +27,7 @@ var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
|
||||
var mulligan_selected: Array = []
|
||||
var tutorial_step := 0
|
||||
var challenge := {} # wöchentliche Herausforderung oder leer
|
||||
var preset_opponents: Array = [] # aus der Gegnervorschau übernommene Gegner
|
||||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||||
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
||||
@@ -129,6 +130,12 @@ static func guided() -> BattleScreen:
|
||||
return b
|
||||
|
||||
|
||||
static func against(opps: Array) -> BattleScreen:
|
||||
var b := BattleScreen.new(opps.size(), -1)
|
||||
b.preset_opponents = opps
|
||||
return b
|
||||
|
||||
|
||||
static func weekly(ch: Dictionary) -> BattleScreen:
|
||||
var b := BattleScreen.new(int(ch.n_opp), -1)
|
||||
b.challenge = ch
|
||||
@@ -181,6 +188,8 @@ func _ready() -> void:
|
||||
elif stage >= 0:
|
||||
var r := Game.make_rng()
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules())
|
||||
elif not preset_opponents.is_empty():
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), preset_opponents, null, false, own_rules())
|
||||
else:
|
||||
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules())
|
||||
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
||||
|
||||
@@ -12,6 +12,8 @@ var coins_label: Label
|
||||
var msg_label: Label
|
||||
var b_buy: Button
|
||||
var b_all: Button
|
||||
var b_improve: Button
|
||||
var suggestion := ""
|
||||
var slots: Control
|
||||
|
||||
|
||||
@@ -52,6 +54,8 @@ func _ready() -> void:
|
||||
msg_label = Style.label(self, "", 18, Style.RED, Rect2(0, 610, 1280, 30), "center")
|
||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(360, 720, 260, 48), buy, Color("5a4628"))
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 140, 48), reveal_all)
|
||||
b_improve = Style.button(self, "", Rect2(360, 650, 560, 44), improve, Color("285a3c"))
|
||||
b_improve.visible = false
|
||||
Style.button(self, "Zurück", Rect2(800, 720, 120, 48), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh()
|
||||
|
||||
@@ -95,11 +99,37 @@ func reveal_all() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Nimmt die vorgeschlagene Karte ins aktive Deck (ersetzt die schwächste).
|
||||
func improve() -> void:
|
||||
if suggestion == "":
|
||||
return
|
||||
var err: String = app.profile.improve_deck_with(suggestion)
|
||||
if err == "":
|
||||
app.save()
|
||||
msg_label.text = "%s ist jetzt im Deck %s" % [Cards.by_id(suggestion).name, app.profile.deck_name()]
|
||||
msg_label.add_theme_color_override("font_color", Style.GREEN)
|
||||
suggestion = ""
|
||||
else:
|
||||
msg_label.text = err
|
||||
msg_label.add_theme_color_override("font_color", Style.RED)
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
coins_label.text = "Münzen: %d" % p.coins
|
||||
b_buy.disabled = p.coins < Profile.BOOSTER_COST
|
||||
b_all.disabled = cards.is_empty() or revealed.size() >= 5
|
||||
# Deck-Vorschlag, sobald alle Karten offen liegen
|
||||
suggestion = ""
|
||||
if not cards.is_empty() and revealed.size() >= 5:
|
||||
var ids: Array = cards.map(func(c): return c.id)
|
||||
var better: Array = p.deck_suggestions(ids)
|
||||
if not better.is_empty():
|
||||
suggestion = better[0]
|
||||
b_improve.visible = suggestion != ""
|
||||
if suggestion != "":
|
||||
b_improve.text = tr("Deck verbessern: %s ins aktive Deck (%s)") % [Cards.by_id(suggestion).name, p.deck_name()]
|
||||
Style.clear(slots)
|
||||
for i in 5:
|
||||
var r := slot_rect(i)
|
||||
|
||||
@@ -63,7 +63,8 @@ func build() -> void:
|
||||
var extra: int = mini(st.extra, Game.MAX_OPPONENTS - 1)
|
||||
var table: String = "Duell" if extra == 0 else "%d am Tisch" % (extra + 2)
|
||||
var perks_text: String = ", ".join(st.perks.map(func(pk): return Game.PERKS[pk]))
|
||||
Style.clabel(r, "%s · %d Münzen\n★ %s" % [table, st.reward, perks_text], 12, Style.MUTED, "left", false, 360)
|
||||
var target := Campaign.target_rating(i, p.target_rating())
|
||||
Style.clabel(r, "%s · Stärke %.1f · %d Münzen\n★ %s" % [table, target, st.reward, perks_text], 12, Style.MUTED, "left", false, 360)
|
||||
if done:
|
||||
Style.clabel(r, "abgeschlossen", 13, Style.GREEN, "right", false, 100)
|
||||
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i))
|
||||
|
||||
@@ -1,46 +1,66 @@
|
||||
class_name OpponentSelectScreen
|
||||
extends Control
|
||||
## Einzelspieler: Schwierigkeit und Gegnerzahl. Gegnerdecks werden auf die eigene Deckstärke gebaut.
|
||||
## Einzelspieler: Schwierigkeit, Gegnerzahl und Vorschau der Gegner (Element, Stärke).
|
||||
|
||||
var app
|
||||
var rating_label: Label
|
||||
var tip_label: Label
|
||||
var diff_buttons := {}
|
||||
var previews := {} # n -> Array von Game.Player
|
||||
var preview_labels := {}
|
||||
var b_tip: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 900, 40, 12)
|
||||
var col := Style.column(self, 1000, 30, 10)
|
||||
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
|
||||
Style.clabel(col, "Jeder kämpft gegen jeden, alle nach denselben Regeln mit %d Leben. Du gewinnst, wenn alle anderen besiegt sind." % Game.START_LIFE, 16, Style.MUTED)
|
||||
Style.spacer(col, 4)
|
||||
Style.clabel(col, "Jeder kämpft gegen jeden, alle nach denselben Regeln mit %d Leben. Du gewinnst, wenn alle anderen besiegt sind." % Game.START_LIFE, 15, Style.MUTED)
|
||||
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
|
||||
var dr := Style.row(col, 10)
|
||||
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for key in ["leicht", "normal", "schwer"]:
|
||||
var k: String = key
|
||||
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 42), func(): set_difficulty(k))
|
||||
rating_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||
Style.spacer(col, 4)
|
||||
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k))
|
||||
rating_label = Style.clabel(col, "", 14, Style.MUTED)
|
||||
var tr_row := Style.row(col, 10)
|
||||
tr_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
tip_label = Style.clabel(tr_row, "", 14, Style.GOLD, "right", false, 520)
|
||||
b_tip = Style.cbutton(tr_row, "Übernehmen", Vector2(140, 32), take_tip)
|
||||
Style.spacer(col, 2)
|
||||
for i in range(1, Game.MAX_OPPONENTS + 1):
|
||||
var n := i
|
||||
var r := Style.row(col, 20)
|
||||
var r := Style.row(col, 16)
|
||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(r, "%d Gegner" % n, Vector2(200, 50), func(): app.show_screen(BattleScreen.new(n)), Color("285a3c"))
|
||||
var info := ("Duell" if n == 1 else "%d Spieler am Tisch" % (n + 1)) + " · Sieg: %d Münzen" % Profile.REWARD_WIN[n]
|
||||
Style.clabel(r, info, 16, Style.MUTED, "left", false, 420)
|
||||
Style.spacer(col, 10)
|
||||
var br := Style.row(col)
|
||||
Style.cbutton(r, "%d Gegner" % n, Vector2(150, 46), func(): start(n), Color("285a3c"))
|
||||
var info := ("Duell" if n == 1 else "%d am Tisch" % (n + 1)) + " · %d Münzen" % Profile.REWARD_WIN[n]
|
||||
Style.clabel(r, info, 13, Style.MUTED, "left", false, 150)
|
||||
preview_labels[n] = Style.clabel(r, "", 13, Style.TEXT, "left", false, 600)
|
||||
Style.spacer(col, 4)
|
||||
var br := Style.row(col, 16)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh()
|
||||
Style.cbutton(br, "Neu würfeln", Vector2(150, 40), func(): refresh(true))
|
||||
Style.cbutton(br, "Zurück", Vector2(150, 40), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh(true)
|
||||
|
||||
|
||||
func set_difficulty(key: String) -> void:
|
||||
app.profile.settings.difficulty = key
|
||||
app.save()
|
||||
refresh()
|
||||
refresh(true)
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
func take_tip() -> void:
|
||||
var tip: String = app.profile.suggest_difficulty()
|
||||
if tip != "":
|
||||
set_difficulty(tip)
|
||||
|
||||
|
||||
func start(n: int) -> void:
|
||||
app.show_screen(BattleScreen.against(previews[n]))
|
||||
|
||||
|
||||
## Baut die Gegner für jede Anzahl vor und zeigt Element und Stärke; der Kampf nutzt genau diese.
|
||||
func refresh(reroll: bool = false) -> void:
|
||||
var p: Profile = app.profile
|
||||
var d := p.difficulty()
|
||||
for key in diff_buttons:
|
||||
@@ -50,7 +70,23 @@ func refresh() -> void:
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Color("285a3c") if active else Style.PANEL_LIGHT), 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
var arche: String = tr("schwache Archetypen") if d.tier == "leicht" else (tr("starke Archetypen") if d.tier == "schwer" else tr("alle Archetypen"))
|
||||
rating_label.text = tr("Deine Deckstärke %.1f · Gegnerdecks %.1f · %s · Münzen x%.2f · KI: %s") % [
|
||||
Game.deck_rating(p.deck_cards()), p.target_rating(), arche, d.reward, tr("Vorausschau") if d.smart else tr("einfach")]
|
||||
p.deck_rating(), p.target_rating(), arche, d.reward, tr("Vorausschau") if d.smart else tr("einfach")]
|
||||
var tip := p.suggest_difficulty()
|
||||
tip_label.text = (tr("Tipp: %s") % (("%d Siege in Folge, versuche %s" % [p.records.streak, Profile.DIFFICULTIES[tip].name]) if p.records.streak >= 3
|
||||
else ("%d Niederlagen in Folge, versuche %s" % [p.records.loss_streak, Profile.DIFFICULTIES[tip].name]))) if tip != "" else ""
|
||||
b_tip.visible = tip != ""
|
||||
if reroll:
|
||||
var r := Game.make_rng()
|
||||
for n in range(1, Game.MAX_OPPONENTS + 1):
|
||||
var opps := []
|
||||
for i in n:
|
||||
opps.append(Game.random_opponent(r, i + 1, Game.START_LIFE, p.target_rating(), str(d.tier)))
|
||||
previews[n] = opps
|
||||
for n in previews:
|
||||
var parts: Array[String] = []
|
||||
for o in previews[n]:
|
||||
parts.append("%s (%.1f)" % [o.title, Game.deck_rating(o.deck)])
|
||||
preview_labels[n].text = ", ".join(parts)
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
||||
Reference in New Issue
Block a user