feature updates

This commit is contained in:
2026-02-02 16:28:53 -05:00
parent bf9aa3fa23
commit 44c06530ac
83 changed files with 282641 additions and 11251 deletions

View File

@@ -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)

View 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)

View 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)

View 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")

View 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)")

View 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

View 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

View 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)

View 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

View 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

View 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

View 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")

View 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")

View 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()