diff --git a/.coverage b/.coverage index a378a20..92c0efa 100644 Binary files a/.coverage and b/.coverage differ diff --git a/src/config/config.py b/src/config/config.py index c8b4090..e7c16ab 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -9,8 +9,10 @@ def __init__(self): self.godMode = False # Disable god mode self.maxTicks = 1000 self.tickLength = 0.1 - + # Early-game survival settings self.earlyGameGracePeriod = 50 # Number of ticks of protection for player - self.playerDamageReduction = 0.4 # 40% damage reduction for player during grace period + self.playerDamageReduction = ( + 0.4 # 40% damage reduction for player during grace period + ) # During grace period, other creatures have 85% chance to avoid attacking player diff --git a/src/entity/livingEntity.py b/src/entity/livingEntity.py index 7e6233d..e5e2ff6 100644 --- a/src/entity/livingEntity.py +++ b/src/entity/livingEntity.py @@ -58,10 +58,13 @@ def fight(self, kreature): if self.health > 0: damage = random.randint(15, 25) # Random damage between 15-25 # Apply damage reduction if target has it - if hasattr(kreature, 'damageReduction') and kreature.damageReduction > 0: + if ( + hasattr(kreature, "damageReduction") + and kreature.damageReduction > 0 + ): damage = int(damage * (1 - kreature.damageReduction)) damage = max(damage, 1) # Ensure at least 1 damage - + kreature.health -= damage if kreature.health <= 0: self.log.append( @@ -86,10 +89,10 @@ def fight(self, kreature): if kreature.health > 0: damage = random.randint(15, 25) # Random damage between 15-25 # Apply damage reduction if target has it - if hasattr(self, 'damageReduction') and self.damageReduction > 0: + if hasattr(self, "damageReduction") and self.damageReduction > 0: damage = int(damage * (1 - self.damageReduction)) damage = max(damage, 1) # Ensure at least 1 damage - + self.health -= damage if self.health <= 0: kreature.log.append( @@ -151,7 +154,13 @@ def isAlive(self): return self.health > 0 def regenerateHealth(self): - """Regenerate a small amount of health over time""" + """Regenerate a small amount of health over time + + This is a passive background process that does not interfere with entity actions. + Entities continue making decisions (fighting, befriending, reproducing) regardless + of their health status or regeneration state. This method is called separately from + getNextAction() to ensure regeneration happens alongside normal entity behavior. + """ if ( self.health < self.maxHealth and random.randint(1, 10) <= 3 ): # 30% chance per tick diff --git a/src/kreatures.py b/src/kreatures.py index 26a523d..e26ee73 100644 --- a/src/kreatures.py +++ b/src/kreatures.py @@ -22,10 +22,12 @@ def __init__(self): self.running = True self.config = Config() self.tick = 0 - + # Initialize player early-game protection self.playerCreature.damageReduction = self.config.playerDamageReduction - self.playerCreature.log.append("%s has early-game protection!" % self.playerCreature.name) + self.playerCreature.log.append( + "%s has early-game protection!" % self.playerCreature.name + ) def _load_names(self): """Load names from configuration file""" @@ -85,13 +87,15 @@ def initiateEntityActions(self): if self.config.godMode: continue # During grace period, 85% chance to skip attacking the player - if (self.tick < self.config.earlyGameGracePeriod and - random.randint(1, 100) <= 85): + if ( + self.tick < self.config.earlyGameGracePeriod + and random.randint(1, 100) <= 85 + ): entity.log.append( "%s decided not to attack %s." % (entity.name, target.name) ) continue - + entity.increaseChanceToFight() entity.decreaseChanceToBefriend() entity.fight(target) @@ -113,12 +117,24 @@ def updatePlayerProtection(self): """Update player protection based on current tick""" if self.tick >= self.config.earlyGameGracePeriod: # Grace period has ended - if hasattr(self.playerCreature, 'damageReduction') and self.playerCreature.damageReduction > 0: + if ( + hasattr(self.playerCreature, "damageReduction") + and self.playerCreature.damageReduction > 0 + ): self.playerCreature.damageReduction = 0 - self.playerCreature.log.append("%s's protection has worn off!" % self.playerCreature.name) + self.playerCreature.log.append( + "%s's protection has worn off!" % self.playerCreature.name + ) def regenerateAllEntities(self): - """Regenerate health for all living entities""" + """Regenerate health for all living entities + + This method is called every game tick AFTER entities take their actions in + initiateEntityActions(). Health regeneration is a passive background process + that does not prevent entities from making decisions or taking actions. + Entities continue to fight, befriend, and reproduce regardless of their + health status or whether they are regenerating. + """ for entity in self.environment.getEntities(): if entity.isAlive(): entity.regenerateHealth() @@ -225,12 +241,18 @@ def printSummary(self): "%s's chance to be nice was %d percent." % (self.playerCreature.name, self.playerCreature.chanceToBefriend) ) - + # Show protection status - if hasattr(self.playerCreature, 'damageReduction') and self.playerCreature.damageReduction > 0: + if ( + hasattr(self.playerCreature, "damageReduction") + and self.playerCreature.damageReduction > 0 + ): protection_percent = int(self.playerCreature.damageReduction * 100) - print("%s still has %d%% damage reduction." % (self.playerCreature.name, protection_percent)) - + print( + "%s still has %d%% damage reduction." + % (self.playerCreature.name, protection_percent) + ) + if self.playerCreature.isAlive(): print( "%s ended with %d health (out of %d max)." @@ -269,9 +291,14 @@ def run(self): except: # if list is empty, just keep going pass + # Game tick order is important: + # 1. Entities take actions (fight, befriend, reproduce) based on their decision-making self.initiateEntityActions() - self.updatePlayerProtection() # Update player protection status - self.regenerateAllEntities() # Regenerate health for all entities + # 2. Update player protection status based on current tick + self.updatePlayerProtection() + # 3. Passive health regeneration happens AFTER actions, as a background process + # This ensures entities can take actions regardless of health/regeneration status + self.regenerateAllEntities() time.sleep(self.config.tickLength) self.tick += 1 if self.tick >= self.config.maxTicks: diff --git a/tests/test_health_regeneration.py b/tests/test_health_regeneration.py new file mode 100644 index 0000000..d3e8453 --- /dev/null +++ b/tests/test_health_regeneration.py @@ -0,0 +1,360 @@ +# Copyright (c) 2022 Daniel McCoy Stephenson +# Apache License 2.0 + +import sys +import os +import unittest +from unittest.mock import patch + +# Add src to path to import modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +from entity.livingEntity import LivingEntity + + +class TestHealthRegenerationBehavior(unittest.TestCase): + """Test suite to verify health regeneration is a passive background process""" + + def test_entity_continues_making_decisions_while_regenerating(self): + """Test that entities continue to take actions even when health is below max""" + entity = LivingEntity("TestEntity") + target = LivingEntity("TargetEntity") + + # Reduce entity's health to trigger potential regeneration + entity.health = entity.maxHealth - 20 + + # Entity should still be able to make decisions + with patch("random.randint") as mock_random: + # First call for getNextAction decision (50 = fight) + # Subsequent calls for other random operations + mock_random.side_effect = [50, 10, 10, 10, 10] + + decision = entity.getNextAction(target) + + # Entity should make a decision even with low health + self.assertIn(decision, ["fight", "befriend", "love", "nothing"]) + + # Stats should be updated (indicating action was taken) + self.assertGreater(entity.stats.numActionsTaken, 0) + + def test_entity_continues_making_decisions_after_full_health(self): + """Test that entities don't become idle after reaching full health""" + entity = LivingEntity("TestEntity") + target = LivingEntity("TargetEntity") + + # Set entity to full health + entity.health = entity.maxHealth + + # Entity should still be able to make decisions at full health + with patch("random.randint") as mock_random: + # Set up to return befriend action + mock_random.return_value = 80 # Greater than typical chanceToFight + + decision = entity.getNextAction(target) + + # Entity should make a decision at full health + self.assertIn(decision, ["fight", "befriend", "love", "nothing"]) + + def test_regeneration_does_not_prevent_fighting(self): + """Test that entities can fight while regenerating health""" + attacker = LivingEntity("Attacker") + defender = LivingEntity("Defender") + + # Reduce attacker's health + attacker.health = attacker.maxHealth - 30 + defender.health = 50 # Low enough to be killed in fight + + # Store original health + attacker.health + + # Attacker should still be able to fight + with patch("random.randint") as mock_random: + # Set damage values to ensure defender dies + mock_random.return_value = 60 + + attacker.fight(defender) + + # Fight should have occurred (defender should be dead or damaged) + self.assertLessEqual(defender.health, 0) + + # Attacker should have log entries about the fight + fight_logs = [log for log in attacker.log if "fought" in log.lower()] + self.assertGreater(len(fight_logs), 0) + + def test_regeneration_does_not_prevent_befriending(self): + """Test that entities can befriend while regenerating health""" + entity1 = LivingEntity("Entity1") + entity2 = LivingEntity("Entity2") + + # Reduce entity1's health + entity1.health = entity1.maxHealth - 25 + + # Entity should still be able to befriend + entity1.befriend(entity2) + + # Befriending should have occurred + self.assertIn(entity2, entity1.friends) + self.assertIn(entity1, entity2.friends) + self.assertEqual(entity1.stats.numFriendshipsForged, 1) + + def test_regeneration_does_not_prevent_reproduction(self): + """Test that entities can reproduce while regenerating health""" + parent1 = LivingEntity("Parent1") + parent2 = LivingEntity("Parent2") + + # Reduce parent1's health + parent1.health = parent1.maxHealth - 40 + + # Parents should still be able to reproduce + result = parent1.reproduce(parent2) + + # Reproduction should return both parents + self.assertEqual(result, (parent1, parent2)) + + # Stats should be updated + self.assertEqual(parent1.stats.numOffspring, 1) + self.assertEqual(parent2.stats.numOffspring, 1) + + def test_regeneration_happens_independently_of_actions(self): + """Test that regeneration is called independently of entity actions""" + entity = LivingEntity("TestEntity") + target = LivingEntity("TargetEntity") + + # Reduce entity's health + entity.health = entity.maxHealth - 30 + original_health = entity.health + + # Make entity take an action + with patch("random.randint") as mock_random: + mock_random.return_value = 80 # Befriend action + entity.getNextAction(target) + + # Now call regeneration separately (as the game loop does) + with patch("random.randint") as mock_random: + # Set to trigger regeneration (roll <= 3) and heal 2 HP + mock_random.side_effect = [2, 2] + entity.regenerateHealth() + + # Entity should have regenerated health + self.assertGreater(entity.health, original_health) + + # And should still have taken an action + self.assertGreater(entity.stats.numActionsTaken, 0) + + def test_multiple_actions_between_regeneration_ticks(self): + """Test that entity can take multiple actions while health is regenerating""" + entity = LivingEntity("TestEntity") + target1 = LivingEntity("Target1") + target2 = LivingEntity("Target2") + + # Reduce entity's health + entity.health = entity.maxHealth - 50 + entity.health + + # Simulate multiple ticks where entity takes actions + action_count = 0 + for _ in range(5): + with patch("random.randint") as mock_random: + mock_random.return_value = 80 # Befriend action + decision = entity.getNextAction( + target1 if action_count % 2 == 0 else target2 + ) + if decision in ["fight", "befriend", "love"]: + action_count += 1 + + # Entity should have taken multiple actions + self.assertGreater(entity.stats.numActionsTaken, 1) + + # Health may or may not have changed (regeneration is probabilistic) + # But entity should still be functional + self.assertTrue(entity.isAlive()) + + def test_regeneration_at_full_health_does_not_block_actions(self): + """Test that entity at full health can still take actions""" + entity = LivingEntity("TestEntity") + target = LivingEntity("TargetEntity") + + # Entity is at full health + self.assertEqual(entity.health, entity.maxHealth) + + # Call regeneration (should do nothing as health is full) + with patch("random.randint") as mock_random: + mock_random.return_value = 1 # Would trigger regeneration if needed + entity.regenerateHealth() + + # Entity should still be able to take actions after regeneration call + with patch("random.randint") as mock_random: + mock_random.return_value = 70 + decision = entity.getNextAction(target) + + self.assertIn(decision, ["fight", "befriend", "love", "nothing"]) + self.assertGreater(entity.stats.numActionsTaken, 0) + + def test_regeneration_logs_do_not_interfere_with_action_logs(self): + """Test that regeneration logs are separate from action logs""" + entity = LivingEntity("TestEntity") + target = LivingEntity("TargetEntity") + + # Reduce health to trigger regeneration + entity.health = entity.maxHealth - 20 + + # Clear creation log + entity.log.clear() + + # Trigger regeneration + with patch("random.randint") as mock_random: + # Trigger regeneration (roll <= 3) with significant heal (>= 2) + mock_random.side_effect = [2, 3] + entity.regenerateHealth() + + regen_log_count = len(entity.log) + + # Take an action + with patch("random.randint") as mock_random: + mock_random.return_value = 70 + entity.getNextAction(target) + + entity.befriend(target) + + # Should have both regeneration and action logs + self.assertGreater(len(entity.log), regen_log_count) + + +class TestHealthRegenerationMechanism(unittest.TestCase): + """Test suite for the regeneration mechanism itself""" + + def test_regeneration_heals_partial_health(self): + """Test that regeneration heals between 1-3 health per successful tick""" + entity = LivingEntity("TestEntity") + entity.health = entity.maxHealth - 50 + original_health = entity.health + + # Force successful regeneration + with patch("random.randint") as mock_random: + # First call: trigger regeneration (roll <= 3) + # Second call: heal amount (1-3) + mock_random.side_effect = [2, 2] + entity.regenerateHealth() + + # Should have healed + self.assertEqual(entity.health, original_health + 2) + + def test_regeneration_does_not_exceed_max_health(self): + """Test that regeneration caps at max health""" + entity = LivingEntity("TestEntity") + entity.health = entity.maxHealth - 1 + + # Force regeneration with high heal amount + with patch("random.randint") as mock_random: + mock_random.side_effect = [2, 10] # Large heal + entity.regenerateHealth() + + # Should not exceed max health + self.assertEqual(entity.health, entity.maxHealth) + + def test_regeneration_probabilistic_nature(self): + """Test that regeneration has ~30% chance per tick""" + entity = LivingEntity("TestEntity") + entity.health = entity.maxHealth - 50 + + # Test that rolls > 3 don't trigger regeneration + with patch("random.randint") as mock_random: + mock_random.return_value = 5 # > 3, should not trigger + original_health = entity.health + entity.regenerateHealth() + + # Should not have healed + self.assertEqual(entity.health, original_health) + + +class TestHealthRegenerationIntegration(unittest.TestCase): + """Integration tests for health regeneration with game loop simulation""" + + def test_entities_take_actions_across_multiple_ticks_while_regenerating(self): + """Test that entities continue taking actions across multiple game ticks while health regenerates""" + entity = LivingEntity("TestEntity") + target = LivingEntity("TargetEntity") + + # Reduce health significantly + entity.health = entity.maxHealth - 50 + initial_health = entity.health + + # Simulate 10 game ticks (like the game loop does) + action_count = 0 + regen_count = 0 + + for tick in range(10): + # Step 1: Entity takes an action (initiateEntityActions) + with patch("random.randint") as mock_random: + mock_random.return_value = 70 # Befriend action + decision = entity.getNextAction(target) + if decision in ["fight", "befriend", "love"]: + action_count += 1 + + # Step 2: Entity regenerates health (regenerateAllEntities) + old_health = entity.health + with patch("random.randint") as mock_random: + # Trigger regeneration every 3rd tick + if tick % 3 == 0: + mock_random.side_effect = [2, 2] + entity.regenerateHealth() + if entity.health > old_health: + regen_count += 1 + else: + mock_random.return_value = 5 # Don't trigger + entity.regenerateHealth() + + # Verify entity took actions every tick + self.assertEqual(action_count, 10, "Entity should take action every tick") + + # Verify health increased from regeneration + self.assertGreater( + entity.health, initial_health, "Health should have increased" + ) + + # Verify regeneration happened + self.assertGreater(regen_count, 0, "Regeneration should have occurred") + + # Verify entity continued functioning + self.assertTrue(entity.isAlive()) + self.assertEqual(entity.stats.numActionsTaken, 10) + + def test_game_tick_order_actions_then_regeneration(self): + """Test that game tick order is: actions first, then regeneration""" + entity = LivingEntity("TestEntity") + target = LivingEntity("TargetEntity") + + # Reduce health + entity.health = entity.maxHealth - 30 + + # Clear log + entity.log.clear() + + # Simulate one game tick with specific order + # Step 1: Take action + with patch("random.randint") as mock_random: + mock_random.return_value = 70 + entity.getNextAction(target) + entity.befriend(target) + + action_log_count = len(entity.log) + + # Step 2: Regenerate (happens after action) + with patch("random.randint") as mock_random: + mock_random.side_effect = [2, 3] # Trigger regeneration + entity.regenerateHealth() + + total_log_count = len(entity.log) + + # Both action and regeneration logs should exist + self.assertGreater(action_log_count, 0, "Action should be logged") + self.assertGreater( + total_log_count, action_log_count, "Regeneration should also be logged" + ) + + # Stats should show action was taken + self.assertGreater(entity.stats.numActionsTaken, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_regeneration_e2e.py b/tests/test_regeneration_e2e.py new file mode 100644 index 0000000..b405cd8 --- /dev/null +++ b/tests/test_regeneration_e2e.py @@ -0,0 +1,194 @@ +# Copyright (c) 2022 Daniel McCoy Stephenson +# Apache License 2.0 + +""" +End-to-end test to verify health regeneration behavior in a realistic game scenario. + +This test simulates a realistic game scenario where: +1. An entity gets damaged in combat +2. The entity continues taking actions while health is low +3. Health regenerates passively over time +4. The entity never becomes idle due to regeneration +""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +import unittest +from unittest.mock import patch +from entity.livingEntity import LivingEntity + + +class TestRealisticGameScenario(unittest.TestCase): + """End-to-end test simulating realistic game scenarios""" + + def test_entity_damaged_in_combat_continues_acting_while_regenerating(self): + """ + Realistic scenario: Entity gets damaged in fight, then continues taking + actions while passively regenerating health. + """ + # Setup: Two entities + entity = LivingEntity("Fighter") + opponent1 = LivingEntity("Opponent1") + opponent2 = LivingEntity("Opponent2") + friend = LivingEntity("Friend") + + # Make them friends so they can reproduce later + entity.friends.append(friend) + friend.friends.append(entity) + + # Phase 1: Entity gets damaged in combat + initial_health = entity.health + entity.health -= 40 # Simulate damage from combat + damaged_health = entity.health + + self.assertLess(damaged_health, initial_health, "Entity should be damaged") + + # Phase 2: Entity continues taking actions while damaged + # This simulates multiple game ticks where entity acts, then regenerates + + actions_taken = [] + health_progression = [damaged_health] + + for tick in range(20): + # Step 1: Entity makes decision and takes action (like initiateEntityActions()) + if tick < 5: + # First 5 ticks: fight different opponents + target = opponent1 if tick % 2 == 0 else opponent2 + with patch("random.randint") as mock_random: + mock_random.return_value = 30 # Fight action + decision = entity.getNextAction(target) + actions_taken.append(("fight", tick)) + elif tick < 10: + # Next 5 ticks: befriend others + with patch("random.randint") as mock_random: + mock_random.return_value = 70 # Befriend action + decision = entity.getNextAction(opponent1) + if decision == "befriend": + actions_taken.append(("befriend", tick)) + elif decision == "love": + actions_taken.append(("love", tick)) + else: + # Last 10 ticks: reproduce with friend + with patch("random.randint") as mock_random: + mock_random.return_value = 70 # Love action (with friend) + decision = entity.getNextAction(friend) + if decision == "love": + actions_taken.append(("love", tick)) + + # Step 2: Passive health regeneration (like regenerateAllEntities()) + with patch("random.randint") as mock_random: + # 30% chance to regenerate (simulate realistic probability) + if tick % 3 == 0: # Trigger every 3rd tick for testing + mock_random.side_effect = [2, 2] + entity.regenerateHealth() + else: + mock_random.return_value = 5 # Don't trigger + entity.regenerateHealth() + + health_progression.append(entity.health) + + # Verification 1: Entity took actions every tick + self.assertEqual( + len(actions_taken), + 20, + "Entity should take action every tick regardless of health", + ) + + # Verification 2: Entity's health increased over time from regeneration + final_health = entity.health + self.assertGreater( + final_health, + damaged_health, + "Health should increase from regeneration over time", + ) + + # Verification 3: Entity took diverse actions (not stuck in one behavior) + action_types = set([action[0] for action in actions_taken]) + self.assertGreater( + len(action_types), 1, "Entity should take different types of actions" + ) + + # Verification 4: Actions were taken while health was below max + # (proves regeneration didn't block actions) + actions_while_damaged = [ + action + for action in actions_taken + if health_progression[action[1]] < entity.maxHealth + ] + self.assertGreater( + len(actions_while_damaged), + 10, + "Many actions should be taken while health is still regenerating", + ) + + # Verification 5: Entity remained functional throughout + self.assertTrue(entity.isAlive()) + self.assertGreater(entity.stats.numActionsTaken, 15) + + def test_entity_at_full_health_continues_acting(self): + """ + Test that entity at full health doesn't become idle. + This verifies there's no "regeneration complete → stop acting" logic. + """ + entity = LivingEntity("HealthyEntity") + target = LivingEntity("Target") + + # Entity is at full health + entity.health = entity.maxHealth + + # Take 10 actions + for tick in range(10): + # Regeneration call (should do nothing at full health) + entity.regenerateHealth() + + # Entity should still be able to take actions + with patch("random.randint") as mock_random: + mock_random.return_value = 70 + decision = entity.getNextAction(target) + + self.assertIn(decision, ["fight", "befriend", "love", "nothing"]) + + # Entity should have taken all 10 actions + self.assertEqual(entity.stats.numActionsTaken, 10) + self.assertEqual(entity.health, entity.maxHealth) + + def test_multiple_entities_regenerate_and_act_independently(self): + """ + Test that multiple entities can regenerate and act independently, + as would happen in the actual game loop. + """ + entities = [LivingEntity(f"Entity{i}") for i in range(5)] + + # Damage all entities differently + for i, entity in enumerate(entities): + entity.health -= (i + 1) * 10 + + # Simulate 10 ticks + for tick in range(10): + for entity in entities: + # Each entity takes an action + target = entities[(entities.index(entity) + 1) % len(entities)] + with patch("random.randint") as mock_random: + mock_random.return_value = 70 + entity.getNextAction(target) + + # Each entity regenerates + with patch("random.randint") as mock_random: + if tick % 2 == 0: + mock_random.side_effect = [2, 2] + entity.regenerateHealth() + else: + mock_random.return_value = 5 + entity.regenerateHealth() + + # All entities should have taken actions + for entity in entities: + self.assertGreater(entity.stats.numActionsTaken, 5) + self.assertTrue(entity.isAlive()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_survival_mechanism.py b/tests/test_survival_mechanism.py index fee5bb6..4503bcf 100644 --- a/tests/test_survival_mechanism.py +++ b/tests/test_survival_mechanism.py @@ -2,133 +2,135 @@ # Apache License 2.0 import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) -import pytest +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + import random -from unittest.mock import patch, MagicMock +from unittest.mock import patch from entity.livingEntity import LivingEntity from config.config import Config class TestDamageReduction: """Test suite for damage reduction functionality""" - + def test_damage_reduction_applied(self): """Test that damage reduction is applied correctly during fights""" # Create two creatures - attacker = LivingEntity("Attacker") + LivingEntity("Attacker") defender = LivingEntity("Defender") - + # Give defender damage reduction defender.damageReduction = 0.4 # 40% damage reduction - + # Record initial health - initial_health = defender.health - + defender.health + # Simulate a single attack (we'll patch the random damage to be predictable) original_randint = random.randint random.randint = lambda a, b: 20 # Always deal 20 damage - + try: # Make attacker attack defender once, then stop the fight early original_health = defender.health damage = 20 - reduced_damage = int(damage * (1 - defender.damageReduction)) # Should be 12 - + reduced_damage = int( + damage * (1 - defender.damageReduction) + ) # Should be 12 + # Apply damage manually to test calculation defender.health -= reduced_damage - + # Verify damage reduction worked expected_health = original_health - reduced_damage assert defender.health == expected_health assert reduced_damage == 12 # 20 * 0.6 = 12 - + finally: # Restore original random function random.randint = original_randint def test_minimum_damage(self): """Test that damage reduction doesn't reduce damage below 1""" - attacker = LivingEntity("Attacker") + LivingEntity("Attacker") defender = LivingEntity("Defender") - + # Give defender very high damage reduction defender.damageReduction = 0.99 # 99% damage reduction - + # Test that even with high reduction, at least 1 damage is dealt original_randint = random.randint random.randint = lambda a, b: 1 # Minimum damage - + try: original_health = defender.health damage = 1 reduced_damage = int(damage * (1 - defender.damageReduction)) # Would be 0 reduced_damage = max(reduced_damage, 1) # Should be adjusted to 1 - + defender.health -= reduced_damage - + # Verify at least 1 damage was dealt assert defender.health == original_health - 1 - + finally: random.randint = original_randint def test_no_damage_reduction_without_attribute(self): """Test that creatures without damageReduction attribute take full damage""" - attacker = LivingEntity("Attacker") + LivingEntity("Attacker") defender = LivingEntity("Defender") - + # Ensure defender has no damageReduction attribute - if hasattr(defender, 'damageReduction'): - delattr(defender, 'damageReduction') - + if hasattr(defender, "damageReduction"): + delattr(defender, "damageReduction") + original_health = defender.health - - with patch('random.randint', return_value=20): + + with patch("random.randint", return_value=20): # Manually apply damage as the fight method would damage = 20 # No damage reduction should be applied defender.health -= damage - + assert defender.health == original_health - 20 def test_zero_damage_reduction(self): """Test that 0% damage reduction works correctly""" - attacker = LivingEntity("Attacker") + LivingEntity("Attacker") defender = LivingEntity("Defender") - + # Give defender zero damage reduction defender.damageReduction = 0.0 - + original_health = defender.health - - with patch('random.randint', return_value=20): + + with patch("random.randint", return_value=20): damage = 20 # Apply damage reduction calculation - if hasattr(defender, 'damageReduction') and defender.damageReduction > 0: + if hasattr(defender, "damageReduction") and defender.damageReduction > 0: damage = int(damage * (1 - defender.damageReduction)) damage = max(damage, 1) - + defender.health -= damage - + assert defender.health == original_health - 20 def test_full_fight_with_damage_reduction(self): """Test a complete fight scenario with damage reduction""" attacker = LivingEntity("Attacker") defender = LivingEntity("Defender") - + # Set predictable health values for testing attacker.health = 50 attacker.maxHealth = 50 defender.health = 60 defender.maxHealth = 60 defender.damageReduction = 0.5 # 50% damage reduction - - with patch('random.randint', return_value=20): + + with patch("random.randint", return_value=20): attacker.fight(defender) - + # Defender should have survived longer due to damage reduction # With 50% reduction, 20 damage becomes 10 damage # This test verifies the fight mechanism works with damage reduction @@ -137,15 +139,15 @@ def test_full_fight_with_damage_reduction(self): class TestConfigurationSettings: """Test suite for configuration settings""" - + def test_config_survival_settings(self): """Test that config includes the new survival settings""" config = Config() - + # Check that early game settings exist - assert hasattr(config, 'earlyGameGracePeriod') - assert hasattr(config, 'playerDamageReduction') - + assert hasattr(config, "earlyGameGracePeriod") + assert hasattr(config, "playerDamageReduction") + # Check default values assert config.earlyGameGracePeriod == 50 assert config.playerDamageReduction == 0.4 @@ -153,85 +155,89 @@ def test_config_survival_settings(self): def test_config_values_are_reasonable(self): """Test that config values are within reasonable ranges""" config = Config() - + # Grace period should be positive assert config.earlyGameGracePeriod > 0 assert config.earlyGameGracePeriod <= 1000 # Not too long - + # Damage reduction should be between 0 and 1 assert 0 <= config.playerDamageReduction < 1 - + # Ensure other config values still exist - assert hasattr(config, 'godMode') - assert hasattr(config, 'maxTicks') - assert hasattr(config, 'tickLength') + assert hasattr(config, "godMode") + assert hasattr(config, "maxTicks") + assert hasattr(config, "tickLength") class TestGracePeriodMechanics: """Test suite for grace period and attack avoidance mechanics""" - + def test_grace_period_expiration(self): """Test that protection expires after grace period""" # Import Kreatures class only, not the module from kreatures import Kreatures - + # Mock input to avoid interactive prompt - with patch('builtins.input', return_value='TestPlayer'): - with patch('builtins.print'): # Suppress print output + with patch("builtins.input", return_value="TestPlayer"): + with patch("builtins.print"): # Suppress print output game = Kreatures() - + # Player should start with protection - assert hasattr(game.playerCreature, 'damageReduction') + assert hasattr(game.playerCreature, "damageReduction") assert game.playerCreature.damageReduction > 0 - + # Simulate time passing beyond grace period game.tick = game.config.earlyGameGracePeriod + 1 game.updatePlayerProtection() - + # Protection should be removed assert game.playerCreature.damageReduction == 0 def test_grace_period_active_during_period(self): """Test that protection remains active during grace period""" from kreatures import Kreatures - - with patch('builtins.input', return_value='TestPlayer'): - with patch('builtins.print'): + + with patch("builtins.input", return_value="TestPlayer"): + with patch("builtins.print"): game = Kreatures() - + # Set tick to middle of grace period game.tick = game.config.earlyGameGracePeriod // 2 original_reduction = game.playerCreature.damageReduction - + game.updatePlayerProtection() - + # Protection should still be active assert game.playerCreature.damageReduction == original_reduction def test_attack_avoidance_during_grace_period(self): """Test that creatures avoid attacking player during grace period""" from kreatures import Kreatures - - with patch('builtins.input', return_value='TestPlayer'): - with patch('builtins.print'): + + with patch("builtins.input", return_value="TestPlayer"): + with patch("builtins.print"): game = Kreatures() - + # Create a mock entity that would attack the player attacker = LivingEntity("Attacker") - + # Mock the environment to return our attacker and player - with patch.object(game.environment, 'getEntities', return_value=[attacker]): - with patch.object(game.environment, 'getRandomEntity', return_value=game.playerCreature): - with patch.object(attacker, 'getNextAction', return_value='fight'): - + with patch.object(game.environment, "getEntities", return_value=[attacker]): + with patch.object( + game.environment, "getRandomEntity", return_value=game.playerCreature + ): + with patch.object(attacker, "getNextAction", return_value="fight"): + # Set game to be in grace period game.tick = 10 # Well within grace period - + # Mock random to always trigger attack avoidance (return value <= 85) - with patch('random.randint', return_value=50): # 50 <= 85, so attack should be avoided + with patch( + "random.randint", return_value=50 + ): # 50 <= 85, so attack should be avoided initial_log_length = len(attacker.log) game.initiateEntityActions() - + # Check that attacker decided not to attack assert len(attacker.log) > initial_log_length assert "decided not to attack" in attacker.log[-1] @@ -239,91 +245,93 @@ def test_attack_avoidance_during_grace_period(self): def test_attack_not_avoided_after_grace_period(self): """Test that attack avoidance doesn't apply after grace period ends""" from kreatures import Kreatures - - with patch('builtins.input', return_value='TestPlayer'): - with patch('builtins.print'): + + with patch("builtins.input", return_value="TestPlayer"): + with patch("builtins.print"): game = Kreatures() - + # Set game to be past grace period game.tick = game.config.earlyGameGracePeriod + 10 - + # Grace period check should return False assert game.tick >= game.config.earlyGameGracePeriod class TestPlayerInitialization: """Test suite for player initialization with protection""" - + def test_player_starts_with_protection(self): """Test that player creature is initialized with damage reduction""" from kreatures import Kreatures - - with patch('builtins.input', return_value='TestPlayer'): - with patch('builtins.print'): + + with patch("builtins.input", return_value="TestPlayer"): + with patch("builtins.print"): game = Kreatures() - + # Player should have damage reduction attribute - assert hasattr(game.playerCreature, 'damageReduction') + assert hasattr(game.playerCreature, "damageReduction") assert game.playerCreature.damageReduction == game.config.playerDamageReduction - + # Player should have protection message in log - protection_messages = [msg for msg in game.playerCreature.log if 'protection' in msg.lower()] + protection_messages = [ + msg for msg in game.playerCreature.log if "protection" in msg.lower() + ] assert len(protection_messages) > 0 def test_player_protection_message_logged(self): """Test that protection initialization is logged""" from kreatures import Kreatures - - with patch('builtins.input', return_value='TestHero'): - with patch('builtins.print'): + + with patch("builtins.input", return_value="TestHero"): + with patch("builtins.print"): game = Kreatures() - + # Check that protection message was added to log - assert any('early-game protection' in msg for msg in game.playerCreature.log) + assert any("early-game protection" in msg for msg in game.playerCreature.log) class TestEdgeCases: """Test suite for edge cases and error conditions""" - + def test_negative_damage_reduction(self): """Test behavior with negative damage reduction values""" - attacker = LivingEntity("Attacker") + LivingEntity("Attacker") defender = LivingEntity("Defender") - + # Give defender negative damage reduction (should not increase damage) defender.damageReduction = -0.2 - + original_health = defender.health - - with patch('random.randint', return_value=20): + + with patch("random.randint", return_value=20): damage = 20 # Apply damage reduction logic - if hasattr(defender, 'damageReduction') and defender.damageReduction > 0: + if hasattr(defender, "damageReduction") and defender.damageReduction > 0: damage = int(damage * (1 - defender.damageReduction)) damage = max(damage, 1) - + defender.health -= damage - + # Should take normal damage (20) since negative reduction is ignored assert defender.health == original_health - 20 def test_very_high_damage_with_reduction(self): """Test damage reduction with very high damage values""" - attacker = LivingEntity("Attacker") + LivingEntity("Attacker") defender = LivingEntity("Defender") - + defender.damageReduction = 0.8 # 80% reduction original_health = defender.health - + # Calculate expected result damage = 100 reduced_damage = int(damage * (1 - defender.damageReduction)) # Should be 20 reduced_damage = max(reduced_damage, 1) expected_health = original_health - reduced_damage - + # Apply the damage defender.health -= reduced_damage - + assert defender.health == expected_health # Verify significant damage reduction occurred (should be much less than original) assert reduced_damage < damage * 0.5 # Less than 50% of original damage @@ -331,45 +339,47 @@ def test_very_high_damage_with_reduction(self): def test_protection_expiration_logging(self): """Test that protection expiration is properly logged""" from kreatures import Kreatures - - with patch('builtins.input', return_value='TestPlayer'): - with patch('builtins.print'): + + with patch("builtins.input", return_value="TestPlayer"): + with patch("builtins.print"): game = Kreatures() - + # Clear existing logs to focus on expiration message game.playerCreature.log.clear() - + # Set up conditions for protection expiration game.tick = game.config.earlyGameGracePeriod game.playerCreature.damageReduction = 0.4 # Ensure protection is active - + game.updatePlayerProtection() - + # Check that expiration was logged assert len(game.playerCreature.log) > 0 - assert any('protection has worn off' in msg for msg in game.playerCreature.log) + assert any("protection has worn off" in msg for msg in game.playerCreature.log) class TestIntegrationScenarios: """Test suite for integration scenarios""" - + def test_protection_survives_multiple_attacks(self): """Test that player can survive multiple attacks during grace period""" player = LivingEntity("Player") player.damageReduction = 0.4 # 40% reduction player.health = 100 - + enemies = [LivingEntity(f"Enemy{i}") for i in range(3)] - + # Simulate multiple attacks with damage reduction for enemy in enemies: - with patch('random.randint', return_value=25): # 25 damage becomes ~15 with reduction + with patch( + "random.randint", return_value=25 + ): # 25 damage becomes ~15 with reduction if player.health > 0: damage = 25 reduced_damage = int(damage * (1 - player.damageReduction)) reduced_damage = max(reduced_damage, 1) player.health -= reduced_damage - + # Player should survive multiple attacks due to damage reduction assert player.health > 0 @@ -379,21 +389,21 @@ def test_protection_vs_no_protection_survival(self): protected_player = LivingEntity("ProtectedPlayer") protected_player.damageReduction = 0.4 protected_player.health = 80 - + # Unprotected player normal_player = LivingEntity("NormalPlayer") normal_player.health = 80 - + # Simulate same attacks on both - with patch('random.randint', return_value=20): + with patch("random.randint", return_value=20): # Attack protected player damage = 20 protected_damage = int(damage * (1 - protected_player.damageReduction)) protected_damage = max(protected_damage, 1) protected_player.health -= protected_damage - + # Attack normal player normal_player.health -= damage - + # Protected player should have more health remaining - assert protected_player.health > normal_player.health \ No newline at end of file + assert protected_player.health > normal_player.health