test(combat-resolver): Update test cases to validate new combat resolution logic, including edge cases in test_combat_resolver.gd

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-07 17:50:42 -07:00
parent 893fd2e524
commit d3d6dccd22
2 changed files with 355 additions and 0 deletions

View file

@ -0,0 +1,354 @@
extends GutTest
## Combat resolver unit tests.
## Covers: melee attack, defender death, ranged no-retaliation, flanking,
## fortification, XP/promotion, city siege, combat preview.
const CombatResolverScript: GDScript = preload(
"res://engine/src/modules/combat/combat_resolver.gd"
)
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
const TileScript: GDScript = preload("res://engine/src/map/tile.gd")
const GameMapScript: GDScript = preload("res://engine/src/map/game_map.gd")
var _resolver: CombatResolverScript = null
var _player_a: PlayerScript = null
var _player_b: PlayerScript = null
var _game_map: GameMapScript = null
func before_all() -> void:
DataLoader.load_theme("age-of-dwarves")
func before_each() -> void:
_resolver = CombatResolverScript.new()
_player_a = PlayerScript.new()
_player_a.index = 0
_player_a.race_id = "dwarf"
_player_a.units = []
_player_a.cities = []
_player_a.researched_techs = []
_player_a.pending_improvements = []
_player_b = PlayerScript.new()
_player_b.index = 1
_player_b.race_id = "terrans"
_player_b.units = []
_player_b.cities = []
_player_b.researched_techs = []
_player_b.pending_improvements = []
GameState.players = [_player_a, _player_b]
_game_map = GameMapScript.new()
_game_map.initialize(20, 20, 1)
for x: int in range(20):
for y: int in range(20):
var tile: TileScript = TileScript.new()
tile.position = Vector2i(x, y)
tile.biome_id = "grassland"
tile.quality = 3
_game_map.set_tile(Vector2i(x, y), tile)
func _make_unit(
owner_idx: int,
pos: Vector2i,
atk: int = 10,
dr: int = 2,
hp_val: int = 30,
) -> UnitScript:
var unit: UnitScript = UnitScript.new()
unit.id = "u_%d_%d_%d" % [owner_idx, pos.x, pos.y]
unit.type_id = "dwarf_spearman"
unit.owner = owner_idx
unit.position = pos
unit.hp = hp_val
unit.max_hp = hp_val
unit.bonus_attack = atk
unit.bonus_defense = dr
unit.unit_type = "military"
unit.movement_remaining = 2
return unit
func _make_ranged_unit(
owner_idx: int,
pos: Vector2i,
atk: int = 10,
dr: int = 2,
hp_val: int = 30,
) -> UnitScript:
var unit: UnitScript = _make_unit(owner_idx, pos, atk, dr, hp_val)
unit.type_id = "dwarf_crossbowman"
return unit
func _make_city(
owner_idx: int,
pos: Vector2i,
city_hp_val: int = 200,
) -> CityScript:
var city: CityScript = CityScript.new()
city.id = "city_%d" % owner_idx
city.owner = owner_idx
city.position = pos
city.population = 1
city.city_hp = city_hp_val
city.max_city_hp = city_hp_val
city.buildings = []
city.is_capital = false
return city
# ---------------------------------------------------------------------------
# Basic melee attack
# ---------------------------------------------------------------------------
func test_melee_attack_deals_damage() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 15, 2, 50)
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 10, 2, 50)
var result: Dictionary = _resolver.resolve(
attacker, defender, _game_map, [attacker, defender]
)
assert_gt(
result.get("damage_dealt", 0), 0,
"Melee attack must deal positive damage"
)
assert_lt(defender.hp, 50, "Defender HP must decrease after melee attack")
func test_melee_attacker_takes_retaliation() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 10, 2, 50)
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 10, 2, 50)
var result: Dictionary = _resolver.resolve(
attacker, defender, _game_map, [attacker, defender]
)
assert_gt(
result.get("damage_taken", 0), 0,
"Melee attacker must take retaliation damage"
)
assert_lt(attacker.hp, 50, "Attacker HP must decrease from retaliation")
# ---------------------------------------------------------------------------
# Defender / attacker death
# ---------------------------------------------------------------------------
func test_defender_dies_when_hp_depleted() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 80, 5, 50)
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 3, 0, 5)
var result: Dictionary = _resolver.resolve(
attacker, defender, _game_map, [attacker, defender]
)
assert_false(
result.get("defender_alive", true),
"Defender must be dead when HP depleted"
)
assert_eq(defender.hp, 0, "Dead defender HP must be 0")
func test_attacker_dies_when_overpowered() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 1, 0, 3)
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 80, 5, 100)
var result: Dictionary = _resolver.resolve(
attacker, defender, _game_map, [attacker, defender]
)
assert_false(
result.get("attacker_alive", true),
"Weak attacker must die from retaliation by strong defender"
)
assert_eq(attacker.hp, 0, "Dead attacker HP must be 0")
# ---------------------------------------------------------------------------
# Ranged attack — no retaliation
# ---------------------------------------------------------------------------
func test_ranged_attack_no_retaliation() -> void:
var attacker: UnitScript = _make_ranged_unit(0, Vector2i(1, 0), 15, 2, 50)
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 10, 2, 50)
var result: Dictionary = _resolver.resolve(
attacker, defender, _game_map, [attacker, defender]
)
assert_gt(result.get("damage_dealt", 0), 0, "Ranged attack must deal damage")
assert_eq(attacker.hp, 50, "Ranged attacker must take no retaliation damage")
# ---------------------------------------------------------------------------
# Flanking bonus
# ---------------------------------------------------------------------------
func test_flanking_ally_increases_damage() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 1), 10, 2, 50)
var defender: UnitScript = _make_unit(1, Vector2i(2, 1), 5, 2, 100)
var flanker: UnitScript = _make_unit(0, Vector2i(3, 1), 10, 2, 50)
var result: Dictionary = _resolver.resolve(
attacker, defender, _game_map, [attacker, defender, flanker]
)
assert_gt(
result.get("damage_dealt", 0), 0,
"Flanking attack must deal positive damage"
)
# ---------------------------------------------------------------------------
# Fortification
# ---------------------------------------------------------------------------
func test_fortification_bonus_values() -> void:
var unit: UnitScript = _make_unit(0, Vector2i(1, 0))
assert_eq(unit.get_fortification_bonus(), 0, "Unfortified must be 0 DR")
unit.fortified_turns = 1
assert_eq(unit.get_fortification_bonus(), 3, "1-turn fortified must be +3 DR")
unit.fortified_turns = 2
assert_eq(unit.get_fortification_bonus(), 5, "2+ turn fortified must be +5 DR")
# ---------------------------------------------------------------------------
# XP and promotion
# ---------------------------------------------------------------------------
func test_combat_awards_xp() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 15, 2, 50)
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 10, 2, 50)
_resolver.resolve(attacker, defender, _game_map, [attacker, defender])
assert_true(
attacker.xp > 0 or defender.xp > 0,
"At least one surviving combatant must gain XP"
)
func test_xp_kill_bonus_on_defender_death() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 80, 5, 50)
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 3, 0, 5)
var result: Dictionary = _resolver.resolve(
attacker, defender, _game_map, [attacker, defender]
)
assert_false(result.get("defender_alive", true), "Defender must die")
assert_gte(
result.get("attacker_xp", 0),
CombatResolverScript.XP_ATTACKER_BASE,
"Attacker must earn at least base XP from killing"
)
func test_promotion_threshold() -> void:
var unit: UnitScript = _make_unit(0, Vector2i(1, 0))
unit.xp = 14
assert_false(unit.can_promote(), "14 XP must not reach threshold 15")
unit.xp = 15
assert_true(unit.can_promote(), "15 XP must reach promotion threshold")
# ---------------------------------------------------------------------------
# City siege
# ---------------------------------------------------------------------------
func test_siege_deals_city_hp_damage() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 20, 5, 50)
var city: CityScript = _make_city(1, Vector2i(2, 0), 200)
var result: Dictionary = _resolver.resolve(
attacker, city, _game_map, [attacker]
)
assert_gt(result.get("city_damage", 0), 0, "Siege must deal city HP damage")
assert_lt(city.city_hp, 200, "City HP must decrease after siege")
func test_city_captured_when_hp_zero_melee() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 80, 10, 100)
var city: CityScript = _make_city(1, Vector2i(2, 0), 1)
_player_b.cities = [city]
var result: Dictionary = _resolver.resolve(
attacker, city, _game_map, [attacker]
)
assert_true(
result.get("captured", false),
"City must be captured when HP=0 and melee unit attacks"
)
func test_city_not_captured_by_ranged() -> void:
var attacker: UnitScript = _make_ranged_unit(0, Vector2i(1, 0), 80, 10, 100)
var city: CityScript = _make_city(1, Vector2i(2, 0), 1)
_player_b.cities = [city]
var result: Dictionary = _resolver.resolve(
attacker, city, _game_map, [attacker]
)
assert_false(
result.get("captured", false),
"Ranged unit must not capture a city"
)
func test_walls_reduce_city_damage() -> void:
var atk_a: UnitScript = _make_unit(0, Vector2i(1, 0), 15, 2, 50)
var city_bare: CityScript = _make_city(1, Vector2i(2, 0), 200)
var result_bare: Dictionary = _resolver.resolve(
atk_a, city_bare, _game_map, [atk_a]
)
var atk_b: UnitScript = _make_unit(0, Vector2i(1, 1), 15, 2, 50)
var city_walls: CityScript = _make_city(1, Vector2i(2, 1), 200)
city_walls.buildings = ["walls"]
var result_walls: Dictionary = _resolver.resolve(
atk_b, city_walls, _game_map, [atk_b]
)
assert_lte(
result_walls.get("city_damage", 0),
result_bare.get("city_damage", 0),
"Walled city must take <= damage than unwalled"
)
# ---------------------------------------------------------------------------
# Combat preview
# ---------------------------------------------------------------------------
func test_preview_returns_expected_keys() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 15, 2, 50)
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 10, 2, 50)
var preview: Dictionary = _resolver.preview(
attacker, defender, _game_map, [attacker, defender]
)
assert_true(preview.has("avg_damage_dealt"), "Must have avg_damage_dealt")
assert_true(preview.has("avg_damage_taken"), "Must have avg_damage_taken")
assert_true(preview.has("defender_survives"), "Must have defender_survives")
func test_preview_does_not_modify_units() -> void:
var attacker: UnitScript = _make_unit(0, Vector2i(1, 0), 15, 2, 50)
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 10, 2, 50)
_resolver.preview(attacker, defender, _game_map, [attacker, defender])
assert_eq(attacker.hp, 50, "Preview must not modify attacker HP")
assert_eq(defender.hp, 50, "Preview must not modify defender HP")

View file

@ -0,0 +1 @@
uid://kck6skn6qy83