feature updates
This commit is contained in:
@@ -528,3 +528,100 @@ func test_block_with_dull_card_fails():
|
||||
var result = game_state.declare_block(blocker)
|
||||
|
||||
assert_false(result)
|
||||
|
||||
|
||||
## ============================================
|
||||
## ONLINE GAME STATE SYNCHRONIZATION TESTS
|
||||
## These test that game state can be updated from
|
||||
## network phase change messages
|
||||
## ============================================
|
||||
|
||||
|
||||
func test_turn_manager_player_index_can_be_set():
|
||||
# Simulates receiving network phase_changed message
|
||||
game_state.start_game(0)
|
||||
|
||||
# Manually set player index like network handler would
|
||||
game_state.turn_manager.current_player_index = 1
|
||||
|
||||
assert_eq(game_state.turn_manager.current_player_index, 1)
|
||||
|
||||
|
||||
func test_turn_manager_phase_can_be_set():
|
||||
# Simulates receiving network phase_changed message
|
||||
game_state.start_game(0)
|
||||
|
||||
# Manually set phase like network handler would
|
||||
game_state.turn_manager.current_phase = Enums.TurnPhase.ATTACK
|
||||
|
||||
assert_eq(game_state.turn_manager.current_phase, Enums.TurnPhase.ATTACK)
|
||||
|
||||
|
||||
func test_turn_manager_turn_number_can_be_set():
|
||||
# Simulates receiving network game_state_sync message
|
||||
game_state.start_game(0)
|
||||
|
||||
# Manually set turn number like network handler would
|
||||
game_state.turn_manager.turn_number = 5
|
||||
|
||||
assert_eq(game_state.turn_manager.turn_number, 5)
|
||||
|
||||
|
||||
func test_turn_manager_initial_values():
|
||||
# Verify initial turn manager state
|
||||
game_state.start_game(0)
|
||||
|
||||
assert_eq(game_state.turn_manager.current_player_index, 0)
|
||||
assert_eq(game_state.turn_manager.turn_number, 1)
|
||||
|
||||
|
||||
func test_turn_manager_attack_step_can_be_set():
|
||||
# For online games, attack step is managed by server
|
||||
game_state.start_game(0)
|
||||
game_state.end_main_phase() # Get to ATTACK phase
|
||||
|
||||
# Manually set attack step like network handler would
|
||||
game_state.turn_manager.attack_step = Enums.AttackStep.BLOCK_DECLARATION
|
||||
|
||||
assert_eq(game_state.turn_manager.attack_step, Enums.AttackStep.BLOCK_DECLARATION)
|
||||
|
||||
|
||||
func test_phase_changed_updates_current_player():
|
||||
# Test that changing phase properly reflects in get_current_player()
|
||||
game_state.start_game(0)
|
||||
|
||||
# Simulate switching turns from network
|
||||
game_state.turn_manager.current_player_index = 1
|
||||
|
||||
var current = game_state.get_current_player()
|
||||
assert_eq(current, game_state.players[1])
|
||||
|
||||
|
||||
func test_phase_changed_updates_opponent():
|
||||
# Test that changing phase properly reflects in get_opponent()
|
||||
game_state.start_game(0)
|
||||
|
||||
# Simulate switching turns from network
|
||||
game_state.turn_manager.current_player_index = 1
|
||||
|
||||
var opponent = game_state.get_opponent()
|
||||
assert_eq(opponent, game_state.players[0])
|
||||
|
||||
|
||||
func test_turn_manager_all_phases_valid():
|
||||
# Verify all TurnPhase enum values can be set
|
||||
game_state.start_game(0)
|
||||
|
||||
for phase in Enums.TurnPhase.values():
|
||||
game_state.turn_manager.current_phase = phase
|
||||
assert_eq(game_state.turn_manager.current_phase, phase)
|
||||
|
||||
|
||||
func test_turn_manager_all_attack_steps_valid():
|
||||
# Verify all AttackStep enum values can be set
|
||||
game_state.start_game(0)
|
||||
game_state.end_main_phase() # Get to ATTACK phase
|
||||
|
||||
for step in Enums.AttackStep.values():
|
||||
game_state.turn_manager.attack_step = step
|
||||
assert_eq(game_state.turn_manager.attack_step, step)
|
||||
|
||||
332
tests/test_ability_processor_conditionals.py
Normal file
332
tests/test_ability_processor_conditionals.py
Normal file
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for the conditional ability parsing in ability_processor.py
|
||||
|
||||
Run with: python -m pytest tests/test_ability_processor_conditionals.py -v
|
||||
Or directly: python tests/test_ability_processor_conditionals.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
|
||||
# Add tools directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'tools'))
|
||||
|
||||
from ability_processor import (
|
||||
parse_condition,
|
||||
parse_conditional_ability,
|
||||
parse_chained_effect,
|
||||
parse_scaling_effect,
|
||||
parse_effects,
|
||||
)
|
||||
|
||||
|
||||
class TestParseCondition(unittest.TestCase):
|
||||
"""Tests for parse_condition function"""
|
||||
|
||||
def test_control_card_basic(self):
|
||||
"""Should parse 'control [Card Name]' condition"""
|
||||
condition = parse_condition("you control Cloud")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "CONTROL_CARD")
|
||||
self.assertEqual(condition["card_name"], "Cloud")
|
||||
|
||||
def test_control_card_with_prefix(self):
|
||||
"""Should parse 'control a Card Name X' condition"""
|
||||
condition = parse_condition("you control a Card Name Tidus")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "CONTROL_CARD")
|
||||
|
||||
def test_control_count(self):
|
||||
"""Should parse 'control X or more Forwards' condition"""
|
||||
condition = parse_condition("you control 3 or more Forwards")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "CONTROL_COUNT")
|
||||
self.assertEqual(condition["comparison"], "GTE")
|
||||
self.assertEqual(condition["value"], 3)
|
||||
self.assertEqual(condition["card_type"], "FORWARD")
|
||||
|
||||
def test_control_count_backups(self):
|
||||
"""Should parse 'control X or more Backups' condition"""
|
||||
condition = parse_condition("you control 5 or more Backups")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "CONTROL_COUNT")
|
||||
self.assertEqual(condition["value"], 5)
|
||||
self.assertEqual(condition["card_type"], "BACKUP")
|
||||
|
||||
def test_damage_received(self):
|
||||
"""Should parse 'have received X or more damage' condition"""
|
||||
condition = parse_condition("you have received 5 or more damage")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "DAMAGE_RECEIVED")
|
||||
self.assertEqual(condition["comparison"], "GTE")
|
||||
self.assertEqual(condition["value"], 5)
|
||||
|
||||
def test_damage_received_points_of(self):
|
||||
"""Should parse 'X points of damage or more' condition"""
|
||||
condition = parse_condition("you have received 6 points of damage or more")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "DAMAGE_RECEIVED")
|
||||
self.assertEqual(condition["value"], 6)
|
||||
|
||||
def test_break_zone_count(self):
|
||||
"""Should parse break zone count condition"""
|
||||
condition = parse_condition("you have 5 or more Ifrit in your break zone")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "BREAK_ZONE_COUNT")
|
||||
self.assertEqual(condition["comparison"], "GTE")
|
||||
self.assertEqual(condition["value"], 5)
|
||||
|
||||
def test_forward_state_dull(self):
|
||||
"""Should parse 'this forward is dull' condition"""
|
||||
condition = parse_condition("this forward is dull")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "FORWARD_STATE")
|
||||
self.assertEqual(condition["state"], "DULL")
|
||||
self.assertTrue(condition["check_self"])
|
||||
|
||||
def test_forward_state_active(self):
|
||||
"""Should parse 'this forward is active' condition"""
|
||||
condition = parse_condition("this forward is active")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "FORWARD_STATE")
|
||||
self.assertEqual(condition["state"], "ACTIVE")
|
||||
|
||||
def test_cost_comparison_less(self):
|
||||
"""Should parse 'of cost X or less' condition"""
|
||||
condition = parse_condition("of cost 3 or less")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "COST_COMPARISON")
|
||||
self.assertEqual(condition["comparison"], "LTE")
|
||||
self.assertEqual(condition["value"], 3)
|
||||
|
||||
def test_cost_comparison_more(self):
|
||||
"""Should parse 'of cost X or more' condition"""
|
||||
condition = parse_condition("of cost 4 or more")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "COST_COMPARISON")
|
||||
self.assertEqual(condition["comparison"], "GTE")
|
||||
self.assertEqual(condition["value"], 4)
|
||||
|
||||
def test_opponent_no_forwards(self):
|
||||
"""Should parse 'opponent doesn't control any forwards' condition"""
|
||||
condition = parse_condition("your opponent doesn't control any Forwards")
|
||||
|
||||
self.assertIsNotNone(condition)
|
||||
self.assertEqual(condition["type"], "CONTROL_COUNT")
|
||||
self.assertEqual(condition["comparison"], "EQ")
|
||||
self.assertEqual(condition["value"], 0)
|
||||
self.assertEqual(condition["owner"], "OPPONENT")
|
||||
|
||||
def test_unknown_condition(self):
|
||||
"""Should return None for unparseable conditions"""
|
||||
condition = parse_condition("something random and complex")
|
||||
|
||||
self.assertIsNone(condition)
|
||||
|
||||
|
||||
class TestParseConditionalAbility(unittest.TestCase):
|
||||
"""Tests for parse_conditional_ability function"""
|
||||
|
||||
def test_if_control_deal_damage(self):
|
||||
"""Should parse 'If you control X, deal Y damage' pattern"""
|
||||
text = "If you control Cloud, deal 5000 damage to target Forward."
|
||||
|
||||
result = parse_conditional_ability(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "CONDITIONAL")
|
||||
self.assertIn("condition", result)
|
||||
self.assertIn("then_effects", result)
|
||||
self.assertTrue(len(result["then_effects"]) > 0)
|
||||
|
||||
def test_if_damage_received_gain_brave(self):
|
||||
"""Should parse damage received condition with ability grant"""
|
||||
text = "If you have received 5 or more damage, this Forward gains Brave."
|
||||
|
||||
result = parse_conditional_ability(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "CONDITIONAL")
|
||||
# Even if condition is UNKNOWN, the structure should be correct
|
||||
self.assertIn("condition", result)
|
||||
self.assertIn("then_effects", result)
|
||||
|
||||
def test_not_if_you_do(self):
|
||||
"""Should NOT parse 'If you do so' as conditional (that's chained)"""
|
||||
text = "If you do so, draw 2 cards."
|
||||
|
||||
result = parse_conditional_ability(text)
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_non_conditional_text(self):
|
||||
"""Should return None for non-conditional text"""
|
||||
text = "Deal 5000 damage to target Forward."
|
||||
|
||||
result = parse_conditional_ability(text)
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestParseChainedEffect(unittest.TestCase):
|
||||
"""Tests for parse_chained_effect function"""
|
||||
|
||||
def test_discard_if_you_do_damage(self):
|
||||
"""Should parse 'Discard 1 card. If you do so, deal damage' pattern"""
|
||||
text = "Discard 1 card from your hand. If you do so, deal 7000 damage to target Forward."
|
||||
|
||||
result = parse_chained_effect(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "CHAINED_EFFECT")
|
||||
self.assertEqual(result["chain_condition"], "IF_YOU_DO")
|
||||
self.assertIn("primary_effect", result)
|
||||
self.assertIn("chain_effects", result)
|
||||
self.assertEqual(result["primary_effect"]["type"], "DISCARD")
|
||||
|
||||
def test_when_you_do(self):
|
||||
"""Should parse 'When you do so' variant"""
|
||||
text = "Dull 1 Backup. When you do so, draw 1 card."
|
||||
|
||||
result = parse_chained_effect(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "CHAINED_EFFECT")
|
||||
|
||||
def test_no_chain_pattern(self):
|
||||
"""Should return None for text without chain pattern"""
|
||||
text = "Deal 5000 damage. Draw 1 card."
|
||||
|
||||
result = parse_chained_effect(text)
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestParseScalingEffect(unittest.TestCase):
|
||||
"""Tests for parse_scaling_effect function"""
|
||||
|
||||
def test_damage_for_each_damage_received(self):
|
||||
"""Should parse 'Deal X damage for each damage received' pattern"""
|
||||
text = "Deal it 1000 damage for each point of damage you have received."
|
||||
|
||||
result = parse_scaling_effect(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "SCALING_EFFECT")
|
||||
self.assertEqual(result["scale_by"], "DAMAGE_RECEIVED")
|
||||
self.assertEqual(result["multiplier"], 1000)
|
||||
self.assertEqual(result["base_effect"]["type"], "DAMAGE")
|
||||
|
||||
def test_power_for_each_forward(self):
|
||||
"""Should parse '+X power for each Forward you control' pattern"""
|
||||
text = "This Forward gains +1000 power for each Forward you control until the end of the turn."
|
||||
|
||||
result = parse_scaling_effect(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "SCALING_EFFECT")
|
||||
self.assertEqual(result["scale_by"], "FORWARDS_CONTROLLED")
|
||||
self.assertEqual(result["multiplier"], 1000)
|
||||
self.assertEqual(result["base_effect"]["type"], "POWER_MOD")
|
||||
|
||||
def test_draw_for_each_forward(self):
|
||||
"""Should parse 'Draw X cards for each Forward' pattern"""
|
||||
text = "Draw 1 card for each Forward your opponent controls."
|
||||
|
||||
result = parse_scaling_effect(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "SCALING_EFFECT")
|
||||
self.assertEqual(result["multiplier"], 1)
|
||||
self.assertEqual(result["base_effect"]["type"], "DRAW")
|
||||
|
||||
def test_no_scaling_pattern(self):
|
||||
"""Should return None for text without scaling pattern"""
|
||||
text = "Deal 5000 damage to target Forward."
|
||||
|
||||
result = parse_scaling_effect(text)
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestParseEffectsIntegration(unittest.TestCase):
|
||||
"""Integration tests for parse_effects with conditionals"""
|
||||
|
||||
def test_chained_effect_detected(self):
|
||||
"""parse_effects should return CHAINED_EFFECT for 'if you do' text"""
|
||||
text = "Discard 1 card from your hand. If you do, draw 2 cards."
|
||||
|
||||
effects = parse_effects(text)
|
||||
|
||||
self.assertEqual(len(effects), 1)
|
||||
self.assertEqual(effects[0]["type"], "CHAINED_EFFECT")
|
||||
|
||||
def test_scaling_effect_detected(self):
|
||||
"""parse_effects should return SCALING_EFFECT for 'for each' text"""
|
||||
text = "Deal it 1000 damage for each point of damage you have received."
|
||||
|
||||
effects = parse_effects(text)
|
||||
|
||||
self.assertEqual(len(effects), 1)
|
||||
self.assertEqual(effects[0]["type"], "SCALING_EFFECT")
|
||||
|
||||
def test_conditional_effect_detected(self):
|
||||
"""parse_effects should return CONDITIONAL for 'If X, Y' text"""
|
||||
text = "If you control Cloud, deal 5000 damage to target Forward."
|
||||
|
||||
effects = parse_effects(text)
|
||||
|
||||
self.assertEqual(len(effects), 1)
|
||||
self.assertEqual(effects[0]["type"], "CONDITIONAL")
|
||||
|
||||
def test_normal_effect_not_conditional(self):
|
||||
"""parse_effects should NOT return CONDITIONAL for normal effects"""
|
||||
text = "Draw 2 cards."
|
||||
|
||||
effects = parse_effects(text)
|
||||
|
||||
self.assertEqual(len(effects), 1)
|
||||
self.assertEqual(effects[0]["type"], "DRAW")
|
||||
|
||||
|
||||
class TestRealCardExamples(unittest.TestCase):
|
||||
"""Tests using real card text examples"""
|
||||
|
||||
def test_warrior_damage_scaling(self):
|
||||
"""Test Warrior card with damage scaling"""
|
||||
text = "Deal it 1000 damage for each point of damage you have received."
|
||||
|
||||
result = parse_scaling_effect(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "SCALING_EFFECT")
|
||||
self.assertEqual(result["scale_by"], "DAMAGE_RECEIVED")
|
||||
self.assertEqual(result["multiplier"], 1000)
|
||||
|
||||
def test_zidane_chained_effect(self):
|
||||
"""Test Zidane-style chained effect"""
|
||||
text = "Discard 1 card from your hand. If you do so, choose 1 Forward. Deal it 7000 damage."
|
||||
|
||||
result = parse_chained_effect(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "CHAINED_EFFECT")
|
||||
self.assertEqual(result["primary_effect"]["type"], "DISCARD")
|
||||
self.assertTrue(len(result["chain_effects"]) > 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
218
tests/test_ability_processor_modal.py
Normal file
218
tests/test_ability_processor_modal.py
Normal file
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for the modal ability parsing in ability_processor.py
|
||||
|
||||
Run with: python -m pytest tests/test_ability_processor_modal.py -v
|
||||
Or directly: python tests/test_ability_processor_modal.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
|
||||
# Add tools directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'tools'))
|
||||
|
||||
from ability_processor import (
|
||||
extract_modal_actions,
|
||||
parse_modal_ability,
|
||||
parse_effects,
|
||||
parse_field_ability,
|
||||
)
|
||||
|
||||
|
||||
class TestExtractModalActions(unittest.TestCase):
|
||||
"""Tests for extract_modal_actions function"""
|
||||
|
||||
def test_extracts_quoted_actions(self):
|
||||
"""Should extract all quoted action strings"""
|
||||
text = '''Select 1 of the 3 following actions.
|
||||
"Choose 1 Forward. Deal it 7000 damage."
|
||||
"Choose 1 Monster of cost 3 or less. Break it."
|
||||
"Deal 3000 damage to all the Forwards opponent controls."'''
|
||||
|
||||
actions = extract_modal_actions(text)
|
||||
|
||||
self.assertEqual(len(actions), 3)
|
||||
self.assertEqual(actions[0]["index"], 0)
|
||||
self.assertEqual(actions[1]["index"], 1)
|
||||
self.assertEqual(actions[2]["index"], 2)
|
||||
|
||||
def test_preserves_action_descriptions(self):
|
||||
"""Should preserve the full description text"""
|
||||
text = '"Choose 1 Forward. Deal it 7000 damage." "Draw 2 cards."'
|
||||
|
||||
actions = extract_modal_actions(text)
|
||||
|
||||
self.assertEqual(actions[0]["description"], "Choose 1 Forward. Deal it 7000 damage.")
|
||||
self.assertEqual(actions[1]["description"], "Draw 2 cards.")
|
||||
|
||||
def test_parses_effects_for_each_action(self):
|
||||
"""Should parse effects for each action"""
|
||||
text = '"Draw 2 cards." "Deal 5000 damage to all Forwards."'
|
||||
|
||||
actions = extract_modal_actions(text)
|
||||
|
||||
# First action should have DRAW effect
|
||||
self.assertTrue(len(actions[0]["effects"]) > 0)
|
||||
self.assertEqual(actions[0]["effects"][0]["type"], "DRAW")
|
||||
|
||||
def test_handles_empty_text(self):
|
||||
"""Should return empty array for text without quotes"""
|
||||
actions = extract_modal_actions("No quoted text here")
|
||||
self.assertEqual(len(actions), 0)
|
||||
|
||||
|
||||
class TestParseModalAbility(unittest.TestCase):
|
||||
"""Tests for parse_modal_ability function"""
|
||||
|
||||
def test_parses_select_1_of_3(self):
|
||||
"""Should parse 'select 1 of the 3' format"""
|
||||
text = '''Select 1 of the 3 following actions.
|
||||
"Option A" "Option B" "Option C"'''
|
||||
|
||||
result = parse_modal_ability(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "CHOOSE_MODE")
|
||||
self.assertEqual(result["select_count"], 1)
|
||||
self.assertEqual(result["mode_count"], 3)
|
||||
self.assertFalse(result["select_up_to"])
|
||||
|
||||
def test_parses_select_up_to(self):
|
||||
"""Should parse 'select up to X' format"""
|
||||
text = '''Select up to 2 of the 4 following actions.
|
||||
"A" "B" "C" "D"'''
|
||||
|
||||
result = parse_modal_ability(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["select_count"], 2)
|
||||
self.assertEqual(result["mode_count"], 4)
|
||||
self.assertTrue(result["select_up_to"])
|
||||
|
||||
def test_parses_enhanced_condition(self):
|
||||
"""Should parse enhanced/conditional upgrade clause"""
|
||||
text = '''Select 1 of the 3 following actions. If you have 5 or more Ifrit in your Break Zone, select up to 3 of the 3 following actions instead.
|
||||
"A" "B" "C"'''
|
||||
|
||||
result = parse_modal_ability(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue("enhanced_condition" in result)
|
||||
self.assertEqual(result["enhanced_condition"]["select_count"], 3)
|
||||
self.assertTrue(result["enhanced_condition"]["select_up_to"])
|
||||
|
||||
def test_returns_none_for_non_modal(self):
|
||||
"""Should return None for non-modal text"""
|
||||
text = "Choose 1 Forward. Deal it 5000 damage."
|
||||
|
||||
result = parse_modal_ability(text)
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_extracts_modes_with_effects(self):
|
||||
"""Should extract modes with parsed effects"""
|
||||
text = '''Select 1 of the 2 following actions.
|
||||
"Choose 1 Forward. Deal it 7000 damage."
|
||||
"Draw 2 cards."'''
|
||||
|
||||
result = parse_modal_ability(text)
|
||||
|
||||
self.assertEqual(len(result["modes"]), 2)
|
||||
# First mode should have damage effect
|
||||
self.assertTrue(len(result["modes"][0]["effects"]) > 0)
|
||||
|
||||
|
||||
class TestParseEffectsModal(unittest.TestCase):
|
||||
"""Tests for parse_effects handling of modal abilities"""
|
||||
|
||||
def test_returns_choose_mode_for_modal_text(self):
|
||||
"""parse_effects should return CHOOSE_MODE for modal text"""
|
||||
text = '''Select 1 of the 2 following actions.
|
||||
"Draw 1 card." "Dull 1 Forward."'''
|
||||
|
||||
effects = parse_effects(text)
|
||||
|
||||
self.assertEqual(len(effects), 1)
|
||||
self.assertEqual(effects[0]["type"], "CHOOSE_MODE")
|
||||
|
||||
def test_non_modal_returns_normal_effect(self):
|
||||
"""parse_effects should return normal effects for non-modal text"""
|
||||
text = "Draw 2 cards."
|
||||
|
||||
effects = parse_effects(text)
|
||||
|
||||
self.assertEqual(len(effects), 1)
|
||||
self.assertEqual(effects[0]["type"], "DRAW")
|
||||
|
||||
|
||||
class TestParseFieldAbilityModal(unittest.TestCase):
|
||||
"""Tests for parse_field_ability handling of modal abilities"""
|
||||
|
||||
def test_returns_choose_mode_for_modal_field(self):
|
||||
"""parse_field_ability should return CHOOSE_MODE for modal field ability"""
|
||||
text = '''Select 1 of the 3 following actions.
|
||||
"Deal 7000 damage." "Break a Monster." "Deal 3000 to all."'''
|
||||
|
||||
result = parse_field_ability(text, "Test Card")
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "CHOOSE_MODE")
|
||||
|
||||
|
||||
class TestRealCardExamples(unittest.TestCase):
|
||||
"""Tests using real card text examples"""
|
||||
|
||||
def test_ifrita_15_001r(self):
|
||||
"""Test Ifrita card modal ability"""
|
||||
text = '''Select 1 of the 3 following actions. If you have a total of 5 or more Card Name Ifrita and/or Card Name Ifrit in your Break Zone (before paying the cost for Ifrita), select up to 3 of the 3 following actions instead.
|
||||
"Choose 1 Forward. Deal it 7000 damage."
|
||||
"Choose 1 Monster of cost 3 or less. Break it."
|
||||
"Deal 3000 damage to all the Forwards opponent controls."'''
|
||||
|
||||
result = parse_modal_ability(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["type"], "CHOOSE_MODE")
|
||||
self.assertEqual(result["select_count"], 1)
|
||||
self.assertEqual(result["mode_count"], 3)
|
||||
self.assertEqual(len(result["modes"]), 3)
|
||||
|
||||
# Check enhanced condition
|
||||
self.assertTrue("enhanced_condition" in result)
|
||||
self.assertEqual(result["enhanced_condition"]["select_count"], 3)
|
||||
|
||||
# Check mode descriptions
|
||||
self.assertIn("7000 damage", result["modes"][0]["description"])
|
||||
self.assertIn("Monster", result["modes"][1]["description"])
|
||||
self.assertIn("3000 damage", result["modes"][2]["description"])
|
||||
|
||||
def test_warrior_10_075c(self):
|
||||
"""Test Warrior card modal ability"""
|
||||
text = '''Select 1 of the 2 following actions.
|
||||
"Choose 1 Forward. Until the end of the turn, it gains Brave and it gains +1000 power for each point of damage you have received."
|
||||
"Choose 1 Forward. Deal it 1000 damage for each point of damage you have received."'''
|
||||
|
||||
result = parse_modal_ability(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["select_count"], 1)
|
||||
self.assertEqual(result["mode_count"], 2)
|
||||
self.assertEqual(len(result["modes"]), 2)
|
||||
|
||||
def test_action_ability_modal(self):
|
||||
"""Test action ability with select up to"""
|
||||
text = '''Select up to 2 of the 4 following actions. "Choose 1 Forward you control. It gains +1000 power until the end of the turn." "All the Forwards you control gain Brave until the end of the turn." "All the Forwards you control gain 'This Forward cannot become dull by your opponent's Summons or abilities' until the end of the turn." "Draw 1 card."'''
|
||||
|
||||
result = parse_modal_ability(text)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["select_count"], 2)
|
||||
self.assertTrue(result["select_up_to"])
|
||||
self.assertEqual(result["mode_count"], 4)
|
||||
self.assertEqual(len(result["modes"]), 4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
56
tests/unit/test_ability_system.gd
Normal file
56
tests/unit/test_ability_system.gd
Normal file
@@ -0,0 +1,56 @@
|
||||
extends GutTest
|
||||
|
||||
## Tests for the AbilitySystem and related components
|
||||
|
||||
|
||||
func test_target_selector_instantiates() -> void:
|
||||
var selector = TargetSelector.new()
|
||||
assert_not_null(selector, "TargetSelector should instantiate")
|
||||
|
||||
|
||||
func test_trigger_matcher_instantiates() -> void:
|
||||
var matcher = TriggerMatcher.new()
|
||||
assert_not_null(matcher, "TriggerMatcher should instantiate")
|
||||
|
||||
|
||||
func test_effect_resolver_instantiates() -> void:
|
||||
var resolver = EffectResolver.new()
|
||||
assert_not_null(resolver, "EffectResolver should instantiate")
|
||||
|
||||
|
||||
func test_field_effect_manager_instantiates() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
assert_not_null(manager, "FieldEffectManager should instantiate")
|
||||
|
||||
|
||||
func test_field_effect_manager_clear() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
manager.clear_all()
|
||||
assert_eq(manager.get_active_ability_count(), 0, "Should have no active abilities after clear")
|
||||
|
||||
|
||||
func test_target_selector_empty_spec() -> void:
|
||||
var selector = TargetSelector.new()
|
||||
var result = selector.get_valid_targets({}, null, null)
|
||||
assert_eq(result.size(), 0, "Empty spec should return empty array")
|
||||
|
||||
|
||||
func test_trigger_matcher_event_matching() -> void:
|
||||
var matcher = TriggerMatcher.new()
|
||||
|
||||
# Test direct event match
|
||||
assert_true(matcher._event_matches("ATTACKS", "ATTACKS"), "Direct match should work")
|
||||
|
||||
# Test enters field variations
|
||||
assert_true(matcher._event_matches("ENTERS_FIELD", "CARD_PLAYED"), "ENTERS_FIELD should match CARD_PLAYED")
|
||||
|
||||
# Test leaves field variations
|
||||
assert_true(matcher._event_matches("LEAVES_FIELD", "FORWARD_BROKEN"), "LEAVES_FIELD should match FORWARD_BROKEN")
|
||||
|
||||
|
||||
func test_trigger_matcher_source_matching() -> void:
|
||||
var matcher = TriggerMatcher.new()
|
||||
|
||||
# ANY should always match
|
||||
var event_data = {"card": null}
|
||||
assert_true(matcher._source_matches("ANY", event_data, null, null), "ANY source should always match")
|
||||
86
tests/unit/test_attack_step_enum.gd
Normal file
86
tests/unit/test_attack_step_enum.gd
Normal file
@@ -0,0 +1,86 @@
|
||||
extends GutTest
|
||||
|
||||
## Unit tests to verify AttackStep enum alignment between client and server
|
||||
## Server uses: NONE=0, PREPARATION=1, DECLARATION=2, BLOCK_DECLARATION=3, DAMAGE_RESOLUTION=4
|
||||
|
||||
|
||||
## Test that AttackStep enum values match expected server values
|
||||
## Critical for network synchronization
|
||||
|
||||
func test_attack_step_none_equals_zero():
|
||||
assert_eq(Enums.AttackStep.NONE, 0, "AttackStep.NONE should be 0 to match server")
|
||||
|
||||
|
||||
func test_attack_step_preparation_equals_one():
|
||||
assert_eq(Enums.AttackStep.PREPARATION, 1, "AttackStep.PREPARATION should be 1 to match server")
|
||||
|
||||
|
||||
func test_attack_step_declaration_equals_two():
|
||||
assert_eq(Enums.AttackStep.DECLARATION, 2, "AttackStep.DECLARATION should be 2 to match server")
|
||||
|
||||
|
||||
func test_attack_step_block_declaration_equals_three():
|
||||
assert_eq(Enums.AttackStep.BLOCK_DECLARATION, 3, "AttackStep.BLOCK_DECLARATION should be 3 to match server")
|
||||
|
||||
|
||||
func test_attack_step_damage_resolution_equals_four():
|
||||
assert_eq(Enums.AttackStep.DAMAGE_RESOLUTION, 4, "AttackStep.DAMAGE_RESOLUTION should be 4 to match server")
|
||||
|
||||
|
||||
## Test enum ordering is correct
|
||||
func test_attack_step_enum_order():
|
||||
# Verify the enum values are in correct order
|
||||
assert_true(
|
||||
Enums.AttackStep.NONE < Enums.AttackStep.PREPARATION,
|
||||
"NONE should be less than PREPARATION"
|
||||
)
|
||||
assert_true(
|
||||
Enums.AttackStep.PREPARATION < Enums.AttackStep.DECLARATION,
|
||||
"PREPARATION should be less than DECLARATION"
|
||||
)
|
||||
assert_true(
|
||||
Enums.AttackStep.DECLARATION < Enums.AttackStep.BLOCK_DECLARATION,
|
||||
"DECLARATION should be less than BLOCK_DECLARATION"
|
||||
)
|
||||
assert_true(
|
||||
Enums.AttackStep.BLOCK_DECLARATION < Enums.AttackStep.DAMAGE_RESOLUTION,
|
||||
"BLOCK_DECLARATION should be less than DAMAGE_RESOLUTION"
|
||||
)
|
||||
|
||||
|
||||
## Test TurnPhase enum alignment with server as well
|
||||
## Server uses: ACTIVE=0, DRAW=1, MAIN_1=2, ATTACK=3, MAIN_2=4, END=5
|
||||
|
||||
func test_turn_phase_active_equals_zero():
|
||||
assert_eq(Enums.TurnPhase.ACTIVE, 0, "TurnPhase.ACTIVE should be 0 to match server")
|
||||
|
||||
|
||||
func test_turn_phase_draw_equals_one():
|
||||
assert_eq(Enums.TurnPhase.DRAW, 1, "TurnPhase.DRAW should be 1 to match server")
|
||||
|
||||
|
||||
func test_turn_phase_main_1_equals_two():
|
||||
assert_eq(Enums.TurnPhase.MAIN_1, 2, "TurnPhase.MAIN_1 should be 2 to match server")
|
||||
|
||||
|
||||
func test_turn_phase_attack_equals_three():
|
||||
assert_eq(Enums.TurnPhase.ATTACK, 3, "TurnPhase.ATTACK should be 3 to match server")
|
||||
|
||||
|
||||
func test_turn_phase_main_2_equals_four():
|
||||
assert_eq(Enums.TurnPhase.MAIN_2, 4, "TurnPhase.MAIN_2 should be 4 to match server")
|
||||
|
||||
|
||||
func test_turn_phase_end_equals_five():
|
||||
assert_eq(Enums.TurnPhase.END, 5, "TurnPhase.END should be 5 to match server")
|
||||
|
||||
|
||||
## Test that all enum values are present
|
||||
func test_attack_step_has_five_values():
|
||||
var values = Enums.AttackStep.values()
|
||||
assert_eq(values.size(), 5, "AttackStep should have 5 values (NONE, PREPARATION, DECLARATION, BLOCK_DECLARATION, DAMAGE_RESOLUTION)")
|
||||
|
||||
|
||||
func test_turn_phase_has_six_values():
|
||||
var values = Enums.TurnPhase.values()
|
||||
assert_eq(values.size(), 6, "TurnPhase should have 6 values (ACTIVE, DRAW, MAIN_1, ATTACK, MAIN_2, END)")
|
||||
491
tests/unit/test_card_filter.gd
Normal file
491
tests/unit/test_card_filter.gd
Normal file
@@ -0,0 +1,491 @@
|
||||
extends GutTest
|
||||
|
||||
## Tests for CardFilter utility class
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FILTER MATCHING TESTS - ELEMENT
|
||||
# =============================================================================
|
||||
|
||||
func test_element_filter_matches_fire() -> void:
|
||||
var filter = {"element": "FIRE"}
|
||||
var card = _create_mock_forward_with_element(Enums.Element.FIRE)
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "Fire card should match FIRE filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_element_filter_rejects_wrong_element() -> void:
|
||||
var filter = {"element": "FIRE"}
|
||||
var card = _create_mock_forward_with_element(Enums.Element.ICE)
|
||||
|
||||
assert_false(CardFilter.matches_filter(card, filter), "Ice card should not match FIRE filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_element_filter_case_insensitive() -> void:
|
||||
var filter = {"element": "fire"}
|
||||
var card = _create_mock_forward_with_element(Enums.Element.FIRE)
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "Element filter should be case insensitive")
|
||||
card.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FILTER MATCHING TESTS - JOB
|
||||
# =============================================================================
|
||||
|
||||
func test_job_filter_matches_samurai() -> void:
|
||||
var filter = {"job": "Samurai"}
|
||||
var card = _create_mock_forward_with_job("Samurai")
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "Samurai should match Samurai filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_job_filter_rejects_wrong_job() -> void:
|
||||
var filter = {"job": "Samurai"}
|
||||
var card = _create_mock_forward_with_job("Warrior")
|
||||
|
||||
assert_false(CardFilter.matches_filter(card, filter), "Warrior should not match Samurai filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_job_filter_case_insensitive() -> void:
|
||||
var filter = {"job": "SAMURAI"}
|
||||
var card = _create_mock_forward_with_job("samurai")
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "Job filter should be case insensitive")
|
||||
card.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FILTER MATCHING TESTS - CATEGORY
|
||||
# =============================================================================
|
||||
|
||||
func test_category_filter_matches() -> void:
|
||||
var filter = {"category": "VII"}
|
||||
var card = _create_mock_forward_with_category("VII")
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "VII card should match VII filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_category_filter_rejects_wrong_category() -> void:
|
||||
var filter = {"category": "VII"}
|
||||
var card = _create_mock_forward_with_category("X")
|
||||
|
||||
assert_false(CardFilter.matches_filter(card, filter), "Category X should not match VII filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_category_filter_matches_partial() -> void:
|
||||
var filter = {"category": "FF"}
|
||||
var card = _create_mock_forward_with_category("FFVII")
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "FFVII should match FF filter (partial)")
|
||||
card.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FILTER MATCHING TESTS - COST
|
||||
# =============================================================================
|
||||
|
||||
func test_cost_exact_filter_matches() -> void:
|
||||
var filter = {"cost": 3}
|
||||
var card = _create_mock_forward_with_cost(3)
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "Cost 3 should match exact cost 3")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_exact_filter_rejects_wrong_cost() -> void:
|
||||
var filter = {"cost": 3}
|
||||
var card = _create_mock_forward_with_cost(5)
|
||||
|
||||
assert_false(CardFilter.matches_filter(card, filter), "Cost 5 should not match exact cost 3")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_comparison_lte() -> void:
|
||||
var filter = {"cost_comparison": "LTE", "cost_value": 4}
|
||||
var card3 = _create_mock_forward_with_cost(3)
|
||||
var card4 = _create_mock_forward_with_cost(4)
|
||||
var card5 = _create_mock_forward_with_cost(5)
|
||||
|
||||
assert_true(CardFilter.matches_filter(card3, filter), "Cost 3 should match LTE 4")
|
||||
assert_true(CardFilter.matches_filter(card4, filter), "Cost 4 should match LTE 4")
|
||||
assert_false(CardFilter.matches_filter(card5, filter), "Cost 5 should not match LTE 4")
|
||||
|
||||
card3.free()
|
||||
card4.free()
|
||||
card5.free()
|
||||
|
||||
|
||||
func test_cost_comparison_gte() -> void:
|
||||
var filter = {"cost_comparison": "GTE", "cost_value": 4}
|
||||
var card3 = _create_mock_forward_with_cost(3)
|
||||
var card4 = _create_mock_forward_with_cost(4)
|
||||
var card5 = _create_mock_forward_with_cost(5)
|
||||
|
||||
assert_false(CardFilter.matches_filter(card3, filter), "Cost 3 should not match GTE 4")
|
||||
assert_true(CardFilter.matches_filter(card4, filter), "Cost 4 should match GTE 4")
|
||||
assert_true(CardFilter.matches_filter(card5, filter), "Cost 5 should match GTE 4")
|
||||
|
||||
card3.free()
|
||||
card4.free()
|
||||
card5.free()
|
||||
|
||||
|
||||
func test_cost_min_max_filters() -> void:
|
||||
var filter = {"cost_min": 3, "cost_max": 5}
|
||||
var card2 = _create_mock_forward_with_cost(2)
|
||||
var card3 = _create_mock_forward_with_cost(3)
|
||||
var card4 = _create_mock_forward_with_cost(4)
|
||||
var card5 = _create_mock_forward_with_cost(5)
|
||||
var card6 = _create_mock_forward_with_cost(6)
|
||||
|
||||
assert_false(CardFilter.matches_filter(card2, filter), "Cost 2 should not be in range 3-5")
|
||||
assert_true(CardFilter.matches_filter(card3, filter), "Cost 3 should be in range 3-5")
|
||||
assert_true(CardFilter.matches_filter(card4, filter), "Cost 4 should be in range 3-5")
|
||||
assert_true(CardFilter.matches_filter(card5, filter), "Cost 5 should be in range 3-5")
|
||||
assert_false(CardFilter.matches_filter(card6, filter), "Cost 6 should not be in range 3-5")
|
||||
|
||||
card2.free()
|
||||
card3.free()
|
||||
card4.free()
|
||||
card5.free()
|
||||
card6.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FILTER MATCHING TESTS - CARD TYPE
|
||||
# =============================================================================
|
||||
|
||||
func test_card_type_forward_matches() -> void:
|
||||
var filter = {"card_type": "FORWARD"}
|
||||
var forward = _create_mock_forward()
|
||||
var backup = _create_mock_backup()
|
||||
|
||||
assert_true(CardFilter.matches_filter(forward, filter), "Forward should match FORWARD filter")
|
||||
assert_false(CardFilter.matches_filter(backup, filter), "Backup should not match FORWARD filter")
|
||||
|
||||
forward.free()
|
||||
backup.free()
|
||||
|
||||
|
||||
func test_card_type_backup_matches() -> void:
|
||||
var filter = {"card_type": "BACKUP"}
|
||||
var forward = _create_mock_forward()
|
||||
var backup = _create_mock_backup()
|
||||
|
||||
assert_false(CardFilter.matches_filter(forward, filter), "Forward should not match BACKUP filter")
|
||||
assert_true(CardFilter.matches_filter(backup, filter), "Backup should match BACKUP filter")
|
||||
|
||||
forward.free()
|
||||
backup.free()
|
||||
|
||||
|
||||
func test_card_type_character_matches_both() -> void:
|
||||
var filter = {"card_type": "CHARACTER"}
|
||||
var forward = _create_mock_forward()
|
||||
var backup = _create_mock_backup()
|
||||
|
||||
assert_true(CardFilter.matches_filter(forward, filter), "Forward should match CHARACTER filter")
|
||||
assert_true(CardFilter.matches_filter(backup, filter), "Backup should match CHARACTER filter")
|
||||
|
||||
forward.free()
|
||||
backup.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FILTER MATCHING TESTS - NAME
|
||||
# =============================================================================
|
||||
|
||||
func test_card_name_filter_matches() -> void:
|
||||
var filter = {"card_name": "Cloud"}
|
||||
var card = _create_mock_forward_with_name("Cloud")
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "Cloud should match Cloud filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_card_name_filter_rejects_wrong_name() -> void:
|
||||
var filter = {"card_name": "Cloud"}
|
||||
var card = _create_mock_forward_with_name("Sephiroth")
|
||||
|
||||
assert_false(CardFilter.matches_filter(card, filter), "Sephiroth should not match Cloud filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_name_filter_alternative_key() -> void:
|
||||
var filter = {"name": "Cloud"}
|
||||
var card = _create_mock_forward_with_name("Cloud")
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "Should support 'name' as well as 'card_name'")
|
||||
card.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FILTER MATCHING TESTS - POWER
|
||||
# =============================================================================
|
||||
|
||||
func test_power_comparison_gte() -> void:
|
||||
var filter = {"power_comparison": "GTE", "power_value": 8000}
|
||||
var card7000 = _create_mock_forward_with_power(7000)
|
||||
var card8000 = _create_mock_forward_with_power(8000)
|
||||
var card9000 = _create_mock_forward_with_power(9000)
|
||||
|
||||
assert_false(CardFilter.matches_filter(card7000, filter), "Power 7000 should not match GTE 8000")
|
||||
assert_true(CardFilter.matches_filter(card8000, filter), "Power 8000 should match GTE 8000")
|
||||
assert_true(CardFilter.matches_filter(card9000, filter), "Power 9000 should match GTE 8000")
|
||||
|
||||
card7000.free()
|
||||
card8000.free()
|
||||
card9000.free()
|
||||
|
||||
|
||||
func test_power_min_max_filters() -> void:
|
||||
var filter = {"power_min": 5000, "power_max": 7000}
|
||||
var card4000 = _create_mock_forward_with_power(4000)
|
||||
var card6000 = _create_mock_forward_with_power(6000)
|
||||
var card8000 = _create_mock_forward_with_power(8000)
|
||||
|
||||
assert_false(CardFilter.matches_filter(card4000, filter), "Power 4000 should not be in range 5000-7000")
|
||||
assert_true(CardFilter.matches_filter(card6000, filter), "Power 6000 should be in range 5000-7000")
|
||||
assert_false(CardFilter.matches_filter(card8000, filter), "Power 8000 should not be in range 5000-7000")
|
||||
|
||||
card4000.free()
|
||||
card6000.free()
|
||||
card8000.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EMPTY/NULL FILTER TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_empty_filter_matches_all() -> void:
|
||||
var filter = {}
|
||||
var card = _create_mock_forward()
|
||||
|
||||
assert_true(CardFilter.matches_filter(card, filter), "Empty filter should match any card")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_null_card_returns_false() -> void:
|
||||
var filter = {"element": "FIRE"}
|
||||
|
||||
assert_false(CardFilter.matches_filter(null, filter), "Null card should return false")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COMBINED FILTER TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_combined_job_and_cost_filter() -> void:
|
||||
var filter = {"job": "Warrior", "cost_comparison": "LTE", "cost_value": 4}
|
||||
|
||||
var cheap_warrior = _create_mock_forward_with_job_and_cost("Warrior", 3)
|
||||
var expensive_warrior = _create_mock_forward_with_job_and_cost("Warrior", 5)
|
||||
var cheap_mage = _create_mock_forward_with_job_and_cost("Mage", 3)
|
||||
|
||||
assert_true(CardFilter.matches_filter(cheap_warrior, filter), "Cheap Warrior should match")
|
||||
assert_false(CardFilter.matches_filter(expensive_warrior, filter), "Expensive Warrior should not match")
|
||||
assert_false(CardFilter.matches_filter(cheap_mage, filter), "Cheap Mage should not match")
|
||||
|
||||
cheap_warrior.free()
|
||||
expensive_warrior.free()
|
||||
cheap_mage.free()
|
||||
|
||||
|
||||
func test_combined_element_and_type_filter() -> void:
|
||||
var filter = {"element": "FIRE", "card_type": "FORWARD"}
|
||||
|
||||
var fire_forward = _create_mock_forward_with_element(Enums.Element.FIRE)
|
||||
var ice_forward = _create_mock_forward_with_element(Enums.Element.ICE)
|
||||
var fire_backup = _create_mock_backup_with_element(Enums.Element.FIRE)
|
||||
|
||||
assert_true(CardFilter.matches_filter(fire_forward, filter), "Fire Forward should match")
|
||||
assert_false(CardFilter.matches_filter(ice_forward, filter), "Ice Forward should not match (wrong element)")
|
||||
assert_false(CardFilter.matches_filter(fire_backup, filter), "Fire Backup should not match (wrong type)")
|
||||
|
||||
fire_forward.free()
|
||||
ice_forward.free()
|
||||
fire_backup.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EXCLUDE SELF TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_exclude_self_filter() -> void:
|
||||
var filter = {"card_type": "FORWARD", "exclude_self": true}
|
||||
var source = _create_mock_forward()
|
||||
var other = _create_mock_forward()
|
||||
|
||||
assert_false(CardFilter.matches_filter(source, filter, source), "Source should be excluded")
|
||||
assert_true(CardFilter.matches_filter(other, filter, source), "Other card should not be excluded")
|
||||
|
||||
source.free()
|
||||
other.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COLLECTION METHODS TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_count_matching() -> void:
|
||||
var filter = {"element": "FIRE"}
|
||||
var cards = [
|
||||
_create_mock_forward_with_element(Enums.Element.FIRE),
|
||||
_create_mock_forward_with_element(Enums.Element.ICE),
|
||||
_create_mock_forward_with_element(Enums.Element.FIRE),
|
||||
_create_mock_forward_with_element(Enums.Element.WATER)
|
||||
]
|
||||
|
||||
assert_eq(CardFilter.count_matching(cards, filter), 2, "Should count 2 Fire cards")
|
||||
|
||||
for card in cards:
|
||||
card.free()
|
||||
|
||||
|
||||
func test_get_matching() -> void:
|
||||
var filter = {"element": "FIRE"}
|
||||
var cards = [
|
||||
_create_mock_forward_with_element(Enums.Element.FIRE),
|
||||
_create_mock_forward_with_element(Enums.Element.ICE),
|
||||
_create_mock_forward_with_element(Enums.Element.FIRE)
|
||||
]
|
||||
|
||||
var matching = CardFilter.get_matching(cards, filter)
|
||||
assert_eq(matching.size(), 2, "Should get 2 Fire cards")
|
||||
|
||||
for card in cards:
|
||||
card.free()
|
||||
|
||||
|
||||
func test_get_highest_power() -> void:
|
||||
var cards = [
|
||||
_create_mock_forward_with_power(5000),
|
||||
_create_mock_forward_with_power(9000),
|
||||
_create_mock_forward_with_power(7000)
|
||||
]
|
||||
|
||||
assert_eq(CardFilter.get_highest_power(cards), 9000, "Should return highest power 9000")
|
||||
|
||||
for card in cards:
|
||||
card.free()
|
||||
|
||||
|
||||
func test_get_highest_power_with_filter() -> void:
|
||||
var filter = {"element": "FIRE"}
|
||||
var fire1 = _create_mock_forward_with_element(Enums.Element.FIRE)
|
||||
fire1.card_data.power = 5000
|
||||
var fire2 = _create_mock_forward_with_element(Enums.Element.FIRE)
|
||||
fire2.card_data.power = 8000
|
||||
var ice = _create_mock_forward_with_element(Enums.Element.ICE)
|
||||
ice.card_data.power = 10000
|
||||
|
||||
var cards = [fire1, fire2, ice]
|
||||
|
||||
assert_eq(CardFilter.get_highest_power(cards, filter), 8000, "Should return highest Fire power 8000")
|
||||
|
||||
for card in cards:
|
||||
card.free()
|
||||
|
||||
|
||||
func test_get_lowest_power() -> void:
|
||||
var cards = [
|
||||
_create_mock_forward_with_power(5000),
|
||||
_create_mock_forward_with_power(3000),
|
||||
_create_mock_forward_with_power(7000)
|
||||
]
|
||||
|
||||
assert_eq(CardFilter.get_lowest_power(cards), 3000, "Should return lowest power 3000")
|
||||
|
||||
for card in cards:
|
||||
card.free()
|
||||
|
||||
|
||||
func test_get_lowest_power_empty_array() -> void:
|
||||
var cards: Array = []
|
||||
|
||||
assert_eq(CardFilter.get_lowest_power(cards), 0, "Should return 0 for empty array")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
func _create_mock_forward() -> CardInstance:
|
||||
var card = CardInstance.new()
|
||||
var data = CardDatabase.CardData.new()
|
||||
data.type = Enums.CardType.FORWARD
|
||||
data.cost = 3
|
||||
data.power = 7000
|
||||
data.name = "Test Forward"
|
||||
data.job = "Warrior"
|
||||
data.elements = [Enums.Element.FIRE]
|
||||
card.card_data = data
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_backup() -> CardInstance:
|
||||
var card = CardInstance.new()
|
||||
var data = CardDatabase.CardData.new()
|
||||
data.type = Enums.CardType.BACKUP
|
||||
data.cost = 2
|
||||
data.name = "Test Backup"
|
||||
data.job = "Scholar"
|
||||
data.elements = [Enums.Element.WATER]
|
||||
card.card_data = data
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_element(element: Enums.Element) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.elements = [element]
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_backup_with_element(element: Enums.Element) -> CardInstance:
|
||||
var card = _create_mock_backup()
|
||||
card.card_data.elements = [element]
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_job(job: String) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.job = job
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_category(category: String) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.category = category
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_cost(cost: int) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.cost = cost
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_name(card_name: String) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.name = card_name
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_power(power: int) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.power = power
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_job_and_cost(job: String, cost: int) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.job = job
|
||||
card.card_data.cost = cost
|
||||
return card
|
||||
420
tests/unit/test_condition_checker.gd
Normal file
420
tests/unit/test_condition_checker.gd
Normal file
@@ -0,0 +1,420 @@
|
||||
extends GutTest
|
||||
|
||||
## Tests for the ConditionChecker class
|
||||
## Tests various condition types and logical operators
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SETUP
|
||||
# =============================================================================
|
||||
|
||||
var checker: ConditionChecker
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
checker = ConditionChecker.new()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BASIC TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_checker_instantiates() -> void:
|
||||
assert_not_null(checker, "ConditionChecker should instantiate")
|
||||
|
||||
|
||||
func test_empty_condition_returns_true() -> void:
|
||||
var result = checker.evaluate({}, {})
|
||||
assert_true(result, "Empty condition should return true (unconditional)")
|
||||
|
||||
|
||||
func test_unknown_condition_type_returns_false() -> void:
|
||||
var condition = {"type": "NONEXISTENT_TYPE"}
|
||||
var result = checker.evaluate(condition, {})
|
||||
assert_false(result, "Unknown condition type should return false")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COMPARISON HELPER TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_compare_eq() -> void:
|
||||
assert_true(checker._compare(5, "EQ", 5), "5 == 5")
|
||||
assert_false(checker._compare(5, "EQ", 3), "5 != 3")
|
||||
|
||||
|
||||
func test_compare_neq() -> void:
|
||||
assert_true(checker._compare(5, "NEQ", 3), "5 != 3")
|
||||
assert_false(checker._compare(5, "NEQ", 5), "5 == 5")
|
||||
|
||||
|
||||
func test_compare_gt() -> void:
|
||||
assert_true(checker._compare(5, "GT", 3), "5 > 3")
|
||||
assert_false(checker._compare(5, "GT", 5), "5 not > 5")
|
||||
assert_false(checker._compare(3, "GT", 5), "3 not > 5")
|
||||
|
||||
|
||||
func test_compare_gte() -> void:
|
||||
assert_true(checker._compare(5, "GTE", 3), "5 >= 3")
|
||||
assert_true(checker._compare(5, "GTE", 5), "5 >= 5")
|
||||
assert_false(checker._compare(3, "GTE", 5), "3 not >= 5")
|
||||
|
||||
|
||||
func test_compare_lt() -> void:
|
||||
assert_true(checker._compare(3, "LT", 5), "3 < 5")
|
||||
assert_false(checker._compare(5, "LT", 5), "5 not < 5")
|
||||
assert_false(checker._compare(5, "LT", 3), "5 not < 3")
|
||||
|
||||
|
||||
func test_compare_lte() -> void:
|
||||
assert_true(checker._compare(3, "LTE", 5), "3 <= 5")
|
||||
assert_true(checker._compare(5, "LTE", 5), "5 <= 5")
|
||||
assert_false(checker._compare(5, "LTE", 3), "5 not <= 3")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# LOGICAL OPERATOR TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_and_with_all_true() -> void:
|
||||
var condition = {
|
||||
"type": "AND",
|
||||
"conditions": [
|
||||
{"type": "DAMAGE_RECEIVED", "comparison": "GTE", "value": 3},
|
||||
{"type": "DAMAGE_RECEIVED", "comparison": "LTE", "value": 5}
|
||||
]
|
||||
}
|
||||
|
||||
var context = _create_mock_context_with_damage(4)
|
||||
var result = checker.evaluate(condition, context)
|
||||
assert_true(result, "AND with all true conditions should return true")
|
||||
|
||||
|
||||
func test_and_with_one_false() -> void:
|
||||
var condition = {
|
||||
"type": "AND",
|
||||
"conditions": [
|
||||
{"type": "DAMAGE_RECEIVED", "comparison": "GTE", "value": 3},
|
||||
{"type": "DAMAGE_RECEIVED", "comparison": "LTE", "value": 2}
|
||||
]
|
||||
}
|
||||
|
||||
var context = _create_mock_context_with_damage(4)
|
||||
var result = checker.evaluate(condition, context)
|
||||
assert_false(result, "AND with one false condition should return false")
|
||||
|
||||
|
||||
func test_or_with_one_true() -> void:
|
||||
var condition = {
|
||||
"type": "OR",
|
||||
"conditions": [
|
||||
{"type": "DAMAGE_RECEIVED", "comparison": "GTE", "value": 10},
|
||||
{"type": "DAMAGE_RECEIVED", "comparison": "GTE", "value": 3}
|
||||
]
|
||||
}
|
||||
|
||||
var context = _create_mock_context_with_damage(5)
|
||||
var result = checker.evaluate(condition, context)
|
||||
assert_true(result, "OR with one true condition should return true")
|
||||
|
||||
|
||||
func test_or_with_all_false() -> void:
|
||||
var condition = {
|
||||
"type": "OR",
|
||||
"conditions": [
|
||||
{"type": "DAMAGE_RECEIVED", "comparison": "GTE", "value": 10},
|
||||
{"type": "DAMAGE_RECEIVED", "comparison": "EQ", "value": 0}
|
||||
]
|
||||
}
|
||||
|
||||
var context = _create_mock_context_with_damage(5)
|
||||
var result = checker.evaluate(condition, context)
|
||||
assert_false(result, "OR with all false conditions should return false")
|
||||
|
||||
|
||||
func test_not_inverts_true() -> void:
|
||||
var condition = {
|
||||
"type": "NOT",
|
||||
"condition": {"type": "DAMAGE_RECEIVED", "comparison": "GTE", "value": 10}
|
||||
}
|
||||
|
||||
var context = _create_mock_context_with_damage(5)
|
||||
var result = checker.evaluate(condition, context)
|
||||
assert_true(result, "NOT should invert false to true")
|
||||
|
||||
|
||||
func test_not_inverts_false() -> void:
|
||||
var condition = {
|
||||
"type": "NOT",
|
||||
"condition": {"type": "DAMAGE_RECEIVED", "comparison": "GTE", "value": 3}
|
||||
}
|
||||
|
||||
var context = _create_mock_context_with_damage(5)
|
||||
var result = checker.evaluate(condition, context)
|
||||
assert_false(result, "NOT should invert true to false")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DAMAGE_RECEIVED CONDITION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_damage_received_gte() -> void:
|
||||
var condition = {
|
||||
"type": "DAMAGE_RECEIVED",
|
||||
"comparison": "GTE",
|
||||
"value": 5
|
||||
}
|
||||
|
||||
# Not enough damage
|
||||
var context = _create_mock_context_with_damage(3)
|
||||
assert_false(checker.evaluate(condition, context), "3 damage not >= 5")
|
||||
|
||||
# Exactly enough
|
||||
context = _create_mock_context_with_damage(5)
|
||||
assert_true(checker.evaluate(condition, context), "5 damage >= 5")
|
||||
|
||||
# More than enough
|
||||
context = _create_mock_context_with_damage(7)
|
||||
assert_true(checker.evaluate(condition, context), "7 damage >= 5")
|
||||
|
||||
|
||||
func test_damage_received_no_game_state() -> void:
|
||||
var condition = {
|
||||
"type": "DAMAGE_RECEIVED",
|
||||
"comparison": "GTE",
|
||||
"value": 5
|
||||
}
|
||||
|
||||
var context = {"game_state": null, "player_id": 0}
|
||||
var result = checker.evaluate(condition, context)
|
||||
assert_false(result, "Should return false without game state")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FORWARD STATE TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_forward_state_dull() -> void:
|
||||
var condition = {
|
||||
"type": "FORWARD_STATE",
|
||||
"state": "DULL",
|
||||
"check_self": false
|
||||
}
|
||||
|
||||
# Create mock card
|
||||
var card = _create_mock_forward()
|
||||
card.is_dull = true
|
||||
|
||||
var context = {"target_card": card}
|
||||
assert_true(checker.evaluate(condition, context), "Dull card should match DULL state")
|
||||
|
||||
card.is_dull = false
|
||||
assert_false(checker.evaluate(condition, context), "Active card should not match DULL state")
|
||||
|
||||
card.free()
|
||||
|
||||
|
||||
func test_forward_state_active() -> void:
|
||||
var condition = {
|
||||
"type": "FORWARD_STATE",
|
||||
"state": "ACTIVE",
|
||||
"check_self": false
|
||||
}
|
||||
|
||||
var card = _create_mock_forward()
|
||||
card.is_dull = false
|
||||
|
||||
var context = {"target_card": card}
|
||||
assert_true(checker.evaluate(condition, context), "Active card should match ACTIVE state")
|
||||
|
||||
card.is_dull = true
|
||||
assert_false(checker.evaluate(condition, context), "Dull card should not match ACTIVE state")
|
||||
|
||||
card.free()
|
||||
|
||||
|
||||
func test_forward_state_check_self() -> void:
|
||||
var condition = {
|
||||
"type": "FORWARD_STATE",
|
||||
"state": "DULL",
|
||||
"check_self": true
|
||||
}
|
||||
|
||||
var source_card = _create_mock_forward()
|
||||
source_card.is_dull = true
|
||||
|
||||
var context = {"source_card": source_card, "target_card": null}
|
||||
assert_true(checker.evaluate(condition, context), "check_self=true should use source_card")
|
||||
|
||||
source_card.free()
|
||||
|
||||
|
||||
func test_forward_state_no_card() -> void:
|
||||
var condition = {
|
||||
"type": "FORWARD_STATE",
|
||||
"state": "DULL",
|
||||
"check_self": false
|
||||
}
|
||||
|
||||
var context = {"target_card": null}
|
||||
assert_false(checker.evaluate(condition, context), "Should return false without target card")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COST COMPARISON TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_cost_comparison_lte() -> void:
|
||||
var condition = {
|
||||
"type": "COST_COMPARISON",
|
||||
"comparison": "LTE",
|
||||
"value": 3
|
||||
}
|
||||
|
||||
var card = _create_mock_forward_with_cost(2)
|
||||
var context = {"target_card": card}
|
||||
assert_true(checker.evaluate(condition, context), "Cost 2 <= 3")
|
||||
|
||||
card.card_data.cost = 3
|
||||
assert_true(checker.evaluate(condition, context), "Cost 3 <= 3")
|
||||
|
||||
card.card_data.cost = 5
|
||||
assert_false(checker.evaluate(condition, context), "Cost 5 not <= 3")
|
||||
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_comparison_gte() -> void:
|
||||
var condition = {
|
||||
"type": "COST_COMPARISON",
|
||||
"comparison": "GTE",
|
||||
"value": 4
|
||||
}
|
||||
|
||||
var card = _create_mock_forward_with_cost(5)
|
||||
var context = {"target_card": card}
|
||||
assert_true(checker.evaluate(condition, context), "Cost 5 >= 4")
|
||||
|
||||
card.card_data.cost = 4
|
||||
assert_true(checker.evaluate(condition, context), "Cost 4 >= 4")
|
||||
|
||||
card.card_data.cost = 3
|
||||
assert_false(checker.evaluate(condition, context), "Cost 3 not >= 4")
|
||||
|
||||
card.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POWER COMPARISON TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_power_comparison_lt_value() -> void:
|
||||
var condition = {
|
||||
"type": "POWER_COMPARISON",
|
||||
"comparison": "LT",
|
||||
"value": 8000
|
||||
}
|
||||
|
||||
var card = _create_mock_forward_with_power(5000)
|
||||
var context = {"target_card": card}
|
||||
assert_true(checker.evaluate(condition, context), "Power 5000 < 8000")
|
||||
|
||||
card.current_power = 8000
|
||||
assert_false(checker.evaluate(condition, context), "Power 8000 not < 8000")
|
||||
|
||||
card.free()
|
||||
|
||||
|
||||
func test_power_comparison_self_power() -> void:
|
||||
var condition = {
|
||||
"type": "POWER_COMPARISON",
|
||||
"comparison": "LT",
|
||||
"compare_to": "SELF_POWER"
|
||||
}
|
||||
|
||||
var attacker = _create_mock_forward_with_power(8000)
|
||||
var blocker = _create_mock_forward_with_power(5000)
|
||||
|
||||
var context = {
|
||||
"source_card": attacker,
|
||||
"target_card": blocker
|
||||
}
|
||||
|
||||
assert_true(checker.evaluate(condition, context), "Blocker 5000 < Attacker 8000")
|
||||
|
||||
blocker.current_power = 9000
|
||||
assert_false(checker.evaluate(condition, context), "Blocker 9000 not < Attacker 8000")
|
||||
|
||||
attacker.free()
|
||||
blocker.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
func _create_mock_context_with_damage(damage: int) -> Dictionary:
|
||||
var mock_game_state = MockGameState.new(damage)
|
||||
return {
|
||||
"game_state": mock_game_state,
|
||||
"player_id": 0
|
||||
}
|
||||
|
||||
|
||||
func _create_mock_forward() -> CardInstance:
|
||||
var card = CardInstance.new()
|
||||
var data = CardDatabase.CardData.new()
|
||||
data.type = Enums.CardType.FORWARD
|
||||
data.cost = 3
|
||||
data.power = 7000
|
||||
card.card_data = data
|
||||
card.is_dull = false
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_cost(cost: int) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.cost = cost
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_power(power: int) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.power = power
|
||||
card.current_power = power
|
||||
return card
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MOCK CLASSES
|
||||
# =============================================================================
|
||||
|
||||
class MockGameState:
|
||||
var players: Array
|
||||
|
||||
func _init(player_damage: int = 0):
|
||||
players = [MockPlayer.new(player_damage), MockPlayer.new(0)]
|
||||
|
||||
func get_player_damage(player_id: int) -> int:
|
||||
if player_id < players.size():
|
||||
return players[player_id].damage
|
||||
return 0
|
||||
|
||||
func get_field_cards(player_id: int) -> Array:
|
||||
if player_id < players.size():
|
||||
return players[player_id].field
|
||||
return []
|
||||
|
||||
func get_break_zone(player_id: int) -> Array:
|
||||
if player_id < players.size():
|
||||
return players[player_id].break_zone
|
||||
return []
|
||||
|
||||
|
||||
class MockPlayer:
|
||||
var damage: int = 0
|
||||
var field: Array = []
|
||||
var break_zone: Array = []
|
||||
|
||||
func _init(p_damage: int = 0):
|
||||
damage = p_damage
|
||||
348
tests/unit/test_cost_validation.gd
Normal file
348
tests/unit/test_cost_validation.gd
Normal file
@@ -0,0 +1,348 @@
|
||||
extends GutTest
|
||||
|
||||
## Tests for ability cost validation in AbilitySystem
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CP COST VALIDATION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_validate_empty_cost_always_valid() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {}
|
||||
var player = MockPlayer.new()
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_true(result.valid, "Empty cost should always be valid")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_cp_cost_with_sufficient_cp() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 3}
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 5)
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_true(result.valid, "Should be valid with sufficient CP")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_cp_cost_with_insufficient_cp() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 5}
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 3)
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_false(result.valid, "Should be invalid with insufficient CP")
|
||||
assert_true("Not enough CP" in result.reason, "Reason should mention CP")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_specific_element_cp_cost_valid() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 2, "element": "FIRE"}
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 3)
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_true(result.valid, "Should be valid with sufficient Fire CP")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_specific_element_cp_cost_invalid() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 2, "element": "FIRE"}
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.ICE, 5) # Wrong element
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_false(result.valid, "Should be invalid with wrong element CP")
|
||||
assert_true("FIRE CP" in result.reason, "Reason should mention Fire")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_any_element_cp_cost() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 4, "element": "ANY"}
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 2)
|
||||
player.cp_pool.add_cp(Enums.Element.ICE, 2)
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_true(result.valid, "Should be valid with total CP from multiple elements")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DISCARD COST VALIDATION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_validate_discard_cost_with_sufficient_cards() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"discard": 2}
|
||||
var player = MockPlayer.new()
|
||||
# Add 3 cards to hand
|
||||
for i in range(3):
|
||||
var card = _create_mock_card()
|
||||
player.hand.add_card(card)
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_true(result.valid, "Should be valid with sufficient cards to discard")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_discard_cost_with_insufficient_cards() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"discard": 3}
|
||||
var player = MockPlayer.new()
|
||||
# Add only 1 card to hand
|
||||
player.hand.add_card(_create_mock_card())
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_false(result.valid, "Should be invalid with insufficient cards")
|
||||
assert_true("discard" in result.reason, "Reason should mention discard")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DULL SELF COST VALIDATION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_validate_dull_self_cost_when_active() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"dull_self": true}
|
||||
var source = _create_mock_card()
|
||||
source.activate() # Ensure active
|
||||
var player = MockPlayer.new()
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, source, player)
|
||||
|
||||
assert_true(result.valid, "Should be valid when card is active")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_dull_self_cost_when_already_dull() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"dull_self": true}
|
||||
var source = _create_mock_card()
|
||||
source.dull() # Already dull
|
||||
var player = MockPlayer.new()
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, source, player)
|
||||
|
||||
assert_false(result.valid, "Should be invalid when card is already dull")
|
||||
assert_true("dulled" in result.reason, "Reason should mention dulled")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SPECIFIC DISCARD COST VALIDATION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_validate_specific_discard_cost_with_matching_card() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"specific_discard": "Cloud"}
|
||||
var player = MockPlayer.new()
|
||||
var cloud = _create_mock_card_with_name("Cloud")
|
||||
player.hand.add_card(cloud)
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_true(result.valid, "Should be valid with matching card in hand")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_specific_discard_cost_without_matching_card() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"specific_discard": "Cloud"}
|
||||
var player = MockPlayer.new()
|
||||
var sephiroth = _create_mock_card_with_name("Sephiroth")
|
||||
player.hand.add_card(sephiroth)
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, null, player)
|
||||
|
||||
assert_false(result.valid, "Should be invalid without matching card")
|
||||
assert_true("Cloud" in result.reason, "Reason should mention required card")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COMBINED COST VALIDATION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_validate_combined_cp_and_dull_cost() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 2, "dull_self": true}
|
||||
var source = _create_mock_card()
|
||||
source.activate()
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 3)
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, source, player)
|
||||
|
||||
assert_true(result.valid, "Should be valid when all requirements met")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_combined_cost_fails_on_cp() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 5, "dull_self": true}
|
||||
var source = _create_mock_card()
|
||||
source.activate()
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 2) # Insufficient
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, source, player)
|
||||
|
||||
assert_false(result.valid, "Should fail on CP requirement")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_validate_combined_cost_fails_on_dull() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 2, "dull_self": true}
|
||||
var source = _create_mock_card()
|
||||
source.dull() # Already dull
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 5)
|
||||
|
||||
var result = ability_system._validate_ability_cost(cost, source, player)
|
||||
|
||||
assert_false(result.valid, "Should fail on dull requirement")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COST PAYMENT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_pay_empty_cost() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {}
|
||||
var player = MockPlayer.new()
|
||||
|
||||
var success = ability_system._pay_ability_cost(cost, null, player)
|
||||
|
||||
assert_true(success, "Paying empty cost should succeed")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_pay_generic_cp_cost() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 3}
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 5)
|
||||
|
||||
var initial_cp = player.cp_pool.get_total_cp()
|
||||
var success = ability_system._pay_ability_cost(cost, null, player)
|
||||
|
||||
assert_true(success, "Should successfully pay cost")
|
||||
assert_eq(player.cp_pool.get_total_cp(), initial_cp - 3, "Should spend 3 CP")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_pay_specific_element_cp_cost() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 2, "element": "FIRE"}
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 4)
|
||||
player.cp_pool.add_cp(Enums.Element.ICE, 3)
|
||||
|
||||
var success = ability_system._pay_ability_cost(cost, null, player)
|
||||
|
||||
assert_true(success, "Should successfully pay cost")
|
||||
assert_eq(player.cp_pool.get_cp(Enums.Element.FIRE), 2, "Should spend 2 Fire CP")
|
||||
assert_eq(player.cp_pool.get_cp(Enums.Element.ICE), 3, "Ice CP should be unchanged")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_pay_dull_self_cost() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"dull_self": true}
|
||||
var source = _create_mock_card()
|
||||
source.activate()
|
||||
var player = MockPlayer.new()
|
||||
|
||||
assert_true(source.is_active(), "Source should start active")
|
||||
|
||||
var success = ability_system._pay_ability_cost(cost, source, player)
|
||||
|
||||
assert_true(success, "Should successfully pay cost")
|
||||
assert_true(source.is_dull(), "Source should be dulled")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_pay_combined_cost() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var cost = {"cp": 2, "dull_self": true}
|
||||
var source = _create_mock_card()
|
||||
source.activate()
|
||||
var player = MockPlayer.new()
|
||||
player.cp_pool.add_cp(Enums.Element.FIRE, 5)
|
||||
|
||||
var success = ability_system._pay_ability_cost(cost, source, player)
|
||||
|
||||
assert_true(success, "Should successfully pay combined cost")
|
||||
assert_eq(player.cp_pool.get_total_cp(), 3, "Should spend 2 CP")
|
||||
assert_true(source.is_dull(), "Source should be dulled")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SIGNAL TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_ability_cost_failed_signal_declared() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
|
||||
# Check signal exists
|
||||
var signal_exists = ability_system.has_signal("ability_cost_failed")
|
||||
assert_true(signal_exists, "AbilitySystem should have ability_cost_failed signal")
|
||||
|
||||
ability_system.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
func _create_mock_card() -> CardInstance:
|
||||
var card = CardInstance.new()
|
||||
var data = CardDatabase.CardData.new()
|
||||
data.type = Enums.CardType.FORWARD
|
||||
data.power = 5000
|
||||
data.cost = 3
|
||||
data.name = "Test Card"
|
||||
data.elements = [Enums.Element.FIRE]
|
||||
card.card_data = data
|
||||
card.current_power = 5000
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_card_with_name(card_name: String) -> CardInstance:
|
||||
var card = _create_mock_card()
|
||||
card.card_data.name = card_name
|
||||
return card
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MOCK CLASSES
|
||||
# =============================================================================
|
||||
|
||||
class MockPlayer:
|
||||
var cp_pool: CPPool
|
||||
var hand: Zone
|
||||
|
||||
func _init():
|
||||
cp_pool = CPPool.new()
|
||||
hand = Zone.new(Enums.ZoneType.HAND, 0)
|
||||
751
tests/unit/test_effect_resolver.gd
Normal file
751
tests/unit/test_effect_resolver.gd
Normal file
@@ -0,0 +1,751 @@
|
||||
extends GutTest
|
||||
|
||||
## Tests for EffectResolver effect execution
|
||||
## Covers core effects: DAMAGE, POWER_MOD, DULL, ACTIVATE, DRAW, BREAK, RETURN
|
||||
|
||||
|
||||
var resolver: EffectResolver
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
resolver = EffectResolver.new()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DAMAGE EFFECT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_resolve_damage_basic() -> void:
|
||||
var card = _create_mock_forward(7000)
|
||||
var game_state = _create_mock_game_state_with_forward(card, 0)
|
||||
var effect = {"type": "DAMAGE", "amount": 3000}
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_eq(card.damage_received, 3000, "Card should have received 3000 damage")
|
||||
|
||||
|
||||
func test_resolve_damage_breaks_forward() -> void:
|
||||
var card = _create_mock_forward(5000)
|
||||
var game_state = _create_mock_game_state_with_forward(card, 0)
|
||||
var effect = {"type": "DAMAGE", "amount": 5000}
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_true(game_state.players[0].break_zone.has_card(card), "Forward should be in break zone")
|
||||
assert_false(game_state.players[0].field_forwards.has_card(card), "Forward should not be on field")
|
||||
|
||||
|
||||
func test_resolve_damage_exact_power_breaks() -> void:
|
||||
var card = _create_mock_forward(6000)
|
||||
var game_state = _create_mock_game_state_with_forward(card, 0)
|
||||
var effect = {"type": "DAMAGE", "amount": 6000}
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_true(game_state.players[0].break_zone.has_card(card), "Forward should break when damage equals power")
|
||||
|
||||
|
||||
func test_resolve_damage_multiple_targets() -> void:
|
||||
var card1 = _create_mock_forward(8000)
|
||||
var card2 = _create_mock_forward(5000)
|
||||
var game_state = _create_mock_game_state()
|
||||
game_state.players[0].field_forwards.add_card(card1)
|
||||
game_state.players[0].field_forwards.add_card(card2)
|
||||
card1.controller_index = 0
|
||||
card2.controller_index = 0
|
||||
|
||||
var effect = {"type": "DAMAGE", "amount": 3000}
|
||||
|
||||
resolver.resolve(effect, null, [card1, card2], game_state)
|
||||
|
||||
assert_eq(card1.damage_received, 3000, "First card should have 3000 damage")
|
||||
assert_eq(card2.damage_received, 3000, "Second card should have 3000 damage")
|
||||
|
||||
|
||||
func test_resolve_damage_ignores_non_forwards() -> void:
|
||||
var backup = _create_mock_backup()
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "DAMAGE", "amount": 5000}
|
||||
|
||||
resolver.resolve(effect, null, [backup], game_state)
|
||||
|
||||
assert_eq(backup.damage_received, 0, "Backup should not receive damage")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POWER_MOD EFFECT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_resolve_power_mod_positive() -> void:
|
||||
var card = _create_mock_forward(5000)
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "POWER_MOD", "amount": 2000}
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_eq(card.power_modifiers.size(), 1, "Should have one power modifier")
|
||||
assert_eq(card.power_modifiers[0], 2000, "Power modifier should be +2000")
|
||||
|
||||
|
||||
func test_resolve_power_mod_negative() -> void:
|
||||
var card = _create_mock_forward(7000)
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "POWER_MOD", "amount": -3000}
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_eq(card.power_modifiers[0], -3000, "Power modifier should be -3000")
|
||||
|
||||
|
||||
func test_resolve_power_mod_multiple_targets() -> void:
|
||||
var card1 = _create_mock_forward(5000)
|
||||
var card2 = _create_mock_forward(6000)
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "POWER_MOD", "amount": 1000}
|
||||
|
||||
resolver.resolve(effect, null, [card1, card2], game_state)
|
||||
|
||||
assert_eq(card1.power_modifiers[0], 1000, "First card should have +1000")
|
||||
assert_eq(card2.power_modifiers[0], 1000, "Second card should have +1000")
|
||||
|
||||
|
||||
func test_resolve_power_mod_applies_to_source_if_no_targets() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "POWER_MOD", "amount": 3000}
|
||||
|
||||
resolver.resolve(effect, source, [], game_state)
|
||||
|
||||
assert_eq(source.power_modifiers[0], 3000, "Source should get modifier when no targets")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DULL EFFECT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_resolve_dull_single_target() -> void:
|
||||
var card = _create_mock_forward(5000)
|
||||
card.activate() # Ensure card starts active
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "DULL"}
|
||||
|
||||
assert_true(card.is_active(), "Card should start active")
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_true(card.is_dull(), "Card should be dulled")
|
||||
|
||||
|
||||
func test_resolve_dull_multiple_targets() -> void:
|
||||
var card1 = _create_mock_forward(5000)
|
||||
var card2 = _create_mock_forward(6000)
|
||||
card1.activate()
|
||||
card2.activate()
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "DULL"}
|
||||
|
||||
resolver.resolve(effect, null, [card1, card2], game_state)
|
||||
|
||||
assert_true(card1.is_dull(), "First card should be dulled")
|
||||
assert_true(card2.is_dull(), "Second card should be dulled")
|
||||
|
||||
|
||||
func test_resolve_dull_already_dull_card() -> void:
|
||||
var card = _create_mock_forward(5000)
|
||||
card.dull() # Already dull
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "DULL"}
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_true(card.is_dull(), "Card should remain dulled")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ACTIVATE EFFECT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_resolve_activate_single_target() -> void:
|
||||
var card = _create_mock_forward(5000)
|
||||
card.dull() # Start dull
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "ACTIVATE"}
|
||||
|
||||
assert_true(card.is_dull(), "Card should start dull")
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_true(card.is_active(), "Card should be activated")
|
||||
|
||||
|
||||
func test_resolve_activate_multiple_targets() -> void:
|
||||
var card1 = _create_mock_forward(5000)
|
||||
var card2 = _create_mock_forward(6000)
|
||||
card1.dull()
|
||||
card2.dull()
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "ACTIVATE"}
|
||||
|
||||
resolver.resolve(effect, null, [card1, card2], game_state)
|
||||
|
||||
assert_true(card1.is_active(), "First card should be active")
|
||||
assert_true(card2.is_active(), "Second card should be active")
|
||||
|
||||
|
||||
func test_resolve_activate_already_active_card() -> void:
|
||||
var card = _create_mock_forward(5000)
|
||||
card.activate() # Already active
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "ACTIVATE"}
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_true(card.is_active(), "Card should remain active")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DRAW EFFECT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_resolve_draw_single_card() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var game_state = _create_mock_game_state_with_deck(5)
|
||||
var effect = {"type": "DRAW", "amount": 1}
|
||||
|
||||
var initial_deck_size = game_state.players[0].deck.get_count()
|
||||
var initial_hand_size = game_state.players[0].hand.get_count()
|
||||
|
||||
resolver.resolve(effect, source, [], game_state)
|
||||
|
||||
assert_eq(game_state.players[0].deck.get_count(), initial_deck_size - 1, "Deck should have 1 less card")
|
||||
assert_eq(game_state.players[0].hand.get_count(), initial_hand_size + 1, "Hand should have 1 more card")
|
||||
|
||||
|
||||
func test_resolve_draw_multiple_cards() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var game_state = _create_mock_game_state_with_deck(10)
|
||||
var effect = {"type": "DRAW", "amount": 3}
|
||||
|
||||
var initial_deck_size = game_state.players[0].deck.get_count()
|
||||
|
||||
resolver.resolve(effect, source, [], game_state)
|
||||
|
||||
assert_eq(game_state.players[0].deck.get_count(), initial_deck_size - 3, "Deck should have 3 fewer cards")
|
||||
|
||||
|
||||
func test_resolve_draw_opponent() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var game_state = _create_mock_game_state_with_deck(5)
|
||||
# Add cards to opponent's deck too
|
||||
for i in range(5):
|
||||
var deck_card = _create_mock_forward(1000)
|
||||
game_state.players[1].deck.add_card(deck_card)
|
||||
|
||||
var effect = {"type": "DRAW", "amount": 1, "target": {"type": "OPPONENT"}}
|
||||
|
||||
var initial_opp_deck = game_state.players[1].deck.get_count()
|
||||
var initial_opp_hand = game_state.players[1].hand.get_count()
|
||||
|
||||
resolver.resolve(effect, source, [], game_state)
|
||||
|
||||
assert_eq(game_state.players[1].deck.get_count(), initial_opp_deck - 1, "Opponent deck should have 1 less card")
|
||||
assert_eq(game_state.players[1].hand.get_count(), initial_opp_hand + 1, "Opponent hand should have 1 more card")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BREAK EFFECT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_resolve_break_single_target() -> void:
|
||||
var card = _create_mock_forward(7000)
|
||||
var game_state = _create_mock_game_state_with_forward(card, 0)
|
||||
var effect = {"type": "BREAK"}
|
||||
|
||||
assert_true(game_state.players[0].field_forwards.has_card(card), "Card should start on field")
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_false(game_state.players[0].field_forwards.has_card(card), "Card should not be on field")
|
||||
assert_true(game_state.players[0].break_zone.has_card(card), "Card should be in break zone")
|
||||
|
||||
|
||||
func test_resolve_break_multiple_targets() -> void:
|
||||
var card1 = _create_mock_forward(5000)
|
||||
var card2 = _create_mock_forward(6000)
|
||||
var game_state = _create_mock_game_state()
|
||||
game_state.players[0].field_forwards.add_card(card1)
|
||||
game_state.players[0].field_forwards.add_card(card2)
|
||||
card1.controller_index = 0
|
||||
card2.controller_index = 0
|
||||
|
||||
var effect = {"type": "BREAK"}
|
||||
|
||||
resolver.resolve(effect, null, [card1, card2], game_state)
|
||||
|
||||
assert_true(game_state.players[0].break_zone.has_card(card1), "First card should be in break zone")
|
||||
assert_true(game_state.players[0].break_zone.has_card(card2), "Second card should be in break zone")
|
||||
|
||||
|
||||
func test_resolve_break_backup() -> void:
|
||||
var backup = _create_mock_backup()
|
||||
var game_state = _create_mock_game_state()
|
||||
game_state.players[0].field_backups.add_card(backup)
|
||||
backup.controller_index = 0
|
||||
|
||||
var effect = {"type": "BREAK"}
|
||||
|
||||
resolver.resolve(effect, null, [backup], game_state)
|
||||
|
||||
assert_true(game_state.players[0].break_zone.has_card(backup), "Backup should be in break zone")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# RETURN EFFECT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_resolve_return_forward_to_hand() -> void:
|
||||
var card = _create_mock_forward(5000)
|
||||
var game_state = _create_mock_game_state_with_forward(card, 0)
|
||||
var effect = {"type": "RETURN"}
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_false(game_state.players[0].field_forwards.has_card(card), "Card should not be on field")
|
||||
assert_true(game_state.players[0].hand.has_card(card), "Card should be in hand")
|
||||
|
||||
|
||||
func test_resolve_return_backup_to_hand() -> void:
|
||||
var backup = _create_mock_backup()
|
||||
var game_state = _create_mock_game_state()
|
||||
game_state.players[0].field_backups.add_card(backup)
|
||||
backup.controller_index = 0
|
||||
backup.owner_index = 0
|
||||
|
||||
var effect = {"type": "RETURN"}
|
||||
|
||||
resolver.resolve(effect, null, [backup], game_state)
|
||||
|
||||
assert_false(game_state.players[0].field_backups.has_card(backup), "Backup should not be on field")
|
||||
assert_true(game_state.players[0].hand.has_card(backup), "Backup should be in hand")
|
||||
|
||||
|
||||
func test_resolve_return_multiple_targets() -> void:
|
||||
var card1 = _create_mock_forward(5000)
|
||||
var card2 = _create_mock_forward(6000)
|
||||
var game_state = _create_mock_game_state()
|
||||
game_state.players[0].field_forwards.add_card(card1)
|
||||
game_state.players[0].field_forwards.add_card(card2)
|
||||
card1.controller_index = 0
|
||||
card1.owner_index = 0
|
||||
card2.controller_index = 0
|
||||
card2.owner_index = 0
|
||||
|
||||
var effect = {"type": "RETURN"}
|
||||
|
||||
resolver.resolve(effect, null, [card1, card2], game_state)
|
||||
|
||||
assert_true(game_state.players[0].hand.has_card(card1), "First card should be in hand")
|
||||
assert_true(game_state.players[0].hand.has_card(card2), "Second card should be in hand")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SCALING_EFFECT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_resolve_scaling_effect_by_forwards() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var target = _create_mock_forward(10000)
|
||||
target.controller_index = 1
|
||||
|
||||
var game_state = _create_mock_game_state()
|
||||
# Add 3 forwards to controller's field
|
||||
for i in range(3):
|
||||
var forward = _create_mock_forward(4000)
|
||||
forward.controller_index = 0
|
||||
game_state.players[0].field_forwards.add_card(forward)
|
||||
# Add target to opponent's field
|
||||
game_state.players[1].field_forwards.add_card(target)
|
||||
|
||||
var effect = {
|
||||
"type": "SCALING_EFFECT",
|
||||
"base_effect": {"type": "DAMAGE"},
|
||||
"scale_by": "FORWARDS_CONTROLLED",
|
||||
"multiplier": 1000,
|
||||
"scale_filter": {}
|
||||
}
|
||||
|
||||
resolver.resolve(effect, source, [target], game_state)
|
||||
|
||||
assert_eq(target.damage_received, 3000, "Should deal 3000 damage (3 forwards * 1000)")
|
||||
|
||||
|
||||
func test_resolve_scaling_effect_by_backups() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var target = _create_mock_forward(10000)
|
||||
target.controller_index = 1
|
||||
|
||||
var game_state = _create_mock_game_state()
|
||||
# Add 4 backups to controller's field
|
||||
for i in range(4):
|
||||
var backup = _create_mock_backup()
|
||||
backup.controller_index = 0
|
||||
game_state.players[0].field_backups.add_card(backup)
|
||||
game_state.players[1].field_forwards.add_card(target)
|
||||
|
||||
var effect = {
|
||||
"type": "SCALING_EFFECT",
|
||||
"base_effect": {"type": "DAMAGE"},
|
||||
"scale_by": "BACKUPS_CONTROLLED",
|
||||
"multiplier": 500,
|
||||
"scale_filter": {}
|
||||
}
|
||||
|
||||
resolver.resolve(effect, source, [target], game_state)
|
||||
|
||||
assert_eq(target.damage_received, 2000, "Should deal 2000 damage (4 backups * 500)")
|
||||
|
||||
|
||||
func test_resolve_scaling_effect_with_filter() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var target = _create_mock_forward(10000)
|
||||
target.controller_index = 1
|
||||
|
||||
var game_state = _create_mock_game_state()
|
||||
# Add 2 Fire forwards and 2 Ice forwards
|
||||
for i in range(2):
|
||||
var fire_forward = _create_mock_forward_with_element(Enums.Element.FIRE)
|
||||
fire_forward.controller_index = 0
|
||||
game_state.players[0].field_forwards.add_card(fire_forward)
|
||||
for i in range(2):
|
||||
var ice_forward = _create_mock_forward_with_element(Enums.Element.ICE)
|
||||
ice_forward.controller_index = 0
|
||||
game_state.players[0].field_forwards.add_card(ice_forward)
|
||||
game_state.players[1].field_forwards.add_card(target)
|
||||
|
||||
var effect = {
|
||||
"type": "SCALING_EFFECT",
|
||||
"base_effect": {"type": "DAMAGE"},
|
||||
"scale_by": "FORWARDS_CONTROLLED",
|
||||
"multiplier": 2000,
|
||||
"scale_filter": {"element": "FIRE"} # Only count Fire forwards
|
||||
}
|
||||
|
||||
resolver.resolve(effect, source, [target], game_state)
|
||||
|
||||
assert_eq(target.damage_received, 4000, "Should deal 4000 damage (2 Fire forwards * 2000)")
|
||||
|
||||
|
||||
func test_resolve_scaling_effect_zero_count() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var target = _create_mock_forward(10000)
|
||||
target.controller_index = 1
|
||||
|
||||
var game_state = _create_mock_game_state()
|
||||
# No forwards on controller's field
|
||||
game_state.players[1].field_forwards.add_card(target)
|
||||
|
||||
var effect = {
|
||||
"type": "SCALING_EFFECT",
|
||||
"base_effect": {"type": "DAMAGE"},
|
||||
"scale_by": "FORWARDS_CONTROLLED",
|
||||
"multiplier": 1000,
|
||||
"scale_filter": {}
|
||||
}
|
||||
|
||||
resolver.resolve(effect, source, [target], game_state)
|
||||
|
||||
assert_eq(target.damage_received, 0, "Should deal 0 damage (0 forwards * 1000)")
|
||||
|
||||
|
||||
func test_resolve_scaling_effect_power_mod() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
|
||||
var game_state = _create_mock_game_state()
|
||||
# Add 2 backups
|
||||
for i in range(2):
|
||||
var backup = _create_mock_backup()
|
||||
backup.controller_index = 0
|
||||
game_state.players[0].field_backups.add_card(backup)
|
||||
|
||||
var effect = {
|
||||
"type": "SCALING_EFFECT",
|
||||
"base_effect": {"type": "POWER_MOD"},
|
||||
"scale_by": "BACKUPS_CONTROLLED",
|
||||
"multiplier": 1000,
|
||||
"scale_filter": {}
|
||||
}
|
||||
|
||||
resolver.resolve(effect, source, [source], game_state)
|
||||
|
||||
assert_eq(source.power_modifiers[0], 2000, "Should get +2000 power (2 backups * 1000)")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONDITIONAL EFFECT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_resolve_conditional_then_branch() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var target = _create_mock_forward(7000)
|
||||
|
||||
var game_state = _create_mock_game_state()
|
||||
game_state.players[0].damage = 5 # 5 damage points
|
||||
|
||||
# Setup ConditionChecker mock
|
||||
resolver.condition_checker = MockConditionChecker.new(true)
|
||||
|
||||
var effect = {
|
||||
"type": "CONDITIONAL",
|
||||
"condition": {"type": "DAMAGE_RECEIVED", "comparison": "GTE", "value": 3},
|
||||
"then_effects": [{"type": "DAMAGE", "amount": 5000}],
|
||||
"else_effects": []
|
||||
}
|
||||
|
||||
resolver.resolve(effect, source, [target], game_state)
|
||||
|
||||
assert_eq(target.damage_received, 5000, "Should execute then_effects branch")
|
||||
|
||||
|
||||
func test_resolve_conditional_else_branch() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var target = _create_mock_forward(7000)
|
||||
|
||||
var game_state = _create_mock_game_state()
|
||||
game_state.players[0].damage = 1 # Only 1 damage
|
||||
|
||||
# Setup ConditionChecker mock that returns false
|
||||
resolver.condition_checker = MockConditionChecker.new(false)
|
||||
|
||||
var effect = {
|
||||
"type": "CONDITIONAL",
|
||||
"condition": {"type": "DAMAGE_RECEIVED", "comparison": "GTE", "value": 5},
|
||||
"then_effects": [{"type": "DAMAGE", "amount": 5000}],
|
||||
"else_effects": [{"type": "DAMAGE", "amount": 1000}]
|
||||
}
|
||||
|
||||
resolver.resolve(effect, source, [target], game_state)
|
||||
|
||||
assert_eq(target.damage_received, 1000, "Should execute else_effects branch")
|
||||
|
||||
|
||||
func test_resolve_conditional_no_else_branch() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var target = _create_mock_forward(7000)
|
||||
|
||||
var game_state = _create_mock_game_state()
|
||||
|
||||
# Setup ConditionChecker mock that returns false
|
||||
resolver.condition_checker = MockConditionChecker.new(false)
|
||||
|
||||
var effect = {
|
||||
"type": "CONDITIONAL",
|
||||
"condition": {"type": "SOME_CONDITION"},
|
||||
"then_effects": [{"type": "DAMAGE", "amount": 5000}],
|
||||
"else_effects": [] # No else branch
|
||||
}
|
||||
|
||||
resolver.resolve(effect, source, [target], game_state)
|
||||
|
||||
assert_eq(target.damage_received, 0, "Should do nothing when condition false and no else")
|
||||
|
||||
|
||||
func test_resolve_conditional_multiple_then_effects() -> void:
|
||||
var source = _create_mock_forward(5000)
|
||||
source.controller_index = 0
|
||||
var target = _create_mock_forward(7000)
|
||||
|
||||
var game_state = _create_mock_game_state()
|
||||
|
||||
resolver.condition_checker = MockConditionChecker.new(true)
|
||||
|
||||
var effect = {
|
||||
"type": "CONDITIONAL",
|
||||
"condition": {"type": "ALWAYS_TRUE"},
|
||||
"then_effects": [
|
||||
{"type": "DAMAGE", "amount": 2000},
|
||||
{"type": "POWER_MOD", "amount": -1000}
|
||||
],
|
||||
"else_effects": []
|
||||
}
|
||||
|
||||
resolver.resolve(effect, source, [target], game_state)
|
||||
|
||||
assert_eq(target.damage_received, 2000, "Should apply damage")
|
||||
assert_eq(target.power_modifiers[0], -1000, "Should apply power mod")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EFFECT_COMPLETED SIGNAL TEST
|
||||
# =============================================================================
|
||||
|
||||
func test_effect_completed_signal_emitted() -> void:
|
||||
var card = _create_mock_forward(5000)
|
||||
var game_state = _create_mock_game_state()
|
||||
var effect = {"type": "DULL"}
|
||||
var signal_received = false
|
||||
var received_effect = {}
|
||||
var received_targets = []
|
||||
|
||||
resolver.effect_completed.connect(func(e, t):
|
||||
signal_received = true
|
||||
received_effect = e
|
||||
received_targets = t
|
||||
)
|
||||
|
||||
resolver.resolve(effect, null, [card], game_state)
|
||||
|
||||
assert_true(signal_received, "effect_completed signal should be emitted")
|
||||
assert_eq(received_effect.type, "DULL", "Signal should contain effect")
|
||||
assert_eq(received_targets.size(), 1, "Signal should contain targets")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COMBINED DAMAGE AND BREAK TEST
|
||||
# =============================================================================
|
||||
|
||||
func test_damage_accumulates_before_break() -> void:
|
||||
var card = _create_mock_forward(10000)
|
||||
var game_state = _create_mock_game_state_with_forward(card, 0)
|
||||
|
||||
# Apply 4000 damage (shouldn't break)
|
||||
resolver.resolve({"type": "DAMAGE", "amount": 4000}, null, [card], game_state)
|
||||
assert_eq(card.damage_received, 4000, "Should have 4000 damage")
|
||||
assert_true(game_state.players[0].field_forwards.has_card(card), "Should still be on field")
|
||||
|
||||
# Apply 6000 more (total 10000, should break)
|
||||
resolver.resolve({"type": "DAMAGE", "amount": 6000}, null, [card], game_state)
|
||||
assert_true(game_state.players[0].break_zone.has_card(card), "Should be broken with 10000 total damage")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
func _create_mock_forward(power: int) -> CardInstance:
|
||||
var card = CardInstance.new()
|
||||
var data = CardDatabase.CardData.new()
|
||||
data.type = Enums.CardType.FORWARD
|
||||
data.power = power
|
||||
data.cost = 3
|
||||
data.name = "Test Forward"
|
||||
data.elements = [Enums.Element.FIRE]
|
||||
card.card_data = data
|
||||
card.current_power = power
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_backup() -> CardInstance:
|
||||
var card = CardInstance.new()
|
||||
var data = CardDatabase.CardData.new()
|
||||
data.type = Enums.CardType.BACKUP
|
||||
data.cost = 2
|
||||
data.name = "Test Backup"
|
||||
data.elements = [Enums.Element.WATER]
|
||||
card.card_data = data
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_element(element: Enums.Element) -> CardInstance:
|
||||
var card = _create_mock_forward(5000)
|
||||
card.card_data.elements = [element]
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_game_state() -> MockGameState:
|
||||
return MockGameState.new()
|
||||
|
||||
|
||||
func _create_mock_game_state_with_forward(card: CardInstance, player_index: int) -> MockGameState:
|
||||
var gs = MockGameState.new()
|
||||
gs.players[player_index].field_forwards.add_card(card)
|
||||
card.controller_index = player_index
|
||||
card.owner_index = player_index
|
||||
return gs
|
||||
|
||||
|
||||
func _create_mock_game_state_with_deck(card_count: int) -> MockGameState:
|
||||
var gs = MockGameState.new()
|
||||
for i in range(card_count):
|
||||
var deck_card = _create_mock_forward(1000 + i * 1000)
|
||||
gs.players[0].deck.add_card(deck_card)
|
||||
return gs
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MOCK CLASSES
|
||||
# =============================================================================
|
||||
|
||||
class MockGameState:
|
||||
var players: Array
|
||||
|
||||
func _init():
|
||||
players = [MockPlayer.new(0), MockPlayer.new(1)]
|
||||
|
||||
func get_player(index: int):
|
||||
if index >= 0 and index < players.size():
|
||||
return players[index]
|
||||
return null
|
||||
|
||||
|
||||
class MockPlayer:
|
||||
var player_index: int
|
||||
var damage: int = 0
|
||||
var deck: Zone
|
||||
var hand: Zone
|
||||
var field_forwards: Zone
|
||||
var field_backups: Zone
|
||||
var break_zone: Zone
|
||||
|
||||
func _init(index: int = 0):
|
||||
player_index = index
|
||||
deck = Zone.new(Enums.ZoneType.DECK, index)
|
||||
hand = Zone.new(Enums.ZoneType.HAND, index)
|
||||
field_forwards = Zone.new(Enums.ZoneType.FIELD, index)
|
||||
field_backups = Zone.new(Enums.ZoneType.FIELD, index)
|
||||
break_zone = Zone.new(Enums.ZoneType.BREAK_ZONE, index)
|
||||
|
||||
func draw_cards(count: int) -> Array:
|
||||
var drawn: Array = []
|
||||
for i in range(count):
|
||||
var card = deck.pop_top_card()
|
||||
if card:
|
||||
hand.add_card(card)
|
||||
drawn.append(card)
|
||||
return drawn
|
||||
|
||||
func break_card(card: CardInstance) -> bool:
|
||||
var removed = false
|
||||
|
||||
if field_forwards.has_card(card):
|
||||
field_forwards.remove_card(card)
|
||||
removed = true
|
||||
elif field_backups.has_card(card):
|
||||
field_backups.remove_card(card)
|
||||
removed = true
|
||||
|
||||
if removed:
|
||||
break_zone.add_card(card)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
class MockConditionChecker:
|
||||
var _return_value: bool
|
||||
|
||||
func _init(return_value: bool = true):
|
||||
_return_value = return_value
|
||||
|
||||
func evaluate(_condition: Dictionary, _context: Dictionary) -> bool:
|
||||
return _return_value
|
||||
306
tests/unit/test_filtered_scaling.gd
Normal file
306
tests/unit/test_filtered_scaling.gd
Normal file
@@ -0,0 +1,306 @@
|
||||
extends GutTest
|
||||
|
||||
## Tests for filtered scaling in EffectResolver
|
||||
## Tests element, job, category, cost, and card name filters
|
||||
|
||||
# =============================================================================
|
||||
# SETUP
|
||||
# =============================================================================
|
||||
|
||||
var resolver: EffectResolver
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
resolver = EffectResolver.new()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FILTER MATCHING TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_element_filter_matches_fire() -> void:
|
||||
var filter = {"element": "FIRE"}
|
||||
var card = _create_mock_forward_with_element(Enums.Element.FIRE)
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Fire card should match FIRE filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_element_filter_rejects_wrong_element() -> void:
|
||||
var filter = {"element": "FIRE"}
|
||||
var card = _create_mock_forward_with_element(Enums.Element.ICE)
|
||||
|
||||
assert_false(resolver._card_matches_scale_filter(card, filter), "Ice card should not match FIRE filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_job_filter_matches_samurai() -> void:
|
||||
var filter = {"job": "Samurai"}
|
||||
var card = _create_mock_forward_with_job("Samurai")
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Samurai should match Samurai filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_job_filter_rejects_wrong_job() -> void:
|
||||
var filter = {"job": "Samurai"}
|
||||
var card = _create_mock_forward_with_job("Warrior")
|
||||
|
||||
assert_false(resolver._card_matches_scale_filter(card, filter), "Warrior should not match Samurai filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_job_filter_case_insensitive() -> void:
|
||||
var filter = {"job": "SAMURAI"}
|
||||
var card = _create_mock_forward_with_job("samurai")
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Job filter should be case insensitive")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_category_filter_matches() -> void:
|
||||
var filter = {"category": "VII"}
|
||||
var card = _create_mock_forward_with_category("VII")
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "VII card should match VII filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_category_filter_rejects_wrong_category() -> void:
|
||||
var filter = {"category": "VII"}
|
||||
var card = _create_mock_forward_with_category("X")
|
||||
|
||||
assert_false(resolver._card_matches_scale_filter(card, filter), "Category X should not match VII filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_exact_filter_matches() -> void:
|
||||
var filter = {"cost": 3}
|
||||
var card = _create_mock_forward_with_cost(3)
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Cost 3 should match cost 3 filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_exact_filter_rejects_wrong_cost() -> void:
|
||||
var filter = {"cost": 3}
|
||||
var card = _create_mock_forward_with_cost(5)
|
||||
|
||||
assert_false(resolver._card_matches_scale_filter(card, filter), "Cost 5 should not match cost 3 filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_lte_filter_matches_lower() -> void:
|
||||
var filter = {"cost_comparison": "LTE", "cost_value": 3}
|
||||
var card = _create_mock_forward_with_cost(2)
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Cost 2 should match cost <= 3")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_lte_filter_matches_equal() -> void:
|
||||
var filter = {"cost_comparison": "LTE", "cost_value": 3}
|
||||
var card = _create_mock_forward_with_cost(3)
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Cost 3 should match cost <= 3")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_lte_filter_rejects_higher() -> void:
|
||||
var filter = {"cost_comparison": "LTE", "cost_value": 3}
|
||||
var card = _create_mock_forward_with_cost(5)
|
||||
|
||||
assert_false(resolver._card_matches_scale_filter(card, filter), "Cost 5 should not match cost <= 3")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_gte_filter_matches_higher() -> void:
|
||||
var filter = {"cost_comparison": "GTE", "cost_value": 3}
|
||||
var card = _create_mock_forward_with_cost(5)
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Cost 5 should match cost >= 3")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_cost_gte_filter_rejects_lower() -> void:
|
||||
var filter = {"cost_comparison": "GTE", "cost_value": 3}
|
||||
var card = _create_mock_forward_with_cost(2)
|
||||
|
||||
assert_false(resolver._card_matches_scale_filter(card, filter), "Cost 2 should not match cost >= 3")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_card_name_filter_matches() -> void:
|
||||
var filter = {"card_name": "Cloud"}
|
||||
var card = _create_mock_forward_with_name("Cloud")
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Cloud should match Cloud filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_card_name_filter_rejects_wrong_name() -> void:
|
||||
var filter = {"card_name": "Cloud"}
|
||||
var card = _create_mock_forward_with_name("Tifa")
|
||||
|
||||
assert_false(resolver._card_matches_scale_filter(card, filter), "Tifa should not match Cloud filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_card_type_filter_forward() -> void:
|
||||
var filter = {"card_type": "FORWARD"}
|
||||
var card = _create_mock_forward()
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Forward should match FORWARD filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_card_type_filter_backup() -> void:
|
||||
var filter = {"card_type": "BACKUP"}
|
||||
var card = _create_mock_backup()
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Backup should match BACKUP filter")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_card_type_filter_rejects_wrong_type() -> void:
|
||||
var filter = {"card_type": "BACKUP"}
|
||||
var card = _create_mock_forward()
|
||||
|
||||
assert_false(resolver._card_matches_scale_filter(card, filter), "Forward should not match BACKUP filter")
|
||||
card.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COMBINED FILTER TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_combined_element_and_type_filter() -> void:
|
||||
var filter = {"element": "FIRE", "card_type": "FORWARD"}
|
||||
|
||||
var fire_forward = _create_mock_forward_with_element(Enums.Element.FIRE)
|
||||
var fire_backup = _create_mock_backup_with_element(Enums.Element.FIRE)
|
||||
var ice_forward = _create_mock_forward_with_element(Enums.Element.ICE)
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(fire_forward, filter), "Fire Forward should match")
|
||||
assert_false(resolver._card_matches_scale_filter(fire_backup, filter), "Fire Backup should not match")
|
||||
assert_false(resolver._card_matches_scale_filter(ice_forward, filter), "Ice Forward should not match")
|
||||
|
||||
fire_forward.free()
|
||||
fire_backup.free()
|
||||
ice_forward.free()
|
||||
|
||||
|
||||
func test_combined_job_and_cost_filter() -> void:
|
||||
var filter = {"job": "Warrior", "cost_comparison": "LTE", "cost_value": 3}
|
||||
|
||||
var cheap_warrior = _create_mock_forward_with_job_and_cost("Warrior", 2)
|
||||
var expensive_warrior = _create_mock_forward_with_job_and_cost("Warrior", 5)
|
||||
var cheap_mage = _create_mock_forward_with_job_and_cost("Mage", 2)
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(cheap_warrior, filter), "Cheap Warrior should match")
|
||||
assert_false(resolver._card_matches_scale_filter(expensive_warrior, filter), "Expensive Warrior should not match")
|
||||
assert_false(resolver._card_matches_scale_filter(cheap_mage, filter), "Cheap Mage should not match")
|
||||
|
||||
cheap_warrior.free()
|
||||
expensive_warrior.free()
|
||||
cheap_mage.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EMPTY/NULL FILTER TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_empty_filter_matches_all() -> void:
|
||||
var filter = {}
|
||||
var card = _create_mock_forward()
|
||||
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Empty filter should match any card")
|
||||
card.free()
|
||||
|
||||
|
||||
func test_null_card_returns_false() -> void:
|
||||
var filter = {"element": "FIRE"}
|
||||
|
||||
assert_false(resolver._card_matches_scale_filter(null, filter), "Null card should return false")
|
||||
|
||||
|
||||
func test_owner_only_filter_treated_as_empty() -> void:
|
||||
# Owner is used for collection selection, not card matching
|
||||
var filter = {"owner": "CONTROLLER"}
|
||||
var card = _create_mock_forward()
|
||||
|
||||
# The filter with only owner should effectively be empty for card matching
|
||||
assert_true(resolver._card_matches_scale_filter(card, filter), "Owner-only filter should match any card")
|
||||
card.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
func _create_mock_forward() -> CardInstance:
|
||||
var card = CardInstance.new()
|
||||
var data = CardDatabase.CardData.new()
|
||||
data.type = Enums.CardType.FORWARD
|
||||
data.cost = 3
|
||||
data.power = 7000
|
||||
data.name = "Test Forward"
|
||||
data.job = "Warrior"
|
||||
data.elements = [Enums.Element.FIRE]
|
||||
card.card_data = data
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_backup() -> CardInstance:
|
||||
var card = CardInstance.new()
|
||||
var data = CardDatabase.CardData.new()
|
||||
data.type = Enums.CardType.BACKUP
|
||||
data.cost = 2
|
||||
data.name = "Test Backup"
|
||||
data.job = "Scholar"
|
||||
data.elements = [Enums.Element.WATER]
|
||||
card.card_data = data
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_element(element: Enums.Element) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.elements = [element]
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_backup_with_element(element: Enums.Element) -> CardInstance:
|
||||
var card = _create_mock_backup()
|
||||
card.card_data.elements = [element]
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_job(job: String) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.job = job
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_category(category: String) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.category = category
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_cost(cost: int) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.cost = cost
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_name(card_name: String) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.name = card_name
|
||||
return card
|
||||
|
||||
|
||||
func _create_mock_forward_with_job_and_cost(job: String, cost: int) -> CardInstance:
|
||||
var card = _create_mock_forward()
|
||||
card.card_data.job = job
|
||||
card.card_data.cost = cost
|
||||
return card
|
||||
363
tests/unit/test_modal_choice_system.gd
Normal file
363
tests/unit/test_modal_choice_system.gd
Normal file
@@ -0,0 +1,363 @@
|
||||
extends GutTest
|
||||
|
||||
## Tests for the Modal Choice System (CHOOSE_MODE effects)
|
||||
## Tests parser output, ChoiceModal UI, and AbilitySystem integration
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PARSER OUTPUT TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_modal_effect_has_correct_structure() -> void:
|
||||
# Simulate a parsed CHOOSE_MODE effect
|
||||
var effect = {
|
||||
"type": "CHOOSE_MODE",
|
||||
"select_count": 1,
|
||||
"select_up_to": false,
|
||||
"mode_count": 3,
|
||||
"modes": [
|
||||
{
|
||||
"index": 0,
|
||||
"description": "Choose 1 Forward. Deal it 7000 damage.",
|
||||
"effects": [{"type": "DAMAGE", "amount": 7000}]
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"description": "Choose 1 Monster. Break it.",
|
||||
"effects": [{"type": "BREAK"}]
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"description": "Deal 3000 damage to all Forwards.",
|
||||
"effects": [{"type": "DAMAGE", "amount": 3000}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert_eq(effect.type, "CHOOSE_MODE", "Effect type should be CHOOSE_MODE")
|
||||
assert_eq(effect.select_count, 1, "Should select 1 mode")
|
||||
assert_eq(effect.mode_count, 3, "Should have 3 modes")
|
||||
assert_eq(effect.modes.size(), 3, "Modes array should have 3 entries")
|
||||
assert_false(effect.select_up_to, "Should not be 'up to' selection")
|
||||
|
||||
|
||||
func test_modal_effect_with_enhanced_condition() -> void:
|
||||
var effect = {
|
||||
"type": "CHOOSE_MODE",
|
||||
"select_count": 1,
|
||||
"select_up_to": false,
|
||||
"mode_count": 3,
|
||||
"modes": [],
|
||||
"enhanced_condition": {
|
||||
"description": "if you have 5 or more ifrit in your break zone",
|
||||
"select_count": 3,
|
||||
"select_up_to": true
|
||||
}
|
||||
}
|
||||
|
||||
assert_true(effect.has("enhanced_condition"), "Should have enhanced condition")
|
||||
assert_eq(effect.enhanced_condition.select_count, 3, "Enhanced should allow 3 selections")
|
||||
assert_true(effect.enhanced_condition.select_up_to, "Enhanced should be 'up to'")
|
||||
|
||||
|
||||
func test_modal_mode_has_description_and_effects() -> void:
|
||||
var mode = {
|
||||
"index": 0,
|
||||
"description": "Draw 2 cards.",
|
||||
"effects": [{"type": "DRAW", "amount": 2}]
|
||||
}
|
||||
|
||||
assert_true(mode.has("description"), "Mode should have description")
|
||||
assert_true(mode.has("effects"), "Mode should have effects array")
|
||||
assert_eq(mode.effects.size(), 1, "Mode should have 1 effect")
|
||||
assert_eq(mode.effects[0].type, "DRAW", "Effect should be DRAW")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CHOICE MODAL TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_choice_modal_instantiates() -> void:
|
||||
var modal = ChoiceModal.new()
|
||||
assert_not_null(modal, "ChoiceModal should instantiate")
|
||||
modal.free()
|
||||
|
||||
|
||||
func test_choice_modal_starts_invisible() -> void:
|
||||
var modal = ChoiceModal.new()
|
||||
add_child_autofree(modal)
|
||||
|
||||
# Need to wait for _ready to complete
|
||||
await get_tree().process_frame
|
||||
|
||||
assert_false(modal.visible, "ChoiceModal should start invisible")
|
||||
|
||||
|
||||
func test_choice_modal_has_correct_layer() -> void:
|
||||
var modal = ChoiceModal.new()
|
||||
add_child_autofree(modal)
|
||||
|
||||
await get_tree().process_frame
|
||||
|
||||
assert_eq(modal.layer, 200, "ChoiceModal should be at layer 200")
|
||||
|
||||
|
||||
func test_choice_modal_signals_exist() -> void:
|
||||
var modal = ChoiceModal.new()
|
||||
|
||||
assert_true(modal.has_signal("choice_made"), "Should have choice_made signal")
|
||||
assert_true(modal.has_signal("choice_cancelled"), "Should have choice_cancelled signal")
|
||||
|
||||
modal.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EFFECT RESOLVER TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_effect_resolver_has_choice_required_signal() -> void:
|
||||
var resolver = EffectResolver.new()
|
||||
|
||||
assert_true(resolver.has_signal("choice_required"), "Should have choice_required signal")
|
||||
|
||||
|
||||
func test_effect_resolver_emits_choice_required_for_choose_mode() -> void:
|
||||
var resolver = EffectResolver.new()
|
||||
var signal_received = false
|
||||
var received_effect = {}
|
||||
var received_modes = []
|
||||
|
||||
resolver.choice_required.connect(func(effect, modes):
|
||||
signal_received = true
|
||||
received_effect = effect
|
||||
received_modes = modes
|
||||
)
|
||||
|
||||
var effect = {
|
||||
"type": "CHOOSE_MODE",
|
||||
"modes": [
|
||||
{"index": 0, "description": "Option 1", "effects": []},
|
||||
{"index": 1, "description": "Option 2", "effects": []}
|
||||
]
|
||||
}
|
||||
|
||||
# Resolve the effect
|
||||
resolver.resolve(effect, null, [], null)
|
||||
|
||||
assert_true(signal_received, "Should emit choice_required signal")
|
||||
assert_eq(received_modes.size(), 2, "Should pass modes to signal")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FIELD EFFECT MANAGER TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_field_effect_manager_block_immunity_empty() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
|
||||
# With no abilities registered, should return false
|
||||
var result = manager.has_block_immunity(null, null, null)
|
||||
assert_false(result, "Should return false with no abilities")
|
||||
|
||||
|
||||
func test_field_effect_manager_attack_restriction_empty() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
|
||||
var result = manager.has_attack_restriction(null, null)
|
||||
assert_false(result, "Should return false with no abilities")
|
||||
|
||||
|
||||
func test_field_effect_manager_block_restriction_empty() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
|
||||
var result = manager.has_block_restriction(null, null)
|
||||
assert_false(result, "Should return false with no abilities")
|
||||
|
||||
|
||||
func test_field_effect_manager_taunt_targets_empty() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
|
||||
var result = manager.get_taunt_targets(0, null)
|
||||
assert_eq(result.size(), 0, "Should return empty array with no abilities")
|
||||
|
||||
|
||||
func test_field_effect_manager_cost_modifier_empty() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
|
||||
var result = manager.get_cost_modifier(null, 0, null)
|
||||
assert_eq(result, 0, "Should return 0 with no abilities")
|
||||
|
||||
|
||||
func test_field_effect_manager_max_attacks_default() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
|
||||
var result = manager.get_max_attacks(null, null)
|
||||
assert_eq(result, 1, "Default max attacks should be 1")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BLOCKER IMMUNITY CONDITION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_blocker_immunity_condition_empty() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
|
||||
# Empty condition means unconditional immunity
|
||||
var result = manager._blocker_matches_immunity_condition(null, {}, null)
|
||||
assert_true(result, "Empty condition should return true (unconditional)")
|
||||
|
||||
|
||||
func test_blocker_immunity_condition_cost_gte() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
|
||||
# Create mock card data
|
||||
var blocker = _create_mock_card(3, 5000) # cost 3, power 5000
|
||||
|
||||
var condition = {
|
||||
"comparison": "GTE",
|
||||
"attribute": "cost",
|
||||
"value": 4
|
||||
}
|
||||
|
||||
var result = manager._blocker_matches_immunity_condition(blocker, condition, null)
|
||||
assert_false(result, "Cost 3 is not >= 4")
|
||||
|
||||
blocker.card_data.cost = 4
|
||||
result = manager._blocker_matches_immunity_condition(blocker, condition, null)
|
||||
assert_true(result, "Cost 4 is >= 4")
|
||||
|
||||
blocker.free()
|
||||
|
||||
|
||||
func test_blocker_immunity_condition_power_lt_self() -> void:
|
||||
var manager = FieldEffectManager.new()
|
||||
|
||||
var blocker = _create_mock_card(3, 5000)
|
||||
var attacker = _create_mock_card(4, 8000)
|
||||
|
||||
var condition = {
|
||||
"comparison": "LT",
|
||||
"attribute": "power",
|
||||
"compare_to": "SELF_POWER"
|
||||
}
|
||||
|
||||
# Blocker power 5000 < attacker power 8000
|
||||
var result = manager._blocker_matches_immunity_condition(blocker, condition, attacker)
|
||||
assert_true(result, "5000 < 8000 should be true")
|
||||
|
||||
# Change blocker power to be higher
|
||||
blocker.current_power = 9000
|
||||
result = manager._blocker_matches_immunity_condition(blocker, condition, attacker)
|
||||
assert_false(result, "9000 < 8000 should be false")
|
||||
|
||||
blocker.free()
|
||||
attacker.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ABILITY SYSTEM INTEGRATION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_ability_system_has_choice_modal_reference() -> void:
|
||||
# Check that AbilitySystem has the choice_modal variable
|
||||
var ability_system_script = load("res://scripts/game/abilities/AbilitySystem.gd")
|
||||
assert_not_null(ability_system_script, "AbilitySystem script should exist")
|
||||
|
||||
|
||||
func test_modes_selected_queues_effects() -> void:
|
||||
# This tests the logic of _on_modes_selected indirectly
|
||||
var effect = {
|
||||
"type": "CHOOSE_MODE",
|
||||
"modes": [
|
||||
{"index": 0, "description": "Deal damage", "effects": [{"type": "DAMAGE", "amount": 5000}]},
|
||||
{"index": 1, "description": "Draw cards", "effects": [{"type": "DRAW", "amount": 2}]}
|
||||
]
|
||||
}
|
||||
|
||||
var modes = effect.modes
|
||||
var selected_indices = [1] # Player selected option 2 (draw)
|
||||
|
||||
# Verify mode selection logic
|
||||
var queued_effects = []
|
||||
for index in selected_indices:
|
||||
if index >= 0 and index < modes.size():
|
||||
var mode = modes[index]
|
||||
for mode_effect in mode.get("effects", []):
|
||||
queued_effects.append(mode_effect)
|
||||
|
||||
assert_eq(queued_effects.size(), 1, "Should queue 1 effect")
|
||||
assert_eq(queued_effects[0].type, "DRAW", "Queued effect should be DRAW")
|
||||
assert_eq(queued_effects[0].amount, 2, "Draw amount should be 2")
|
||||
|
||||
|
||||
func test_multi_select_queues_multiple_effects() -> void:
|
||||
var effect = {
|
||||
"type": "CHOOSE_MODE",
|
||||
"select_count": 2,
|
||||
"modes": [
|
||||
{"index": 0, "effects": [{"type": "DAMAGE", "amount": 5000}]},
|
||||
{"index": 1, "effects": [{"type": "DRAW", "amount": 2}]},
|
||||
{"index": 2, "effects": [{"type": "BREAK"}]}
|
||||
]
|
||||
}
|
||||
|
||||
var modes = effect.modes
|
||||
var selected_indices = [0, 2] # Player selected damage and break
|
||||
|
||||
var queued_effects = []
|
||||
for index in selected_indices:
|
||||
if index >= 0 and index < modes.size():
|
||||
var mode = modes[index]
|
||||
for mode_effect in mode.get("effects", []):
|
||||
queued_effects.append(mode_effect)
|
||||
|
||||
assert_eq(queued_effects.size(), 2, "Should queue 2 effects")
|
||||
assert_eq(queued_effects[0].type, "DAMAGE", "First effect should be DAMAGE")
|
||||
assert_eq(queued_effects[1].type, "BREAK", "Second effect should be BREAK")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ENHANCED CONDITION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_enhanced_condition_parsing() -> void:
|
||||
var condition = {
|
||||
"description": "if you have a total of 5 or more card name ifrita and/or card name ifrit in your break zone",
|
||||
"select_count": 3,
|
||||
"select_up_to": true
|
||||
}
|
||||
|
||||
# Test that description contains expected keywords
|
||||
assert_true("break zone" in condition.description, "Should mention break zone")
|
||||
assert_true("5 or more" in condition.description, "Should mention count requirement")
|
||||
|
||||
|
||||
func test_regex_count_extraction() -> void:
|
||||
var description = "if you have 5 or more ifrit in your break zone"
|
||||
|
||||
var regex = RegEx.new()
|
||||
regex.compile(r"(\d+) or more")
|
||||
var match_result = regex.search(description)
|
||||
|
||||
assert_not_null(match_result, "Should find count pattern")
|
||||
assert_eq(match_result.get_string(1), "5", "Should extract '5'")
|
||||
assert_eq(int(match_result.get_string(1)), 5, "Should convert to int 5")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
func _create_mock_card(cost: int, power: int) -> CardInstance:
|
||||
var card = CardInstance.new()
|
||||
|
||||
# Create minimal card data
|
||||
var data = CardDatabase.CardData.new()
|
||||
data.cost = cost
|
||||
data.power = power
|
||||
data.type = Enums.CardType.FORWARD
|
||||
|
||||
card.card_data = data
|
||||
card.current_power = power
|
||||
|
||||
return card
|
||||
293
tests/unit/test_network_signals.gd
Normal file
293
tests/unit/test_network_signals.gd
Normal file
@@ -0,0 +1,293 @@
|
||||
extends GutTest
|
||||
|
||||
## Unit tests for NetworkManager signal emissions
|
||||
## Tests signal behavior without requiring actual network connections
|
||||
|
||||
|
||||
var network_manager: NetworkManager
|
||||
var signal_received: bool = false
|
||||
var last_signal_data: Variant = null
|
||||
|
||||
|
||||
func before_each():
|
||||
# Create a fresh NetworkManager instance for testing
|
||||
network_manager = NetworkManager.new()
|
||||
add_child_autoqfree(network_manager)
|
||||
signal_received = false
|
||||
last_signal_data = null
|
||||
|
||||
|
||||
func after_each():
|
||||
# Cleanup handled by autoqfree
|
||||
pass
|
||||
|
||||
|
||||
# ======= HELPER FUNCTIONS =======
|
||||
|
||||
func _connect_signal_and_track(signal_obj: Signal) -> void:
|
||||
signal_obj.connect(_on_signal_received)
|
||||
|
||||
|
||||
func _on_signal_received(data = null) -> void:
|
||||
signal_received = true
|
||||
last_signal_data = data
|
||||
|
||||
|
||||
# ======= CONNECTION STATE SIGNAL TESTS =======
|
||||
|
||||
func test_connection_state_changed_signal_exists():
|
||||
assert_has_signal(network_manager, "connection_state_changed")
|
||||
|
||||
|
||||
func test_authenticated_signal_exists():
|
||||
assert_has_signal(network_manager, "authenticated")
|
||||
|
||||
|
||||
func test_authentication_failed_signal_exists():
|
||||
assert_has_signal(network_manager, "authentication_failed")
|
||||
|
||||
|
||||
func test_logged_out_signal_exists():
|
||||
assert_has_signal(network_manager, "logged_out")
|
||||
|
||||
|
||||
func test_network_error_signal_exists():
|
||||
assert_has_signal(network_manager, "network_error")
|
||||
|
||||
|
||||
# ======= MATCHMAKING SIGNAL TESTS =======
|
||||
|
||||
func test_queue_joined_signal_exists():
|
||||
assert_has_signal(network_manager, "queue_joined")
|
||||
|
||||
|
||||
func test_queue_left_signal_exists():
|
||||
assert_has_signal(network_manager, "queue_left")
|
||||
|
||||
|
||||
func test_match_found_signal_exists():
|
||||
assert_has_signal(network_manager, "match_found")
|
||||
|
||||
|
||||
func test_room_created_signal_exists():
|
||||
assert_has_signal(network_manager, "room_created")
|
||||
|
||||
|
||||
func test_room_joined_signal_exists():
|
||||
assert_has_signal(network_manager, "room_joined")
|
||||
|
||||
|
||||
func test_room_updated_signal_exists():
|
||||
assert_has_signal(network_manager, "room_updated")
|
||||
|
||||
|
||||
func test_matchmaking_update_signal_exists():
|
||||
assert_has_signal(network_manager, "matchmaking_update")
|
||||
|
||||
|
||||
# ======= GAME MESSAGE SIGNAL TESTS =======
|
||||
|
||||
func test_opponent_action_received_signal_exists():
|
||||
assert_has_signal(network_manager, "opponent_action_received")
|
||||
|
||||
|
||||
func test_turn_timer_update_signal_exists():
|
||||
assert_has_signal(network_manager, "turn_timer_update")
|
||||
|
||||
|
||||
func test_game_started_signal_exists():
|
||||
assert_has_signal(network_manager, "game_started")
|
||||
|
||||
|
||||
func test_game_ended_signal_exists():
|
||||
assert_has_signal(network_manager, "game_ended")
|
||||
|
||||
|
||||
func test_phase_changed_signal_exists():
|
||||
assert_has_signal(network_manager, "phase_changed")
|
||||
|
||||
|
||||
func test_action_confirmed_signal_exists():
|
||||
assert_has_signal(network_manager, "action_confirmed")
|
||||
|
||||
|
||||
func test_action_failed_signal_exists():
|
||||
assert_has_signal(network_manager, "action_failed")
|
||||
|
||||
|
||||
func test_opponent_disconnected_signal_exists():
|
||||
assert_has_signal(network_manager, "opponent_disconnected")
|
||||
|
||||
|
||||
func test_opponent_reconnected_signal_exists():
|
||||
assert_has_signal(network_manager, "opponent_reconnected")
|
||||
|
||||
|
||||
func test_game_state_sync_signal_exists():
|
||||
assert_has_signal(network_manager, "game_state_sync")
|
||||
|
||||
|
||||
# ======= CONNECTION STATE ENUM TESTS =======
|
||||
|
||||
func test_connection_state_disconnected_value():
|
||||
assert_eq(NetworkManager.ConnectionState.DISCONNECTED, 0)
|
||||
|
||||
|
||||
func test_connection_state_connecting_value():
|
||||
assert_eq(NetworkManager.ConnectionState.CONNECTING, 1)
|
||||
|
||||
|
||||
func test_connection_state_connected_value():
|
||||
assert_eq(NetworkManager.ConnectionState.CONNECTED, 2)
|
||||
|
||||
|
||||
func test_connection_state_authenticating_value():
|
||||
assert_eq(NetworkManager.ConnectionState.AUTHENTICATING, 3)
|
||||
|
||||
|
||||
func test_connection_state_authenticated_value():
|
||||
assert_eq(NetworkManager.ConnectionState.AUTHENTICATED, 4)
|
||||
|
||||
|
||||
func test_connection_state_in_queue_value():
|
||||
assert_eq(NetworkManager.ConnectionState.IN_QUEUE, 5)
|
||||
|
||||
|
||||
func test_connection_state_in_room_value():
|
||||
assert_eq(NetworkManager.ConnectionState.IN_ROOM, 6)
|
||||
|
||||
|
||||
func test_connection_state_in_game_value():
|
||||
assert_eq(NetworkManager.ConnectionState.IN_GAME, 7)
|
||||
|
||||
|
||||
# ======= INITIAL STATE TESTS =======
|
||||
|
||||
func test_initial_connection_state_is_disconnected():
|
||||
assert_eq(network_manager.connection_state, NetworkManager.ConnectionState.DISCONNECTED)
|
||||
|
||||
|
||||
func test_initial_auth_token_is_empty():
|
||||
assert_eq(network_manager.auth_token, "")
|
||||
|
||||
|
||||
func test_initial_is_authenticated_is_false():
|
||||
assert_false(network_manager.is_authenticated)
|
||||
|
||||
|
||||
func test_initial_current_game_id_is_empty():
|
||||
assert_eq(network_manager.current_game_id, "")
|
||||
|
||||
|
||||
func test_initial_current_room_code_is_empty():
|
||||
assert_eq(network_manager.current_room_code, "")
|
||||
|
||||
|
||||
func test_initial_local_player_index_is_zero():
|
||||
assert_eq(network_manager.local_player_index, 0)
|
||||
|
||||
|
||||
# ======= LOGOUT TESTS =======
|
||||
|
||||
func test_logout_clears_auth_token():
|
||||
network_manager.auth_token = "test_token"
|
||||
network_manager.logout()
|
||||
assert_eq(network_manager.auth_token, "")
|
||||
|
||||
|
||||
func test_logout_clears_current_user():
|
||||
network_manager.current_user = { "id": "123", "username": "test" }
|
||||
network_manager.logout()
|
||||
assert_eq(network_manager.current_user, {})
|
||||
|
||||
|
||||
func test_logout_sets_is_authenticated_false():
|
||||
network_manager.is_authenticated = true
|
||||
network_manager.logout()
|
||||
assert_false(network_manager.is_authenticated)
|
||||
|
||||
|
||||
func test_logout_emits_logged_out_signal():
|
||||
watch_signals(network_manager)
|
||||
network_manager.logout()
|
||||
assert_signal_emitted(network_manager, "logged_out")
|
||||
|
||||
|
||||
# ======= CONNECTION STATE NAME TESTS =======
|
||||
|
||||
func test_get_connection_state_name_disconnected():
|
||||
network_manager.connection_state = NetworkManager.ConnectionState.DISCONNECTED
|
||||
assert_eq(network_manager.get_connection_state_name(), "Disconnected")
|
||||
|
||||
|
||||
func test_get_connection_state_name_connecting():
|
||||
network_manager.connection_state = NetworkManager.ConnectionState.CONNECTING
|
||||
assert_eq(network_manager.get_connection_state_name(), "Connecting")
|
||||
|
||||
|
||||
func test_get_connection_state_name_connected():
|
||||
network_manager.connection_state = NetworkManager.ConnectionState.CONNECTED
|
||||
assert_eq(network_manager.get_connection_state_name(), "Connected")
|
||||
|
||||
|
||||
func test_get_connection_state_name_authenticating():
|
||||
network_manager.connection_state = NetworkManager.ConnectionState.AUTHENTICATING
|
||||
assert_eq(network_manager.get_connection_state_name(), "Authenticating")
|
||||
|
||||
|
||||
func test_get_connection_state_name_authenticated():
|
||||
network_manager.connection_state = NetworkManager.ConnectionState.AUTHENTICATED
|
||||
assert_eq(network_manager.get_connection_state_name(), "Online")
|
||||
|
||||
|
||||
func test_get_connection_state_name_in_queue():
|
||||
network_manager.connection_state = NetworkManager.ConnectionState.IN_QUEUE
|
||||
assert_eq(network_manager.get_connection_state_name(), "In Queue")
|
||||
|
||||
|
||||
func test_get_connection_state_name_in_room():
|
||||
network_manager.connection_state = NetworkManager.ConnectionState.IN_ROOM
|
||||
assert_eq(network_manager.get_connection_state_name(), "In Room")
|
||||
|
||||
|
||||
func test_get_connection_state_name_in_game():
|
||||
network_manager.connection_state = NetworkManager.ConnectionState.IN_GAME
|
||||
assert_eq(network_manager.get_connection_state_name(), "In Game")
|
||||
|
||||
|
||||
# ======= CONFIGURATION TESTS =======
|
||||
|
||||
func test_configure_sets_http_url():
|
||||
network_manager.configure("http://test.com", "ws://test.com")
|
||||
assert_eq(network_manager.http_base_url, "http://test.com")
|
||||
|
||||
|
||||
func test_configure_sets_ws_url():
|
||||
network_manager.configure("http://test.com", "ws://test.com:3001")
|
||||
assert_eq(network_manager.ws_url, "ws://test.com:3001")
|
||||
|
||||
|
||||
func test_default_http_url():
|
||||
assert_eq(network_manager.http_base_url, "http://localhost:3000")
|
||||
|
||||
|
||||
func test_default_ws_url():
|
||||
assert_eq(network_manager.ws_url, "ws://localhost:3001")
|
||||
|
||||
|
||||
# ======= CONSTANTS TESTS =======
|
||||
|
||||
func test_heartbeat_interval_constant():
|
||||
assert_eq(NetworkManager.HEARTBEAT_INTERVAL, 10.0)
|
||||
|
||||
|
||||
func test_reconnect_delay_constant():
|
||||
assert_eq(NetworkManager.RECONNECT_DELAY, 5.0)
|
||||
|
||||
|
||||
func test_max_reconnect_attempts_constant():
|
||||
assert_eq(NetworkManager.MAX_RECONNECT_ATTEMPTS, 3)
|
||||
|
||||
|
||||
func test_token_file_constant():
|
||||
assert_eq(NetworkManager.TOKEN_FILE, "user://auth_token.dat")
|
||||
280
tests/unit/test_online_game_helpers.gd
Normal file
280
tests/unit/test_online_game_helpers.gd
Normal file
@@ -0,0 +1,280 @@
|
||||
extends GutTest
|
||||
|
||||
## Unit tests for online game helper functions in Main.gd
|
||||
## Tests the logic for determining if local player can act
|
||||
|
||||
|
||||
# Mock classes for testing
|
||||
class MockTurnManager:
|
||||
var current_player_index: int = 0
|
||||
var turn_number: int = 1
|
||||
var current_phase: int = 0
|
||||
|
||||
|
||||
class MockGameState:
|
||||
var turn_manager: MockTurnManager
|
||||
|
||||
func _init():
|
||||
turn_manager = MockTurnManager.new()
|
||||
|
||||
|
||||
# Simulated Main.gd helper functions for testing
|
||||
# These mirror the actual functions in Main.gd
|
||||
|
||||
var is_online_game: bool = false
|
||||
var local_player_index: int = 0
|
||||
var mock_game_state: MockGameState = null
|
||||
|
||||
|
||||
func before_each():
|
||||
is_online_game = false
|
||||
local_player_index = 0
|
||||
mock_game_state = MockGameState.new()
|
||||
|
||||
|
||||
# Helper function that mirrors Main.gd._is_local_player_turn()
|
||||
func _is_local_player_turn() -> bool:
|
||||
if not is_online_game:
|
||||
return true
|
||||
if not mock_game_state:
|
||||
return true
|
||||
return mock_game_state.turn_manager.current_player_index == local_player_index
|
||||
|
||||
|
||||
# Helper function that mirrors Main.gd._can_perform_local_action()
|
||||
func _can_perform_local_action() -> bool:
|
||||
if not is_online_game:
|
||||
return true
|
||||
return _is_local_player_turn()
|
||||
|
||||
|
||||
# ======= _is_local_player_turn() Tests =======
|
||||
|
||||
func test_is_local_player_turn_returns_true_when_not_online():
|
||||
is_online_game = false
|
||||
mock_game_state.turn_manager.current_player_index = 1
|
||||
local_player_index = 0
|
||||
|
||||
assert_true(_is_local_player_turn(), "Should return true when not in online game")
|
||||
|
||||
|
||||
func test_is_local_player_turn_returns_true_when_no_game_state():
|
||||
is_online_game = true
|
||||
mock_game_state = null
|
||||
|
||||
assert_true(_is_local_player_turn(), "Should return true when game_state is null")
|
||||
|
||||
|
||||
func test_is_local_player_turn_returns_true_when_local_index_matches():
|
||||
is_online_game = true
|
||||
local_player_index = 0
|
||||
mock_game_state.turn_manager.current_player_index = 0
|
||||
|
||||
assert_true(_is_local_player_turn(), "Should return true when it's local player's turn")
|
||||
|
||||
|
||||
func test_is_local_player_turn_returns_false_when_opponent_turn():
|
||||
is_online_game = true
|
||||
local_player_index = 0
|
||||
mock_game_state.turn_manager.current_player_index = 1
|
||||
|
||||
assert_false(_is_local_player_turn(), "Should return false when it's opponent's turn")
|
||||
|
||||
|
||||
func test_is_local_player_turn_player_1_perspective():
|
||||
is_online_game = true
|
||||
local_player_index = 1
|
||||
mock_game_state.turn_manager.current_player_index = 1
|
||||
|
||||
assert_true(_is_local_player_turn(), "Player 1 should be able to act on their turn")
|
||||
|
||||
|
||||
func test_is_local_player_turn_player_1_opponent_turn():
|
||||
is_online_game = true
|
||||
local_player_index = 1
|
||||
mock_game_state.turn_manager.current_player_index = 0
|
||||
|
||||
assert_false(_is_local_player_turn(), "Player 1 should not act on opponent's turn")
|
||||
|
||||
|
||||
# ======= _can_perform_local_action() Tests =======
|
||||
|
||||
func test_can_perform_local_action_delegates_to_is_local_player_turn_online():
|
||||
is_online_game = true
|
||||
local_player_index = 0
|
||||
mock_game_state.turn_manager.current_player_index = 0
|
||||
|
||||
assert_true(_can_perform_local_action())
|
||||
|
||||
mock_game_state.turn_manager.current_player_index = 1
|
||||
assert_false(_can_perform_local_action())
|
||||
|
||||
|
||||
func test_can_perform_local_action_always_true_offline():
|
||||
is_online_game = false
|
||||
local_player_index = 0
|
||||
mock_game_state.turn_manager.current_player_index = 1
|
||||
|
||||
assert_true(_can_perform_local_action(), "Should always be able to act in offline game")
|
||||
|
||||
|
||||
# ======= Player Index Validation Tests =======
|
||||
|
||||
func test_valid_player_index_0():
|
||||
var player_index = 0
|
||||
assert_true(player_index >= 0 and player_index <= 1, "Player index 0 should be valid")
|
||||
|
||||
|
||||
func test_valid_player_index_1():
|
||||
var player_index = 1
|
||||
assert_true(player_index >= 0 and player_index <= 1, "Player index 1 should be valid")
|
||||
|
||||
|
||||
func test_invalid_player_index_negative():
|
||||
var player_index = -1
|
||||
assert_false(player_index >= 0 and player_index <= 1, "Negative player index should be invalid")
|
||||
|
||||
|
||||
func test_invalid_player_index_too_high():
|
||||
var player_index = 2
|
||||
assert_false(player_index >= 0 and player_index <= 1, "Player index > 1 should be invalid")
|
||||
|
||||
|
||||
# ======= Turn Timer Format Tests =======
|
||||
|
||||
func test_timer_format_full_minutes():
|
||||
var seconds = 120
|
||||
var minutes = seconds / 60
|
||||
var secs = seconds % 60
|
||||
var formatted = "%d:%02d" % [minutes, secs]
|
||||
|
||||
assert_eq(formatted, "2:00")
|
||||
|
||||
|
||||
func test_timer_format_partial_minutes():
|
||||
var seconds = 90
|
||||
var minutes = seconds / 60
|
||||
var secs = seconds % 60
|
||||
var formatted = "%d:%02d" % [minutes, secs]
|
||||
|
||||
assert_eq(formatted, "1:30")
|
||||
|
||||
|
||||
func test_timer_format_under_minute():
|
||||
var seconds = 45
|
||||
var minutes = seconds / 60
|
||||
var secs = seconds % 60
|
||||
var formatted = "%d:%02d" % [minutes, secs]
|
||||
|
||||
assert_eq(formatted, "0:45")
|
||||
|
||||
|
||||
func test_timer_format_single_digit_seconds():
|
||||
var seconds = 65
|
||||
var minutes = seconds / 60
|
||||
var secs = seconds % 60
|
||||
var formatted = "%d:%02d" % [minutes, secs]
|
||||
|
||||
assert_eq(formatted, "1:05")
|
||||
|
||||
|
||||
func test_timer_format_zero():
|
||||
var seconds = 0
|
||||
var minutes = seconds / 60
|
||||
var secs = seconds % 60
|
||||
var formatted = "%d:%02d" % [minutes, secs]
|
||||
|
||||
assert_eq(formatted, "0:00")
|
||||
|
||||
|
||||
# ======= Timer Color Thresholds Tests =======
|
||||
|
||||
func test_timer_color_threshold_critical():
|
||||
var seconds = 10
|
||||
var is_critical = seconds <= 10
|
||||
|
||||
assert_true(is_critical, "10 seconds should be critical (red)")
|
||||
|
||||
|
||||
func test_timer_color_threshold_warning():
|
||||
var seconds = 30
|
||||
var is_warning = seconds > 10 and seconds <= 30
|
||||
|
||||
assert_true(is_warning, "30 seconds should be warning (yellow)")
|
||||
|
||||
|
||||
func test_timer_color_threshold_normal():
|
||||
var seconds = 60
|
||||
var is_normal = seconds > 30
|
||||
|
||||
assert_true(is_normal, "60 seconds should be normal (white)")
|
||||
|
||||
|
||||
func test_timer_color_threshold_boundary_10():
|
||||
var seconds = 10
|
||||
var is_critical = seconds <= 10
|
||||
|
||||
assert_true(is_critical, "Exactly 10 should be critical")
|
||||
|
||||
|
||||
func test_timer_color_threshold_boundary_11():
|
||||
var seconds = 11
|
||||
var is_warning = seconds > 10 and seconds <= 30
|
||||
|
||||
assert_true(is_warning, "11 should be warning, not critical")
|
||||
|
||||
|
||||
func test_timer_color_threshold_boundary_30():
|
||||
var seconds = 30
|
||||
var is_warning = seconds > 10 and seconds <= 30
|
||||
|
||||
assert_true(is_warning, "Exactly 30 should be warning")
|
||||
|
||||
|
||||
func test_timer_color_threshold_boundary_31():
|
||||
var seconds = 31
|
||||
var is_normal = seconds > 30
|
||||
|
||||
assert_true(is_normal, "31 should be normal, not warning")
|
||||
|
||||
|
||||
# ======= Online Game State Sync Tests =======
|
||||
|
||||
func test_game_state_sync_updates_current_player():
|
||||
var state = { "current_player_index": 1, "current_phase": 2, "turn_number": 3 }
|
||||
|
||||
mock_game_state.turn_manager.current_player_index = state.get("current_player_index", 0)
|
||||
|
||||
assert_eq(mock_game_state.turn_manager.current_player_index, 1)
|
||||
|
||||
|
||||
func test_game_state_sync_updates_phase():
|
||||
var state = { "current_player_index": 0, "current_phase": 3, "turn_number": 1 }
|
||||
|
||||
mock_game_state.turn_manager.current_phase = state.get("current_phase", 0)
|
||||
|
||||
assert_eq(mock_game_state.turn_manager.current_phase, 3)
|
||||
|
||||
|
||||
func test_game_state_sync_updates_turn_number():
|
||||
var state = { "current_player_index": 0, "current_phase": 0, "turn_number": 5 }
|
||||
|
||||
mock_game_state.turn_manager.turn_number = state.get("turn_number", 1)
|
||||
|
||||
assert_eq(mock_game_state.turn_manager.turn_number, 5)
|
||||
|
||||
|
||||
func test_game_state_sync_timer_extraction():
|
||||
var state = { "turn_timer_seconds": 90 }
|
||||
|
||||
var timer_seconds = state.get("turn_timer_seconds", 120)
|
||||
|
||||
assert_eq(timer_seconds, 90)
|
||||
|
||||
|
||||
func test_game_state_sync_timer_default():
|
||||
var state = {}
|
||||
|
||||
var timer_seconds = state.get("turn_timer_seconds", 120)
|
||||
|
||||
assert_eq(timer_seconds, 120, "Should default to 120 seconds")
|
||||
191
tests/unit/test_optional_effects.gd
Normal file
191
tests/unit/test_optional_effects.gd
Normal file
@@ -0,0 +1,191 @@
|
||||
extends GutTest
|
||||
|
||||
## Tests for optional effect prompting in AbilitySystem
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BUILD EFFECT DESCRIPTION TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_build_description_draw_single() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "DRAW", "amount": 1}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Draw 1 card", "Should describe single card draw")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_draw_multiple() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "DRAW", "amount": 3}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Draw 3 cards", "Should describe multiple card draw with plural")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_damage() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "DAMAGE", "amount": 5000}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Deal 5000 damage", "Should describe damage amount")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_power_mod_positive() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "POWER_MOD", "amount": 2000}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Give +2000 power", "Should describe positive power mod with +")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_power_mod_negative() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "POWER_MOD", "amount": -3000}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Give -3000 power", "Should describe negative power mod")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_dull() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "DULL"}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Dull a Forward", "Should describe dull effect")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_activate() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "ACTIVATE"}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Activate a card", "Should describe activate effect")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_break() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "BREAK"}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Break a card", "Should describe break effect")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_return() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "RETURN"}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Return a card to hand", "Should describe return effect")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_search() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "SEARCH"}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Search your deck", "Should describe search effect")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_discard_single() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "DISCARD", "amount": 1}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Discard 1 card", "Should describe single discard")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_discard_multiple() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "DISCARD", "amount": 2}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Discard 2 cards", "Should describe multiple discard with plural")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_uses_original_text() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "UNKNOWN_EFFECT", "original_text": "Do something special"}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Do something special", "Should use original_text for unknown effects")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
func test_build_description_fallback() -> void:
|
||||
var ability_system = AbilitySystem.new()
|
||||
var effect = {"type": "UNKNOWN_EFFECT"}
|
||||
|
||||
var description = ability_system._build_effect_description(effect)
|
||||
|
||||
assert_eq(description, "Use this effect", "Should use fallback for unknown effects without original_text")
|
||||
ability_system.free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OPTIONAL EFFECT FLAG TESTS
|
||||
# =============================================================================
|
||||
|
||||
func test_optional_flag_is_detected() -> void:
|
||||
var effect_with_optional = {"type": "DRAW", "amount": 1, "optional": true}
|
||||
var effect_without_optional = {"type": "DRAW", "amount": 1}
|
||||
|
||||
assert_true(effect_with_optional.get("optional", false), "Should detect optional flag")
|
||||
assert_false(effect_without_optional.get("optional", false), "Should default to false")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# INTEGRATION TESTS (Signal Emission)
|
||||
# =============================================================================
|
||||
|
||||
func test_optional_effect_signal_contains_correct_data() -> void:
|
||||
# This test verifies the signal parameters are correct
|
||||
var ability_system = AbilitySystem.new()
|
||||
var received_player_index = -1
|
||||
var received_effect = {}
|
||||
var received_description = ""
|
||||
var received_callback = null
|
||||
|
||||
ability_system.optional_effect_prompt.connect(func(player_index, effect, description, callback):
|
||||
received_player_index = player_index
|
||||
received_effect = effect
|
||||
received_description = description
|
||||
received_callback = callback
|
||||
)
|
||||
|
||||
# Manually emit to test signal structure
|
||||
var test_effect = {"type": "DRAW", "amount": 2, "optional": true}
|
||||
var test_callback = func(_accepted): pass
|
||||
ability_system.optional_effect_prompt.emit(0, test_effect, "Draw 2 cards", test_callback)
|
||||
|
||||
assert_eq(received_player_index, 0, "Player index should be passed")
|
||||
assert_eq(received_effect.type, "DRAW", "Effect should be passed")
|
||||
assert_eq(received_description, "Draw 2 cards", "Description should be passed")
|
||||
assert_not_null(received_callback, "Callback should be passed")
|
||||
|
||||
ability_system.free()
|
||||
Reference in New Issue
Block a user