-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsheep.py
More file actions
339 lines (272 loc) · 12.5 KB
/
Copy pathsheep.py
File metadata and controls
339 lines (272 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# sheep.py - Individual sheep entity with autonomous flocking behavior
"""
The Sheep class represents a single sheep in our herding simulation.
Each sheep exhibits emergent flocking behavior through the combination of simple rules:
separation, cohesion, alignment, and response to herding pressure.
"""
import pygame
import random
from vector_utils import Vector2D
import config
class Sheep:
"""
Individual sheep with flocking behavior and response to herding.
Each sheep follows Craig Reynolds' boids algorithm with additional
response to dog herding pressure.
"""
def __init__(self, x, y, sheep_config):
"""
Initialize a sheep with position, movement characteristics, and flocking behavior.
Args:
x, y: Starting position coordinates
sheep_config: Dictionary containing all sheep configuration parameters
"""
try:
# Validate input coordinates
if not (isinstance(x, (int, float)) and isinstance(y, (int, float))):
raise ValueError(f"Invalid position coordinates: x={x}, y={y}")
# Initialize position using Vector2D
self.position = Vector2D(x, y)
# Validate position vector
if not hasattr(self.position, 'distance_to'):
raise ValueError(f"Position object missing required methods. Got: {type(self.position)}")
# Initialize movement vectors with validation
random_vec = Vector2D.random(-1, 1)
if not hasattr(random_vec, 'normalize'):
raise ValueError("Random vector generation failed")
random_speed = random.uniform(0.5, 1.5)
self.velocity = random_vec.normalize() * random_speed if random_vec.length() > 0 else Vector2D()
self.acceleration = Vector2D()
# Store and validate the configuration
self.sheep_config = sheep_config.copy() if sheep_config else {}
# Set parameters with defaults and validation
self.flocking_params = self.sheep_config.get('flocking_params', {
'separation': 0.1,
'alignment': 0.1,
'cohesion': 0.1,
'perception_radius': 50
})
self.fear_response = self.sheep_config.get('fear_response', {
'fear_radius': 100,
'fear_strength': 0.5,
'memory': 0.1 # Fear memory decay rate
})
# Physical properties with validation
self.size = float(self.sheep_config.get('size', 10))
self.max_speed = float(self.sheep_config.get('max_speed', 2.0))
self.max_force = float(self.sheep_config.get('max_force', 0.1))
# Validate physical properties
if self.size <= 0:
raise ValueError(f"Invalid size value: {self.size}")
if self.max_speed <= 0:
raise ValueError(f"Invalid max_speed value: {self.max_speed}")
if self.max_force <= 0:
raise ValueError(f"Invalid max_force value: {self.max_force}")
# Behavioral properties
self.fear_level = 0.0 # Range: 0.0 (calm) to 1.0 (panicked)
self.energy = 1.0 # Range: 0.0 (exhausted) to 1.0 (energetic)
# Visual trail for debugging
self.trail = []
self.max_trail_length = 20
# Debug information
debug_info = [
"\nDEBUG: New sheep created:",
f" - Position: ({x:.1f}, {y:.1f})",
f" - Initial velocity: ({self.velocity.x:.3f}, {self.velocity.y:.3f})",
f" - Size: {self.size}, Max speed: {self.max_speed}, Max force: {self.max_force}",
" - Flocking parameters:",
]
for param, value in self.flocking_params.items():
debug_info.append(f" {param}: {value}")
debug_info.extend([
" - Fear response parameters:",
f" fear_radius: {self.fear_response.get('fear_radius', 'N/A')}",
f" fear_strength: {self.fear_response.get('fear_strength', 'N/A')}",
f" memory: {self.fear_response.get('memory', 'N/A')}"
])
# Print formatted debug information
for line in debug_info:
print(line)
except Exception as e:
print(f"ERROR: Failed to initialize sheep: {str(e)}")
import traceback
traceback.print_exc()
raise
def update(self, flock, dog_position=None, dog_pressure=0.0):
"""
Update sheep position and behavior.
Args:
flock: List of all sheep in the simulation
dog_position: Position of the herding dog (Vector2D)
dog_pressure: Intensity of herding pressure (0.0 to 1.0)
"""
# Reset acceleration
self.acceleration = Vector2D()
# Calculate flocking forces
separation_force = self._separate(flock)
alignment_force = self._align(flock)
cohesion_force = self._cohesion(flock)
# Apply flocking forces with weights from config
separation_weight = self.flocking_params['separation_weight']
alignment_weight = self.flocking_params['alignment_weight']
cohesion_weight = self.flocking_params['cohesion_weight']
self.acceleration += separation_force * separation_weight
self.acceleration += alignment_force * alignment_weight
self.acceleration += cohesion_force * cohesion_weight
# Apply dog herding pressure
if dog_position:
herding_force = self._avoid_dog(dog_position, dog_pressure)
self.acceleration += herding_force
# Apply boundary forces to keep sheep on screen
boundary_force = self._avoid_boundaries()
self.acceleration += boundary_force
# Update physics
self.velocity += self.acceleration
self.velocity = self.velocity.limit(self.max_speed * self.energy)
self.position += self.velocity
# Update trail for visual debugging
self._update_trail()
# Gradually reduce fear over time
self.fear_level *= 0.99
def _separate(self, flock):
"""Separation: steer to avoid crowding local flockmates."""
steer = Vector2D()
count = 0
for sheep in flock:
if sheep is self:
continue
distance = self.position.distance_to(sheep.position)
if 0 < distance < self.flocking_params['separation_radius']:
# Calculate vector pointing away from neighbor
diff = self.position - sheep.position
diff = diff.normalize()
diff = diff / distance # Weight by distance (closer = stronger force)
steer += diff
count += 1
if count > 0:
steer = steer / count
steer = steer.normalize() * self.max_speed
steer = steer - self.velocity
steer = steer.limit(self.max_force)
return steer
def _align(self, flock):
"""Alignment: steer towards the average heading of neighbors."""
sum_velocity = Vector2D()
count = 0
for sheep in flock:
if sheep is self:
continue
distance = self.position.distance_to(sheep.position)
if distance < self.flocking_params['alignment_radius']:
sum_velocity += sheep.velocity
count += 1
if count > 0:
sum_velocity = sum_velocity / count
sum_velocity = sum_velocity.normalize() * self.max_speed
steer = sum_velocity - self.velocity
steer = steer.limit(self.max_force)
return steer
return Vector2D()
def _cohesion(self, flock):
"""Cohesion: steer to move toward the average position of neighbors."""
sum_position = Vector2D()
count = 0
for sheep in flock:
if sheep is self:
continue
distance = self.position.distance_to(sheep.position)
if distance < self.flocking_params['cohesion_radius']:
sum_position += sheep.position
count += 1
if count > 0:
sum_position = sum_position / count
return self._seek(sum_position)
return Vector2D()
def _seek(self, target):
"""Seek behavior: steer towards a target position."""
desired = target - self.position
desired = desired.normalize() * self.max_speed
steer = desired - self.velocity
steer = steer.limit(self.max_force)
return steer
def _avoid_dog(self, dog_position, pressure):
"""Avoid the herding dog with intensity based on pressure."""
distance = self.position.distance_to(dog_position)
# Get dog vision radius from config.DOG_CONFIG
dog_vision_radius = getattr(config, 'DOG_VISION_RADIUS', 150)
fear_radius = self.fear_response['fear_radius']
max_fear_level = self.fear_response['max_fear_level']
# Use the smaller of dog vision radius and sheep fear radius as the effective radius
effective_radius = min(dog_vision_radius, fear_radius)
if distance < effective_radius:
# Calculate avoidance vector
avoid = self.position - dog_position
avoid = avoid.normalize()
# Intensity based on distance and pressure
intensity = pressure * (effective_radius - distance) / effective_radius
intensity = min(intensity, 1.0)
# Update fear level
self.fear_level = min(self.fear_level + intensity * 0.1, 1.0)
# Apply avoidance force with strength from fear_response config
fear_strength = self.fear_response['fear_strength']
avoid = avoid * intensity * self.max_force * fear_strength
return avoid
return Vector2D()
def _avoid_boundaries(self):
"""Apply forces to keep sheep within screen boundaries."""
force = Vector2D()
# Get boundary parameters from physics_config in config.py
margin = getattr(config, 'EDGE_MARGIN', 50)
boundary_force = getattr(config, 'BOUNDARY_FORCE', 0.5)
screen_width = getattr(config, 'SCREEN_WIDTH', 1200)
screen_height = getattr(config, 'SCREEN_HEIGHT', 800)
# Left boundary
if self.position.x < margin:
force.x = boundary_force
# Right boundary
elif self.position.x > screen_width - margin:
force.x = -boundary_force
# Top boundary
if self.position.y < margin:
force.y = boundary_force
# Bottom boundary
elif self.position.y > screen_height - margin:
force.y = -boundary_force
return force
def _update_trail(self):
"""Update visual trail for debugging."""
trail_length = getattr(config, 'TRAIL_LENGTH', 20)
show_debug_info = getattr(config, 'SHOW_DEBUG_INFO', False)
if show_debug_info:
self.trail.append(self.position.to_tuple())
if len(self.trail) > trail_length:
self.trail.pop(0)
else:
self.trail = [] # Clear trail if not showing debug info
def draw(self, screen):
"""Draw the sheep on the screen."""
# Draw trail if debugging is enabled
show_debug_info = getattr(config, 'SHOW_DEBUG_INFO', False)
if show_debug_info and len(self.trail) > 1:
for i in range(len(self.trail) - 1):
alpha = i / len(self.trail)
color = (int(200 * alpha), int(200 * alpha), int(200 * alpha))
pygame.draw.line(screen, color, self.trail[i], self.trail[i + 1], 1)
# Calculate color based on fear level
fear_red = min(int(self.fear_level * 255), 255)
color = (255, 255 - fear_red, 255 - fear_red)
# Draw sheep body
pygame.draw.circle(screen, color, self.position.to_tuple(), self.size)
# Draw direction indicator
if self.velocity.magnitude() > 0.1:
direction = self.velocity.normalize() * (self.size + 5)
end_pos = self.position + direction
pygame.draw.line(screen, (0, 0, 0),
self.position.to_tuple(),
end_pos.to_tuple(), 2)
def get_position(self):
"""Get current position as Vector2D."""
return self.position
def get_distance_to_target(self, target_position):
"""Calculate distance to target position."""
return self.position.distance_to(target_position)