From b1d0d9a4390ac1ac11f0af2506ea9e96e67cc3f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 04:13:36 +0000 Subject: [PATCH 1/5] Initial plan From bb1b90fb4a446a97ee1f88eb8cc3fa1a2578fb1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 04:23:15 +0000 Subject: [PATCH 2/5] Implement comprehensive lag prevention mechanisms Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/config/config.py | 5 + src/entity/livingEntity.py | 33 +++--- src/kreatures.py | 45 ++++++-- src/world/world.py | 30 ++++- tests/test_lag_prevention.py | 214 +++++++++++++++++++++++++++++++++++ 5 files changed, 306 insertions(+), 21 deletions(-) create mode 100644 tests/test_lag_prevention.py diff --git a/src/config/config.py b/src/config/config.py index c8b4090..dcabbd7 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -14,3 +14,8 @@ def __init__(self): self.earlyGameGracePeriod = 50 # Number of ticks of protection for 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 + + # Population control settings to prevent lag + self.maxEntities = 100 # Maximum number of entities allowed in simulation + self.entityCullThreshold = 0.9 # Cull entities when population reaches 90% of max + self.entityLogMaxSize = 50 # Maximum number of log entries per entity to prevent memory bloat diff --git a/src/entity/livingEntity.py b/src/entity/livingEntity.py index 7e6233d..927483f 100644 --- a/src/entity/livingEntity.py +++ b/src/entity/livingEntity.py @@ -44,8 +44,8 @@ def getNextAction(self, kreature): return "befriend" def reproduce(self, kreature): - self.log.append("%s made a baby with %s!" % (self.name, kreature.name)) - kreature.log.append("%s made a baby with %s!" % (kreature.name, self.name)) + self.addLogEntry("%s made a baby with %s!" % (self.name, kreature.name)) + kreature.addLogEntry("%s made a baby with %s!" % (kreature.name, self.name)) self.stats.numOffspring += 1 kreature.stats.numOffspring += 1 # Return the parent entities so the child can be created with proper references @@ -64,20 +64,20 @@ def fight(self, kreature): kreature.health -= damage if kreature.health <= 0: - self.log.append( + self.addLogEntry( "%s fought and ate %s!" % (self.name, kreature.name) ) - kreature.log.append( + kreature.addLogEntry( "%s was eaten by %s!" % (kreature.name, self.name) ) self.stats.numCreaturesEaten += 1 break else: - self.log.append( + self.addLogEntry( "%s fought %s and dealt %d damage!" % (self.name, kreature.name, damage) ) - kreature.log.append( + kreature.addLogEntry( "%s took %d damage from %s! Health: %d" % (kreature.name, damage, self.name, kreature.health) ) @@ -92,25 +92,25 @@ def fight(self, kreature): self.health -= damage if self.health <= 0: - kreature.log.append( + kreature.addLogEntry( "%s fought and ate %s!" % (kreature.name, self.name) ) - self.log.append("%s was eaten by %s!" % (self.name, kreature.name)) + self.addLogEntry("%s was eaten by %s!" % (self.name, kreature.name)) kreature.stats.numCreaturesEaten += 1 break else: - kreature.log.append( + kreature.addLogEntry( "%s fought %s and dealt %d damage!" % (kreature.name, self.name, damage) ) - self.log.append( + self.addLogEntry( "%s took %d damage from %s! Health: %d" % (self.name, damage, kreature.name, self.health) ) def befriend(self, kreature): - self.log.append("%s made friends with %s!" % (self.name, kreature.name)) - kreature.log.append("%s made friends with %s!" % (kreature.name, self.name)) + self.addLogEntry("%s made friends with %s!" % (self.name, kreature.name)) + kreature.addLogEntry("%s made friends with %s!" % (kreature.name, self.name)) self.friends.append(kreature) kreature.friends.append( self @@ -159,7 +159,14 @@ def regenerateHealth(self): self.health = min(self.health + regeneration, self.maxHealth) # Only log significant regeneration events to avoid spam if regeneration >= 2: - self.log.append( + self.addLogEntry( "%s regenerated %d health! Health: %d/%d" % (self.name, regeneration, self.health, self.maxHealth) ) + + def addLogEntry(self, message, maxLogSize=50): + """Add a log entry and maintain log size limit""" + self.log.append(message) + # Keep only the most recent entries to prevent memory bloat + if len(self.log) > maxLogSize: + self.log = self.log[-maxLogSize:] diff --git a/src/kreatures.py b/src/kreatures.py index 8c5ed43..17ef9f8 100644 --- a/src/kreatures.py +++ b/src/kreatures.py @@ -40,7 +40,7 @@ def __init__(self): # 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.addLogEntry("%s has early-game protection!" % self.playerCreature.name) def initiateEntityActions(self): entities_to_remove = [] # Track entities that die this turn @@ -48,13 +48,13 @@ def initiateEntityActions(self): for entity in self.environment.getEntities(): target = self.environment.getRandomEntity() - if target == entity: + if target == entity or target is None: continue decision = entity.getNextAction(target) if decision == "nothing": - entity.log.append( + entity.addLogEntry( "%s had an argument with %s!" % (entity.name, target.name) ) elif decision == "love": @@ -70,7 +70,7 @@ def initiateEntityActions(self): # During grace period, 85% chance to skip attacking the player if (self.tick < self.config.earlyGameGracePeriod and random.randint(1, 100) <= 85): - entity.log.append( + entity.addLogEntry( "%s decided not to attack %s." % (entity.name, target.name) ) continue @@ -91,6 +91,9 @@ def initiateEntityActions(self): # Remove all entities that died this turn for entity in entities_to_remove: self.environment.removeEntity(entity) + + # Manage population to prevent lag + self.managePopulation() def updatePlayerProtection(self): """Update player protection based on current tick""" @@ -98,7 +101,25 @@ def updatePlayerProtection(self): # 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) + self.playerCreature.addLogEntry("%s's protection has worn off!" % self.playerCreature.name) + + def managePopulation(self): + """Manage entity population to prevent performance issues""" + current_count = self.environment.getNumEntities() + + # Check if we need to cull entities + cull_threshold = int(self.config.maxEntities * self.config.entityCullThreshold) + + if current_count > cull_threshold: + target_count = int(self.config.maxEntities * 0.7) # Reduce to 70% of max + removed_entities = self.environment.cullWeakestEntities(target_count, self.playerCreature) + + if removed_entities: + print(f"Population management: Removed {len(removed_entities)} weak entities (Population: {current_count} -> {self.environment.getNumEntities()})") + + def canCreateNewEntity(self): + """Check if we can create a new entity without exceeding limits""" + return self.environment.getNumEntities() < self.config.maxEntities def regenerateAllEntities(self): """Regenerate health for all living entities""" @@ -107,11 +128,20 @@ def regenerateAllEntities(self): entity.regenerateHealth() def createEntity(self): + if not self.canCreateNewEntity(): + return None newEntity = LivingEntity(self.names[random.randint(0, len(self.names) - 1)]) self.environment.addEntity(newEntity) + return newEntity def createChildEntity(self, parent1, parent2): """Create a child entity with proper parent-child relationships""" + if not self.canCreateNewEntity(): + # Population limit reached, no new child can be created + parent1.addLogEntry(f"{parent1.name} and {parent2.name} tried to have a child, but the world is too crowded!") + parent2.addLogEntry(f"{parent1.name} and {parent2.name} tried to have a child, but the world is too crowded!") + return None + childName = self.names[random.randint(0, len(self.names) - 1)] child = LivingEntity(childName) @@ -130,8 +160,9 @@ def createChildEntity(self, parent1, parent2): child.health = parentHealthAvg + random.randint(-10, 10) # Add some variation child.maxHealth = child.health - child.log.append( - "%s is the child of %s and %s." % (childName, parent1.name, parent2.name) + child.addLogEntry( + "%s is the child of %s and %s." % (childName, parent1.name, parent2.name), + self.config.entityLogMaxSize ) self.environment.addEntity(child) diff --git a/src/world/world.py b/src/world/world.py index 7cb7924..6bec90b 100644 --- a/src/world/world.py +++ b/src/world/world.py @@ -23,7 +23,6 @@ def __init__(self): self.Jasper = LivingEntity("Jasper") self.starterEntities = [ - "placeholder", self.Alison, self.Barry, self.Conrad, @@ -52,4 +51,33 @@ def getEntities(self): return self.entities def getRandomEntity(self): + if len(self.entities) == 0: + return None return self.entities[random.randint(0, len(self.entities) - 1)] + + def cullWeakestEntities(self, targetCount, protectedEntity=None): + """Remove the weakest entities to reduce population to targetCount""" + if len(self.entities) <= targetCount: + return [] + + # Create list of entities that can be culled (excluding protected entity) + cullable_entities = [e for e in self.entities if e != protectedEntity and hasattr(e, 'health')] + + if len(cullable_entities) == 0: + return [] + + # Sort by health (weakest first), then by number of children (fewer children first) + cullable_entities.sort(key=lambda x: (x.health, len(getattr(x, 'children', [])))) + + # Calculate how many to remove + entities_to_remove = len(self.entities) - targetCount + entities_to_remove = min(entities_to_remove, len(cullable_entities)) + + # Remove the weakest entities + removed_entities = [] + for i in range(entities_to_remove): + entity = cullable_entities[i] + self.removeEntity(entity) + removed_entities.append(entity) + + return removed_entities diff --git a/tests/test_lag_prevention.py b/tests/test_lag_prevention.py new file mode 100644 index 0000000..10e14af --- /dev/null +++ b/tests/test_lag_prevention.py @@ -0,0 +1,214 @@ +# 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 +import random +from unittest.mock import patch, MagicMock +from entity.livingEntity import LivingEntity +from world.world import World +from config.config import Config + + +class TestPopulationControl: + """Test suite for population management and lag prevention""" + + def test_config_has_population_settings(self): + """Test that config includes population control settings""" + config = Config() + assert hasattr(config, 'maxEntities') + assert hasattr(config, 'entityCullThreshold') + assert hasattr(config, 'entityLogMaxSize') + assert config.maxEntities > 0 + assert 0 < config.entityCullThreshold < 1 + + def test_entity_log_size_management(self): + """Test that entity logs are kept within size limits""" + entity = LivingEntity("TestEntity") + + # Add many log entries + for i in range(100): + entity.addLogEntry(f"Log entry {i}", maxLogSize=10) + + # Should only keep the most recent 10 entries + assert len(entity.log) == 10 + assert "Log entry 99" in entity.log[-1] + assert "Log entry 90" in entity.log[0] + + def test_world_cull_weakest_entities(self): + """Test that world can cull weakest entities""" + world = World() + + # Clear initial entities and add test entities with different health + world.entities = [] + strong_entity = LivingEntity("Strong") + strong_entity.health = 100 + weak_entity1 = LivingEntity("Weak1") + weak_entity1.health = 10 + weak_entity2 = LivingEntity("Weak2") + weak_entity2.health = 20 + + world.addEntity(strong_entity) + world.addEntity(weak_entity1) + world.addEntity(weak_entity2) + + # Cull to 1 entity + removed = world.cullWeakestEntities(1) + + assert len(removed) == 2 + assert len(world.entities) == 1 + assert world.entities[0] == strong_entity + + def test_world_cull_protects_player(self): + """Test that culling protects the player entity""" + world = World() + world.entities = [] + + player = LivingEntity("Player") + player.health = 10 # Very weak + other1 = LivingEntity("Other1") + other1.health = 50 + other2 = LivingEntity("Other2") + other2.health = 60 + + world.addEntity(player) + world.addEntity(other1) + world.addEntity(other2) + + # Cull to 1, but protect player + removed = world.cullWeakestEntities(1, protectedEntity=player) + + assert len(world.entities) == 1 + assert player in world.entities + assert len(removed) == 2 + + def test_world_get_random_entity_empty(self): + """Test that getRandomEntity handles empty entity list""" + world = World() + world.entities = [] + + result = world.getRandomEntity() + assert result is None + + +class TestLagPreventionIntegration: + """Test suite for integration scenarios with lag prevention""" + + def test_child_creation_respects_population_limit(self): + """Test that child creation is blocked when population limit is reached""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + + # World starts with 10 entities + player = 11, so set limit to current count + initial_count = game.environment.getNumEntities() + game.config.maxEntities = initial_count # Set limit to current population + + # Try to create a child - should fail because we're at the limit + parent1 = LivingEntity("Parent1") + parent2 = LivingEntity("Parent2") + + child = game.createChildEntity(parent1, parent2) + assert child is None + + # Population should stay at limit + assert game.environment.getNumEntities() == game.config.maxEntities + + def test_population_management_triggers_culling(self): + """Test that population management triggers culling when needed""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + + # Set low limits to trigger culling + game.config.maxEntities = 10 + game.config.entityCullThreshold = 0.8 # Cull at 8 entities + + # Add entities to trigger culling threshold + while game.environment.getNumEntities() < 9: # Above threshold + entity = LivingEntity("TestEntity") + entity.health = random.randint(10, 100) + game.environment.addEntity(entity) + + initial_count = game.environment.getNumEntities() + + # Trigger population management + game.managePopulation() + + # Should have fewer entities now + final_count = game.environment.getNumEntities() + assert final_count < initial_count + assert final_count <= int(game.config.maxEntities * 0.7) # Target is 70% of max + + def test_can_create_new_entity_logic(self): + """Test the logic for determining if new entities can be created""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + + # Set limit higher than current count to allow creation + current_count = game.environment.getNumEntities() + game.config.maxEntities = current_count + 5 + + # Should be able to create when under limit + assert game.canCreateNewEntity() == True + + # Set limit to current count to block creation + game.config.maxEntities = current_count + + # Should not be able to create when at limit + assert game.canCreateNewEntity() == False + + +class TestPerformanceOptimizations: + """Test suite for performance-related optimizations""" + + def test_entity_addlogentry_uses_config_limit(self): + """Test that addLogEntry respects the configured log size limit""" + config = Config() + entity = LivingEntity("TestEntity") + + # Add more entries than the config limit + for i in range(config.entityLogMaxSize + 20): + entity.addLogEntry(f"Entry {i}", maxLogSize=config.entityLogMaxSize) + + # Should not exceed the configured limit + assert len(entity.log) <= config.entityLogMaxSize + + def test_empty_entity_list_handling(self): + """Test that empty entity lists are handled gracefully""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + + # Clear all entities except player + game.environment.entities = [game.playerCreature] + + # Should handle empty interactions gracefully + game.initiateEntityActions() # Should not crash + + assert len(game.environment.entities) == 1 # Player should remain + + +class TestWorldInitializationFix: + """Test suite for the World initialization bug fix""" + + def test_world_starts_without_placeholder(self): + """Test that World no longer includes 'placeholder' string in entities""" + world = World() + + # All entities should be LivingEntity instances, not strings + for entity in world.entities: + assert isinstance(entity, LivingEntity) + assert hasattr(entity, 'name') + assert hasattr(entity, 'health') \ No newline at end of file From 21490478c6c16ffc17f2ca684ecedb86b69e61e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 04:24:55 +0000 Subject: [PATCH 3/5] Implement comprehensive lag prevention mechanisms to prevent simulation performance degradation Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- .coverage | Bin 53248 -> 53248 bytes cov.xml | 450 +++++++++++++++++++++++++++++++++--------------------- 2 files changed, 277 insertions(+), 173 deletions(-) diff --git a/.coverage b/.coverage index a378a20b7f21a98c9314e8fb56f256b8ce681f08..abd75334093a6a71d05c55f281ae713529de2866 100644 GIT binary patch delta 418 zcmZozz}&Ead4s3}w>|G)-o3m{ydJzvn*{|P@l0m&ZD6wGntagLij|Xvp^!l7^(JTe2{W=!uJhC7F3&H@Nzn(Bn=Ab$GK%s3UGgE+#(~+f-6~fK(6Jq7CVXg)9Pz{gkRTRr) z;Qzt@j{iCTDgHzJJNbL~TllN_Gl7Qt^4AM+u`qHfu}hr$|L^>^x1z4kxY;>@A_8m; z@@yPH1|KVW?p|9&y{J^a7;Kl5J%I&=ZQG{{^=b}lf*#tEiaIlvUxYlIXIa>7IJbd HT3`SGqv2?g delta 381 zcmYk0KS)AB9LMk8y?^)e-tS4M{Tb3J2$Y6~(n#Hk@YNWEN`q4kQ8v6Gc-h!sa9fLy zR+qpfMAD_Pe^YxcO*OQLf_CTB5AHs9@cn-H`2q0*au>->rP+XWS&eNnr4@EQLR=~M zO2*=OH>0glf;@iVF4>1Cm4oWpN#!(k>}{C5TgYOA_jXb|=}u!^gp;kNTgFj4!-miW z4_56V>oH=t?Q?c1#GfHfiayVk)-@|lJS?s3zm@WbViucxRLl!?u9RvuOZ$mv6+s_< z;0vDM5jw!(3TjXl!aXR5EEUr^Yvy@8Y9zM|Dt?acFX*Ot*cx6m1b1ow9-;4}?eI+u zUAN;3LXLyvnBw?xc^SHFsy@***(7*Y6KeF@Rrq ggN|4xfO+Y_mNkJ+C(z6ZD1UDxOjD6^YTO3Pe_Rb+4*&oF diff --git a/cov.xml b/cov.xml index 6801238..2e043c7 100644 --- a/cov.xml +++ b/cov.xml @@ -1,220 +1,260 @@ - + /home/runner/work/Kreatures/Kreatures/src - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - - - + + + - - - - - - + + + - + + + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + + - - - - - - + + + + + + + + - - + - - - + - - - - + - - + - + + + + + + + - + + - + + - - + - - + - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - - - + + + + + + + + + + - + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + @@ -229,90 +269,100 @@ - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - + - + - + - + + - + + + - + - - - - - + + + + + + + + + + + + + + - + - + - - - + + + - + @@ -322,15 +372,69 @@ - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 88692a2e806a91b6a0dbc7329185424f7207551a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 05:07:58 +0000 Subject: [PATCH 4/5] Implement dynamic max entities based on lag recognition Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/config/config.py | 8 +- src/kreatures.py | 50 ++++++++ tests/test_dynamic_entities.py | 217 +++++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 tests/test_dynamic_entities.py diff --git a/src/config/config.py b/src/config/config.py index dcabbd7..bb2be50 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -16,6 +16,12 @@ def __init__(self): # During grace period, other creatures have 85% chance to avoid attacking player # Population control settings to prevent lag - self.maxEntities = 100 # Maximum number of entities allowed in simulation + self.maxEntities = 50 # Starting maximum number of entities (will adjust dynamically) + self.minEntities = 20 # Minimum entities to maintain for gameplay + self.maxEntitiesLimit = 200 # Hard upper limit for entities self.entityCullThreshold = 0.9 # Cull entities when population reaches 90% of max self.entityLogMaxSize = 50 # Maximum number of log entries per entity to prevent memory bloat + + # Dynamic performance monitoring settings + self.lagThreshold = 0.05 # Tick time in seconds that indicates lag (50ms) + self.performanceWindow = 10 # Number of recent ticks to analyze for performance diff --git a/src/kreatures.py b/src/kreatures.py index 17ef9f8..609fa8f 100644 --- a/src/kreatures.py +++ b/src/kreatures.py @@ -38,6 +38,9 @@ def __init__(self): self.config = Config() self.tick = 0 + # Performance monitoring for dynamic entity limits + self.tickTimes = [] # Store recent tick times for lag detection + # Initialize player early-game protection self.playerCreature.damageReduction = self.config.playerDamageReduction self.playerCreature.addLogEntry("%s has early-game protection!" % self.playerCreature.name) @@ -223,6 +226,37 @@ def continueAsChild(self): return False + def monitorPerformance(self, tick_duration): + """Monitor tick performance and adjust max entities dynamically""" + # Track recent tick times + self.tickTimes.append(tick_duration) + + # Keep only recent performance window + if len(self.tickTimes) > self.config.performanceWindow: + self.tickTimes = self.tickTimes[-self.config.performanceWindow:] + + # Only adjust after we have some data + if len(self.tickTimes) >= 5: + avg_tick_time = sum(self.tickTimes) / len(self.tickTimes) + self.adjustMaxEntitiesBasedOnLag(avg_tick_time) + + def adjustMaxEntitiesBasedOnLag(self, avg_tick_time): + """Dynamically adjust max entities based on performance""" + current_max = self.config.maxEntities + + if avg_tick_time > self.config.lagThreshold: + # Performance is poor, reduce max entities + new_max = max(self.config.minEntities, int(current_max * 0.8)) + if new_max != current_max: + self.config.maxEntities = new_max + print(f"Performance lag detected (avg: {avg_tick_time:.3f}s). Reducing max entities to {new_max}") + elif avg_tick_time < self.config.lagThreshold * 0.5: + # Performance is good, cautiously increase max entities + new_max = min(self.config.maxEntitiesLimit, int(current_max * 1.1)) + if new_max != current_max and self.environment.getNumEntities() > current_max * 0.8: + self.config.maxEntities = new_max + print(f"Good performance (avg: {avg_tick_time:.3f}s). Increasing max entities to {new_max}") + def printSummary(self): print("=== Summary ===") if self.playerCreature.chanceToFight > self.playerCreature.chanceToBefriend: @@ -258,6 +292,12 @@ def printSummary(self): 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) + + # Show performance and dynamic entity limit info + if self.tickTimes: + avg_tick_time = sum(self.tickTimes) / len(self.tickTimes) + print("Average tick time: %.4f seconds" % avg_tick_time) + print("Final max entities limit: %d (started at 50)" % self.config.maxEntities) def printStats(self): print("=== Stats ===") @@ -283,9 +323,19 @@ def run(self): except: # if list is empty, just keep going pass + # Monitor performance and run simulation tick + tick_start_time = time.time() + self.initiateEntityActions() self.updatePlayerProtection() # Update player protection status self.regenerateAllEntities() # Regenerate health for all entities + + tick_end_time = time.time() + tick_duration = tick_end_time - tick_start_time + + # Track performance and adjust entity limits dynamically + self.monitorPerformance(tick_duration) + time.sleep(self.config.tickLength) self.tick += 1 if self.tick >= self.config.maxTicks: diff --git a/tests/test_dynamic_entities.py b/tests/test_dynamic_entities.py new file mode 100644 index 0000000..433d53f --- /dev/null +++ b/tests/test_dynamic_entities.py @@ -0,0 +1,217 @@ +# 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 unittest.mock import patch +from config.config import Config + + +class TestDynamicEntityLimits: + """Test suite for dynamic entity limit adjustment based on performance""" + + def test_config_has_dynamic_settings(self): + """Test that config includes dynamic performance settings""" + config = Config() + assert hasattr(config, 'maxEntities') + assert hasattr(config, 'minEntities') + assert hasattr(config, 'maxEntitiesLimit') + assert hasattr(config, 'lagThreshold') + assert hasattr(config, 'performanceWindow') + + # Check reasonable defaults + assert config.minEntities < config.maxEntities < config.maxEntitiesLimit + assert config.lagThreshold > 0 + assert config.performanceWindow > 0 + + def test_performance_monitoring_initialization(self): + """Test that Kreatures initializes performance monitoring""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + + assert hasattr(game, 'tickTimes') + assert isinstance(game.tickTimes, list) + assert len(game.tickTimes) == 0 + + def test_monitor_performance_tracks_times(self): + """Test that performance monitoring tracks tick times""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + + # Simulate some tick times + game.monitorPerformance(0.01) # Fast tick + game.monitorPerformance(0.02) # Another fast tick + game.monitorPerformance(0.03) # Slower tick + + assert len(game.tickTimes) == 3 + assert 0.01 in game.tickTimes + assert 0.02 in game.tickTimes + assert 0.03 in game.tickTimes + + def test_performance_window_limit(self): + """Test that performance monitoring respects window size""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + game.config.performanceWindow = 3 + + # Add more tick times than window size + for i in range(5): + game.monitorPerformance(0.01 * (i + 1)) + + # Should only keep the most recent window size + assert len(game.tickTimes) == 3 + assert game.tickTimes == [0.03, 0.04, 0.05] + + def test_adjust_max_entities_reduces_on_lag(self): + """Test that max entities is reduced when lag is detected""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print') as mock_print: + game = Kreatures() + initial_max = game.config.maxEntities + + # Simulate high lag (above threshold) + lag_time = game.config.lagThreshold * 2 # Double the threshold + game.adjustMaxEntitiesBasedOnLag(lag_time) + + # Max entities should be reduced + assert game.config.maxEntities < initial_max + assert game.config.maxEntities >= game.config.minEntities + + # Should print a message about reducing entities + mock_print.assert_called() + printed_text = str(mock_print.call_args) + assert "lag detected" in printed_text.lower() + assert "reducing" in printed_text.lower() + + def test_adjust_max_entities_increases_on_good_performance(self): + """Test that max entities is increased when performance is good""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print') as mock_print: + game = Kreatures() + initial_max = game.config.maxEntities + + # Add some entities to make the system consider increasing limit + from entity.livingEntity import LivingEntity + for i in range(int(initial_max * 0.9)): # Fill to 90% capacity + entity = LivingEntity(f"Entity{i}") + game.environment.addEntity(entity) + + # Simulate very good performance (well below threshold) + good_time = game.config.lagThreshold * 0.25 # Quarter of the threshold + game.adjustMaxEntitiesBasedOnLag(good_time) + + # Max entities should be increased (but not exceed limit) + assert game.config.maxEntities >= initial_max + assert game.config.maxEntities <= game.config.maxEntitiesLimit + + # Should print a message about increasing entities + mock_print.assert_called() + printed_text = str(mock_print.call_args) + assert "good performance" in printed_text.lower() + assert "increasing" in printed_text.lower() + + def test_max_entities_respects_hard_limits(self): + """Test that max entities never goes below min or above hard limit""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + + # Test lower bound - extreme lag + extreme_lag_time = game.config.lagThreshold * 10 + for _ in range(10): # Multiple adjustments + game.adjustMaxEntitiesBasedOnLag(extreme_lag_time) + + assert game.config.maxEntities >= game.config.minEntities + + # Reset and test upper bound - extreme good performance + game.config.maxEntities = game.config.maxEntitiesLimit - 10 + from entity.livingEntity import LivingEntity + for i in range(game.config.maxEntities): + entity = LivingEntity(f"Entity{i}") + game.environment.addEntity(entity) + + excellent_time = game.config.lagThreshold * 0.1 + for _ in range(10): # Multiple adjustments + game.adjustMaxEntitiesBasedOnLag(excellent_time) + + assert game.config.maxEntities <= game.config.maxEntitiesLimit + + def test_no_adjustment_with_insufficient_data(self): + """Test that no adjustment occurs without sufficient performance data""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + initial_max = game.config.maxEntities + + # Add only a few data points (less than 5) + game.monitorPerformance(0.1) # High lag time + game.monitorPerformance(0.1) # High lag time + + # Max entities should not change + assert game.config.maxEntities == initial_max + + +class TestDynamicEntityIntegration: + """Test suite for integration of dynamic entity limits with existing systems""" + + def test_dynamic_limits_work_with_population_management(self): + """Test that dynamic limits integrate with existing population management""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + + # Reduce max entities dynamically + game.config.maxEntities = 30 + + # Fill beyond the new limit + from entity.livingEntity import LivingEntity + for i in range(35): + entity = LivingEntity(f"Entity{i}") + game.environment.addEntity(entity) + + # Population management should respect the new dynamic limit + game.managePopulation() + + # Should have culled based on the new limit + assert game.environment.getNumEntities() <= game.config.maxEntities + + def test_child_creation_respects_dynamic_limits(self): + """Test that child creation uses the current dynamic max entities""" + from kreatures import Kreatures + + with patch('builtins.input', return_value='TestPlayer'): + with patch('builtins.print'): + game = Kreatures() + + # Dynamically reduce the limit to current population + current_pop = game.environment.getNumEntities() + game.config.maxEntities = current_pop + + # Try to create a child - should fail + from entity.livingEntity import LivingEntity + parent1 = LivingEntity("Parent1") + parent2 = LivingEntity("Parent2") + + child = game.createChildEntity(parent1, parent2) + assert child is None # Should be blocked by dynamic limit \ No newline at end of file From 6a4f32b3b0547346ec7f236169823384898949f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 05:36:55 +0000 Subject: [PATCH 5/5] Merge master branch and resolve conflicts in cov.xml Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- .gitignore | 7 +- src/config/names.json | 433 ++++++++++++++++++++++++++++++++++++++ src/kreatures.py | 53 +++-- tests/test_integration.py | 98 +++++++++ tests/test_names.py | 360 +++++++++++++++++++++++++++++++ 5 files changed, 932 insertions(+), 19 deletions(-) create mode 100644 src/config/names.json create mode 100644 tests/test_integration.py create mode 100644 tests/test_names.py diff --git a/.gitignore b/.gitignore index 7e99e36..102b2e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -*.pyc \ No newline at end of file +*.pyc +.coverage +cov.xml +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/src/config/names.json b/src/config/names.json new file mode 100644 index 0000000..6380d20 --- /dev/null +++ b/src/config/names.json @@ -0,0 +1,433 @@ +{ + "names": [ + "Jesse", + "Juan", + "Jose", + "Ralph", + "Jeremy", + "Bobby", + "Johnny", + "Douglas", + "Peter", + "Scott", + "Kyle", + "Billy", + "Terry", + "Randy", + "Adam", + "Alexander", + "Andrew", + "Anthony", + "Austin", + "Benjamin", + "Brandon", + "Brian", + "Christopher", + "Daniel", + "David", + "Edward", + "Ethan", + "Gabriel", + "George", + "Henry", + "Isaac", + "Jacob", + "James", + "Jason", + "John", + "Joseph", + "Joshua", + "Kevin", + "Lucas", + "Matthew", + "Michael", + "Nathan", + "Nicholas", + "Oliver", + "Patrick", + "Richard", + "Robert", + "Samuel", + "Stephen", + "Thomas", + "Timothy", + "Tyler", + "William", + "Zachary", + "Abigail", + "Alice", + "Amanda", + "Amy", + "Angela", + "Anna", + "Ashley", + "Barbara", + "Betty", + "Brenda", + "Brittany", + "Carol", + "Catherine", + "Charlotte", + "Christina", + "Christine", + "Deborah", + "Diana", + "Donna", + "Dorothy", + "Elizabeth", + "Emily", + "Emma", + "Grace", + "Hannah", + "Helen", + "Isabella", + "Jessica", + "Jennifer", + "Julie", + "Karen", + "Katherine", + "Kimberly", + "Laura", + "Linda", + "Lisa", + "Margaret", + "Maria", + "Marie", + "Mary", + "Michelle", + "Nancy", + "Nicole", + "Olivia", + "Patricia", + "Rachel", + "Rebecca", + "Ruth", + "Sandra", + "Sarah", + "Sharon", + "Sophia", + "Susan", + "Victoria", + "Aiden", + "Aria", + "Avery", + "Blake", + "Caleb", + "Chloe", + "Dakota", + "Elijah", + "Felix", + "Harper", + "Hunter", + "Ian", + "Jaxon", + "Liam", + "Luna", + "Madison", + "Mason", + "Maya", + "Mia", + "Noah", + "Owen", + "Parker", + "Quinn", + "Riley", + "Sage", + "Taylor", + "Zoe", + "Ahmed", + "Aiko", + "Carlos", + "Chen", + "Diego", + "Elena", + "Giovanni", + "Hassan", + "Ivan", + "Jin", + "Kai", + "Lucia", + "Mohammed", + "Natasha", + "Oscar", + "Pierre", + "Raj", + "Sofia", + "Takeshi", + "Valentina", + "Wei", + "Yuki", + "Zara", + "Aaron", + "Adrian", + "Adriana", + "Albert", + "Alicia", + "Allen", + "Allison", + "Andre", + "Andrea", + "Antonio", + "Ariana", + "Arthur", + "Ashton", + "Aubrey", + "Audrey", + "Autumn", + "Bella", + "Bernard", + "Bethany", + "Beverly", + "Brandi", + "Brooke", + "Bruce", + "Cameron", + "Camila", + "Carl", + "Carmen", + "Caroline", + "Cassandra", + "Cedric", + "Celeste", + "Charles", + "Chelsea", + "Chester", + "Christian", + "Claire", + "Clara", + "Clarence", + "Claudia", + "Clayton", + "Clifford", + "Colin", + "Colleen", + "Cooper", + "Courtney", + "Crystal", + "Curtis", + "Cynthia", + "Damian", + "Dana", + "Danielle", + "Darius", + "Darryl", + "Dawn", + "Dean", + "Debbie", + "Dennis", + "Derek", + "Desiree", + "Devin", + "Diane", + "Dominic", + "Donovan", + "Edgar", + "Edith", + "Edmund", + "Eduardo", + "Edwin", + "Eleanor", + "Elias", + "Ellen", + "Elsie", + "Emanuel", + "Emilia", + "Eric", + "Erica", + "Ernest", + "Eugene", + "Eva", + "Evan", + "Evelyn", + "Faith", + "Fernando", + "Florence", + "Floyd", + "Frances", + "Francisco", + "Frank", + "Frederick", + "Gabriela", + "Gareth", + "Garrett", + "Geoffrey", + "Gerald", + "Gilbert", + "Gina", + "Glenn", + "Gloria", + "Gordon", + "Graham", + "Grant", + "Gregory", + "Gwendolyn", + "Harold", + "Hazel", + "Heather", + "Hector", + "Herbert", + "Holly", + "Howard", + "Hugo", + "Iris", + "Jack", + "Jackie", + "Jackson", + "Jacqueline", + "Jake", + "Janet", + "Javier", + "Jeffery", + "Jenna", + "Jerome", + "Jillian", + "Joanna", + "Joel", + "Johnathan", + "Jonah", + "Jonathan", + "Jordan", + "Josiah", + "Joyce", + "Judith", + "Julian", + "Juliana", + "Julius", + "June", + "Justin", + "Kaitlyn", + "Keith", + "Kelvin", + "Kenneth", + "Kent", + "Kirk", + "Kristen", + "Lance", + "Larry", + "Lauren", + "Lawrence", + "Leah", + "Leonard", + "Lillian", + "Logan", + "Lorraine", + "Louis", + "Louise", + "Lucy", + "Luis", + "Luke", + "Lydia", + "Marcus", + "Marina", + "Mark", + "Marshall", + "Martin", + "Marvin", + "Mateo", + "Maureen", + "Maurice", + "Max", + "Melanie", + "Melissa", + "Miguel", + "Miranda", + "Mitchell", + "Monica", + "Morgan", + "Natalie", + "Neil", + "Nelson", + "Nora", + "Norman", + "Omar", + "Paige", + "Paul", + "Penny", + "Philip", + "Phyllis", + "Preston", + "Priscilla", + "Quentin", + "Raymond", + "Regina", + "Reginald", + "Renee", + "Ricardo", + "Rita", + "Roberto", + "Robin", + "Roger", + "Ronald", + "Rosa", + "Rose", + "Roy", + "Ruby", + "Russell", + "Ryan", + "Sabrina", + "Sally", + "Samantha", + "Sean", + "Sebastian", + "Sergio", + "Shane", + "Sheila", + "Shelby", + "Shirley", + "Simon", + "Stacy", + "Stanley", + "Stella", + "Stephanie", + "Steve", + "Steven", + "Stuart", + "Tamara", + "Tanya", + "Tara", + "Teresa", + "Theresa", + "Todd", + "Tony", + "Tracy", + "Trevor", + "Troy", + "Vanessa", + "Vernon", + "Victor", + "Vincent", + "Virginia", + "Walter", + "Warren", + "Wayne", + "Wendy", + "Wesley", + "Whitney" + ], + "metadata": { + "total_count": 403, + "description": "Names available for Kreatures entities", + "categories": { + "original": [ + "Jesse", + "Juan", + "Jose", + "Ralph", + "Jeremy", + "Bobby", + "Johnny", + "Douglas", + "Peter", + "Scott", + "Kyle", + "Billy", + "Terry", + "Randy", + "Adam" + ], + "traditional": "Names 16-150 approximately", + "modern": "Names 151-200 approximately", + "international": "Names 201+ approximately" + } + } +} \ No newline at end of file diff --git a/src/kreatures.py b/src/kreatures.py index 609fa8f..447b3c3 100644 --- a/src/kreatures.py +++ b/src/kreatures.py @@ -1,5 +1,7 @@ # Copyright (c) 2022 Daniel McCoy Stephenson # Apache License 2.0 +import json +import os import random import time from world.world import World @@ -11,24 +13,7 @@ class Kreatures: def __init__(self): self.environment = World() - - self.names = [ - "Jesse", - "Juan", - "Jose", - "Ralph", - "Jeremy", - "Bobby", - "Johnny", - "Douglas", - "Peter", - "Scott", - "Kyle", - "Billy", - "Terry", - "Randy", - "Adam", - ] + self.names = self._load_names() print("What would you like to name your kreature?") self.creatureName = input("> ") @@ -45,6 +30,38 @@ def __init__(self): self.playerCreature.damageReduction = self.config.playerDamageReduction self.playerCreature.addLogEntry("%s has early-game protection!" % self.playerCreature.name) + def _load_names(self): + """Load names from configuration file""" + try: + # Get the directory of this file to build the path to names.json + current_dir = os.path.dirname(os.path.abspath(__file__)) + names_file = os.path.join(current_dir, "config", "names.json") + + with open(names_file, "r") as f: + config = json.load(f) + return config["names"] + except (FileNotFoundError, KeyError, json.JSONDecodeError) as e: + # Fallback to a minimal set of names if config file is missing/corrupted + print(f"Warning: Could not load names from config file: {e}") + print("Using fallback names list.") + return [ + "Jesse", + "Juan", + "Jose", + "Ralph", + "Jeremy", + "Bobby", + "Johnny", + "Douglas", + "Peter", + "Scott", + "Kyle", + "Billy", + "Terry", + "Randy", + "Adam", + ] + def initiateEntityActions(self): entities_to_remove = [] # Track entities that die this turn diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..a7a6a01 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,98 @@ +# 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 TestKreaturesIntegration(unittest.TestCase): + """Integration test to verify the game works with expanded names""" + + @patch("builtins.input", return_value="TestPlayer") + @patch("builtins.print") + def setUp(self, mock_print, mock_input): + """Set up test environment""" + from kreatures import Kreatures + + self.kreatures = Kreatures() + + def test_create_multiple_entities_have_different_names(self): + """Test that creating multiple entities can result in different names""" + # Create several entities and collect their names + entity_names = set() + for _ in range(20): + self.kreatures.createEntity() + + # Get names of all entities (excluding player creature and starter entities) + # Filter out non-LivingEntity objects (like the "placeholder" string) + for entity in self.kreatures.environment.getEntities(): + if hasattr(entity, "name") and entity != self.kreatures.playerCreature: + entity_names.add(entity.name) + + # With 159 names available, we should see some variety + # Even with random selection, getting 20+ different names should be likely + self.assertGreater( + len(entity_names), 1, "Should have at least some name variety" + ) + + def test_child_entities_get_names_from_expanded_list(self): + """Test that child entities use names from the expanded list""" + parent1 = LivingEntity("Parent1") + parent2 = LivingEntity("Parent2") + + # Create multiple children to test name variety + child_names = set() + for _ in range(10): + child = self.kreatures.createChildEntity(parent1, parent2) + child_names.add(child.name) + # Remove child to avoid cluttering the environment + self.kreatures.environment.removeEntity(child) + + # All child names should be from our expanded list + for name in child_names: + self.assertIn(name, self.kreatures.names) + + # With 159 names, we should get some variety + self.assertGreaterEqual(len(child_names), 1) + + def test_world_starter_entities_still_work(self): + """Test that the world still initializes with starter entities correctly""" + entities = self.kreatures.environment.getEntities() + + # Should have player creature plus starter entities from world + self.assertGreater(len(entities), 1) + + # Verify starter entities have valid names + starter_names = [ + "Alison", + "Barry", + "Conrad", + "Derrick", + "Eric", + "Francis", + "Gary", + "Harry", + "Isabelle", + "Jasper", + ] + + # Filter out non-LivingEntity objects (like the "placeholder" string) + found_starter_names = [ + entity.name + for entity in entities + if hasattr(entity, "name") and entity.name in starter_names + ] + self.assertGreater( + len(found_starter_names), 0, "Should find some starter entities" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_names.py b/tests/test_names.py new file mode 100644 index 0000000..db04986 --- /dev/null +++ b/tests/test_names.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 TestNames(unittest.TestCase): + """Test that the names functionality works correctly""" + + @patch("builtins.input", return_value="TestPlayer") + @patch("builtins.print") + def setUp(self, mock_print, mock_input): + """Set up test environment""" + # Import Kreatures class only (not the module level instance) + from kreatures import Kreatures + + self.kreatures = Kreatures() + + def test_names_list_exists(self): + """Test that names list exists and has content""" + self.assertIsInstance(self.kreatures.names, list) + self.assertGreater(len(self.kreatures.names), 0) + + def test_names_are_strings(self): + """Test that all names are strings""" + for name in self.kreatures.names: + self.assertIsInstance(name, str) + self.assertGreater(len(name), 0) # Non-empty strings + + def test_create_entity_uses_name_from_list(self): + """Test that createEntity uses a name from the names list""" + entity = LivingEntity(self.kreatures.names[0]) + self.assertIn(entity.name, self.kreatures.names) + + def test_create_child_entity_uses_name_from_list(self): + """Test that createChildEntity uses a name from the names list""" + parent1 = LivingEntity("Parent1") + parent2 = LivingEntity("Parent2") + + child = self.kreatures.createChildEntity(parent1, parent2) + self.assertIn(child.name, self.kreatures.names) + + def test_names_contain_original_names(self): + """Test that original names are still present""" + original_names = [ + "Jesse", + "Juan", + "Jose", + "Ralph", + "Jeremy", + "Bobby", + "Johnny", + "Douglas", + "Peter", + "Scott", + "Kyle", + "Billy", + "Terry", + "Randy", + "Adam", + ] + for name in original_names: + self.assertIn(name, self.kreatures.names) + + def test_names_list_expanded(self): + """Test that names list has been significantly expanded""" + # Should be much larger than the original 15 names + self.assertGreater(len(self.kreatures.names), 400) + + def test_names_include_diverse_names(self): + """Test that expanded names include diverse/modern names""" + expected_new_names = [ + "Sophia", + "Liam", + "Emma", + "Noah", + "Olivia", + "Alexander", + "Isabella", + "Mason", + "Aria", + "Diego", + "Sofia", + "Ahmed", + "Chen", + "Elena", + "Hassan", + "Yuki", + "Zara", + ] + for name in expected_new_names: + self.assertIn(name, self.kreatures.names) + + def test_names_no_duplicates(self): + """Test that there are no duplicate names in the list""" + names_set = set(self.kreatures.names) + self.assertEqual( + len(names_set), + len(self.kreatures.names), + "Names list should not contain duplicates", + ) + + def test_names_proper_capitalization(self): + """Test that all names are properly capitalized""" + for name in self.kreatures.names: + self.assertTrue( + name[0].isupper(), f"Name '{name}' should start with uppercase letter" + ) + # Check that rest of name is lowercase (basic name format) + if len(name) > 1: + self.assertTrue( + name[1:].islower(), + f"Name '{name}' should have only first letter capitalized", + ) + + def test_names_length_validation(self): + """Test that all names have reasonable lengths""" + for name in self.kreatures.names: + self.assertGreaterEqual( + len(name), 2, f"Name '{name}' should be at least 2 characters long" + ) + self.assertLessEqual( + len(name), 15, f"Name '{name}' should be at most 15 characters long" + ) + + def test_names_alphabetic_characters(self): + """Test that all names contain only alphabetic characters""" + for name in self.kreatures.names: + self.assertTrue( + name.isalpha(), + f"Name '{name}' should contain only alphabetic characters", + ) + + def test_name_categories_present(self): + """Test that names from different categories are present""" + # Traditional male names + traditional_male = [ + "Alexander", + "Andrew", + "Benjamin", + "Christopher", + "Daniel", + "David", + "James", + "John", + "Michael", + "William", + ] + male_count = sum(1 for name in traditional_male if name in self.kreatures.names) + self.assertGreater(male_count, 5, "Should have multiple traditional male names") + + # Traditional female names + traditional_female = [ + "Elizabeth", + "Emily", + "Emma", + "Jessica", + "Jennifer", + "Katherine", + "Margaret", + "Mary", + "Patricia", + "Sarah", + ] + female_count = sum( + 1 for name in traditional_female if name in self.kreatures.names + ) + self.assertGreater( + female_count, 5, "Should have multiple traditional female names" + ) + + # International names + international = ["Ahmed", "Chen", "Diego", "Hassan", "Yuki", "Zara", "Sofia"] + intl_count = sum(1 for name in international if name in self.kreatures.names) + self.assertGreater(intl_count, 3, "Should have multiple international names") + + # Modern names + modern = ["Aiden", "Aria", "Blake", "Hunter", "Luna", "Mason", "Quinn", "Riley"] + modern_count = sum(1 for name in modern if name in self.kreatures.names) + self.assertGreater(modern_count, 3, "Should have multiple modern names") + + def test_random_name_selection_variety(self): + """Test that random name selection provides variety over multiple calls""" + import random + + # Seed random for reproducible results + random.seed(42) + + selected_names = set() + # Create many entities to test variety + for _ in range(50): + self.kreatures.createEntity() + + # Collect names of created entities (excluding starter entities and player) + starter_names = [ + "Alison", + "Barry", + "Conrad", + "Derrick", + "Eric", + "Francis", + "Gary", + "Harry", + "Isabelle", + "Jasper", + ] + + for entity in self.kreatures.environment.getEntities(): + if ( + hasattr(entity, "name") + and entity != self.kreatures.playerCreature + and entity.name not in starter_names + ): + selected_names.add(entity.name) + + # With 403 names and 50 entities, we should get reasonable variety + # Expect at least 10 different names (conservative estimate accounting for randomness) + self.assertGreaterEqual( + len(selected_names), + 10, + f"Should have reasonable variety in selected names. Got: {selected_names}", + ) + + def test_create_entity_always_uses_valid_name(self): + """Test that createEntity always selects a valid name from the list""" + # Test multiple entity creations + for _ in range(20): + initial_count = len(self.kreatures.environment.getEntities()) + self.kreatures.createEntity() + new_count = len(self.kreatures.environment.getEntities()) + + # Should have added exactly one entity + self.assertEqual(new_count, initial_count + 1) + + # Get the newly created entity + new_entity = self.kreatures.environment.getEntities()[-1] + if hasattr(new_entity, "name"): # Filter out placeholder strings + self.assertIn(new_entity.name, self.kreatures.names) + + def test_create_child_entity_always_uses_valid_name(self): + """Test that createChildEntity always selects a valid name from the list""" + parent1 = LivingEntity("TestParent1") + parent2 = LivingEntity("TestParent2") + + # Test multiple child creations + for _ in range(20): + child = self.kreatures.createChildEntity(parent1, parent2) + self.assertIn(child.name, self.kreatures.names) + # Clean up to avoid environment clutter + if child in self.kreatures.environment.getEntities(): + self.kreatures.environment.removeEntity(child) + + def test_name_selection_robustness(self): + """Test that name selection handles edge cases properly""" + # Verify names list is not empty (should never happen but good to test) + self.assertGreater(len(self.kreatures.names), 0) + + # Verify all indices in range are valid + import random + + for _ in range(100): # Test many random selections + random_index = random.randint(0, len(self.kreatures.names) - 1) + selected_name = self.kreatures.names[random_index] + self.assertIsInstance(selected_name, str) + self.assertGreater(len(selected_name), 0) + + def test_exact_name_count(self): + """Test that we have exactly the expected number of names""" + # Should be 403 names total (15 original + 388 new) + self.assertEqual(len(self.kreatures.names), 403) + + def test_name_distribution_across_alphabet(self): + """Test that names are distributed across different starting letters""" + first_letters = set() + for name in self.kreatures.names: + first_letters.add(name[0].upper()) + + # Should have names starting with various letters of the alphabet + # With 403 diverse names, we should have at least 20 different starting letters + self.assertGreaterEqual( + len(first_letters), + 20, + f"Names should start with diverse letters. Found: {sorted(first_letters)}", + ) + + def test_names_loaded_from_config_file(self): + """Test that names are successfully loaded from configuration file""" + # This test verifies that the _load_names method works correctly + self.assertIsInstance(self.kreatures.names, list) + self.assertEqual(len(self.kreatures.names), 403) + + # Verify that the names loaded match what we expect from the config file + expected_first_names = ["Jesse", "Juan", "Jose", "Ralph", "Jeremy"] + actual_first_names = self.kreatures.names[:5] + self.assertEqual(actual_first_names, expected_first_names) + + def test_config_file_fallback_behavior(self): + """Test fallback behavior when config file is missing or corrupted""" + from unittest.mock import patch + from kreatures import Kreatures + + # Test with missing config file + with patch("os.path.join") as mock_join: + mock_join.return_value = "/nonexistent/path/names.json" + with patch("builtins.input", return_value="TestPlayer"), patch( + "builtins.print" + ): + k = Kreatures() + # Should fallback to 15 original names + self.assertEqual(len(k.names), 15) + self.assertIn("Jesse", k.names) + self.assertIn("Adam", k.names) + + def test_config_file_json_structure(self): + """Test that the config file has the expected JSON structure""" + import json + import os + + # Get path to names.json + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + names_file = os.path.join(current_dir, "src", "config", "names.json") + + # Verify file exists + self.assertTrue(os.path.exists(names_file)) + + # Verify JSON structure + with open(names_file, "r") as f: + config = json.load(f) + + self.assertIn("names", config) + self.assertIn("metadata", config) + self.assertIsInstance(config["names"], list) + self.assertEqual(len(config["names"]), 403) + + # Verify metadata structure + metadata = config["metadata"] + self.assertIn("total_count", metadata) + self.assertIn("description", metadata) + self.assertEqual(metadata["total_count"], 403) + + def test_load_names_method_directly(self): + """Test the _load_names method directly""" + names = self.kreatures._load_names() + + self.assertIsInstance(names, list) + self.assertEqual(len(names), 403) + self.assertIn("Jesse", names) # First original name + self.assertIn("Whitney", names) # Last name in current list + + +if __name__ == "__main__": + unittest.main()