"""Kampf: Minderung, Reichweite, Bewegung, Zaubern, Determinismus.""" import random from tft.sim import battle as B from tft.sim import hexgrid as H UNITS = { "NAH": {"stats": {"hp": 1000, "armor": 0, "magicResist": 0, "damage": 100, "damageByStar": [0, 100, 200, 400], "attackSpeed": 1.0, "range": 1, "mana": 0, "initialMana": 0, "critChance": 0.0, "critMultiplier": 1.4}}, "FERN": {"stats": {"hp": 500, "armor": 0, "magicResist": 0, "damage": 50, "damageByStar": [0, 50, 100, 200], "attackSpeed": 1.0, "range": 4, "mana": 0, "initialMana": 0, "critChance": 0.0, "critMultiplier": 1.4}}, "PANZER": {"stats": {"hp": 1000, "armor": 100, "magicResist": 100, "damage": 10, "damageByStar": [0, 10, 20, 40], "attackSpeed": 1.0, "range": 1, "mana": 0, "initialMana": 0, "critChance": 0.0, "critMultiplier": 1.4}}, "MAGIER": {"stats": {"hp": 600, "armor": 0, "magicResist": 0, "damage": 10, "damageByStar": [0, 10, 20, 40], "attackSpeed": 1.0, "range": 4, "mana": 30, "initialMana": 0, "critChance": 0.0, "critMultiplier": 1.4}, "spell_damage": [0, 300, 600, 1200], "spell_damage_type": "magic"}, } def make(key, side, col, row, stars=1): f = B.to_fighter({"api_name": key, "stars": stars, "col": col, "row": row}, side, UNITS) assert f is not None return f def test_mitigation_halves_at_hundred_armor(): assert B.mitigate(100, 0) == 100 assert B.mitigate(100, 100) == 50 assert B.mitigate(100, 300) == 25 def test_stars_raise_health_and_damage(): one, two = make("NAH", "a", 0, 0, 1), make("NAH", "a", 0, 0, 2) assert two.max_hp > one.max_hp assert two.attack_damage == 2 * one.attack_damage def test_the_stronger_side_wins(): a = [make("NAH", "a", i, 3) for i in range(3)] b = [make("NAH", "b", i, 4) for i in range(1)] assert B.fight(a, b, random.Random(1)).winner == "a" def test_armour_buys_time(): """Gleiches Leben, aber einer mindert — der hält doppelt so lange durch.""" def survives(defender): a = [make(defender, "a", 3, 3)] b = [make("NAH", "b", 3, 4)] return B.fight(a, b, random.Random(1), max_seconds=120).seconds assert survives("PANZER") > 1.8 * survives("NAH") def test_ranged_units_close_the_gap_and_shoot_first(): """Der Fernkämpfer steht außer Reichweite und muss trotzdem treffen.""" a = [make("FERN", "a", 3, 0)] b = [make("NAH", "b", 3, 7)] res = B.fight(a, b, random.Random(1), max_seconds=60) # Beide bewegen sich; der Fernkämpfer trifft aus vier Feldern. assert res.fighters[0].damage_done > 0 def test_units_move_towards_the_enemy(): one = make("NAH", "a", 3, 0) foe = make("NAH", "b", 3, 7) before = H.distance(one.pos, foe.pos) B._step_towards(one, foe, {one.pos, foe.pos}) assert H.distance(one.pos, foe.pos) == before - 1 def test_occupied_cells_are_not_entered(): one = make("NAH", "a", 3, 3) foe = make("NAH", "b", 3, 4) taken = {one.pos, foe.pos} | set(H.neighbours(3, 3)) B._step_towards(one, foe, taken) assert one.pos == (3, 3) def test_mana_fills_from_attacks_and_then_casts(): caster = make("MAGIER", "a", 3, 3) foe = make("NAH", "b", 3, 4) assert caster.mana == 0 B.fight([caster], [foe], random.Random(1), max_seconds=10) assert caster.casts >= 1 def test_units_without_mana_never_cast(): """NAH hat keine Mana-Kosten — sonst zauberte er im ersten Tick.""" one = make("NAH", "a", 3, 3) foe = make("PANZER", "b", 3, 4) B.fight([one], [foe], random.Random(1), max_seconds=10) assert one.casts == 0 def test_same_seed_same_outcome(): def run(seed): a = [make("NAH", "a", i, 3) for i in range(3)] b = [make("FERN", "b", i, 4) for i in range(3)] res = B.fight(a, b, random.Random(seed)) return res.winner, res.seconds, len(res.survivors_a) assert run(7) == run(7) def test_a_fight_always_ends(): """Zwei Panzer ohne Schaden dürfen nicht ewig laufen.""" a = [make("PANZER", "a", 3, 3)] b = [make("PANZER", "b", 3, 4)] res = B.fight(a, b, random.Random(1), max_seconds=5) assert res.seconds <= 5 def test_unknown_units_are_skipped(): assert B.to_fighter({"api_name": "GIBTS_NICHT"}, "a", UNITS) is None AOE = {"stats": {"hp": 600, "armor": 0, "magicResist": 0, "damage": 10, "damageByStar": [0, 10, 20, 40], "attackSpeed": 1.0, "range": 4, "mana": 30, "initialMana": 30, "critChance": 0.0, "critMultiplier": 1.4}, "spell_damage": [0, 300, 600, 1200], "spell_damage_type": "magic", "ability_vars": {"AoEHexRange": [0, 2, 2, 2]}} def test_area_spells_hit_the_neighbours(): """Radius aus den Fähigkeits-Variablen: alle im Umkreis nehmen Schaden.""" UNITS["FLAECHE"] = AOE try: caster = make("FLAECHE", "a", 3, 3) near = [make("NAH", "b", c, 4) for c in (2, 3, 4)] B.fight([caster], near, random.Random(1), max_seconds=1) assert all(f.hp < f.max_hp for f in near) finally: del UNITS["FLAECHE"] def test_single_target_spells_stay_single(): caster = make("MAGIER", "a", 3, 3) near = [make("NAH", "b", c, 4) for c in (2, 3, 4)] B.fight([caster], near, random.Random(1), max_seconds=1) assert sum(1 for f in near if f.hp < f.max_hp) == 1 def test_spell_shape_reads_radius_and_count(): assert B.spell_shape(AOE, 2) == (2, 1) assert B.spell_shape({"ability_vars": {"NumEnemiesHit": [0, 4, 4, 4]}}, 2) \ == (0, 4) assert B.spell_shape({}, 2) == (0, 1) def test_physical_spells_scale_with_attack_damage(): """Ein Carry mit AD-Items soll auch härter zaubern.""" UNITS["AD_ZAUBER"] = { "stats": {**UNITS["MAGIER"]["stats"], "initialMana": 30}, "spell_damage": [0, 300, 600, 1200], "spell_damage_type": "physical"} ITEMS = {"AD": {"effects": {"AD": 1.0}}} try: plain = make("AD_ZAUBER", "a", 3, 3) armed = make("AD_ZAUBER", "a", 3, 3) B.apply_items(armed, ["AD"], ITEMS) foes = [make("PANZER", "b", 3, 4), make("PANZER", "b", 3, 4)] B.fight([plain], [foes[0]], random.Random(1), max_seconds=0.5) B.fight([armed], [foes[1]], random.Random(1), max_seconds=0.5) assert armed.damage_done > 1.5 * plain.damage_done finally: del UNITS["AD_ZAUBER"] def test_the_log_records_spawn_hit_and_death(): a = [make("NAH", "a", 3, 3)] b = [make("FERN", "b", 3, 4)] res = B.fight(a, b, random.Random(1), max_seconds=30, record=True) kinds = {e["e"] for e in res.events} assert {"spawn", "hit", "die"} <= kinds # Zwei Kämpfer, zwei Spawns, und jeder Eintrag trägt einen Tick. assert sum(1 for e in res.events if e["e"] == "spawn") == 2 assert all("t" in e for e in res.events) def test_the_log_stays_off_by_default(): """Die Auswertung läuft millionenfach — dort darf nichts mitgeschrieben werden.""" a = [make("NAH", "a", 3, 3)] b = [make("NAH", "b", 3, 4)] assert B.fight(a, b, random.Random(1), max_seconds=5).events == [] def test_recording_does_not_change_the_outcome(): def run(record): a = [make("NAH", "a", i, 3) for i in range(3)] b = [make("FERN", "b", i, 4) for i in range(3)] res = B.fight(a, b, random.Random(5), record=record) return res.winner, res.seconds assert run(False) == run(True)