Kampf fortsetzen, Gamepad im Deckbau, Deck-Codes, Kapitel 3, Elementbooster, englische Karten, Host-Wechsel und Zuschauer
- KI-Bewertung mit Bedrohung und Führenden-Bremse (62:38 gegen die alte im Duell, gleichauf zu viert) - Balance: Sand +1/+1 statt Schild, Erde+Luft und Erde geschwächt, Wasser angehoben, Messlauf mit N=200 möglich - Einzelspieler-Kämpfe werden laufend gespeichert (voller Spielstand, viewer -1) und im Menü fortgesetzt - Karten, Deckzeilen und Kartenrücken fokussierbar: Gamepad in Deckbau, Werkstatt und Booster - Kartennamen (name_en) und Kartentexte übersetzbar, Übersetzungstest deckt cards.gd mit ab - Deck-Code DINGER1:id*anzahl mit Zwischenablage; acht neue Karten; Kampagne Kapitel 3; Elementbooster 120 Münzen - Netzwerk: Nachfolge-Host mit vollem Spielstand, Neuverbindung der anderen, Zuschauer (Sicht -2); Bot kann hosten - Große Schrift: Kartennamen, Kartentext, Knopfleisten, Gegnervorschau, Dialogtext - Anleitung 1 garantiert ein 1-Energie-Wesen; Tests: DINGER_ONLY für einzelne Oberflächentests - Oberflächentest auch auf Xephyr mit llvmpipe (wie Xvfb in der CI) grün Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
@@ -55,7 +55,11 @@ func put(p: Game.Player, cid: String, ready: bool = true) -> Game.Creature:
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
TranslationServer.set_locale("de") # das Spiel setzt die Sprache aus den Einstellungen; hier fehlt der Einstieg
|
||||
test_kartenpool()
|
||||
test_kampf_speichern()
|
||||
test_elementbooster_und_deckcode()
|
||||
test_englische_karten()
|
||||
test_kombos()
|
||||
test_wesen_spielen()
|
||||
test_flink()
|
||||
@@ -117,7 +121,7 @@ func _initialize() -> void:
|
||||
|
||||
|
||||
func test_kartenpool() -> void:
|
||||
check(Cards.all().size() == 110, "110 Karten")
|
||||
check(Cards.all().size() == 118, "118 Karten")
|
||||
for key in Cards.COMBOS:
|
||||
var found: bool = Cards.all().any(func(c): return c.is_combo() and Cards.combo_key(c.elements) == key)
|
||||
check(found, "Kombo vorhanden: " + key)
|
||||
@@ -462,7 +466,7 @@ func test_spielstand() -> void:
|
||||
p.coins = 999
|
||||
p.save(path)
|
||||
var p2 := Profile.load_or_new(path)
|
||||
check(p2.coins == 999 and p2.deck.size() == 20 and p2.collection.size() == 42, "geladen")
|
||||
check(p2.coins == 999 and p2.deck.size() == 20 and p2.collection.size() == Cards.of_rarity("gewöhnlich").size() + 2, "geladen")
|
||||
for suffix in ["", ".bak"]:
|
||||
if FileAccess.file_exists(path + suffix):
|
||||
DirAccess.remove_absolute(abs + suffix)
|
||||
@@ -551,7 +555,7 @@ func test_feldkombos() -> void:
|
||||
s[2][1].life = 5
|
||||
g.play_card(h, C("l_wind"))
|
||||
check(s[2][1].life == 3 and s[2][0].life == 15, "Blitz trifft den schwächsten Gegner")
|
||||
# Schlamm heilt, Sturm zieht, Sand gibt Schild
|
||||
# Schlamm heilt, Sturm zieht, Sand gibt +1/+1
|
||||
s = setup(["e_wall", "l_wind", "w_reiter"])
|
||||
g = s[0]
|
||||
h = s[1]
|
||||
@@ -562,13 +566,13 @@ func test_feldkombos() -> void:
|
||||
var hand_before := h.hand.size()
|
||||
g.play_card(h, C("l_wind"))
|
||||
check(h.hand.size() == hand_before, "Sturm zieht 1 (eine gespielt, eine gezogen)")
|
||||
check(not h.board[2].shield, "nur eine Feldkombo je Ausspielen")
|
||||
check(h.board[2].attack == 1, "nur eine Feldkombo je Ausspielen")
|
||||
s = setup(["l_wind"])
|
||||
g = s[0]
|
||||
h = s[1]
|
||||
put(h, "e_wall")
|
||||
g.play_card(h, C("l_wind"))
|
||||
check(h.board[1].shield, "Sand: neues Wesen hat Schild")
|
||||
check(h.board[1].attack == 2 and h.board[1].health == 2, "Sand: neues Wesen erhält +1/+1")
|
||||
# Gleiche Elemente lösen keine Feldkombo aus
|
||||
s = setup(["f_kobold"])
|
||||
g = s[0]
|
||||
@@ -736,7 +740,7 @@ func test_fallen() -> void:
|
||||
g.end_turn()
|
||||
var a := put(o, "e_troll")
|
||||
check(g.attack(a, h) == "", "Angriff in die Feuerfalle")
|
||||
check(h.traps.is_empty() and a.health == 3 and h.life == 26, "Feuerfalle: 3 Schaden, Angriff geht durch")
|
||||
check(h.traps.is_empty() and a.health == 3 and h.life == 27, "Feuerfalle: 3 Schaden, Angriff geht durch")
|
||||
# Frostfalle bricht den Angriff ab
|
||||
s = setup(["w_falle"])
|
||||
g = s[0]
|
||||
@@ -1089,7 +1093,7 @@ func test_deck_umbenennen_und_auffuellen() -> void:
|
||||
|
||||
|
||||
func test_neue_fallen() -> void:
|
||||
check(Cards.all().filter(func(c): return c.is_trap()).size() == 10, "zehn Fallen")
|
||||
check(Cards.all().filter(func(c): return c.is_trap()).size() == 12, "zwölf Fallen")
|
||||
check(C("e_sandfalle").text() == "Falle. Wenn ein Gegner ein Wesen ausspielt: Es wird eingefroren.", "Fallentext je Auslöser")
|
||||
check(C("f_aschefalle").text().ends_with("3 Schaden an dieses Wesen."), "Aschefalle-Text")
|
||||
# Böenfalle: Angreifer zurück auf die Hand, Angriff verpufft
|
||||
@@ -1144,7 +1148,11 @@ func test_neue_fallen() -> void:
|
||||
|
||||
|
||||
func test_kapitel_zwei_und_merkmalwahl() -> void:
|
||||
check(Campaign.total() == 20 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2, "zwanzig Stufen in zwei Kapiteln")
|
||||
check(Campaign.total() == 30 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(29) == 3, "dreißig Stufen in drei Kapiteln")
|
||||
check(Campaign.stage(25).perks.size() == 3 and Campaign.stage(25).name == "Nyx" and Campaign.target_rating(29, 1.0) == 4.4, "Kapitel 3 mit drei Merkmalen und höherer Zielstärke")
|
||||
for st in Campaign.STAGES3:
|
||||
for pk in st.perks:
|
||||
check(Game.PERKS.has(pk), "Merkmal %s bekannt" % pk)
|
||||
for i in Campaign.total():
|
||||
var st := Campaign.stage(i)
|
||||
check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1))
|
||||
@@ -1362,6 +1370,73 @@ func test_ki_zeitbudget() -> void:
|
||||
check(v.settings.has("big_font") and not v.tutorial2_done and v.records.margins is Array, "Version 5 mit neuen Feldern")
|
||||
|
||||
|
||||
## Elementbooster enthalten nur Karten des Elements; Deck-Codes sind umkehrbar und robust.
|
||||
## Englisch: jede Karte hat einen englischen Namen, Kartentexte und Schlüsselwörter werden übersetzt.
|
||||
func test_englische_karten() -> void:
|
||||
check(Cards.all().all(func(c): return c.name_en != ""), "alle Karten mit englischem Namen")
|
||||
var de: String = Cards.by_id("f_funke").text()
|
||||
TranslationServer.set_locale("en")
|
||||
var funke := Cards.by_id("f_funke")
|
||||
var falle := Cards.by_id("w_falle")
|
||||
var wall := Cards.by_id("e_wall")
|
||||
check(funke.title() == "Spark" and funke.text() == "2 damage to a target.", "Funke auf Englisch: %s / %s" % [funke.title(), funke.text()])
|
||||
check(falle.text().begins_with("Trap. When an enemy creature attacks") and "The attack fizzles" in falle.text(), "Fallentext auf Englisch: %s" % falle.text())
|
||||
check(wall.text() == "Guardian." and Cards.by_id("l_spiegel").text().ends_with("Summon a Wind Spirit."), "Schlüsselwort und Beschwörung auf Englisch")
|
||||
TranslationServer.set_locale("de")
|
||||
check(funke.title() == "Funke" and funke.text() == de, "zurück auf Deutsch")
|
||||
|
||||
|
||||
func test_elementbooster_und_deckcode() -> void:
|
||||
var p := Profile.starter()
|
||||
p.coins = 500
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 5
|
||||
var got: Array = p.open_booster(r, "Wasser")
|
||||
check(got.size() == 5 and got.all(func(c): return "Wasser" in c.elements) and p.coins == 500 - Profile.ELEMENT_BOOSTER_COST, "Wasser-Booster: fünf Wasserkarten, %d Münzen" % Profile.ELEMENT_BOOSTER_COST)
|
||||
p.coins = 100
|
||||
check(p.open_booster(r, "Feuer").is_empty() and p.coins == 100, "Elementbooster braucht %d Münzen" % Profile.ELEMENT_BOOSTER_COST)
|
||||
for e in Cards.ELEMENTS:
|
||||
for rar in Profile.RARITY_WEIGHTS:
|
||||
check(not Profile.booster_pool(rar, e).is_empty(), "%s hat %s Karten" % [e, rar])
|
||||
var deck: Array = Profile.starter().deck
|
||||
var code := Profile.deck_code(deck)
|
||||
check(code.begins_with(Profile.CODE_PREFIX) and Profile.parse_deck_code(code).size() == 20, "Deck-Code hin und zurück")
|
||||
var back: Array = Profile.parse_deck_code(code)
|
||||
back.sort()
|
||||
var orig: Array = deck.duplicate()
|
||||
orig.sort()
|
||||
check(back == orig, "Deck-Code enthält genau die Karten")
|
||||
check(Profile.parse_deck_code("kaputt").is_empty() and Profile.parse_deck_code("DINGER1:x_nix*2").is_empty()
|
||||
and Profile.parse_deck_code("DINGER1:f_funke*9").size() == Game.MAX_COPIES, "ungültige Codes leer, Kopien gedeckelt")
|
||||
check(Cards.by_id("w_flutfalle").is_trap() and Cards.by_id("fl_sturmfalke").combo() == "Blitz" and Cards.by_id("el_sandsturm").is_combo(), "neue Karten im Pool")
|
||||
|
||||
|
||||
## Voller Spielstand (viewer -1) stellt Hände, Decks und Bretter wieder her; Zuschauer (-2) sehen nichts Verborgenes.
|
||||
func test_kampf_speichern() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 7
|
||||
var g := Game.new_battle(Profile.starter().deck_cards(), 2, r)
|
||||
g.start()
|
||||
AI.take_turn(g, g.current())
|
||||
var d: Dictionary = JSON.parse_string(JSON.stringify(g.snapshot(-1)))
|
||||
var g2 := Game.from_snapshot(d)
|
||||
var same := g2.players.size() == g.players.size()
|
||||
for i in g.players.size():
|
||||
var a: Game.Player = g.players[i]
|
||||
var b: Game.Player = g2.players[i]
|
||||
var ids := func(cs: Array): return cs.map(func(c): return c.id)
|
||||
same = same and ids.call(a.hand) == ids.call(b.hand) and ids.call(a.deck) == ids.call(b.deck) \
|
||||
and a.life == b.life and a.board.size() == b.board.size() and a.is_human == b.is_human
|
||||
check(same and g2.current_idx == g.current_idx and g2.turn == g.turn and g2.human.is_human, "voller Spielstand: Hände, Decks und Bretter erhalten")
|
||||
var guard := 0
|
||||
while g2.winner == "" and guard < 300:
|
||||
AI.take_turn(g2, g2.current())
|
||||
guard += 1
|
||||
check(g2.winner != "", "fortgesetzte Partie ist bis zum Ende spielbar")
|
||||
var s2: Dictionary = g.snapshot(-2)
|
||||
check(s2.players.all(func(p): return not (p.hand is Array) and not (p.traps is Array) and not (p.deck is Array)), "Zuschauer-Sicht ohne Hände, Decks und Fallen")
|
||||
|
||||
|
||||
## Jeder tr("...")-Text in der Oberfläche und jeder Anleitungsschritt hat einen englischen Eintrag.
|
||||
func test_uebersetzung_vollstaendig() -> void:
|
||||
var keys := {}
|
||||
@@ -1376,6 +1451,9 @@ func test_uebersetzung_vollstaendig() -> void:
|
||||
key = line.substr(0, line.find(","))
|
||||
keys[key] = true
|
||||
var missing := []
|
||||
for k in Cards.COMBOS.values() + Cards.ELEMENTS + Cards.KEYWORDS + Cards.RARITIES + ["Wesen", "Zauber", "Falle"] + Cards.TRIGGER_TEXT.values():
|
||||
if not keys.has(k):
|
||||
missing.append(k)
|
||||
var d := DirAccess.open("res://scripts/ui")
|
||||
d.list_dir_begin()
|
||||
var name := d.get_next()
|
||||
@@ -1389,6 +1467,11 @@ func test_uebersetzung_vollstaendig() -> void:
|
||||
if not keys.has(k) and not (k in missing):
|
||||
missing.append(k)
|
||||
name = d.get_next()
|
||||
var rx_cards := RegEx.new()
|
||||
rx_cards.compile("tr\\(\"([^\"]+)\"\\)")
|
||||
for m in rx_cards.search_all(FileAccess.get_file_as_string("res://scripts/cards.gd")):
|
||||
if not keys.has(m.get_string(1)) and not (m.get_string(1) in missing):
|
||||
missing.append(m.get_string(1))
|
||||
for stepset in [BattleTutorial.STEPS1, BattleTutorial.STEPS2]:
|
||||
for st in stepset:
|
||||
if not keys.has(st[0]):
|
||||
|
||||
Reference in New Issue
Block a user