diff --git a/world/objects.py b/world/objects.py index 7fe3754..b353e75 100644 --- a/world/objects.py +++ b/world/objects.py @@ -96,11 +96,27 @@ class FoodObject(BaseEntity): self.max_decay = 400 self.interaction_radius: int = 50 self.neighbors: int = 0 + self.claimed_this_tick = False # Track if food was claimed this tick self.flags: dict[str, bool] = { "death": False, "can_interact": True, } + def try_claim(self) -> bool: + """ + Atomically claims this food for consumption within the current tick. + + :return: True if successfully claimed, False if already claimed this tick + """ + if not self.claimed_this_tick: + self.claimed_this_tick = True + return True + return False + + def reset_claim(self) -> None: + """Reset the claim for next tick""" + self.claimed_this_tick = False + def tick(self, interactable: Optional[List[BaseEntity]] = None) -> Union["FoodObject", List["FoodObject"]]: """ Updates the food object, increasing decay and flagging for death if decayed. @@ -108,6 +124,9 @@ class FoodObject(BaseEntity): :param interactable: List of nearby entities (unused). :return: Self """ + # Reset claim at the beginning of each tick + self.reset_claim() + if interactable is None: interactable = [] @@ -300,8 +319,10 @@ class DefaultCell(BaseEntity): distance_to_food = get_distance_between_objects(self, food_object) if distance_to_food < self.max_visual_width and food_objects: - self.energy += 140 - food_object.flag_for_death() + # Use atomic consumption to prevent race conditions + if food_object.try_claim(): + self.energy += 140 + food_object.flag_for_death() return self if self.energy >= 1700: diff --git a/world/world.py b/world/world.py index dc479c5..5731985 100644 --- a/world/world.py +++ b/world/world.py @@ -170,6 +170,8 @@ class World: next_buffer: int = 1 - self.current_buffer self.buffers[next_buffer].clear() + # Food claims auto-reset in FoodObject.tick() + for obj_list in self.buffers[self.current_buffer].values(): for obj in obj_list: if obj.flags["death"]: @@ -178,14 +180,17 @@ class World: interactable = self.query_objects_within_radius( obj.position.x, obj.position.y, obj.interaction_radius ) - interactable.remove(obj) + # Create defensive copy to prevent shared state corruption + interactable = interactable.copy() + if obj in interactable: + interactable.remove(obj) new_obj = obj.tick(interactable) else: new_obj = obj.tick() if new_obj is None: continue - # reproduction code + # reproduction code - buffer new entities for atomic state transition if isinstance(new_obj, list): for item in new_obj: if isinstance(item, BaseEntity): @@ -194,6 +199,8 @@ class World: else: cell = self._hash_position(new_obj.position) self.buffers[next_buffer][cell].append(new_obj) + + # Atomic buffer switch - all state changes become visible simultaneously self.current_buffer = next_buffer def add_object(self, new_object: BaseEntity) -> None: