From ee2af7e6325690599e5bf7a276eb74a85d804b08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 03:39:55 +0000 Subject: [PATCH 1/4] Initial plan From f5ae5938dafca4d7b4f31fd77e7146b8bcd88912 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 03:47:36 +0000 Subject: [PATCH 2/4] Implement early-game survival mechanism with damage reduction and attack avoidance Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/config/config.py | 5 ++ src/entity/livingEntity.py | 10 ++++ src/kreatures.py | 34 ++++++++++++- tests/test_survival_mechanism.py | 86 ++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 tests/test_survival_mechanism.py diff --git a/src/config/config.py b/src/config/config.py index d1cbdc1..749f8ff 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -9,3 +9,8 @@ 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.25 # 25% damage reduction for player during grace period + # During grace period, other creatures have 75% chance to avoid attacking player diff --git a/src/entity/livingEntity.py b/src/entity/livingEntity.py index c5c2efb..7e6233d 100644 --- a/src/entity/livingEntity.py +++ b/src/entity/livingEntity.py @@ -57,6 +57,11 @@ def fight(self, kreature): # This creature attacks first 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: + 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( @@ -80,6 +85,11 @@ def fight(self, kreature): # Target creature counter-attacks if still alive 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: + 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( diff --git a/src/kreatures.py b/src/kreatures.py index a6838f7..a20b7ed 100644 --- a/src/kreatures.py +++ b/src/kreatures.py @@ -37,6 +37,10 @@ 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) def initiateEntityActions(self): entities_to_remove = [] # Track entities that die this turn @@ -59,8 +63,18 @@ def initiateEntityActions(self): entity.decreaseChanceToFight() self.createChildEntity(parents[0], parents[1]) elif decision == "fight": - if target == self.playerCreature and self.config.godMode: - continue + # Enhanced protection: during grace period, reduce attacks on player + if target == self.playerCreature: + if self.config.godMode: + continue + # During grace period, 75% chance to skip attacking the player + if (self.tick < self.config.earlyGameGracePeriod and + random.randint(1, 100) <= 75): + entity.log.append( + "%s decided not to attack %s." % (entity.name, target.name) + ) + continue + entity.increaseChanceToFight() entity.decreaseChanceToBefriend() entity.fight(target) @@ -78,6 +92,14 @@ def initiateEntityActions(self): for entity in entities_to_remove: self.environment.removeEntity(entity) + 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: + self.playerCreature.damageReduction = 0 + self.playerCreature.log.append("%s's protection has worn off!" % self.playerCreature.name) + def regenerateAllEntities(self): """Regenerate health for all living entities""" for entity in self.environment.getEntities(): @@ -186,6 +208,12 @@ 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: + protection_percent = int(self.playerCreature.damageReduction * 100) + 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)." @@ -198,6 +226,7 @@ def printSummary(self): else: print("%s died during the simulation." % self.playerCreature.name) print("Kreatures still alive: %d" % self.environment.getNumEntities()) + print("Simulation ran for %d ticks." % self.tick) def printStats(self): print("=== Stats ===") @@ -224,6 +253,7 @@ def run(self): pass self.initiateEntityActions() + self.updatePlayerProtection() # Update player protection status self.regenerateAllEntities() # Regenerate health for all entities time.sleep(self.config.tickLength) self.tick += 1 diff --git a/tests/test_survival_mechanism.py b/tests/test_survival_mechanism.py new file mode 100644 index 0000000..c5376cd --- /dev/null +++ b/tests/test_survival_mechanism.py @@ -0,0 +1,86 @@ +# Copyright (c) 2022 Daniel McCoy Stephenson +# Apache License 2.0 +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +import pytest +from entity.livingEntity import LivingEntity +from config.config import Config + + +def test_damage_reduction_applied(): + """Test that damage reduction is applied correctly during fights""" + # Create two creatures + attacker = LivingEntity("Attacker") + defender = LivingEntity("Defender") + + # Give defender damage reduction + defender.damageReduction = 0.25 # 25% damage reduction + + # Record initial health + initial_health = defender.health + + # Simulate a single attack (we'll patch the random damage to be predictable) + import random + 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 15 + + # 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 == 15 # 20 * 0.75 = 15 + + finally: + # Restore original random function + random.randint = original_randint + + +def test_config_survival_settings(): + """Test that config includes the new survival settings""" + config = Config() + + # Check that early game settings exist + assert hasattr(config, 'earlyGameGracePeriod') + assert hasattr(config, 'playerDamageReduction') + + # Check default values + assert config.earlyGameGracePeriod == 50 + assert config.playerDamageReduction == 0.25 + + +def test_minimum_damage(): + """Test that damage reduction doesn't reduce damage below 1""" + 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 + import random + 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 \ No newline at end of file From c4fe31c80129c45c679dfaf9db1c96a724928468 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 03:51:41 +0000 Subject: [PATCH 3/4] Fine-tune early-game protection: 40% damage reduction, 85% attack avoidance Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/config/config.py | 4 ++-- src/kreatures.py | 4 ++-- tests/test_survival_mechanism.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index 749f8ff..c8b4090 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -12,5 +12,5 @@ def __init__(self): # Early-game survival settings self.earlyGameGracePeriod = 50 # Number of ticks of protection for player - self.playerDamageReduction = 0.25 # 25% damage reduction for player during grace period - # During grace period, other creatures have 75% chance to avoid attacking player + 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/kreatures.py b/src/kreatures.py index a20b7ed..748c50c 100644 --- a/src/kreatures.py +++ b/src/kreatures.py @@ -67,9 +67,9 @@ def initiateEntityActions(self): if target == self.playerCreature: if self.config.godMode: continue - # During grace period, 75% chance to skip attacking the player + # During grace period, 85% chance to skip attacking the player if (self.tick < self.config.earlyGameGracePeriod and - random.randint(1, 100) <= 75): + random.randint(1, 100) <= 85): entity.log.append( "%s decided not to attack %s." % (entity.name, target.name) ) diff --git a/tests/test_survival_mechanism.py b/tests/test_survival_mechanism.py index c5376cd..8eca096 100644 --- a/tests/test_survival_mechanism.py +++ b/tests/test_survival_mechanism.py @@ -16,7 +16,7 @@ def test_damage_reduction_applied(): defender = LivingEntity("Defender") # Give defender damage reduction - defender.damageReduction = 0.25 # 25% damage reduction + defender.damageReduction = 0.4 # 40% damage reduction # Record initial health initial_health = defender.health @@ -30,7 +30,7 @@ def test_damage_reduction_applied(): # 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 15 + reduced_damage = int(damage * (1 - defender.damageReduction)) # Should be 12 # Apply damage manually to test calculation defender.health -= reduced_damage @@ -38,7 +38,7 @@ def test_damage_reduction_applied(): # Verify damage reduction worked expected_health = original_health - reduced_damage assert defender.health == expected_health - assert reduced_damage == 15 # 20 * 0.75 = 15 + assert reduced_damage == 12 # 20 * 0.6 = 12 finally: # Restore original random function @@ -55,7 +55,7 @@ def test_config_survival_settings(): # Check default values assert config.earlyGameGracePeriod == 50 - assert config.playerDamageReduction == 0.25 + assert config.playerDamageReduction == 0.4 def test_minimum_damage(): From 3d3dffacb133a1d7fecb537e06bad65b3510f1be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 04:03:49 +0000 Subject: [PATCH 4/4] Expand unit tests with comprehensive coverage of survival mechanism Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- .coverage | Bin 53248 -> 53248 bytes src/kreatures.py | 5 +- tests/test_survival_mechanism.py | 427 ++++++++++++++++++++++++++----- 3 files changed, 373 insertions(+), 59 deletions(-) diff --git a/.coverage b/.coverage index 66a94602844da855f91c1ef3a49207d53d13bfa5..a378a20b7f21a98c9314e8fb56f256b8ce681f08 100644 GIT binary patch delta 778 zcmZ9J&r4KM6vywc_v1d_eP_nNej!b?hyrI^6pcQ!QAqOkf#)V8Lfd`@EHIpEHl|mp29~)xq+x zB82#g5qJ(a;Y6?zJPU3GooYkPs|j^hG3;LSS>D&VLY}3Y^u1!5Wjx(eY}++jt_)pD zMQd`T-XS@u>&fEL=4+LKQtf(Wpo;1#SCDaPWL@rdn)=Jbmj|z)9y&VL9kPsVCh2m) z_Nui~&2(ff4aFo%frVK{U_pjuIf_}9XNZy1KMO4^q&Og{pA?!CUSxq2)fHsaCd@+} z(!r*Bp?EMG+*Wtc!jiAUi)SpqgO3`Rogu!hh17GA*|Xt)a#P(x?uVa)dgmHWIs@BaLL*w*Dp%wl%qfbHTY zu;^h2V~<>aO33Qax%mCvWO6pa2#LjTG9fO0jf7bo+jNQsw=L7j>0W{H7)x)B(K1WK9U+$4d4#P*ViALiA%&6=w9#%}AmjxM zN=p7unQjag7{$2q;UdIM6{{ zA(3k$G;v@5#AVCTw{e?vO6HOlmyQ-)#}>p81nJ;n66jQx^t0q7LIokvr?P6dp+W5G zhGe*9Lom!X(In~hdsj))9E?IsFS_BDms< 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): + 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 + assert not attacker.isAlive() or not defender.isAlive() -def test_config_survival_settings(): - """Test that config includes the new survival settings""" - config = Config() - - # Check that early game settings exist - assert hasattr(config, 'earlyGameGracePeriod') - assert hasattr(config, 'playerDamageReduction') + +class TestConfigurationSettings: + """Test suite for configuration settings""" - # Check default values - assert config.earlyGameGracePeriod == 50 - assert config.playerDamageReduction == 0.4 + 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') + + # Check default values + assert config.earlyGameGracePeriod == 50 + assert config.playerDamageReduction == 0.4 + 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') -def test_minimum_damage(): - """Test that damage reduction doesn't reduce damage below 1""" - attacker = LivingEntity("Attacker") - defender = LivingEntity("Defender") + +class TestGracePeriodMechanics: + """Test suite for grace period and attack avoidance mechanics""" - # Give defender very high damage reduction - defender.damageReduction = 0.99 # 99% damage reduction + 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 + game = Kreatures() + + # Player should start with protection + 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'): + 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'): + 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'): + + # 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 + 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] + + 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'): + 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""" - # Test that even with high reduction, at least 1 damage is dealt - import random - original_randint = random.randint - random.randint = lambda a, b: 1 # Minimum damage + 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'): + game = Kreatures() + + # Player should have damage reduction attribute + 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()] + 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'): + game = Kreatures() + + # Check that protection message was added to log + assert any('early-game protection' in msg for msg in game.playerCreature.log) + + +class TestEdgeCases: + """Test suite for edge cases and error conditions""" - try: + def test_negative_damage_reduction(self): + """Test behavior with negative damage reduction values""" + 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): + damage = 20 + # Apply damage reduction logic + 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") + defender = LivingEntity("Defender") + + defender.damageReduction = 0.8 # 80% reduction 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 + # 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 - # Verify at least 1 damage was dealt - assert defender.health == original_health - 1 + 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 + + 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'): + 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) + + +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 + 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 + + def test_protection_vs_no_protection_survival(self): + """Test survival comparison between protected and unprotected players""" + # Protected player + 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): + # 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 - finally: - random.randint = original_randint \ No newline at end of file + # Protected player should have more health remaining + assert protected_player.health > normal_player.health \ No newline at end of file