770 lines
26 KiB
GDScript
770 lines
26 KiB
GDScript
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
|