Implement atomic food claiming mechanism to prevent race conditions during consumption

This commit is contained in:
Sam 2025-11-09 17:09:14 -06:00
parent 78438ae768
commit 19b946949d
2 changed files with 32 additions and 4 deletions

View File

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

View File

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