class_name Game extends RefCounted ## Spielregeln: Duell, Mana je Zug, Ziehen aus dem gemeinsamen Stapel, verdeckte Wesen, ## Vier-Elemente-Regel, Verschmelzen und Verbessern. const MAX_BOARD := 5 const MAX_HAND := 5 const START_MANA := 5 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt const DRAW_COST := 1 # Ziehen: drei Karten sehen, eine behalten const OFFER_SIZE := 3 const START_OFFER := 5 # zu Beginn fünf sehen, drei behalten const START_PICKS := 3 const START_LIFE := 30 const ELEMENT_STACK := 4 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1, das geschlagene Element -1/-1 # Merkmale der Kampagnen-Bosse (Player.perks). const PERKS := { "feuer_angriff": "Feuerwesen haben +1 Angriff", "ziehen_gratis": "Ziehen kostet kein Mana", "wesen_angriff": "Wesen kommen mit +1 Angriff", "wesen_leben": "Wesen kommen mit +1 Leben", "extra_mana": "Bekommt jeden Zug 1 Mana mehr", "leben_35": "Beginnt mit 5 Leben mehr", "schild_wesen": "Wesen kommen mit Schild", "verbessern_billig": "Eigenschaften kosten 1 weniger", "start_energie": "Hat im ersten Zug 2 Mana mehr", "element_doppelt": "Elementbonus zählt doppelt", } ## Für wiederholbare Tests: 0 = echter Zufall, sonst Startwert (zählt je Aufruf hoch). static var test_seed := 0 static func make_rng() -> RandomNumberGenerator: var r := RandomNumberGenerator.new() if test_seed != 0: r.seed = test_seed test_seed += 1 else: r.randomize() return r class Creature extends RefCounted: var card: Cards.Card var owner_ref: WeakRef # schwache Referenz: kein Zyklus Spieler <-> Wesen var name: String var uid: int var attack: int var health: int var max_health: int var shield: bool var attacks_left: int var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht für die Elementregel var aura := 0 # Vier-Elemente-Regel: +1 oder -1 auf den Angriff (Leben wird direkt angepasst) func _init(c: Cards.Card, o, id: int) -> void: card = c owner_ref = weakref(o) name = c.name uid = id attack = c.attack health = c.health max_health = c.health shield = "Schild" in c.keywords attacks_left = attacks_per_turn() # frisch gelegte Wesen greifen sofort an func owner(): return owner_ref.get_ref() ## Angriffswert inklusive Elementbonus und Boss-Merkmalen. func power() -> int: var v := attack + aura var o = owner() if o != null: if "feuer_angriff" in o.perks and "Feuer" in card.elements: v += 1 return v func attacks_per_turn() -> int: return 2 if "Doppelschlag" in card.keywords else 1 func can_attack() -> bool: return attacks_left > 0 and not hidden and power() > 0 func clone(new_owner) -> Creature: var c := Creature.new(card, new_owner, uid) c.attack = attack c.health = health c.max_health = max_health c.shield = shield c.attacks_left = attacks_left c.hidden = hidden c.aura = aura return c class Player extends RefCounted: var name: String var title: String var is_human: bool var life: int var max_life: int var offer: Array = [] # offene Wahl: angebotene Karten (Start: 5, Ziehen: 3) var picks_left := 0 # wie viele davon noch zu nehmen sind var hand: Array = [] # eigene Exemplare (Cards.Card.copy) var board: Array = [] var energy := 0 # Mana in diesem Zug; der Rest verfällt var max_energy := 0 # Mana zu Zugbeginn (START_MANA + eigene Züge) var start_bonus := 0 # einmaliges Extra-Mana für den späteren Start var order := 0 # Platz in der Zugreihenfolge var perks: Array = [] # Boss-Merkmale (siehe PERKS) var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile) var turns := 0 # begonnene eigene Züge func _init(n: String, h: bool, l: int, t: String = "") -> void: name = n title = t is_human = h life = l max_life = l func alive() -> bool: return life > 0 func clone() -> Player: var p := Player.new(name, is_human, life, title) p.max_life = max_life p.offer = offer.duplicate() p.picks_left = picks_left p.hand = hand.duplicate() p.energy = energy p.max_energy = max_energy p.start_bonus = start_bonus p.order = order p.perks = perks.duplicate() p.looks = looks p.turns = turns for c in board: p.board.append(c.clone(p)) return p var human: Player # der (erste) menschliche Spieler var humans: Array = [] # alle menschlichen Spieler (im Netz zwei) var opponent: Player # der andere am Tisch var players: Array = [] # in Zugreihenfolge var winner_player: Player = null var rng: RandomNumberGenerator var deck: Array = [] # gemeinsamer Stapel, das Ende ist oben (nur Host und Einzelspieler) var turn := 0 var current_idx := 0 var log: Array[String] = [] var events: Array = [] # strukturierte Ereignisse für Animationen var winner := "" # "", "human" oder "opponents" var quiet := false # in KI-Kopien: kein Verlauf, keine Ereignisse var next_uid := 1 func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void: human = h opponent = o players = [h, o] humans = players.filter(func(p): return p.is_human) if r == null: r = make_rng() rng = r func current() -> Player: return players[current_idx] func say(t: String) -> void: if not quiet: log.append(t) func emit(ev: Dictionary) -> void: if not quiet: events.append(ev) func ref_of(target) -> Dictionary: if target is Creature: return {"kind": "creature", "uid": target.uid} return {"kind": "player", "index": players.find(target)} ## Der Gegner von p, solange er lebt (als Liste, für Schleifen über Ziele). func enemies_of(p: Player) -> Array: return players.filter(func(o): return o != p and o.alive()) func enemy_of(p: Player) -> Player: return opponent if p == human else human func find_creature(uid: int) -> Creature: for p in players: for c in p.board: if c.uid == uid: return c return null ## Unabhängige Kopie des Spielzustands (für Rückgängig und die KI-Vorausschau). func clone() -> Game: var copies := [] for p in players: copies.append(p.clone()) var idx := players.find(human) var g := Game.new(copies[idx], copies[1 - idx], RandomNumberGenerator.new()) g.players = copies # gleiche Zugreihenfolge wie das Original g.deck = deck.duplicate() g.turn = turn g.current_idx = current_idx g.winner = winner g.next_uid = next_uid g.quiet = true return g ## Kopie aus Sicht von viewer: verdeckte gegnerische Wesen sind unbekannt (für die KI). func masked_clone(viewer: Player) -> Game: var g := clone() var me: Player = g.players[players.find(viewer)] for p in g.players: if p == me: continue for c in p.board: if c.hidden: c.card = Cards.unknown() c.name = c.card.name c.attack = c.card.attack c.health = c.card.health c.max_health = c.card.health c.shield = false return g # --- Zugablauf --------------------------------------------------------------- func start(fixed_order: bool = false) -> void: if not fixed_order: shuffle(players, rng) # zufällige Zugreihenfolge deck = DeckGen.deck(rng) for i in players.size(): var p: Player = players[i] p.order = i # Wer nicht beginnt, bekommt im ersten Zug einmalig 1 Mana extra. p.start_bonus = 0 if i == 0 else 1 p.offer = draw_cards(START_OFFER, START_MANA + p.start_bonus) p.picks_left = START_PICKS turn = 1 current_idx = 0 say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name))) for p in players: for perk in p.perks: say("%s: %s." % [p.name, PERKS[perk]]) begin_turn(current()) ## Nimmt n Karten vom Stapel, die höchstens max_cost kosten (was man sich in diesem Zug leisten ## kann); zu teure wandern unter den Stapel. Ist der Stapel leer, kommt ein frischer. func draw_cards(n: int, max_cost: int) -> Array: var out := [] var skipped := [] var looked := 0 while out.size() < n: if deck.is_empty(): deck = DeckGen.deck(rng) say("Der Stapel wird neu gemischt.") var c: Cards.Card = deck.pop_back() looked += 1 if c.cost <= max_cost or looked > deck.size() + skipped.size() + n: out.append(c) else: skipped.append(c) for c in skipped: deck.insert(0, c) return out ## Solange eine Wahl offen ist, geht nichts anderes. func blocked(p: Player) -> String: return "Erst wählen" if not p.offer.is_empty() else "" ## Ziehen: kostet DRAW_COST, zeigt OFFER_SIZE Karten, eine davon kommt auf die Hand (siehe pick). func can_draw(p: Player) -> Array: if winner != "" or p != current(): return [false, "Nicht dein Zug"] if blocked(p) != "": return [false, blocked(p)] if p.hand.size() >= MAX_HAND: return [false, "Deine Hand ist voll"] if p.energy < draw_cost(p): return [false, "Nicht genug Mana"] return [true, ""] func draw_cost(p: Player) -> int: return 0 if "ziehen_gratis" in p.perks else DRAW_COST ## Preis einer Eigenschaft für p. func upgrade_cost(p: Player, keyword: String) -> int: var c := int(Cards.KEYWORD_COST[keyword]) return maxi(1, c - 1) if "verbessern_billig" in p.perks else c func draw(p: Player) -> String: var ok := can_draw(p) if not ok[0]: return ok[1] p.energy -= draw_cost(p) p.offer = draw_cards(OFFER_SIZE, p.max_energy) p.picks_left = 1 say("%s zieht." % p.name) emit({"type": "draw", "player": players.find(p)}) return "" ## Nimmt eine der angebotenen Karten auf die Hand; jederzeit erlaubt, auch außerhalb des Zugs. ## Was liegen bleibt, wandert unter den Stapel. func pick(p: Player, index: int) -> String: if winner != "": return "Vorbei" if p.offer.is_empty(): return "Keine Wahl offen" if index < 0 or index >= p.offer.size(): return "Keine solche Karte" var card: Cards.Card = p.offer[index] p.hand.append(card.copy()) p.offer.remove_at(index) p.picks_left -= 1 if p.picks_left <= 0 or p.hand.size() >= MAX_HAND: for rest in p.offer: deck.insert(0, rest) p.offer = [] p.picks_left = 0 say("%s nimmt eine Karte." % p.name) emit({"type": "pick", "player": players.find(p)}) return "" func begin_turn(p: Player) -> void: p.max_energy = START_MANA + p.turns p.energy = p.max_energy + (1 if "extra_mana" in p.perks else 0) # der Rest vom letzten Zug ist verfallen if p.turns == 0 and "start_energie" in p.perks: p.energy += 2 if p.start_bonus > 0: p.energy += p.start_bonus say("%s erhält %d Mana extra für den späteren Start." % [p.name, p.start_bonus]) p.start_bonus = 0 for c in p.board: c.health = c.max_health # Wesen erholen sich zu Zugbeginn ganz c.attacks_left = c.attacks_per_turn() p.turns += 1 say("--- Zug %d: %s ---" % [turn, p.name]) emit({"type": "turn", "player": players.find(p)}) func end_turn() -> void: if winner != "": return while not current().offer.is_empty(): # offene Wahl: die erste Karte, damit nichts verfällt pick(current(), 0) var n := players.size() for i in range(1, n + 1): var idx := (current_idx + i) % n if players[idx].alive(): if idx <= current_idx: turn += 1 current_idx = idx break begin_turn(current()) # --- Karten spielen ---------------------------------------------------------- ## Ausspielen kostet die Kartenkosten; verdeckt gelegt ist es gleich teuer. func can_play(p: Player, card: Cards.Card) -> Array: if winner != "" or p != current(): return [false, "Nicht dein Zug"] if blocked(p) != "": return [false, blocked(p)] if not (card in p.hand): return [false, "Nicht auf der Hand"] if p.board.size() >= MAX_BOARD: return [false, "Dein Feld ist voll"] if p.energy < card.cost: return [false, "Nicht genug Mana"] return [true, ""] ## Spielt eine Karte, offen oder verdeckt. Gibt "" zurück oder eine Fehlermeldung. func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String: var ok := can_play(p, card) if not ok[0]: return ok[1] p.hand.erase(card) p.energy -= card.cost var cr := Creature.new(card, p, next_uid) next_uid += 1 cr.hidden = face_down if "wesen_angriff" in p.perks: cr.attack += 1 if "wesen_leben" in p.perks: cr.health += 1 cr.max_health += 1 if "schild_wesen" in p.perks: cr.shield = true p.board.append(cr) if face_down: say("%s legt ein Wesen verdeckt." % p.name) else: say("%s spielt %s." % [p.name, card.name]) emit({"type": "play", "player": players.find(p), "uid": cr.uid, "card": card, "hidden": face_down}) apply_auras() cleanup() check_end() return "" ## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug. func can_reveal(p: Player, cr: Creature) -> Array: if winner != "" or p != current(): return [false, "Nicht dein Zug"] if blocked(p) != "": return [false, blocked(p)] if cr == null or not (cr in p.board): return [false, "Nur eigene Wesen"] if not cr.hidden: return [false, "%s liegt schon offen" % cr.name] return [true, ""] func reveal(p: Player, cr: Creature) -> String: var ok := can_reveal(p, cr) if not ok[0]: return ok[1] _flip(cr) cleanup() check_end() return "" ## Deckt ein Wesen auf; die Elementregel gilt sofort. func _flip(cr: Creature) -> void: cr.hidden = false say("%s wird aufgedeckt." % cr.name) emit({"type": "reveal", "uid": cr.uid, "card": cr.card}) apply_auras() # --- Verbessern: Schlüsselwörter für Mana ----------------------------------- ## target: eigenes Wesen auf dem Brett oder eigene Handkarte. func can_upgrade(p: Player, target, keyword: String) -> Array: if winner != "" or p != current(): return [false, "Nicht dein Zug"] if blocked(p) != "": return [false, blocked(p)] if not Cards.KEYWORD_COST.has(keyword): return [false, "Unbekannte Eigenschaft"] var card: Cards.Card = target.card if target is Creature else target if target is Creature: if not (target in p.board): return [false, "Nur eigene Wesen"] elif not (target in p.hand): return [false, "Nicht auf der Hand"] if keyword in card.keywords: return [false, "%s hat schon %s" % [card.name, keyword]] if p.energy < upgrade_cost(p, keyword): return [false, "Nicht genug Mana"] return [true, ""] func upgrade(p: Player, target, keyword: String) -> String: var ok := can_upgrade(p, target, keyword) if not ok[0]: return ok[1] p.energy -= upgrade_cost(p, keyword) var card: Cards.Card = (target.card if target is Creature else target).copy() # Kopien (KI) teilen sich Karten card.keywords.append(keyword) if target is Creature: target.card = card else: p.hand[p.hand.find(target)] = card if target is Creature: match keyword: "Schild": target.shield = true "Doppelschlag": target.attacks_left = mini(target.attacks_per_turn(), target.attacks_left + 1) say("%s bekommt %s." % [card.name, keyword]) emit({"type": "upgrade", "player": players.find(p), "keyword": keyword, "uid": target.uid if target is Creature else -1}) return "" # --- Verschmelzen ------------------------------------------------------------ ## Kosten: 1 Mana, verdoppelt je Verschmelzung, die schon in einem der beiden steckt. static func fusion_cost(a: Creature, b: Creature) -> int: return 1 << maxi(a.card.fusions, b.card.fusions) func can_fuse(p: Player, a: Creature, b: Creature) -> Array: if winner != "" or p != current(): return [false, "Nicht dein Zug"] if blocked(p) != "": return [false, blocked(p)] if a == null or b == null or a == b: return [false, "Zwei verschiedene Wesen wählen"] if not (a in p.board) or not (b in p.board): return [false, "Nur eigene Wesen"] if p.energy < fusion_cost(a, b): return [false, "Nicht genug Mana"] return [true, ""] ## Verschmilzt zwei eigene Wesen zu einem offenen Wesen: Elemente und Schlüsselwörter beider, ## jeweils der bessere Wert (Angriff, Leben, Lebenshöchstwert) plus 1. Der Elementbonus der ## Eltern wird herausgerechnet, das neue Wesen bekommt seinen eigenen. func fuse(p: Player, a: Creature, b: Creature) -> String: var ok := can_fuse(p, a, b) if not ok[0]: return ok[1] p.energy -= fusion_cost(a, b) var card := Cards.fused(a.card, b.card, maxi(a.attack, b.attack) + 1, maxi(a.max_health - a.aura, b.max_health - b.aura) + 1) var cr := Creature.new(card, p, next_uid) next_uid += 1 cr.health = maxi(a.health - a.aura, b.health - b.aura) + 1 cr.shield = cr.shield or a.shield or b.shield cr.attacks_left = mini(a.attacks_left, b.attacks_left) var at := p.board.find(a) p.board.erase(a) p.board.erase(b) p.board.insert(mini(at, p.board.size()), cr) say("%s und %s verschmelzen zu %s (%d/%d)." % [a.name, b.name, cr.name, cr.attack, cr.health]) emit({"type": "fuse", "player": players.find(p), "uid": cr.uid, "gone": [a.uid, b.uid]}) apply_auras() cleanup() check_end() return "" # --- Vier-Elemente-Regel ----------------------------------------------------- ## Offene Wesen je Element auf dem ganzen Tisch. func element_counts() -> Dictionary: var counts := {} for e in Cards.ELEMENTS: counts[e] = 0 for p in players: for c in p.board: if not c.hidden: for e in c.card.elements: counts[e] += 1 return counts ## Bonus aus der Elementregel für diese Elemente: +1, wenn ELEMENT_STACK offene Wesen des ## Elements stehen, -1, wenn das Element, das es schlägt, den Stapel voll hat. ## Mehrere Elemente nehmen den besten Wert. static func aura_of(elements: Array, counts: Dictionary) -> int: var best := 0 for i in elements.size(): var e: String = elements[i] var a := 0 if int(counts[e]) >= ELEMENT_STACK: a += 1 var predator := Cards.beaten_by(e) if predator != "" and int(counts[predator]) >= ELEMENT_STACK: a -= 1 best = a if i == 0 else maxi(best, a) return best ## Bonus eines offenen Wesens aus der Elementregel; verdeckte zählen nicht. func aura_for(c: Creature, counts: Dictionary) -> int: if c.hidden: return 0 var a := aura_of(c.card.elements, counts) var o = c.owner() if o != null and "element_doppelt" in o.perks: a *= 2 return a ## Rechnet die Elementregel neu; Leben folgt dem Bonus direkt (+1/+1 bzw. -1/-1). func apply_auras() -> void: var counts := element_counts() for p in players: for c in p.board: var a := aura_for(c, counts) if a != c.aura: var delta: int = a - c.aura c.health += delta c.max_health += delta c.aura = a ## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme. func forecast(p: Player, card: Cards.Card) -> int: var counts := element_counts() for e in card.elements: counts[e] += 1 return aura_of(card.elements, counts) * (2 if "element_doppelt" in p.perks else 1) # --- Kampf ------------------------------------------------------------------- ## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat. func can_attack(cr: Creature, target) -> Array: if winner != "" or cr.owner() != current(): return [false, "Nicht dein Zug"] if blocked(cr.owner()) != "": return [false, blocked(cr.owner())] if cr.hidden: return [false, "%s liegt verdeckt" % cr.name] if not cr.can_attack(): return [false, "%s kann nicht angreifen" % cr.name] var tgt_owner = target.owner() if target is Creature else target if not (tgt_owner in enemies_of(cr.owner())): return [false, "Kein gültiges Ziel"] if not (target is Creature) and not tgt_owner.board.is_empty(): return [false, "Erst die Wesen"] return [true, ""] ## Greift an. Ein verdecktes Ziel wird zuerst aufgedeckt. Schaden über das Leben eines Wesens ## hinaus schlägt auf seinen Besitzer durch (sonst wäre eine Mauer unverwundbar). ## Gibt "" zurück oder eine Fehlermeldung. func attack(cr: Creature, target) -> String: var ok := can_attack(cr, target) if not ok[0]: return ok[1] cr.attacks_left -= 1 if target is Creature and target.hidden: _flip(target) if target.health <= 0: # der Elementbonus hat es schon erledigt cleanup() check_end() return "" say("%s greift %s an." % [cr.name, target.name]) emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)}) var dealt := cr.power() + matchup_bonus(cr, target) if target is Creature: var had_shield: bool = target.shield var hp_before: int = target.health damage(target, dealt) if not had_shield and dealt > hp_before: var o = target.owner() var excess := dealt - hp_before o.life -= excess say("%d Schaden schlagen durch auf %s." % [excess, o.name]) emit({"type": "damage", "target": ref_of(o), "amount": excess}) if target.power() > 0: # wer nichts austeilt, kontert auch nicht; der Konter kennt keinen Elementvorteil damage(cr, target.power()) else: damage(target, dealt) cleanup() check_end() return "" ## Elementvorteil beim Zuschlagen: +1 im Vorteil, -1 im Nachteil (die Verteidigung des ## Ziels), gegen Helden gibt es keinen. static func matchup_bonus(source: Creature, target) -> int: if not (target is Creature): return 0 return Cards.matchup(source.card.elements, target.card.elements) func damage(target, amount: int) -> void: if amount <= 0: return if target is Creature: if target.shield: target.shield = false say("Schild von %s blockt den Schaden." % target.name) emit({"type": "blocked", "target": ref_of(target)}) return target.health -= amount else: target.life -= amount say("%s erleidet %d Schaden." % [target.name, amount]) emit({"type": "damage", "target": ref_of(target), "amount": amount}) ## Räumt Tote ab; danach gilt die Elementregel neu, was weitere Tote nach sich ziehen kann. func cleanup() -> void: for round in 8: var died := false for p in players: for c in p.board.duplicate(): # Wer besiegt ist, nimmt seine Wesen mit vom Tisch. if c.health <= 0 or not p.alive(): p.board.erase(c) if c.health > 0: say("%s verschwindet mit %s." % [c.name, p.name]) else: say("%s stirbt." % c.name) emit({"type": "death", "target": ref_of(c)}) died = true if not died: return apply_auras() ## Ende: einer von beiden hat kein Leben mehr. func check_end() -> void: if winner != "": return var alive := players.filter(func(p): return p.alive()) if alive.size() != 1: return winner_player = alive[0] winner = "human" if winner_player.is_human else "opponents" if humans.size() == 1: say("Der Gegner ist besiegt!" if winner == "human" else "Du wurdest besiegt.") else: say("%s gewinnt!" % winner_player.name) # --- Zustand für das Netzwerk (siehe NetState) ------------------------------------ func snapshot(viewer: int) -> Dictionary: return NetState.snapshot(self, viewer) static func from_snapshot(d: Dictionary) -> Game: return NetState.from_snapshot(d) # --- Gegner und Kampfstart --------------------------------------------------- static func shuffle(arr: Array, r: RandomNumberGenerator) -> void: for i in range(arr.size() - 1, 0, -1): var j := r.randi_range(0, i) var tmp = arr[i] arr[i] = arr[j] arr[j] = tmp ## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}} static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game: if r == null: r = make_rng() return new_battle_with(DeckGen.random_opponent(int(rules.get("opponent_life", START_LIFE))), r, false, rules) static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game: if r == null: r = make_rng() var human := Player.new("Spieler", true, START_LIFE) human.perks = Array(rules.get("human_perks", [])) human.looks = rules.get("looks", {}) var g := Game.new(human, opp, r) for p in g.players: if "leben_35" in p.perks: p.life += 5 p.max_life += 5 g.start(fixed_order) return g