vi.agent
The Agent class is where all the magic happens!
Inheriting the Agent class allows you to modify the behaviour of the agents in the simulation.
1"""The Agent class is where all the magic happens! 2 3Inheriting the Agent class allows you to modify the behaviour of the agents in the simulation. 4""" 5 6from __future__ import annotations 7 8from copy import copy 9from typing import TYPE_CHECKING 10 11import pygame as pg 12from pygame.math import Vector2 13from pygame.sprite import Sprite 14 15from .util import random_angle, random_pos, round_pos 16 17 18if TYPE_CHECKING: 19 from collections.abc import Generator 20 from typing import Any, Self 21 22 from pygame.mask import Mask 23 from pygame.rect import Rect 24 from pygame.sprite import Group 25 from pygame.surface import Surface 26 27 from ._static import _StaticSprite 28 from .config import Config 29 from .simulation import HeadlessSimulation, Shared 30 31 32__all__ = [ 33 "Agent", 34] 35 36 37class Agent[ConfigClass: Config = Config](Sprite): 38 """The `Agent` class is home to Violet's various additions and is built on top of [PyGame's Sprite](https://www.pygame.org/docs/ref/sprite.html) class. 39 40 While you can simply add this `Agent` class to your simulations by calling `batch_spawn_agents`, 41 the agents won't actually do anything interesting. 42 Sure, they'll move around the screen very energetically, but they don't *interact* with each other. 43 44 That's where you come in! 45 By inheriting this `Agent` class you can make use of all its utilities, 46 while also programming the behaviour of your custom agent. 47 """ 48 49 id: int 50 """The unique identifier of the agent.""" 51 52 config: ConfigClass 53 """The config of the simulation that's shared with all agents. 54 55 The config can be overriden when inheriting the Agent class. 56 However, the config must always: 57 58 1. Inherit `Config` 59 2. Be decorated by `@dataclass` 60 """ 61 62 shared: Shared 63 """Attributes that are shared between the simulation and all agents.""" 64 65 _images: list[Surface] 66 """A list of images which you can use to change the current image of the agent.""" 67 68 _image_index: int 69 """The currently selected image.""" 70 71 _image_cache: tuple[int, Surface] | None = None 72 73 _area: Rect 74 """The area in which the agent is free to move.""" 75 76 move: Vector2 77 """A two-dimensional vector representing the delta between the agent's current and next position. 78 79 Note that `move` isn't added to the agent's `pos` automatically. 80 Instead, you should manually add the move delta to `pos`, like so: 81 82 ```python 83 self.pos += self.move 84 ``` 85 86 The vector `Vector2(2, 1)` indicates that the agent will be moving 2 pixels along the x axis and 1 pixel along the y axis. 87 You can use the `Vector2` class to calculate the vector's magnitude. 88 89 This property is also used to automatically rotate the agent's image 90 when `vi.config.Schema.image_rotation` is enabled. 91 """ 92 93 pos: Vector2 94 """The current (centre) position of the agent.""" 95 96 _obstacles: Group[Any] 97 """The group of obstacles the agent can collide with.""" 98 99 _sites: Group[Any] 100 """The group of sites on which the agent can appear.""" 101 102 __simulation: HeadlessSimulation[ConfigClass] 103 104 _moving: bool = True 105 """The agent's movement will freeze when moving is set to False.""" 106 107 def __init__( 108 self, 109 images: list[Surface], 110 simulation: HeadlessSimulation[ConfigClass], 111 pos: Vector2 | None = None, 112 move: Vector2 | None = None, 113 ) -> None: 114 Sprite.__init__(self, simulation._all, simulation._agents) 115 116 self.__simulation = simulation 117 118 self.id = simulation._agent_id() 119 self.config = simulation.config 120 self.shared = simulation.shared 121 122 # Default to first image in case no image is given 123 self._image_index = 0 124 self._images = images 125 126 self._obstacles = simulation._obstacles 127 self._sites = simulation._sites 128 129 self._area = simulation._area 130 self.move = ( 131 move 132 if move is not None 133 else random_angle(self.config.movement_speed, prng=self.shared.prng_move) 134 ) 135 136 # On spawn acts like the __init__ for non-pygame facing state. 137 # It could be used to override the initial image if necessary. 138 self.on_spawn() 139 140 if pos is not None: 141 self.pos = pos 142 143 if not hasattr(self, "pos"): 144 # Keep changing the position until the position no longer collides with any obstacle. 145 while True: 146 self.pos = random_pos(self._area, prng=self.shared.prng_move) 147 148 obstacle_hit = pg.sprite.spritecollideany( 149 self, # type: ignore[reportArgumentType] 150 self._obstacles, 151 pg.sprite.collide_mask, 152 ) 153 154 if not bool(obstacle_hit) and self._area.contains(self.rect): 155 break 156 157 def _get_image(self) -> Surface: 158 image = self._images[self._image_index] 159 160 if self.config.image_rotation: 161 angle = self.move.angle_to(Vector2((0, -1))) 162 163 return pg.transform.rotate(image, angle) 164 165 return image 166 167 @property 168 def image(self) -> Surface: 169 """The read-only image that's used for PyGame's rendering.""" 170 if self._image_cache is not None: 171 frame, image = self._image_cache 172 if frame == self.shared.counter: 173 return image 174 175 new_image = self._get_image() 176 self._image_cache = (self.shared.counter, new_image) 177 178 return new_image 179 180 @property 181 def center(self) -> tuple[int, int]: 182 """The read-only centre position of the agent.""" 183 return round_pos(self.pos) 184 185 @property 186 def rect(self) -> Rect: 187 """The read-only bounding-box that's used for PyGame's rendering.""" 188 rect = self.image.get_rect() 189 rect.center = self.center 190 191 return rect 192 193 @property 194 def mask(self) -> Mask: 195 """A read-only bit-mask of the image used for collision detection with obstacles and sites.""" 196 return pg.mask.from_surface(self.image) 197 198 def update(self) -> None: 199 """Run your own agent logic at every tick of the simulation. 200 201 Every frame of the simulation, this method is called automatically for every agent of the simulation. 202 To add your own logic, inherit the `Agent` class and override this method with your own. 203 """ 204 205 def obstacle_intersections( 206 self, 207 scale: float = 1, 208 ) -> Generator[Vector2]: 209 """Retrieve the centre coordinates of all obstacle intersections. 210 211 If you not only want to check for obstacle collision, 212 but also want to retrieve the coordinates of pixel groups 213 that are colliding with your agent, then `obstacle_intersections` is for you! 214 215 If you only have one obstacle in your environment, 216 but it doesn't have a fill, only a stroke, 217 then your agent could possibly be colliding with different areas of the stroke. 218 Therefore, this method checks the bitmasks of both the agent and the obstacle 219 to calculate the overlapping bitmask. 220 From this overlapping bitmask, 221 the centre coordinates of all groups of connected pixels are returned. 222 223 To emulate a bigger (or smaller) radius, 224 you can pass along the `scale` option. 225 A scale of 2 makes your agent twice as big, 226 but only for calculating the intersecting bitmasks. 227 """ 228 mask = pg.mask.from_surface(self.image) 229 230 # Scale the mask to the desired size 231 width, height = mask.get_size() 232 mask = mask.scale((width * scale, height * scale)) 233 234 # Align the mask to the centre position of the agent 235 rect = mask.get_rect() 236 rect.center = self.center 237 238 for sprite in self._obstacles.sprites(): 239 obstacle: _StaticSprite = sprite 240 241 # Calculate the mask offset 242 x = obstacle.rect.x - rect.x 243 y = obstacle.rect.y - rect.y 244 245 overlap = mask.overlap_mask(obstacle.mask, offset=(x, y)) 246 247 # For some reason PyGame has the wrong type hint here (single instead of list) 248 overlap_rects: list[pg.rect.Rect] = overlap.get_bounding_rects() # type: ignore[reportAssignmentType] 249 250 for overlap_rect in overlap_rects: 251 # Undo the offset 252 overlap_rect.x += rect.x 253 overlap_rect.y += rect.y 254 255 # Return the centre coordinates of the connected pixels 256 yield Vector2(overlap_rect.center) 257 258 def on_spawn(self) -> None: 259 """Run any code when the agent is spawned into the simulation. 260 261 This method is a replacement for `__init__`, which you should not overwrite directly. 262 Instead, you can make alterations to your Agent within this function instead. 263 264 You should override this method when inheriting Agent to add your own logic. 265 266 Some examples include: 267 - Changing the image or state of your Agent depending on its assigned identifier. 268 """ 269 270 def there_is_no_escape(self) -> bool: 271 """Pac-Man-style teleport the agent to the other side of the screen when it is outside of the playable area. 272 273 Examples 274 -------- 275 An agent that will always move to the right, until snapped back to reality. 276 277 ```python 278 class MyAgent(Agent): 279 def on_spawn(self): 280 self.move = Vector2((5, 0)) 281 282 def change_position(self): 283 self.there_is_no_escape() 284 self.pos += self.move 285 ``` 286 287 """ 288 changed = False 289 290 if self.pos.x < self._area.left: 291 changed = True 292 self.pos.x = self._area.right 293 294 if self.pos.x > self._area.right: 295 changed = True 296 self.pos.x = self._area.left 297 298 if self.pos.y < self._area.top: 299 changed = True 300 self.pos.y = self._area.bottom 301 302 if self.pos.y > self._area.bottom: 303 changed = True 304 self.pos.y = self._area.top 305 306 return changed 307 308 def change_position(self) -> None: 309 """Change the position of the agent. 310 311 The agent's new position is calculated as follows: 312 1. The agent checks whether it's outside of the visible screen area. 313 If this is the case, then the agent will be teleported to the other edge of the screen. 314 2. If the agent collides with any obstacles, then the agent will turn around 180 degrees. 315 3. If the agent has not collided with any obstacles, it will have the opportunity to slightly change its angle. 316 """ 317 if not self._moving: 318 return 319 320 changed = self.there_is_no_escape() 321 322 prng = self.shared.prng_move 323 324 # Always calculate the random angle so a seed could be used. 325 deg = prng.uniform(-30, 30) 326 327 # Only update angle if the agent was teleported to a different area of the simulation. 328 if changed: 329 self.move.rotate_ip(deg) 330 331 # Obstacle Avoidance 332 obstacle_hit = pg.sprite.spritecollideany( 333 self, # type: ignore[reportArgumentType] 334 self._obstacles, 335 pg.sprite.collide_mask, 336 ) 337 338 collision = bool(obstacle_hit) 339 340 # Reverse direction when colliding with an obstacle. 341 if collision and not self._still_stuck: 342 self.move.rotate_ip(180) 343 self._still_stuck = True 344 345 if not collision: 346 self._still_stuck = False 347 348 # Random opportunity to slightly change angle. 349 # Probabilities are pre-computed so a seed could be used. 350 should_change_angle = prng.random() 351 deg = prng.uniform(-10, 10) 352 353 # Only allow the angle opportunity to take place when no collisions have occured. 354 # This is done so an agent always turns 180 degrees. Any small change in the number of degrees 355 # allows the agent to possibly escape the obstacle. 356 if not collision and not self._still_stuck and should_change_angle < 0.25: # noqa: PLR2004 357 self.move.rotate_ip(deg) 358 359 # Actually update the position at last. 360 self.pos += self.move 361 362 def in_proximity_accuracy( 363 self, 364 ) -> Generator[tuple[Agent[ConfigClass], float]]: 365 """Retrieve other agents that are in the `vi.config.Schema.radius` of the current agent. 366 367 This proximity method calculates the distances between agents to determine whether 368 an agent is in the radius of the current agent. 369 370 To calculate the distances between agents, up to four chunks have to be retrieved from the Proximity Engine. 371 These extra lookups, in combination with the vector distance calculation, have a noticable impact on performance. 372 Note however that this performance impact is only noticable with >1000 agents. 373 374 If you want to speed up your simulation at the cost of some accuracy, 375 consider using the `in_proximity_performance` method instead. 376 377 This function doesn't return the agents as a `list` or as a `set`. 378 Instead, you are given a generator. 379 380 Examples 381 -------- 382 Count the number of agents that are in proximity 383 and change to image `1` if there is at least one agent nearby. 384 385 ```python 386 from vi.util import count 387 388 389 class MyAgent(Agent): 390 def update(self) -> None: 391 if count(self.in_proximity_accuracy()) >= 1: 392 self.change_image(1) 393 else: 394 self.change_image(0) 395 ``` 396 397 Kill the first `Human` agent that's in proximity. 398 399 ```python 400 class Zombie(Agent): 401 def update(self) -> None: 402 for agent, _ in self.in_proximity_accuracy(): 403 # Don't want to kill other zombies 404 if isinstance(Agent, Human): 405 agent.kill() 406 break 407 ``` 408 409 Calculate the average distance of agents that are in proximity. 410 411 ```python 412 from statistics import fmean 413 414 class Heimerdinger(Agent): 415 def update(self) -> None: 416 distances = [dist for _, dist in self.in_proximity_accuracy()] 417 dist_mean = fmean(distances) if len(distances) > 0 else 0 418 ``` 419 420 """ 421 return self.__simulation._proximity.in_proximity_accuracy(self) 422 423 def in_proximity_performance( 424 self, 425 ) -> Generator[Agent[ConfigClass]]: 426 """Retrieve other agents that are in the `vi.config.Schema.radius` of the current agent. 427 428 Unlike `in_proximity_accuracy`, this proximity method does not calculate the distances between agents. 429 Instead, it retrieves agents that are in the same chunk as the current agent, 430 irrespective of their position within the chunk. 431 432 If you find yourself limited by the performance of `in_proximity_accuracy`, 433 you can swap the function call for this one instead. 434 This performance method roughly doubles the frame rates of the simulation. 435 """ 436 return self.__simulation._proximity.in_proximity_performance(self) 437 438 def on_site(self) -> bool: 439 """Check whether the agent is currently on a site. 440 441 Examples 442 -------- 443 Stop the agent's movement when it reaches a site (think of a nice beach). 444 445 ```python 446 class TravellingAgent(Agent): 447 def update(self) -> None: 448 if self.on_site(): 449 # crave that star damage 450 self.freeze_movement() 451 ``` 452 453 """ 454 return self.on_site_id() is not None 455 456 def on_site_id(self) -> int | None: 457 """Get the identifier of the site the agent is currently on. 458 459 Examples 460 -------- 461 Stop the agent's movement when it reaches a site to inspect. 462 In addition, the current site identifier is saved to the DataFrame. 463 464 ```python 465 class SiteInspector(Agent): 466 def update(self) -> None: 467 site_id = self.on_site_id() 468 469 # Save the site id to the DataFrame 470 self.save_data("site", site_id) 471 472 # bool(site_id) would be inaccurate 473 # as a site_id of 0 will return False. 474 # Therefore, we check whether it is not None instead. 475 if site_id is not None: 476 # Inspect the site 477 self.freeze_movement() 478 ``` 479 480 """ 481 site: _StaticSprite | None = pg.sprite.spritecollideany( 482 self, # type: ignore[reportArgumentType] 483 self._sites, 484 pg.sprite.collide_mask, 485 ) 486 487 if site is not None: 488 return site.id 489 return None 490 491 def freeze_movement(self) -> None: 492 """Freeze the movement of the agent. The movement can be continued by calling `continue_movement`.""" 493 self._moving = False 494 495 def continue_movement(self) -> None: 496 """Continue the movement of the agent from before its movement was frozen.""" 497 self._moving = True 498 499 def change_image(self, index: int) -> None: 500 """Change the image of the agent. 501 502 If you want to change the agent's image to the second image in the images list, 503 then you can change the image to index 1: 504 505 ```python 506 self.change_image(1) 507 ``` 508 """ 509 self._image_index = index 510 511 def save_data(self, column: str, value: Any) -> None: # noqa: ANN401 512 """Add extra data to the simulation's metrics. 513 514 The following data is collected automatically: 515 - agent identifier 516 - current frame 517 - x and y coordinates 518 519 Examples 520 -------- 521 Saving the number of agents that are currently in proximity: 522 523 ```python 524 from vi.util import count 525 526 527 class MyAgent(Agent): 528 def update(self) -> None: 529 in_proximity = count(self.in_proximity_accuracy()) 530 531 self.save_data("in_proximity", in_proximity) 532 ``` 533 534 """ 535 self.__simulation._metrics._temporary_snapshots[column].append(value) 536 537 def _collect_replay_data(self) -> None: 538 """Add the minimum data needed for the replay simulation to the dataframe.""" 539 x, y = self.center 540 snapshots = self.__simulation._metrics._temporary_snapshots 541 542 snapshots["frame"].append(self.shared.counter) 543 snapshots["id"].append(self.id) 544 545 snapshots["x"].append(x) 546 snapshots["y"].append(y) 547 548 snapshots["image_index"].append(self._image_index) 549 550 if self.config.image_rotation: 551 angle = self.move.angle_to(Vector2((0, -1))) 552 snapshots["angle"].append(round(angle)) 553 554 def __copy__(self) -> Self: 555 """Create a copy of this agent and spawn it into the simulation. 556 557 Note that this only copies the `pos` and `move` vectors. 558 Any other attributes will be set to their defaults. 559 """ 560 cls = self.__class__ 561 agent = cls(self._images, self.__simulation) 562 563 # We want to make sure to copy the position and movement vectors. 564 # Otherwise, the original as well as the clone will continue sharing these vectors. 565 agent.pos = self.pos.copy() 566 agent.move = self.move.copy() 567 568 return agent 569 570 def reproduce(self) -> Self: 571 """Create a new agent and spawn it into the simulation. 572 573 All values will be reset to their defaults, 574 except for the agent's position and movement vector. 575 These will be cloned from the original agent. 576 """ 577 return copy(self) 578 579 def kill(self) -> None: 580 """Kill the agent. 581 582 While violence usually isn't the right option, 583 sometimes you just want to murder some innocent agents inside your little computer. 584 585 But fear not! 586 By *killing* the agent, all you're really doing is removing it from the simulation. 587 """ 588 super().kill() 589 590 def is_dead(self) -> bool: 591 """Is the agent dead? 592 593 Death occurs when `kill` is called. 594 """ 595 return not self.is_alive() 596 597 def is_alive(self) -> bool: 598 """Is the agent still alive? 599 600 Death occurs when `kill` is called. 601 """ 602 return super().alive()
38class Agent[ConfigClass: Config = Config](Sprite): 39 """The `Agent` class is home to Violet's various additions and is built on top of [PyGame's Sprite](https://www.pygame.org/docs/ref/sprite.html) class. 40 41 While you can simply add this `Agent` class to your simulations by calling `batch_spawn_agents`, 42 the agents won't actually do anything interesting. 43 Sure, they'll move around the screen very energetically, but they don't *interact* with each other. 44 45 That's where you come in! 46 By inheriting this `Agent` class you can make use of all its utilities, 47 while also programming the behaviour of your custom agent. 48 """ 49 50 id: int 51 """The unique identifier of the agent.""" 52 53 config: ConfigClass 54 """The config of the simulation that's shared with all agents. 55 56 The config can be overriden when inheriting the Agent class. 57 However, the config must always: 58 59 1. Inherit `Config` 60 2. Be decorated by `@dataclass` 61 """ 62 63 shared: Shared 64 """Attributes that are shared between the simulation and all agents.""" 65 66 _images: list[Surface] 67 """A list of images which you can use to change the current image of the agent.""" 68 69 _image_index: int 70 """The currently selected image.""" 71 72 _image_cache: tuple[int, Surface] | None = None 73 74 _area: Rect 75 """The area in which the agent is free to move.""" 76 77 move: Vector2 78 """A two-dimensional vector representing the delta between the agent's current and next position. 79 80 Note that `move` isn't added to the agent's `pos` automatically. 81 Instead, you should manually add the move delta to `pos`, like so: 82 83 ```python 84 self.pos += self.move 85 ``` 86 87 The vector `Vector2(2, 1)` indicates that the agent will be moving 2 pixels along the x axis and 1 pixel along the y axis. 88 You can use the `Vector2` class to calculate the vector's magnitude. 89 90 This property is also used to automatically rotate the agent's image 91 when `vi.config.Schema.image_rotation` is enabled. 92 """ 93 94 pos: Vector2 95 """The current (centre) position of the agent.""" 96 97 _obstacles: Group[Any] 98 """The group of obstacles the agent can collide with.""" 99 100 _sites: Group[Any] 101 """The group of sites on which the agent can appear.""" 102 103 __simulation: HeadlessSimulation[ConfigClass] 104 105 _moving: bool = True 106 """The agent's movement will freeze when moving is set to False.""" 107 108 def __init__( 109 self, 110 images: list[Surface], 111 simulation: HeadlessSimulation[ConfigClass], 112 pos: Vector2 | None = None, 113 move: Vector2 | None = None, 114 ) -> None: 115 Sprite.__init__(self, simulation._all, simulation._agents) 116 117 self.__simulation = simulation 118 119 self.id = simulation._agent_id() 120 self.config = simulation.config 121 self.shared = simulation.shared 122 123 # Default to first image in case no image is given 124 self._image_index = 0 125 self._images = images 126 127 self._obstacles = simulation._obstacles 128 self._sites = simulation._sites 129 130 self._area = simulation._area 131 self.move = ( 132 move 133 if move is not None 134 else random_angle(self.config.movement_speed, prng=self.shared.prng_move) 135 ) 136 137 # On spawn acts like the __init__ for non-pygame facing state. 138 # It could be used to override the initial image if necessary. 139 self.on_spawn() 140 141 if pos is not None: 142 self.pos = pos 143 144 if not hasattr(self, "pos"): 145 # Keep changing the position until the position no longer collides with any obstacle. 146 while True: 147 self.pos = random_pos(self._area, prng=self.shared.prng_move) 148 149 obstacle_hit = pg.sprite.spritecollideany( 150 self, # type: ignore[reportArgumentType] 151 self._obstacles, 152 pg.sprite.collide_mask, 153 ) 154 155 if not bool(obstacle_hit) and self._area.contains(self.rect): 156 break 157 158 def _get_image(self) -> Surface: 159 image = self._images[self._image_index] 160 161 if self.config.image_rotation: 162 angle = self.move.angle_to(Vector2((0, -1))) 163 164 return pg.transform.rotate(image, angle) 165 166 return image 167 168 @property 169 def image(self) -> Surface: 170 """The read-only image that's used for PyGame's rendering.""" 171 if self._image_cache is not None: 172 frame, image = self._image_cache 173 if frame == self.shared.counter: 174 return image 175 176 new_image = self._get_image() 177 self._image_cache = (self.shared.counter, new_image) 178 179 return new_image 180 181 @property 182 def center(self) -> tuple[int, int]: 183 """The read-only centre position of the agent.""" 184 return round_pos(self.pos) 185 186 @property 187 def rect(self) -> Rect: 188 """The read-only bounding-box that's used for PyGame's rendering.""" 189 rect = self.image.get_rect() 190 rect.center = self.center 191 192 return rect 193 194 @property 195 def mask(self) -> Mask: 196 """A read-only bit-mask of the image used for collision detection with obstacles and sites.""" 197 return pg.mask.from_surface(self.image) 198 199 def update(self) -> None: 200 """Run your own agent logic at every tick of the simulation. 201 202 Every frame of the simulation, this method is called automatically for every agent of the simulation. 203 To add your own logic, inherit the `Agent` class and override this method with your own. 204 """ 205 206 def obstacle_intersections( 207 self, 208 scale: float = 1, 209 ) -> Generator[Vector2]: 210 """Retrieve the centre coordinates of all obstacle intersections. 211 212 If you not only want to check for obstacle collision, 213 but also want to retrieve the coordinates of pixel groups 214 that are colliding with your agent, then `obstacle_intersections` is for you! 215 216 If you only have one obstacle in your environment, 217 but it doesn't have a fill, only a stroke, 218 then your agent could possibly be colliding with different areas of the stroke. 219 Therefore, this method checks the bitmasks of both the agent and the obstacle 220 to calculate the overlapping bitmask. 221 From this overlapping bitmask, 222 the centre coordinates of all groups of connected pixels are returned. 223 224 To emulate a bigger (or smaller) radius, 225 you can pass along the `scale` option. 226 A scale of 2 makes your agent twice as big, 227 but only for calculating the intersecting bitmasks. 228 """ 229 mask = pg.mask.from_surface(self.image) 230 231 # Scale the mask to the desired size 232 width, height = mask.get_size() 233 mask = mask.scale((width * scale, height * scale)) 234 235 # Align the mask to the centre position of the agent 236 rect = mask.get_rect() 237 rect.center = self.center 238 239 for sprite in self._obstacles.sprites(): 240 obstacle: _StaticSprite = sprite 241 242 # Calculate the mask offset 243 x = obstacle.rect.x - rect.x 244 y = obstacle.rect.y - rect.y 245 246 overlap = mask.overlap_mask(obstacle.mask, offset=(x, y)) 247 248 # For some reason PyGame has the wrong type hint here (single instead of list) 249 overlap_rects: list[pg.rect.Rect] = overlap.get_bounding_rects() # type: ignore[reportAssignmentType] 250 251 for overlap_rect in overlap_rects: 252 # Undo the offset 253 overlap_rect.x += rect.x 254 overlap_rect.y += rect.y 255 256 # Return the centre coordinates of the connected pixels 257 yield Vector2(overlap_rect.center) 258 259 def on_spawn(self) -> None: 260 """Run any code when the agent is spawned into the simulation. 261 262 This method is a replacement for `__init__`, which you should not overwrite directly. 263 Instead, you can make alterations to your Agent within this function instead. 264 265 You should override this method when inheriting Agent to add your own logic. 266 267 Some examples include: 268 - Changing the image or state of your Agent depending on its assigned identifier. 269 """ 270 271 def there_is_no_escape(self) -> bool: 272 """Pac-Man-style teleport the agent to the other side of the screen when it is outside of the playable area. 273 274 Examples 275 -------- 276 An agent that will always move to the right, until snapped back to reality. 277 278 ```python 279 class MyAgent(Agent): 280 def on_spawn(self): 281 self.move = Vector2((5, 0)) 282 283 def change_position(self): 284 self.there_is_no_escape() 285 self.pos += self.move 286 ``` 287 288 """ 289 changed = False 290 291 if self.pos.x < self._area.left: 292 changed = True 293 self.pos.x = self._area.right 294 295 if self.pos.x > self._area.right: 296 changed = True 297 self.pos.x = self._area.left 298 299 if self.pos.y < self._area.top: 300 changed = True 301 self.pos.y = self._area.bottom 302 303 if self.pos.y > self._area.bottom: 304 changed = True 305 self.pos.y = self._area.top 306 307 return changed 308 309 def change_position(self) -> None: 310 """Change the position of the agent. 311 312 The agent's new position is calculated as follows: 313 1. The agent checks whether it's outside of the visible screen area. 314 If this is the case, then the agent will be teleported to the other edge of the screen. 315 2. If the agent collides with any obstacles, then the agent will turn around 180 degrees. 316 3. If the agent has not collided with any obstacles, it will have the opportunity to slightly change its angle. 317 """ 318 if not self._moving: 319 return 320 321 changed = self.there_is_no_escape() 322 323 prng = self.shared.prng_move 324 325 # Always calculate the random angle so a seed could be used. 326 deg = prng.uniform(-30, 30) 327 328 # Only update angle if the agent was teleported to a different area of the simulation. 329 if changed: 330 self.move.rotate_ip(deg) 331 332 # Obstacle Avoidance 333 obstacle_hit = pg.sprite.spritecollideany( 334 self, # type: ignore[reportArgumentType] 335 self._obstacles, 336 pg.sprite.collide_mask, 337 ) 338 339 collision = bool(obstacle_hit) 340 341 # Reverse direction when colliding with an obstacle. 342 if collision and not self._still_stuck: 343 self.move.rotate_ip(180) 344 self._still_stuck = True 345 346 if not collision: 347 self._still_stuck = False 348 349 # Random opportunity to slightly change angle. 350 # Probabilities are pre-computed so a seed could be used. 351 should_change_angle = prng.random() 352 deg = prng.uniform(-10, 10) 353 354 # Only allow the angle opportunity to take place when no collisions have occured. 355 # This is done so an agent always turns 180 degrees. Any small change in the number of degrees 356 # allows the agent to possibly escape the obstacle. 357 if not collision and not self._still_stuck and should_change_angle < 0.25: # noqa: PLR2004 358 self.move.rotate_ip(deg) 359 360 # Actually update the position at last. 361 self.pos += self.move 362 363 def in_proximity_accuracy( 364 self, 365 ) -> Generator[tuple[Agent[ConfigClass], float]]: 366 """Retrieve other agents that are in the `vi.config.Schema.radius` of the current agent. 367 368 This proximity method calculates the distances between agents to determine whether 369 an agent is in the radius of the current agent. 370 371 To calculate the distances between agents, up to four chunks have to be retrieved from the Proximity Engine. 372 These extra lookups, in combination with the vector distance calculation, have a noticable impact on performance. 373 Note however that this performance impact is only noticable with >1000 agents. 374 375 If you want to speed up your simulation at the cost of some accuracy, 376 consider using the `in_proximity_performance` method instead. 377 378 This function doesn't return the agents as a `list` or as a `set`. 379 Instead, you are given a generator. 380 381 Examples 382 -------- 383 Count the number of agents that are in proximity 384 and change to image `1` if there is at least one agent nearby. 385 386 ```python 387 from vi.util import count 388 389 390 class MyAgent(Agent): 391 def update(self) -> None: 392 if count(self.in_proximity_accuracy()) >= 1: 393 self.change_image(1) 394 else: 395 self.change_image(0) 396 ``` 397 398 Kill the first `Human` agent that's in proximity. 399 400 ```python 401 class Zombie(Agent): 402 def update(self) -> None: 403 for agent, _ in self.in_proximity_accuracy(): 404 # Don't want to kill other zombies 405 if isinstance(Agent, Human): 406 agent.kill() 407 break 408 ``` 409 410 Calculate the average distance of agents that are in proximity. 411 412 ```python 413 from statistics import fmean 414 415 class Heimerdinger(Agent): 416 def update(self) -> None: 417 distances = [dist for _, dist in self.in_proximity_accuracy()] 418 dist_mean = fmean(distances) if len(distances) > 0 else 0 419 ``` 420 421 """ 422 return self.__simulation._proximity.in_proximity_accuracy(self) 423 424 def in_proximity_performance( 425 self, 426 ) -> Generator[Agent[ConfigClass]]: 427 """Retrieve other agents that are in the `vi.config.Schema.radius` of the current agent. 428 429 Unlike `in_proximity_accuracy`, this proximity method does not calculate the distances between agents. 430 Instead, it retrieves agents that are in the same chunk as the current agent, 431 irrespective of their position within the chunk. 432 433 If you find yourself limited by the performance of `in_proximity_accuracy`, 434 you can swap the function call for this one instead. 435 This performance method roughly doubles the frame rates of the simulation. 436 """ 437 return self.__simulation._proximity.in_proximity_performance(self) 438 439 def on_site(self) -> bool: 440 """Check whether the agent is currently on a site. 441 442 Examples 443 -------- 444 Stop the agent's movement when it reaches a site (think of a nice beach). 445 446 ```python 447 class TravellingAgent(Agent): 448 def update(self) -> None: 449 if self.on_site(): 450 # crave that star damage 451 self.freeze_movement() 452 ``` 453 454 """ 455 return self.on_site_id() is not None 456 457 def on_site_id(self) -> int | None: 458 """Get the identifier of the site the agent is currently on. 459 460 Examples 461 -------- 462 Stop the agent's movement when it reaches a site to inspect. 463 In addition, the current site identifier is saved to the DataFrame. 464 465 ```python 466 class SiteInspector(Agent): 467 def update(self) -> None: 468 site_id = self.on_site_id() 469 470 # Save the site id to the DataFrame 471 self.save_data("site", site_id) 472 473 # bool(site_id) would be inaccurate 474 # as a site_id of 0 will return False. 475 # Therefore, we check whether it is not None instead. 476 if site_id is not None: 477 # Inspect the site 478 self.freeze_movement() 479 ``` 480 481 """ 482 site: _StaticSprite | None = pg.sprite.spritecollideany( 483 self, # type: ignore[reportArgumentType] 484 self._sites, 485 pg.sprite.collide_mask, 486 ) 487 488 if site is not None: 489 return site.id 490 return None 491 492 def freeze_movement(self) -> None: 493 """Freeze the movement of the agent. The movement can be continued by calling `continue_movement`.""" 494 self._moving = False 495 496 def continue_movement(self) -> None: 497 """Continue the movement of the agent from before its movement was frozen.""" 498 self._moving = True 499 500 def change_image(self, index: int) -> None: 501 """Change the image of the agent. 502 503 If you want to change the agent's image to the second image in the images list, 504 then you can change the image to index 1: 505 506 ```python 507 self.change_image(1) 508 ``` 509 """ 510 self._image_index = index 511 512 def save_data(self, column: str, value: Any) -> None: # noqa: ANN401 513 """Add extra data to the simulation's metrics. 514 515 The following data is collected automatically: 516 - agent identifier 517 - current frame 518 - x and y coordinates 519 520 Examples 521 -------- 522 Saving the number of agents that are currently in proximity: 523 524 ```python 525 from vi.util import count 526 527 528 class MyAgent(Agent): 529 def update(self) -> None: 530 in_proximity = count(self.in_proximity_accuracy()) 531 532 self.save_data("in_proximity", in_proximity) 533 ``` 534 535 """ 536 self.__simulation._metrics._temporary_snapshots[column].append(value) 537 538 def _collect_replay_data(self) -> None: 539 """Add the minimum data needed for the replay simulation to the dataframe.""" 540 x, y = self.center 541 snapshots = self.__simulation._metrics._temporary_snapshots 542 543 snapshots["frame"].append(self.shared.counter) 544 snapshots["id"].append(self.id) 545 546 snapshots["x"].append(x) 547 snapshots["y"].append(y) 548 549 snapshots["image_index"].append(self._image_index) 550 551 if self.config.image_rotation: 552 angle = self.move.angle_to(Vector2((0, -1))) 553 snapshots["angle"].append(round(angle)) 554 555 def __copy__(self) -> Self: 556 """Create a copy of this agent and spawn it into the simulation. 557 558 Note that this only copies the `pos` and `move` vectors. 559 Any other attributes will be set to their defaults. 560 """ 561 cls = self.__class__ 562 agent = cls(self._images, self.__simulation) 563 564 # We want to make sure to copy the position and movement vectors. 565 # Otherwise, the original as well as the clone will continue sharing these vectors. 566 agent.pos = self.pos.copy() 567 agent.move = self.move.copy() 568 569 return agent 570 571 def reproduce(self) -> Self: 572 """Create a new agent and spawn it into the simulation. 573 574 All values will be reset to their defaults, 575 except for the agent's position and movement vector. 576 These will be cloned from the original agent. 577 """ 578 return copy(self) 579 580 def kill(self) -> None: 581 """Kill the agent. 582 583 While violence usually isn't the right option, 584 sometimes you just want to murder some innocent agents inside your little computer. 585 586 But fear not! 587 By *killing* the agent, all you're really doing is removing it from the simulation. 588 """ 589 super().kill() 590 591 def is_dead(self) -> bool: 592 """Is the agent dead? 593 594 Death occurs when `kill` is called. 595 """ 596 return not self.is_alive() 597 598 def is_alive(self) -> bool: 599 """Is the agent still alive? 600 601 Death occurs when `kill` is called. 602 """ 603 return super().alive()
The Agent
class is home to Violet's various additions and is built on top of PyGame's Sprite class.
While you can simply add this Agent
class to your simulations by calling batch_spawn_agents
,
the agents won't actually do anything interesting.
Sure, they'll move around the screen very energetically, but they don't interact with each other.
That's where you come in!
By inheriting this Agent
class you can make use of all its utilities,
while also programming the behaviour of your custom agent.
The config of the simulation that's shared with all agents.
The config can be overriden when inheriting the Agent class. However, the config must always:
- Inherit
Config
- Be decorated by
@dataclass
A two-dimensional vector representing the delta between the agent's current and next position.
Note that move
isn't added to the agent's pos
automatically.
Instead, you should manually add the move delta to pos
, like so:
self.pos += self.move
The vector Vector2(2, 1)
indicates that the agent will be moving 2 pixels along the x axis and 1 pixel along the y axis.
You can use the Vector2
class to calculate the vector's magnitude.
This property is also used to automatically rotate the agent's image
when vi.config.Schema.image_rotation
is enabled.
168 @property 169 def image(self) -> Surface: 170 """The read-only image that's used for PyGame's rendering.""" 171 if self._image_cache is not None: 172 frame, image = self._image_cache 173 if frame == self.shared.counter: 174 return image 175 176 new_image = self._get_image() 177 self._image_cache = (self.shared.counter, new_image) 178 179 return new_image
The read-only image that's used for PyGame's rendering.
181 @property 182 def center(self) -> tuple[int, int]: 183 """The read-only centre position of the agent.""" 184 return round_pos(self.pos)
The read-only centre position of the agent.
186 @property 187 def rect(self) -> Rect: 188 """The read-only bounding-box that's used for PyGame's rendering.""" 189 rect = self.image.get_rect() 190 rect.center = self.center 191 192 return rect
The read-only bounding-box that's used for PyGame's rendering.
194 @property 195 def mask(self) -> Mask: 196 """A read-only bit-mask of the image used for collision detection with obstacles and sites.""" 197 return pg.mask.from_surface(self.image)
A read-only bit-mask of the image used for collision detection with obstacles and sites.
206 def obstacle_intersections( 207 self, 208 scale: float = 1, 209 ) -> Generator[Vector2]: 210 """Retrieve the centre coordinates of all obstacle intersections. 211 212 If you not only want to check for obstacle collision, 213 but also want to retrieve the coordinates of pixel groups 214 that are colliding with your agent, then `obstacle_intersections` is for you! 215 216 If you only have one obstacle in your environment, 217 but it doesn't have a fill, only a stroke, 218 then your agent could possibly be colliding with different areas of the stroke. 219 Therefore, this method checks the bitmasks of both the agent and the obstacle 220 to calculate the overlapping bitmask. 221 From this overlapping bitmask, 222 the centre coordinates of all groups of connected pixels are returned. 223 224 To emulate a bigger (or smaller) radius, 225 you can pass along the `scale` option. 226 A scale of 2 makes your agent twice as big, 227 but only for calculating the intersecting bitmasks. 228 """ 229 mask = pg.mask.from_surface(self.image) 230 231 # Scale the mask to the desired size 232 width, height = mask.get_size() 233 mask = mask.scale((width * scale, height * scale)) 234 235 # Align the mask to the centre position of the agent 236 rect = mask.get_rect() 237 rect.center = self.center 238 239 for sprite in self._obstacles.sprites(): 240 obstacle: _StaticSprite = sprite 241 242 # Calculate the mask offset 243 x = obstacle.rect.x - rect.x 244 y = obstacle.rect.y - rect.y 245 246 overlap = mask.overlap_mask(obstacle.mask, offset=(x, y)) 247 248 # For some reason PyGame has the wrong type hint here (single instead of list) 249 overlap_rects: list[pg.rect.Rect] = overlap.get_bounding_rects() # type: ignore[reportAssignmentType] 250 251 for overlap_rect in overlap_rects: 252 # Undo the offset 253 overlap_rect.x += rect.x 254 overlap_rect.y += rect.y 255 256 # Return the centre coordinates of the connected pixels 257 yield Vector2(overlap_rect.center)
Retrieve the centre coordinates of all obstacle intersections.
If you not only want to check for obstacle collision,
but also want to retrieve the coordinates of pixel groups
that are colliding with your agent, then obstacle_intersections
is for you!
If you only have one obstacle in your environment, but it doesn't have a fill, only a stroke, then your agent could possibly be colliding with different areas of the stroke. Therefore, this method checks the bitmasks of both the agent and the obstacle to calculate the overlapping bitmask. From this overlapping bitmask, the centre coordinates of all groups of connected pixels are returned.
To emulate a bigger (or smaller) radius,
you can pass along the scale
option.
A scale of 2 makes your agent twice as big,
but only for calculating the intersecting bitmasks.
259 def on_spawn(self) -> None: 260 """Run any code when the agent is spawned into the simulation. 261 262 This method is a replacement for `__init__`, which you should not overwrite directly. 263 Instead, you can make alterations to your Agent within this function instead. 264 265 You should override this method when inheriting Agent to add your own logic. 266 267 Some examples include: 268 - Changing the image or state of your Agent depending on its assigned identifier. 269 """
Run any code when the agent is spawned into the simulation.
This method is a replacement for __init__
, which you should not overwrite directly.
Instead, you can make alterations to your Agent within this function instead.
You should override this method when inheriting Agent to add your own logic.
Some examples include:
- Changing the image or state of your Agent depending on its assigned identifier.
271 def there_is_no_escape(self) -> bool: 272 """Pac-Man-style teleport the agent to the other side of the screen when it is outside of the playable area. 273 274 Examples 275 -------- 276 An agent that will always move to the right, until snapped back to reality. 277 278 ```python 279 class MyAgent(Agent): 280 def on_spawn(self): 281 self.move = Vector2((5, 0)) 282 283 def change_position(self): 284 self.there_is_no_escape() 285 self.pos += self.move 286 ``` 287 288 """ 289 changed = False 290 291 if self.pos.x < self._area.left: 292 changed = True 293 self.pos.x = self._area.right 294 295 if self.pos.x > self._area.right: 296 changed = True 297 self.pos.x = self._area.left 298 299 if self.pos.y < self._area.top: 300 changed = True 301 self.pos.y = self._area.bottom 302 303 if self.pos.y > self._area.bottom: 304 changed = True 305 self.pos.y = self._area.top 306 307 return changed
Pac-Man-style teleport the agent to the other side of the screen when it is outside of the playable area.
Examples
An agent that will always move to the right, until snapped back to reality.
class MyAgent(Agent):
def on_spawn(self):
self.move = Vector2((5, 0))
def change_position(self):
self.there_is_no_escape()
self.pos += self.move
309 def change_position(self) -> None: 310 """Change the position of the agent. 311 312 The agent's new position is calculated as follows: 313 1. The agent checks whether it's outside of the visible screen area. 314 If this is the case, then the agent will be teleported to the other edge of the screen. 315 2. If the agent collides with any obstacles, then the agent will turn around 180 degrees. 316 3. If the agent has not collided with any obstacles, it will have the opportunity to slightly change its angle. 317 """ 318 if not self._moving: 319 return 320 321 changed = self.there_is_no_escape() 322 323 prng = self.shared.prng_move 324 325 # Always calculate the random angle so a seed could be used. 326 deg = prng.uniform(-30, 30) 327 328 # Only update angle if the agent was teleported to a different area of the simulation. 329 if changed: 330 self.move.rotate_ip(deg) 331 332 # Obstacle Avoidance 333 obstacle_hit = pg.sprite.spritecollideany( 334 self, # type: ignore[reportArgumentType] 335 self._obstacles, 336 pg.sprite.collide_mask, 337 ) 338 339 collision = bool(obstacle_hit) 340 341 # Reverse direction when colliding with an obstacle. 342 if collision and not self._still_stuck: 343 self.move.rotate_ip(180) 344 self._still_stuck = True 345 346 if not collision: 347 self._still_stuck = False 348 349 # Random opportunity to slightly change angle. 350 # Probabilities are pre-computed so a seed could be used. 351 should_change_angle = prng.random() 352 deg = prng.uniform(-10, 10) 353 354 # Only allow the angle opportunity to take place when no collisions have occured. 355 # This is done so an agent always turns 180 degrees. Any small change in the number of degrees 356 # allows the agent to possibly escape the obstacle. 357 if not collision and not self._still_stuck and should_change_angle < 0.25: # noqa: PLR2004 358 self.move.rotate_ip(deg) 359 360 # Actually update the position at last. 361 self.pos += self.move
Change the position of the agent.
The agent's new position is calculated as follows:
- The agent checks whether it's outside of the visible screen area. If this is the case, then the agent will be teleported to the other edge of the screen.
- If the agent collides with any obstacles, then the agent will turn around 180 degrees.
- If the agent has not collided with any obstacles, it will have the opportunity to slightly change its angle.
363 def in_proximity_accuracy( 364 self, 365 ) -> Generator[tuple[Agent[ConfigClass], float]]: 366 """Retrieve other agents that are in the `vi.config.Schema.radius` of the current agent. 367 368 This proximity method calculates the distances between agents to determine whether 369 an agent is in the radius of the current agent. 370 371 To calculate the distances between agents, up to four chunks have to be retrieved from the Proximity Engine. 372 These extra lookups, in combination with the vector distance calculation, have a noticable impact on performance. 373 Note however that this performance impact is only noticable with >1000 agents. 374 375 If you want to speed up your simulation at the cost of some accuracy, 376 consider using the `in_proximity_performance` method instead. 377 378 This function doesn't return the agents as a `list` or as a `set`. 379 Instead, you are given a generator. 380 381 Examples 382 -------- 383 Count the number of agents that are in proximity 384 and change to image `1` if there is at least one agent nearby. 385 386 ```python 387 from vi.util import count 388 389 390 class MyAgent(Agent): 391 def update(self) -> None: 392 if count(self.in_proximity_accuracy()) >= 1: 393 self.change_image(1) 394 else: 395 self.change_image(0) 396 ``` 397 398 Kill the first `Human` agent that's in proximity. 399 400 ```python 401 class Zombie(Agent): 402 def update(self) -> None: 403 for agent, _ in self.in_proximity_accuracy(): 404 # Don't want to kill other zombies 405 if isinstance(Agent, Human): 406 agent.kill() 407 break 408 ``` 409 410 Calculate the average distance of agents that are in proximity. 411 412 ```python 413 from statistics import fmean 414 415 class Heimerdinger(Agent): 416 def update(self) -> None: 417 distances = [dist for _, dist in self.in_proximity_accuracy()] 418 dist_mean = fmean(distances) if len(distances) > 0 else 0 419 ``` 420 421 """ 422 return self.__simulation._proximity.in_proximity_accuracy(self)
Retrieve other agents that are in the vi.config.Schema.radius
of the current agent.
This proximity method calculates the distances between agents to determine whether an agent is in the radius of the current agent.
To calculate the distances between agents, up to four chunks have to be retrieved from the Proximity Engine. These extra lookups, in combination with the vector distance calculation, have a noticable impact on performance. Note however that this performance impact is only noticable with >1000 agents.
If you want to speed up your simulation at the cost of some accuracy,
consider using the in_proximity_performance
method instead.
This function doesn't return the agents as a list
or as a set
.
Instead, you are given a generator.
Examples
Count the number of agents that are in proximity
and change to image 1
if there is at least one agent nearby.
from vi.util import count
class MyAgent(Agent):
def update(self) -> None:
if count(self.in_proximity_accuracy()) >= 1:
self.change_image(1)
else:
self.change_image(0)
Kill the first Human
agent that's in proximity.
class Zombie(Agent):
def update(self) -> None:
for agent, _ in self.in_proximity_accuracy():
# Don't want to kill other zombies
if isinstance(Agent, Human):
agent.kill()
break
Calculate the average distance of agents that are in proximity.
from statistics import fmean
class Heimerdinger(Agent):
def update(self) -> None:
distances = [dist for _, dist in self.in_proximity_accuracy()]
dist_mean = fmean(distances) if len(distances) > 0 else 0
424 def in_proximity_performance( 425 self, 426 ) -> Generator[Agent[ConfigClass]]: 427 """Retrieve other agents that are in the `vi.config.Schema.radius` of the current agent. 428 429 Unlike `in_proximity_accuracy`, this proximity method does not calculate the distances between agents. 430 Instead, it retrieves agents that are in the same chunk as the current agent, 431 irrespective of their position within the chunk. 432 433 If you find yourself limited by the performance of `in_proximity_accuracy`, 434 you can swap the function call for this one instead. 435 This performance method roughly doubles the frame rates of the simulation. 436 """ 437 return self.__simulation._proximity.in_proximity_performance(self)
Retrieve other agents that are in the vi.config.Schema.radius
of the current agent.
Unlike in_proximity_accuracy
, this proximity method does not calculate the distances between agents.
Instead, it retrieves agents that are in the same chunk as the current agent,
irrespective of their position within the chunk.
If you find yourself limited by the performance of in_proximity_accuracy
,
you can swap the function call for this one instead.
This performance method roughly doubles the frame rates of the simulation.
439 def on_site(self) -> bool: 440 """Check whether the agent is currently on a site. 441 442 Examples 443 -------- 444 Stop the agent's movement when it reaches a site (think of a nice beach). 445 446 ```python 447 class TravellingAgent(Agent): 448 def update(self) -> None: 449 if self.on_site(): 450 # crave that star damage 451 self.freeze_movement() 452 ``` 453 454 """ 455 return self.on_site_id() is not None
Check whether the agent is currently on a site.
Examples
Stop the agent's movement when it reaches a site (think of a nice beach).
class TravellingAgent(Agent):
def update(self) -> None:
if self.on_site():
# crave that star damage
self.freeze_movement()
457 def on_site_id(self) -> int | None: 458 """Get the identifier of the site the agent is currently on. 459 460 Examples 461 -------- 462 Stop the agent's movement when it reaches a site to inspect. 463 In addition, the current site identifier is saved to the DataFrame. 464 465 ```python 466 class SiteInspector(Agent): 467 def update(self) -> None: 468 site_id = self.on_site_id() 469 470 # Save the site id to the DataFrame 471 self.save_data("site", site_id) 472 473 # bool(site_id) would be inaccurate 474 # as a site_id of 0 will return False. 475 # Therefore, we check whether it is not None instead. 476 if site_id is not None: 477 # Inspect the site 478 self.freeze_movement() 479 ``` 480 481 """ 482 site: _StaticSprite | None = pg.sprite.spritecollideany( 483 self, # type: ignore[reportArgumentType] 484 self._sites, 485 pg.sprite.collide_mask, 486 ) 487 488 if site is not None: 489 return site.id 490 return None
Get the identifier of the site the agent is currently on.
Examples
Stop the agent's movement when it reaches a site to inspect. In addition, the current site identifier is saved to the DataFrame.
class SiteInspector(Agent):
def update(self) -> None:
site_id = self.on_site_id()
# Save the site id to the DataFrame
self.save_data("site", site_id)
# bool(site_id) would be inaccurate
# as a site_id of 0 will return False.
# Therefore, we check whether it is not None instead.
if site_id is not None:
# Inspect the site
self.freeze_movement()
492 def freeze_movement(self) -> None: 493 """Freeze the movement of the agent. The movement can be continued by calling `continue_movement`.""" 494 self._moving = False
Freeze the movement of the agent. The movement can be continued by calling continue_movement
.
496 def continue_movement(self) -> None: 497 """Continue the movement of the agent from before its movement was frozen.""" 498 self._moving = True
Continue the movement of the agent from before its movement was frozen.
500 def change_image(self, index: int) -> None: 501 """Change the image of the agent. 502 503 If you want to change the agent's image to the second image in the images list, 504 then you can change the image to index 1: 505 506 ```python 507 self.change_image(1) 508 ``` 509 """ 510 self._image_index = index
Change the image of the agent.
If you want to change the agent's image to the second image in the images list, then you can change the image to index 1:
self.change_image(1)
512 def save_data(self, column: str, value: Any) -> None: # noqa: ANN401 513 """Add extra data to the simulation's metrics. 514 515 The following data is collected automatically: 516 - agent identifier 517 - current frame 518 - x and y coordinates 519 520 Examples 521 -------- 522 Saving the number of agents that are currently in proximity: 523 524 ```python 525 from vi.util import count 526 527 528 class MyAgent(Agent): 529 def update(self) -> None: 530 in_proximity = count(self.in_proximity_accuracy()) 531 532 self.save_data("in_proximity", in_proximity) 533 ``` 534 535 """ 536 self.__simulation._metrics._temporary_snapshots[column].append(value)
Add extra data to the simulation's metrics.
The following data is collected automatically:
- agent identifier
- current frame
- x and y coordinates
Examples
Saving the number of agents that are currently in proximity:
from vi.util import count
class MyAgent(Agent):
def update(self) -> None:
in_proximity = count(self.in_proximity_accuracy())
self.save_data("in_proximity", in_proximity)
571 def reproduce(self) -> Self: 572 """Create a new agent and spawn it into the simulation. 573 574 All values will be reset to their defaults, 575 except for the agent's position and movement vector. 576 These will be cloned from the original agent. 577 """ 578 return copy(self)
Create a new agent and spawn it into the simulation.
All values will be reset to their defaults, except for the agent's position and movement vector. These will be cloned from the original agent.