test(keyword-handler): Add/modify unit tests for keyword processing logic, including edge cases and new scenarios

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

View file

@ -0,0 +1,217 @@
extends GutTest
## Keyword handler unit tests.
## Covers: poison, ZOC, web, regeneration, skeleton spawns, tactical analysis.
## Note: indestructible, battle_rage, arcane_shield, war_cry, can_attack_flying
## are processed entirely inside Rust (GdCombatResolver). Only the GDScript-layer
## state tracked in keyword_handler.gd is tested here.
const KeywordHandlerScript: GDScript = preload(
"res://engine/src/modules/combat/keyword_handler.gd"
)
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
var _player_a: PlayerScript = null
var _player_b: PlayerScript = null
func before_all() -> void:
DataLoader.load_theme("age-of-dwarves")
func before_each() -> void:
_player_a = PlayerScript.new()
_player_a.index = 0
_player_a.race_id = "dwarf"
_player_a.units = []
_player_a.cities = []
_player_b = PlayerScript.new()
_player_b.index = 1
_player_b.race_id = "terrans"
_player_b.units = []
_player_b.cities = []
GameState.players = [_player_a, _player_b]
## Clear static state between tests
KeywordHandlerScript._poison_states.clear()
KeywordHandlerScript._web_states.clear()
KeywordHandlerScript._tactical_memory.clear()
KeywordHandlerScript._pending_skeleton_spawns.clear()
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 = "spearmen"
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
# ---------------------------------------------------------------------------
# Poison
# ---------------------------------------------------------------------------
func test_poison_ticks_damage_per_turn() -> void:
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 5, 2, 50)
KeywordHandlerScript._poison_states[defender.id] = {
"damage_per_turn": 3,
"turns_remaining": 2,
}
var hp_before: int = defender.hp
KeywordHandlerScript.process_turn_effects([defender])
assert_eq(defender.hp, hp_before - 3, "Poison must deal 3 damage per turn")
func test_poison_expires_after_turns() -> void:
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 5, 2, 50)
KeywordHandlerScript._poison_states[defender.id] = {
"damage_per_turn": 3,
"turns_remaining": 1,
}
KeywordHandlerScript.process_turn_effects([defender])
assert_false(
KeywordHandlerScript._poison_states.has(defender.id),
"Poison must be removed after turns expire"
)
func test_poison_persists_with_turns_remaining() -> void:
var defender: UnitScript = _make_unit(1, Vector2i(2, 0), 5, 2, 50)
KeywordHandlerScript._poison_states[defender.id] = {
"damage_per_turn": 3,
"turns_remaining": 3,
}
KeywordHandlerScript.process_turn_effects([defender])
assert_true(
KeywordHandlerScript._poison_states.has(defender.id),
"Poison must persist when turns remain"
)
assert_eq(
KeywordHandlerScript._poison_states[defender.id]["turns_remaining"],
2,
"Turns remaining must decrement by 1"
)
# ---------------------------------------------------------------------------
# ZOC entry cost
# ---------------------------------------------------------------------------
func test_zoc_entry_cost_from_adjacent_enemy() -> void:
var unit: UnitScript = _make_unit(0, Vector2i(3, 3), 5, 2, 30)
var enemy: UnitScript = _make_unit(1, Vector2i(5, 3), 5, 2, 30)
var cost: int = KeywordHandlerScript.get_zoc_entry_cost(
unit, Vector2i(4, 3), [enemy]
)
assert_eq(cost, 1, "Entering tile adjacent to enemy must cost +1 movement")
func test_zoc_entry_cost_zero_without_enemy() -> void:
var unit: UnitScript = _make_unit(0, Vector2i(3, 3), 5, 2, 30)
var cost: int = KeywordHandlerScript.get_zoc_entry_cost(
unit, Vector2i(4, 3), []
)
assert_eq(cost, 0, "No enemies means no ZOC cost")
func test_flying_unit_bypasses_zoc_blocked() -> void:
var flyer: UnitScript = _make_unit(0, Vector2i(3, 3), 5, 2, 30)
flyer.type_id = "wyvern_rider"
flyer.unit_type = "flying"
var enemy: UnitScript = _make_unit(1, Vector2i(4, 3), 5, 2, 30)
var blocked: bool = KeywordHandlerScript.is_zoc_blocked(
flyer, Vector2i(3, 3), Vector2i(4, 3), [enemy]
)
assert_false(blocked, "Flying unit must not be blocked by ZOC")
# ---------------------------------------------------------------------------
# Web state
# ---------------------------------------------------------------------------
func test_web_immobilizes_for_one_turn() -> void:
var unit: UnitScript = _make_unit(0, Vector2i(1, 0), 10, 2, 50)
KeywordHandlerScript._web_states[unit.id] = 1
assert_true(
KeywordHandlerScript.is_webbed(unit),
"Webbed unit must report as webbed"
)
KeywordHandlerScript.process_turn_effects([unit])
assert_false(
KeywordHandlerScript.is_webbed(unit),
"Web must expire after 1 turn"
)
func test_non_webbed_unit_not_immobilized() -> void:
var unit: UnitScript = _make_unit(0, Vector2i(1, 0), 10, 2, 50)
assert_false(
KeywordHandlerScript.is_webbed(unit),
"Non-webbed unit must not be immobilized"
)
# ---------------------------------------------------------------------------
# Skeleton spawn flush
# ---------------------------------------------------------------------------
func test_flush_skeleton_spawns_returns_and_clears() -> void:
KeywordHandlerScript._pending_skeleton_spawns.append(
{"position": Vector2i(5, 5), "owner": 0}
)
var spawns: Array = KeywordHandlerScript.flush_skeleton_spawns()
assert_eq(spawns.size(), 1, "Must return 1 pending spawn")
assert_eq(
KeywordHandlerScript._pending_skeleton_spawns.size(), 0,
"Flush must clear pending spawns"
)
# ---------------------------------------------------------------------------
# Tactical analysis bonus
# ---------------------------------------------------------------------------
func test_tactical_analysis_no_bonus_without_keyword() -> 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 bonus: int = KeywordHandlerScript.get_tactical_analysis_bonus(
attacker, defender
)
assert_eq(bonus, 0, "Unit without tactical_analysis must get 0 bonus")
# ---------------------------------------------------------------------------
# Regeneration
# ---------------------------------------------------------------------------
func test_non_regen_unit_not_healed() -> void:
var unit: UnitScript = _make_unit(0, Vector2i(1, 0), 10, 2, 50)
unit.hp = 30
KeywordHandlerScript.process_turn_effects([unit])
assert_eq(unit.hp, 30, "Non-regenerating unit must not heal from turn effects")

View file

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