bastd.actor.spazbot
Bot versions of Spaz.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Bot versions of Spaz.""" 4# pylint: disable=too-many-lines 5 6from __future__ import annotations 7 8import random 9import weakref 10from typing import TYPE_CHECKING 11 12import ba 13from bastd.actor.spaz import Spaz 14 15if TYPE_CHECKING: 16 from typing import Any, Sequence, Callable 17 from bastd.actor.flag import Flag 18 19LITE_BOT_COLOR = (1.2, 0.9, 0.2) 20LITE_BOT_HIGHLIGHT = (1.0, 0.5, 0.6) 21DEFAULT_BOT_COLOR = (0.6, 0.6, 0.6) 22DEFAULT_BOT_HIGHLIGHT = (0.1, 0.3, 0.1) 23PRO_BOT_COLOR = (1.0, 0.2, 0.1) 24PRO_BOT_HIGHLIGHT = (0.6, 0.1, 0.05) 25 26 27class SpazBotPunchedMessage: 28 """A message saying a ba.SpazBot got punched. 29 30 Category: **Message Classes** 31 """ 32 33 spazbot: SpazBot 34 """The ba.SpazBot that got punched.""" 35 36 damage: int 37 """How much damage was done to the SpazBot.""" 38 39 def __init__(self, spazbot: SpazBot, damage: int): 40 """Instantiate a message with the given values.""" 41 self.spazbot = spazbot 42 self.damage = damage 43 44 45class SpazBotDiedMessage: 46 """A message saying a ba.SpazBot has died. 47 48 Category: **Message Classes** 49 """ 50 51 spazbot: SpazBot 52 """The SpazBot that was killed.""" 53 54 killerplayer: ba.Player | None 55 """The ba.Player that killed it (or None).""" 56 57 how: ba.DeathType 58 """The particular type of death.""" 59 60 def __init__( 61 self, 62 spazbot: SpazBot, 63 killerplayer: ba.Player | None, 64 how: ba.DeathType, 65 ): 66 """Instantiate with given values.""" 67 self.spazbot = spazbot 68 self.killerplayer = killerplayer 69 self.how = how 70 71 72class SpazBot(Spaz): 73 """A really dumb AI version of ba.Spaz. 74 75 Category: **Bot Classes** 76 77 Add these to a ba.BotSet to use them. 78 79 Note: currently the AI has no real ability to 80 navigate obstacles and so should only be used 81 on wide-open maps. 82 83 When a SpazBot is killed, it delivers a ba.SpazBotDiedMessage 84 to the current activity. 85 86 When a SpazBot is punched, it delivers a ba.SpazBotPunchedMessage 87 to the current activity. 88 """ 89 90 character = 'Spaz' 91 punchiness = 0.5 92 throwiness = 0.7 93 static = False 94 bouncy = False 95 run = False 96 charge_dist_min = 0.0 # When we can start a new charge. 97 charge_dist_max = 2.0 # When we can start a new charge. 98 run_dist_min = 0.0 # How close we can be to continue running. 99 charge_speed_min = 0.4 100 charge_speed_max = 1.0 101 throw_dist_min = 5.0 102 throw_dist_max = 9.0 103 throw_rate = 1.0 104 default_bomb_type = 'normal' 105 default_bomb_count = 3 106 start_cursed = False 107 color = DEFAULT_BOT_COLOR 108 highlight = DEFAULT_BOT_HIGHLIGHT 109 110 def __init__(self) -> None: 111 """Instantiate a spaz-bot.""" 112 super().__init__( 113 color=self.color, 114 highlight=self.highlight, 115 character=self.character, 116 source_player=None, 117 start_invincible=False, 118 can_accept_powerups=False, 119 ) 120 121 # If you need to add custom behavior to a bot, set this to a callable 122 # which takes one arg (the bot) and returns False if the bot's normal 123 # update should be run and True if not. 124 self.update_callback: Callable[[SpazBot], Any] | None = None 125 activity = self.activity 126 assert isinstance(activity, ba.GameActivity) 127 self._map = weakref.ref(activity.map) 128 self.last_player_attacked_by: ba.Player | None = None 129 self.last_attacked_time = 0.0 130 self.last_attacked_type: tuple[str, str] | None = None 131 self.target_point_default: ba.Vec3 | None = None 132 self.held_count = 0 133 self.last_player_held_by: ba.Player | None = None 134 self.target_flag: Flag | None = None 135 self._charge_speed = 0.5 * ( 136 self.charge_speed_min + self.charge_speed_max 137 ) 138 self._lead_amount = 0.5 139 self._mode = 'wait' 140 self._charge_closing_in = False 141 self._last_charge_dist = 0.0 142 self._running = False 143 self._last_jump_time = 0.0 144 145 self._throw_release_time: float | None = None 146 self._have_dropped_throw_bomb: bool | None = None 147 self._player_pts: list[tuple[ba.Vec3, ba.Vec3]] | None = None 148 149 # These cooldowns didn't exist when these bots were calibrated, 150 # so take them out of the equation. 151 self._jump_cooldown = 0 152 self._pickup_cooldown = 0 153 self._fly_cooldown = 0 154 self._bomb_cooldown = 0 155 156 if self.start_cursed: 157 self.curse() 158 159 @property 160 def map(self) -> ba.Map: 161 """The map this bot was created on.""" 162 mval = self._map() 163 assert mval is not None 164 return mval 165 166 def _get_target_player_pt(self) -> tuple[ba.Vec3 | None, ba.Vec3 | None]: 167 """Returns the position and velocity of our target. 168 169 Both values will be None in the case of no target. 170 """ 171 assert self.node 172 botpt = ba.Vec3(self.node.position) 173 closest_dist: float | None = None 174 closest_vel: ba.Vec3 | None = None 175 closest: ba.Vec3 | None = None 176 assert self._player_pts is not None 177 for plpt, plvel in self._player_pts: 178 dist = (plpt - botpt).length() 179 180 # Ignore player-points that are significantly below the bot 181 # (keeps bots from following players off cliffs). 182 if (closest_dist is None or dist < closest_dist) and ( 183 plpt[1] > botpt[1] - 5.0 184 ): 185 closest_dist = dist 186 closest_vel = plvel 187 closest = plpt 188 if closest_dist is not None: 189 assert closest_vel is not None 190 assert closest is not None 191 return ( 192 ba.Vec3(closest[0], closest[1], closest[2]), 193 ba.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]), 194 ) 195 return None, None 196 197 def set_player_points(self, pts: list[tuple[ba.Vec3, ba.Vec3]]) -> None: 198 """Provide the spaz-bot with the locations of its enemies.""" 199 self._player_pts = pts 200 201 def update_ai(self) -> None: 202 """Should be called periodically to update the spaz' AI.""" 203 # pylint: disable=too-many-branches 204 # pylint: disable=too-many-statements 205 # pylint: disable=too-many-locals 206 if self.update_callback is not None: 207 if self.update_callback(self): 208 # Bot has been handled. 209 return 210 211 if not self.node: 212 return 213 214 pos = self.node.position 215 our_pos = ba.Vec3(pos[0], 0, pos[2]) 216 can_attack = True 217 218 target_pt_raw: ba.Vec3 | None 219 target_vel: ba.Vec3 | None 220 221 # If we're a flag-bearer, we're pretty simple-minded - just walk 222 # towards the flag and try to pick it up. 223 if self.target_flag: 224 if self.node.hold_node: 225 holding_flag = self.node.hold_node.getnodetype() == 'flag' 226 else: 227 holding_flag = False 228 229 # If we're holding the flag, just walk left. 230 if holding_flag: 231 # Just walk left. 232 self.node.move_left_right = -1.0 233 self.node.move_up_down = 0.0 234 235 # Otherwise try to go pick it up. 236 elif self.target_flag.node: 237 target_pt_raw = ba.Vec3(*self.target_flag.node.position) 238 diff = target_pt_raw - our_pos 239 diff = ba.Vec3(diff[0], 0, diff[2]) # Don't care about y. 240 dist = diff.length() 241 to_target = diff.normalized() 242 243 # If we're holding some non-flag item, drop it. 244 if self.node.hold_node: 245 self.node.pickup_pressed = True 246 self.node.pickup_pressed = False 247 return 248 249 # If we're a runner, run only when not super-near the flag. 250 if self.run and dist > 3.0: 251 self._running = True 252 self.node.run = 1.0 253 else: 254 self._running = False 255 self.node.run = 0.0 256 257 self.node.move_left_right = to_target.x 258 self.node.move_up_down = -to_target.z 259 if dist < 1.25: 260 self.node.pickup_pressed = True 261 self.node.pickup_pressed = False 262 return 263 264 # Not a flag-bearer. If we're holding anything but a bomb, drop it. 265 if self.node.hold_node: 266 holding_bomb = self.node.hold_node.getnodetype() in ['bomb', 'prop'] 267 if not holding_bomb: 268 self.node.pickup_pressed = True 269 self.node.pickup_pressed = False 270 return 271 272 target_pt_raw, target_vel = self._get_target_player_pt() 273 274 if target_pt_raw is None: 275 # Use default target if we've got one. 276 if self.target_point_default is not None: 277 target_pt_raw = self.target_point_default 278 target_vel = ba.Vec3(0, 0, 0) 279 can_attack = False 280 281 # With no target, we stop moving and drop whatever we're holding. 282 else: 283 self.node.move_left_right = 0 284 self.node.move_up_down = 0 285 if self.node.hold_node: 286 self.node.pickup_pressed = True 287 self.node.pickup_pressed = False 288 return 289 290 # We don't want height to come into play. 291 target_pt_raw[1] = 0.0 292 assert target_vel is not None 293 target_vel[1] = 0.0 294 295 dist_raw = (target_pt_raw - our_pos).length() 296 297 # Use a point out in front of them as real target. 298 # (more out in front the farther from us they are) 299 target_pt = ( 300 target_pt_raw + target_vel * dist_raw * 0.3 * self._lead_amount 301 ) 302 303 diff = target_pt - our_pos 304 dist = diff.length() 305 to_target = diff.normalized() 306 307 if self._mode == 'throw': 308 # We can only throw if alive and well. 309 if not self._dead and not self.node.knockout: 310 311 assert self._throw_release_time is not None 312 time_till_throw = self._throw_release_time - ba.time() 313 314 if not self.node.hold_node: 315 # If we haven't thrown yet, whip out the bomb. 316 if not self._have_dropped_throw_bomb: 317 self.drop_bomb() 318 self._have_dropped_throw_bomb = True 319 320 # Otherwise our lack of held node means we successfully 321 # released our bomb; lets retreat now. 322 else: 323 self._mode = 'flee' 324 325 # Oh crap, we're holding a bomb; better throw it. 326 elif time_till_throw <= 0.0: 327 # Jump and throw. 328 def _safe_pickup(node: ba.Node) -> None: 329 if node and self.node: 330 self.node.pickup_pressed = True 331 self.node.pickup_pressed = False 332 333 if dist > 5.0: 334 self.node.jump_pressed = True 335 self.node.jump_pressed = False 336 337 # Throws: 338 ba.timer(0.1, ba.Call(_safe_pickup, self.node)) 339 else: 340 # Throws: 341 ba.timer(0.1, ba.Call(_safe_pickup, self.node)) 342 343 if self.static: 344 if time_till_throw < 0.3: 345 speed = 1.0 346 elif time_till_throw < 0.7 and dist > 3.0: 347 speed = -1.0 # Whiplash for long throws. 348 else: 349 speed = 0.02 350 else: 351 if time_till_throw < 0.7: 352 # Right before throw charge full speed towards target. 353 speed = 1.0 354 else: 355 # Earlier we can hold or move backward for a whiplash. 356 speed = 0.0125 357 self.node.move_left_right = to_target.x * speed 358 self.node.move_up_down = to_target.z * -1.0 * speed 359 360 elif self._mode == 'charge': 361 if random.random() < 0.3: 362 self._charge_speed = random.uniform( 363 self.charge_speed_min, self.charge_speed_max 364 ) 365 366 # If we're a runner we run during charges *except when near 367 # an edge (otherwise we tend to fly off easily). 368 if self.run and dist_raw > self.run_dist_min: 369 self._lead_amount = 0.3 370 self._running = True 371 self.node.run = 1.0 372 else: 373 self._lead_amount = 0.01 374 self._running = False 375 self.node.run = 0.0 376 377 self.node.move_left_right = to_target.x * self._charge_speed 378 self.node.move_up_down = to_target.z * -1.0 * self._charge_speed 379 380 elif self._mode == 'wait': 381 # Every now and then, aim towards our target. 382 # Other than that, just stand there. 383 if ba.time(timeformat=ba.TimeFormat.MILLISECONDS) % 1234 < 100: 384 self.node.move_left_right = to_target.x * (400.0 / 33000) 385 self.node.move_up_down = to_target.z * (-400.0 / 33000) 386 else: 387 self.node.move_left_right = 0 388 self.node.move_up_down = 0 389 390 elif self._mode == 'flee': 391 # Even if we're a runner, only run till we get away from our 392 # target (if we keep running we tend to run off edges). 393 if self.run and dist < 3.0: 394 self._running = True 395 self.node.run = 1.0 396 else: 397 self._running = False 398 self.node.run = 0.0 399 self.node.move_left_right = to_target.x * -1.0 400 self.node.move_up_down = to_target.z 401 402 # We might wanna switch states unless we're doing a throw 403 # (in which case that's our sole concern). 404 if self._mode != 'throw': 405 406 # If we're currently charging, keep track of how far we are 407 # from our target. When this value increases it means our charge 408 # is over (ran by them or something). 409 if self._mode == 'charge': 410 if ( 411 self._charge_closing_in 412 and self._last_charge_dist < dist < 3.0 413 ): 414 self._charge_closing_in = False 415 self._last_charge_dist = dist 416 417 # If we have a clean shot, throw! 418 if ( 419 self.throw_dist_min <= dist < self.throw_dist_max 420 and random.random() < self.throwiness 421 and can_attack 422 ): 423 self._mode = 'throw' 424 self._lead_amount = ( 425 (0.4 + random.random() * 0.6) 426 if dist_raw > 4.0 427 else (0.1 + random.random() * 0.4) 428 ) 429 self._have_dropped_throw_bomb = False 430 self._throw_release_time = ba.time() + ( 431 1.0 / self.throw_rate 432 ) * (0.8 + 1.3 * random.random()) 433 434 # If we're static, always charge (which for us means barely move). 435 elif self.static: 436 self._mode = 'wait' 437 438 # If we're too close to charge (and aren't in the middle of an 439 # existing charge) run away. 440 elif dist < self.charge_dist_min and not self._charge_closing_in: 441 # ..unless we're near an edge, in which case we've got no 442 # choice but to charge. 443 if self.map.is_point_near_edge(our_pos, self._running): 444 if self._mode != 'charge': 445 self._mode = 'charge' 446 self._lead_amount = 0.2 447 self._charge_closing_in = True 448 self._last_charge_dist = dist 449 else: 450 self._mode = 'flee' 451 452 # We're within charging distance, backed against an edge, 453 # or farther than our max throw distance.. chaaarge! 454 elif ( 455 dist < self.charge_dist_max 456 or dist > self.throw_dist_max 457 or self.map.is_point_near_edge(our_pos, self._running) 458 ): 459 if self._mode != 'charge': 460 self._mode = 'charge' 461 self._lead_amount = 0.01 462 self._charge_closing_in = True 463 self._last_charge_dist = dist 464 465 # We're too close to throw but too far to charge - either run 466 # away or just chill if we're near an edge. 467 elif dist < self.throw_dist_min: 468 # Charge if either we're within charge range or 469 # cant retreat to throw. 470 self._mode = 'flee' 471 472 # Do some awesome jumps if we're running. 473 # FIXME: pylint: disable=too-many-boolean-expressions 474 if ( 475 self._running 476 and 1.2 < dist < 2.2 477 and ba.time() - self._last_jump_time > 1.0 478 ) or ( 479 self.bouncy 480 and ba.time() - self._last_jump_time > 0.4 481 and random.random() < 0.5 482 ): 483 self._last_jump_time = ba.time() 484 self.node.jump_pressed = True 485 self.node.jump_pressed = False 486 487 # Throw punches when real close. 488 if dist < (1.6 if self._running else 1.2) and can_attack: 489 if random.random() < self.punchiness: 490 self.on_punch_press() 491 self.on_punch_release() 492 493 def on_punched(self, damage: int) -> None: 494 """ 495 Method override; sends ba.SpazBotPunchedMessage 496 to the current activity. 497 """ 498 ba.getactivity().handlemessage(SpazBotPunchedMessage(self, damage)) 499 500 def on_expire(self) -> None: 501 super().on_expire() 502 503 # We're being torn down; release our callback(s) so there's 504 # no chance of them keeping activities or other things alive. 505 self.update_callback = None 506 507 def handlemessage(self, msg: Any) -> Any: 508 # pylint: disable=too-many-branches 509 assert not self.expired 510 511 # Keep track of if we're being held and by who most recently. 512 if isinstance(msg, ba.PickedUpMessage): 513 super().handlemessage(msg) # Augment standard behavior. 514 self.held_count += 1 515 picked_up_by = msg.node.source_player 516 if picked_up_by: 517 self.last_player_held_by = picked_up_by 518 519 elif isinstance(msg, ba.DroppedMessage): 520 super().handlemessage(msg) # Augment standard behavior. 521 self.held_count -= 1 522 if self.held_count < 0: 523 print('ERROR: spaz held_count < 0') 524 525 # Let's count someone dropping us as an attack. 526 try: 527 if msg.node: 528 picked_up_by = msg.node.source_player 529 else: 530 picked_up_by = None 531 except Exception: 532 ba.print_exception('Error on SpazBot DroppedMessage.') 533 picked_up_by = None 534 535 if picked_up_by: 536 self.last_player_attacked_by = picked_up_by 537 self.last_attacked_time = ba.time() 538 self.last_attacked_type = ('picked_up', 'default') 539 540 elif isinstance(msg, ba.DieMessage): 541 542 # Report normal deaths for scoring purposes. 543 if not self._dead and not msg.immediate: 544 545 killerplayer: ba.Player | None 546 547 # If this guy was being held at the time of death, the 548 # holder is the killer. 549 if self.held_count > 0 and self.last_player_held_by: 550 killerplayer = self.last_player_held_by 551 else: 552 # If they were attacked by someone in the last few 553 # seconds that person's the killer. 554 # Otherwise it was a suicide. 555 if ( 556 self.last_player_attacked_by 557 and ba.time() - self.last_attacked_time < 4.0 558 ): 559 killerplayer = self.last_player_attacked_by 560 else: 561 killerplayer = None 562 activity = self._activity() 563 564 # (convert dead player refs to None) 565 if not killerplayer: 566 killerplayer = None 567 if activity is not None: 568 activity.handlemessage( 569 SpazBotDiedMessage(self, killerplayer, msg.how) 570 ) 571 super().handlemessage(msg) # Augment standard behavior. 572 573 # Keep track of the player who last hit us for point rewarding. 574 elif isinstance(msg, ba.HitMessage): 575 source_player = msg.get_source_player(ba.Player) 576 if source_player: 577 self.last_player_attacked_by = source_player 578 self.last_attacked_time = ba.time() 579 self.last_attacked_type = (msg.hit_type, msg.hit_subtype) 580 super().handlemessage(msg) 581 else: 582 super().handlemessage(msg) 583 584 585class BomberBot(SpazBot): 586 """A bot that throws regular bombs and occasionally punches. 587 588 category: Bot Classes 589 """ 590 591 character = 'Spaz' 592 punchiness = 0.3 593 594 595class BomberBotLite(BomberBot): 596 """A less aggressive yellow version of ba.BomberBot. 597 598 category: Bot Classes 599 """ 600 601 color = LITE_BOT_COLOR 602 highlight = LITE_BOT_HIGHLIGHT 603 punchiness = 0.2 604 throw_rate = 0.7 605 throwiness = 0.1 606 charge_speed_min = 0.6 607 charge_speed_max = 0.6 608 609 610class BomberBotStaticLite(BomberBotLite): 611 """A less aggressive generally immobile weak version of ba.BomberBot. 612 613 category: Bot Classes 614 """ 615 616 static = True 617 throw_dist_min = 0.0 618 619 620class BomberBotStatic(BomberBot): 621 """A version of ba.BomberBot who generally stays in one place. 622 623 category: Bot Classes 624 """ 625 626 static = True 627 throw_dist_min = 0.0 628 629 630class BomberBotPro(BomberBot): 631 """A more powerful version of ba.BomberBot. 632 633 category: Bot Classes 634 """ 635 636 points_mult = 2 637 color = PRO_BOT_COLOR 638 highlight = PRO_BOT_HIGHLIGHT 639 default_bomb_count = 3 640 default_boxing_gloves = True 641 punchiness = 0.7 642 throw_rate = 1.3 643 run = True 644 run_dist_min = 6.0 645 646 647class BomberBotProShielded(BomberBotPro): 648 """A more powerful version of ba.BomberBot who starts with shields. 649 650 category: Bot Classes 651 """ 652 653 points_mult = 3 654 default_shields = True 655 656 657class BomberBotProStatic(BomberBotPro): 658 """A more powerful ba.BomberBot who generally stays in one place. 659 660 category: Bot Classes 661 """ 662 663 static = True 664 throw_dist_min = 0.0 665 666 667class BomberBotProStaticShielded(BomberBotProShielded): 668 """A powerful ba.BomberBot with shields who is generally immobile. 669 670 category: Bot Classes 671 """ 672 673 static = True 674 throw_dist_min = 0.0 675 676 677class BrawlerBot(SpazBot): 678 """A bot who walks and punches things. 679 680 category: Bot Classes 681 """ 682 683 character = 'Kronk' 684 punchiness = 0.9 685 charge_dist_max = 9999.0 686 charge_speed_min = 1.0 687 charge_speed_max = 1.0 688 throw_dist_min = 9999 689 throw_dist_max = 9999 690 691 692class BrawlerBotLite(BrawlerBot): 693 """A weaker version of ba.BrawlerBot. 694 695 category: Bot Classes 696 """ 697 698 color = LITE_BOT_COLOR 699 highlight = LITE_BOT_HIGHLIGHT 700 punchiness = 0.3 701 charge_speed_min = 0.6 702 charge_speed_max = 0.6 703 704 705class BrawlerBotPro(BrawlerBot): 706 """A stronger version of ba.BrawlerBot. 707 708 category: Bot Classes 709 """ 710 711 color = PRO_BOT_COLOR 712 highlight = PRO_BOT_HIGHLIGHT 713 run = True 714 run_dist_min = 4.0 715 default_boxing_gloves = True 716 punchiness = 0.95 717 points_mult = 2 718 719 720class BrawlerBotProShielded(BrawlerBotPro): 721 """A stronger version of ba.BrawlerBot who starts with shields. 722 723 category: Bot Classes 724 """ 725 726 default_shields = True 727 points_mult = 3 728 729 730class ChargerBot(SpazBot): 731 """A speedy melee attack bot. 732 733 category: Bot Classes 734 """ 735 736 character = 'Snake Shadow' 737 punchiness = 1.0 738 run = True 739 charge_dist_min = 10.0 740 charge_dist_max = 9999.0 741 charge_speed_min = 1.0 742 charge_speed_max = 1.0 743 throw_dist_min = 9999 744 throw_dist_max = 9999 745 points_mult = 2 746 747 748class BouncyBot(SpazBot): 749 """A speedy attacking melee bot that jumps constantly. 750 751 category: Bot Classes 752 """ 753 754 color = (1, 1, 1) 755 highlight = (1.0, 0.5, 0.5) 756 character = 'Easter Bunny' 757 punchiness = 1.0 758 run = True 759 bouncy = True 760 default_boxing_gloves = True 761 charge_dist_min = 10.0 762 charge_dist_max = 9999.0 763 charge_speed_min = 1.0 764 charge_speed_max = 1.0 765 throw_dist_min = 9999 766 throw_dist_max = 9999 767 points_mult = 2 768 769 770class ChargerBotPro(ChargerBot): 771 """A stronger ba.ChargerBot. 772 773 category: Bot Classes 774 """ 775 776 color = PRO_BOT_COLOR 777 highlight = PRO_BOT_HIGHLIGHT 778 default_shields = True 779 default_boxing_gloves = True 780 points_mult = 3 781 782 783class ChargerBotProShielded(ChargerBotPro): 784 """A stronger ba.ChargerBot who starts with shields. 785 786 category: Bot Classes 787 """ 788 789 default_shields = True 790 points_mult = 4 791 792 793class TriggerBot(SpazBot): 794 """A slow moving bot with trigger bombs. 795 796 category: Bot Classes 797 """ 798 799 character = 'Zoe' 800 punchiness = 0.75 801 throwiness = 0.7 802 charge_dist_max = 1.0 803 charge_speed_min = 0.3 804 charge_speed_max = 0.5 805 throw_dist_min = 3.5 806 throw_dist_max = 5.5 807 default_bomb_type = 'impact' 808 points_mult = 2 809 810 811class TriggerBotStatic(TriggerBot): 812 """A ba.TriggerBot who generally stays in one place. 813 814 category: Bot Classes 815 """ 816 817 static = True 818 throw_dist_min = 0.0 819 820 821class TriggerBotPro(TriggerBot): 822 """A stronger version of ba.TriggerBot. 823 824 category: Bot Classes 825 """ 826 827 color = PRO_BOT_COLOR 828 highlight = PRO_BOT_HIGHLIGHT 829 default_bomb_count = 3 830 default_boxing_gloves = True 831 charge_speed_min = 1.0 832 charge_speed_max = 1.0 833 punchiness = 0.9 834 throw_rate = 1.3 835 run = True 836 run_dist_min = 6.0 837 points_mult = 3 838 839 840class TriggerBotProShielded(TriggerBotPro): 841 """A stronger version of ba.TriggerBot who starts with shields. 842 843 category: Bot Classes 844 """ 845 846 default_shields = True 847 points_mult = 4 848 849 850class StickyBot(SpazBot): 851 """A crazy bot who runs and throws sticky bombs. 852 853 category: Bot Classes 854 """ 855 856 character = 'Mel' 857 punchiness = 0.9 858 throwiness = 1.0 859 run = True 860 charge_dist_min = 4.0 861 charge_dist_max = 10.0 862 charge_speed_min = 1.0 863 charge_speed_max = 1.0 864 throw_dist_min = 0.0 865 throw_dist_max = 4.0 866 throw_rate = 2.0 867 default_bomb_type = 'sticky' 868 default_bomb_count = 3 869 points_mult = 3 870 871 872class StickyBotStatic(StickyBot): 873 """A crazy bot who throws sticky-bombs but generally stays in one place. 874 875 category: Bot Classes 876 """ 877 878 static = True 879 880 881class ExplodeyBot(SpazBot): 882 """A bot who runs and explodes in 5 seconds. 883 884 category: Bot Classes 885 """ 886 887 character = 'Jack Morgan' 888 run = True 889 charge_dist_min = 0.0 890 charge_dist_max = 9999 891 charge_speed_min = 1.0 892 charge_speed_max = 1.0 893 throw_dist_min = 9999 894 throw_dist_max = 9999 895 start_cursed = True 896 points_mult = 4 897 898 899class ExplodeyBotNoTimeLimit(ExplodeyBot): 900 """A bot who runs but does not explode on his own. 901 902 category: Bot Classes 903 """ 904 905 curse_time = None 906 907 908class ExplodeyBotShielded(ExplodeyBot): 909 """A ba.ExplodeyBot who starts with shields. 910 911 category: Bot Classes 912 """ 913 914 default_shields = True 915 points_mult = 5 916 917 918class SpazBotSet: 919 """A container/controller for one or more ba.SpazBots. 920 921 category: Bot Classes 922 """ 923 924 def __init__(self) -> None: 925 """Create a bot-set.""" 926 927 # We spread our bots out over a few lists so we can update 928 # them in a staggered fashion. 929 self._bot_list_count = 5 930 self._bot_add_list = 0 931 self._bot_update_list = 0 932 self._bot_lists: list[list[SpazBot]] = [ 933 [] for _ in range(self._bot_list_count) 934 ] 935 self._spawn_sound = ba.getsound('spawn') 936 self._spawning_count = 0 937 self._bot_update_timer: ba.Timer | None = None 938 self.start_moving() 939 940 def __del__(self) -> None: 941 self.clear() 942 943 def spawn_bot( 944 self, 945 bot_type: type[SpazBot], 946 pos: Sequence[float], 947 spawn_time: float = 3.0, 948 on_spawn_call: Callable[[SpazBot], Any] | None = None, 949 ) -> None: 950 """Spawn a bot from this set.""" 951 from bastd.actor import spawner 952 953 spawner.Spawner( 954 pt=pos, 955 spawn_time=spawn_time, 956 send_spawn_message=False, 957 spawn_callback=ba.Call( 958 self._spawn_bot, bot_type, pos, on_spawn_call 959 ), 960 ) 961 self._spawning_count += 1 962 963 def _spawn_bot( 964 self, 965 bot_type: type[SpazBot], 966 pos: Sequence[float], 967 on_spawn_call: Callable[[SpazBot], Any] | None, 968 ) -> None: 969 spaz = bot_type() 970 ba.playsound(self._spawn_sound, position=pos) 971 assert spaz.node 972 spaz.node.handlemessage('flash') 973 spaz.node.is_area_of_interest = False 974 spaz.handlemessage(ba.StandMessage(pos, random.uniform(0, 360))) 975 self.add_bot(spaz) 976 self._spawning_count -= 1 977 if on_spawn_call is not None: 978 on_spawn_call(spaz) 979 980 def have_living_bots(self) -> bool: 981 """Return whether any bots in the set are alive or spawning.""" 982 return self._spawning_count > 0 or any( 983 any(b.is_alive() for b in l) for l in self._bot_lists 984 ) 985 986 def get_living_bots(self) -> list[SpazBot]: 987 """Get the living bots in the set.""" 988 bots: list[SpazBot] = [] 989 for botlist in self._bot_lists: 990 for bot in botlist: 991 if bot.is_alive(): 992 bots.append(bot) 993 return bots 994 995 def _update(self) -> None: 996 997 # Update one of our bot lists each time through. 998 # First off, remove no-longer-existing bots from the list. 999 try: 1000 bot_list = self._bot_lists[self._bot_update_list] = [ 1001 b for b in self._bot_lists[self._bot_update_list] if b 1002 ] 1003 except Exception: 1004 bot_list = [] 1005 ba.print_exception( 1006 'Error updating bot list: ' 1007 + str(self._bot_lists[self._bot_update_list]) 1008 ) 1009 self._bot_update_list = ( 1010 self._bot_update_list + 1 1011 ) % self._bot_list_count 1012 1013 # Update our list of player points for the bots to use. 1014 player_pts = [] 1015 for player in ba.getactivity().players: 1016 assert isinstance(player, ba.Player) 1017 try: 1018 # TODO: could use abstracted player.position here so we 1019 # don't have to assume their actor type, but we have no 1020 # abstracted velocity as of yet. 1021 if player.is_alive(): 1022 assert isinstance(player.actor, Spaz) 1023 assert player.actor.node 1024 player_pts.append( 1025 ( 1026 ba.Vec3(player.actor.node.position), 1027 ba.Vec3(player.actor.node.velocity), 1028 ) 1029 ) 1030 except Exception: 1031 ba.print_exception('Error on bot-set _update.') 1032 1033 for bot in bot_list: 1034 bot.set_player_points(player_pts) 1035 bot.update_ai() 1036 1037 def clear(self) -> None: 1038 """Immediately clear out any bots in the set.""" 1039 1040 # Don't do this if the activity is shutting down or dead. 1041 activity = ba.getactivity(doraise=False) 1042 if activity is None or activity.expired: 1043 return 1044 1045 for i, bot_list in enumerate(self._bot_lists): 1046 for bot in bot_list: 1047 bot.handlemessage(ba.DieMessage(immediate=True)) 1048 self._bot_lists[i] = [] 1049 1050 def start_moving(self) -> None: 1051 """Start processing bot AI updates so they start doing their thing.""" 1052 self._bot_update_timer = ba.Timer( 1053 0.05, ba.WeakCall(self._update), repeat=True 1054 ) 1055 1056 def stop_moving(self) -> None: 1057 """Tell all bots to stop moving and stops updating their AI. 1058 1059 Useful when players have won and you want the 1060 enemy bots to just stand and look bewildered. 1061 """ 1062 self._bot_update_timer = None 1063 for botlist in self._bot_lists: 1064 for bot in botlist: 1065 if bot.node: 1066 bot.node.move_left_right = 0 1067 bot.node.move_up_down = 0 1068 1069 def celebrate(self, duration: float) -> None: 1070 """Tell all living bots in the set to celebrate momentarily. 1071 1072 Duration is given in seconds. 1073 """ 1074 msg = ba.CelebrateMessage(duration=duration) 1075 for botlist in self._bot_lists: 1076 for bot in botlist: 1077 if bot: 1078 bot.handlemessage(msg) 1079 1080 def final_celebrate(self) -> None: 1081 """Tell all bots in the set to stop what they were doing and celebrate. 1082 1083 Use this when the bots have won a game. 1084 """ 1085 self._bot_update_timer = None 1086 1087 # At this point stop doing anything but jumping and celebrating. 1088 for botlist in self._bot_lists: 1089 for bot in botlist: 1090 if bot: 1091 assert bot.node # (should exist if 'if bot' was True) 1092 bot.node.move_left_right = 0 1093 bot.node.move_up_down = 0 1094 ba.timer( 1095 0.5 * random.random(), 1096 ba.Call(bot.handlemessage, ba.CelebrateMessage()), 1097 ) 1098 jump_duration = random.randrange(400, 500) 1099 j = random.randrange(0, 200) 1100 for _i in range(10): 1101 bot.node.jump_pressed = True 1102 bot.node.jump_pressed = False 1103 j += jump_duration 1104 ba.timer( 1105 random.uniform(0.0, 1.0), 1106 ba.Call(bot.node.handlemessage, 'attack_sound'), 1107 ) 1108 ba.timer( 1109 random.uniform(1.0, 2.0), 1110 ba.Call(bot.node.handlemessage, 'attack_sound'), 1111 ) 1112 ba.timer( 1113 random.uniform(2.0, 3.0), 1114 ba.Call(bot.node.handlemessage, 'attack_sound'), 1115 ) 1116 1117 def add_bot(self, bot: SpazBot) -> None: 1118 """Add a ba.SpazBot instance to the set.""" 1119 self._bot_lists[self._bot_add_list].append(bot) 1120 self._bot_add_list = (self._bot_add_list + 1) % self._bot_list_count
28class SpazBotPunchedMessage: 29 """A message saying a ba.SpazBot got punched. 30 31 Category: **Message Classes** 32 """ 33 34 spazbot: SpazBot 35 """The ba.SpazBot that got punched.""" 36 37 damage: int 38 """How much damage was done to the SpazBot.""" 39 40 def __init__(self, spazbot: SpazBot, damage: int): 41 """Instantiate a message with the given values.""" 42 self.spazbot = spazbot 43 self.damage = damage
A message saying a ba.SpazBot got punched.
Category: Message Classes
40 def __init__(self, spazbot: SpazBot, damage: int): 41 """Instantiate a message with the given values.""" 42 self.spazbot = spazbot 43 self.damage = damage
Instantiate a message with the given values.
46class SpazBotDiedMessage: 47 """A message saying a ba.SpazBot has died. 48 49 Category: **Message Classes** 50 """ 51 52 spazbot: SpazBot 53 """The SpazBot that was killed.""" 54 55 killerplayer: ba.Player | None 56 """The ba.Player that killed it (or None).""" 57 58 how: ba.DeathType 59 """The particular type of death.""" 60 61 def __init__( 62 self, 63 spazbot: SpazBot, 64 killerplayer: ba.Player | None, 65 how: ba.DeathType, 66 ): 67 """Instantiate with given values.""" 68 self.spazbot = spazbot 69 self.killerplayer = killerplayer 70 self.how = how
A message saying a ba.SpazBot has died.
Category: Message Classes
61 def __init__( 62 self, 63 spazbot: SpazBot, 64 killerplayer: ba.Player | None, 65 how: ba.DeathType, 66 ): 67 """Instantiate with given values.""" 68 self.spazbot = spazbot 69 self.killerplayer = killerplayer 70 self.how = how
Instantiate with given values.
73class SpazBot(Spaz): 74 """A really dumb AI version of ba.Spaz. 75 76 Category: **Bot Classes** 77 78 Add these to a ba.BotSet to use them. 79 80 Note: currently the AI has no real ability to 81 navigate obstacles and so should only be used 82 on wide-open maps. 83 84 When a SpazBot is killed, it delivers a ba.SpazBotDiedMessage 85 to the current activity. 86 87 When a SpazBot is punched, it delivers a ba.SpazBotPunchedMessage 88 to the current activity. 89 """ 90 91 character = 'Spaz' 92 punchiness = 0.5 93 throwiness = 0.7 94 static = False 95 bouncy = False 96 run = False 97 charge_dist_min = 0.0 # When we can start a new charge. 98 charge_dist_max = 2.0 # When we can start a new charge. 99 run_dist_min = 0.0 # How close we can be to continue running. 100 charge_speed_min = 0.4 101 charge_speed_max = 1.0 102 throw_dist_min = 5.0 103 throw_dist_max = 9.0 104 throw_rate = 1.0 105 default_bomb_type = 'normal' 106 default_bomb_count = 3 107 start_cursed = False 108 color = DEFAULT_BOT_COLOR 109 highlight = DEFAULT_BOT_HIGHLIGHT 110 111 def __init__(self) -> None: 112 """Instantiate a spaz-bot.""" 113 super().__init__( 114 color=self.color, 115 highlight=self.highlight, 116 character=self.character, 117 source_player=None, 118 start_invincible=False, 119 can_accept_powerups=False, 120 ) 121 122 # If you need to add custom behavior to a bot, set this to a callable 123 # which takes one arg (the bot) and returns False if the bot's normal 124 # update should be run and True if not. 125 self.update_callback: Callable[[SpazBot], Any] | None = None 126 activity = self.activity 127 assert isinstance(activity, ba.GameActivity) 128 self._map = weakref.ref(activity.map) 129 self.last_player_attacked_by: ba.Player | None = None 130 self.last_attacked_time = 0.0 131 self.last_attacked_type: tuple[str, str] | None = None 132 self.target_point_default: ba.Vec3 | None = None 133 self.held_count = 0 134 self.last_player_held_by: ba.Player | None = None 135 self.target_flag: Flag | None = None 136 self._charge_speed = 0.5 * ( 137 self.charge_speed_min + self.charge_speed_max 138 ) 139 self._lead_amount = 0.5 140 self._mode = 'wait' 141 self._charge_closing_in = False 142 self._last_charge_dist = 0.0 143 self._running = False 144 self._last_jump_time = 0.0 145 146 self._throw_release_time: float | None = None 147 self._have_dropped_throw_bomb: bool | None = None 148 self._player_pts: list[tuple[ba.Vec3, ba.Vec3]] | None = None 149 150 # These cooldowns didn't exist when these bots were calibrated, 151 # so take them out of the equation. 152 self._jump_cooldown = 0 153 self._pickup_cooldown = 0 154 self._fly_cooldown = 0 155 self._bomb_cooldown = 0 156 157 if self.start_cursed: 158 self.curse() 159 160 @property 161 def map(self) -> ba.Map: 162 """The map this bot was created on.""" 163 mval = self._map() 164 assert mval is not None 165 return mval 166 167 def _get_target_player_pt(self) -> tuple[ba.Vec3 | None, ba.Vec3 | None]: 168 """Returns the position and velocity of our target. 169 170 Both values will be None in the case of no target. 171 """ 172 assert self.node 173 botpt = ba.Vec3(self.node.position) 174 closest_dist: float | None = None 175 closest_vel: ba.Vec3 | None = None 176 closest: ba.Vec3 | None = None 177 assert self._player_pts is not None 178 for plpt, plvel in self._player_pts: 179 dist = (plpt - botpt).length() 180 181 # Ignore player-points that are significantly below the bot 182 # (keeps bots from following players off cliffs). 183 if (closest_dist is None or dist < closest_dist) and ( 184 plpt[1] > botpt[1] - 5.0 185 ): 186 closest_dist = dist 187 closest_vel = plvel 188 closest = plpt 189 if closest_dist is not None: 190 assert closest_vel is not None 191 assert closest is not None 192 return ( 193 ba.Vec3(closest[0], closest[1], closest[2]), 194 ba.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]), 195 ) 196 return None, None 197 198 def set_player_points(self, pts: list[tuple[ba.Vec3, ba.Vec3]]) -> None: 199 """Provide the spaz-bot with the locations of its enemies.""" 200 self._player_pts = pts 201 202 def update_ai(self) -> None: 203 """Should be called periodically to update the spaz' AI.""" 204 # pylint: disable=too-many-branches 205 # pylint: disable=too-many-statements 206 # pylint: disable=too-many-locals 207 if self.update_callback is not None: 208 if self.update_callback(self): 209 # Bot has been handled. 210 return 211 212 if not self.node: 213 return 214 215 pos = self.node.position 216 our_pos = ba.Vec3(pos[0], 0, pos[2]) 217 can_attack = True 218 219 target_pt_raw: ba.Vec3 | None 220 target_vel: ba.Vec3 | None 221 222 # If we're a flag-bearer, we're pretty simple-minded - just walk 223 # towards the flag and try to pick it up. 224 if self.target_flag: 225 if self.node.hold_node: 226 holding_flag = self.node.hold_node.getnodetype() == 'flag' 227 else: 228 holding_flag = False 229 230 # If we're holding the flag, just walk left. 231 if holding_flag: 232 # Just walk left. 233 self.node.move_left_right = -1.0 234 self.node.move_up_down = 0.0 235 236 # Otherwise try to go pick it up. 237 elif self.target_flag.node: 238 target_pt_raw = ba.Vec3(*self.target_flag.node.position) 239 diff = target_pt_raw - our_pos 240 diff = ba.Vec3(diff[0], 0, diff[2]) # Don't care about y. 241 dist = diff.length() 242 to_target = diff.normalized() 243 244 # If we're holding some non-flag item, drop it. 245 if self.node.hold_node: 246 self.node.pickup_pressed = True 247 self.node.pickup_pressed = False 248 return 249 250 # If we're a runner, run only when not super-near the flag. 251 if self.run and dist > 3.0: 252 self._running = True 253 self.node.run = 1.0 254 else: 255 self._running = False 256 self.node.run = 0.0 257 258 self.node.move_left_right = to_target.x 259 self.node.move_up_down = -to_target.z 260 if dist < 1.25: 261 self.node.pickup_pressed = True 262 self.node.pickup_pressed = False 263 return 264 265 # Not a flag-bearer. If we're holding anything but a bomb, drop it. 266 if self.node.hold_node: 267 holding_bomb = self.node.hold_node.getnodetype() in ['bomb', 'prop'] 268 if not holding_bomb: 269 self.node.pickup_pressed = True 270 self.node.pickup_pressed = False 271 return 272 273 target_pt_raw, target_vel = self._get_target_player_pt() 274 275 if target_pt_raw is None: 276 # Use default target if we've got one. 277 if self.target_point_default is not None: 278 target_pt_raw = self.target_point_default 279 target_vel = ba.Vec3(0, 0, 0) 280 can_attack = False 281 282 # With no target, we stop moving and drop whatever we're holding. 283 else: 284 self.node.move_left_right = 0 285 self.node.move_up_down = 0 286 if self.node.hold_node: 287 self.node.pickup_pressed = True 288 self.node.pickup_pressed = False 289 return 290 291 # We don't want height to come into play. 292 target_pt_raw[1] = 0.0 293 assert target_vel is not None 294 target_vel[1] = 0.0 295 296 dist_raw = (target_pt_raw - our_pos).length() 297 298 # Use a point out in front of them as real target. 299 # (more out in front the farther from us they are) 300 target_pt = ( 301 target_pt_raw + target_vel * dist_raw * 0.3 * self._lead_amount 302 ) 303 304 diff = target_pt - our_pos 305 dist = diff.length() 306 to_target = diff.normalized() 307 308 if self._mode == 'throw': 309 # We can only throw if alive and well. 310 if not self._dead and not self.node.knockout: 311 312 assert self._throw_release_time is not None 313 time_till_throw = self._throw_release_time - ba.time() 314 315 if not self.node.hold_node: 316 # If we haven't thrown yet, whip out the bomb. 317 if not self._have_dropped_throw_bomb: 318 self.drop_bomb() 319 self._have_dropped_throw_bomb = True 320 321 # Otherwise our lack of held node means we successfully 322 # released our bomb; lets retreat now. 323 else: 324 self._mode = 'flee' 325 326 # Oh crap, we're holding a bomb; better throw it. 327 elif time_till_throw <= 0.0: 328 # Jump and throw. 329 def _safe_pickup(node: ba.Node) -> None: 330 if node and self.node: 331 self.node.pickup_pressed = True 332 self.node.pickup_pressed = False 333 334 if dist > 5.0: 335 self.node.jump_pressed = True 336 self.node.jump_pressed = False 337 338 # Throws: 339 ba.timer(0.1, ba.Call(_safe_pickup, self.node)) 340 else: 341 # Throws: 342 ba.timer(0.1, ba.Call(_safe_pickup, self.node)) 343 344 if self.static: 345 if time_till_throw < 0.3: 346 speed = 1.0 347 elif time_till_throw < 0.7 and dist > 3.0: 348 speed = -1.0 # Whiplash for long throws. 349 else: 350 speed = 0.02 351 else: 352 if time_till_throw < 0.7: 353 # Right before throw charge full speed towards target. 354 speed = 1.0 355 else: 356 # Earlier we can hold or move backward for a whiplash. 357 speed = 0.0125 358 self.node.move_left_right = to_target.x * speed 359 self.node.move_up_down = to_target.z * -1.0 * speed 360 361 elif self._mode == 'charge': 362 if random.random() < 0.3: 363 self._charge_speed = random.uniform( 364 self.charge_speed_min, self.charge_speed_max 365 ) 366 367 # If we're a runner we run during charges *except when near 368 # an edge (otherwise we tend to fly off easily). 369 if self.run and dist_raw > self.run_dist_min: 370 self._lead_amount = 0.3 371 self._running = True 372 self.node.run = 1.0 373 else: 374 self._lead_amount = 0.01 375 self._running = False 376 self.node.run = 0.0 377 378 self.node.move_left_right = to_target.x * self._charge_speed 379 self.node.move_up_down = to_target.z * -1.0 * self._charge_speed 380 381 elif self._mode == 'wait': 382 # Every now and then, aim towards our target. 383 # Other than that, just stand there. 384 if ba.time(timeformat=ba.TimeFormat.MILLISECONDS) % 1234 < 100: 385 self.node.move_left_right = to_target.x * (400.0 / 33000) 386 self.node.move_up_down = to_target.z * (-400.0 / 33000) 387 else: 388 self.node.move_left_right = 0 389 self.node.move_up_down = 0 390 391 elif self._mode == 'flee': 392 # Even if we're a runner, only run till we get away from our 393 # target (if we keep running we tend to run off edges). 394 if self.run and dist < 3.0: 395 self._running = True 396 self.node.run = 1.0 397 else: 398 self._running = False 399 self.node.run = 0.0 400 self.node.move_left_right = to_target.x * -1.0 401 self.node.move_up_down = to_target.z 402 403 # We might wanna switch states unless we're doing a throw 404 # (in which case that's our sole concern). 405 if self._mode != 'throw': 406 407 # If we're currently charging, keep track of how far we are 408 # from our target. When this value increases it means our charge 409 # is over (ran by them or something). 410 if self._mode == 'charge': 411 if ( 412 self._charge_closing_in 413 and self._last_charge_dist < dist < 3.0 414 ): 415 self._charge_closing_in = False 416 self._last_charge_dist = dist 417 418 # If we have a clean shot, throw! 419 if ( 420 self.throw_dist_min <= dist < self.throw_dist_max 421 and random.random() < self.throwiness 422 and can_attack 423 ): 424 self._mode = 'throw' 425 self._lead_amount = ( 426 (0.4 + random.random() * 0.6) 427 if dist_raw > 4.0 428 else (0.1 + random.random() * 0.4) 429 ) 430 self._have_dropped_throw_bomb = False 431 self._throw_release_time = ba.time() + ( 432 1.0 / self.throw_rate 433 ) * (0.8 + 1.3 * random.random()) 434 435 # If we're static, always charge (which for us means barely move). 436 elif self.static: 437 self._mode = 'wait' 438 439 # If we're too close to charge (and aren't in the middle of an 440 # existing charge) run away. 441 elif dist < self.charge_dist_min and not self._charge_closing_in: 442 # ..unless we're near an edge, in which case we've got no 443 # choice but to charge. 444 if self.map.is_point_near_edge(our_pos, self._running): 445 if self._mode != 'charge': 446 self._mode = 'charge' 447 self._lead_amount = 0.2 448 self._charge_closing_in = True 449 self._last_charge_dist = dist 450 else: 451 self._mode = 'flee' 452 453 # We're within charging distance, backed against an edge, 454 # or farther than our max throw distance.. chaaarge! 455 elif ( 456 dist < self.charge_dist_max 457 or dist > self.throw_dist_max 458 or self.map.is_point_near_edge(our_pos, self._running) 459 ): 460 if self._mode != 'charge': 461 self._mode = 'charge' 462 self._lead_amount = 0.01 463 self._charge_closing_in = True 464 self._last_charge_dist = dist 465 466 # We're too close to throw but too far to charge - either run 467 # away or just chill if we're near an edge. 468 elif dist < self.throw_dist_min: 469 # Charge if either we're within charge range or 470 # cant retreat to throw. 471 self._mode = 'flee' 472 473 # Do some awesome jumps if we're running. 474 # FIXME: pylint: disable=too-many-boolean-expressions 475 if ( 476 self._running 477 and 1.2 < dist < 2.2 478 and ba.time() - self._last_jump_time > 1.0 479 ) or ( 480 self.bouncy 481 and ba.time() - self._last_jump_time > 0.4 482 and random.random() < 0.5 483 ): 484 self._last_jump_time = ba.time() 485 self.node.jump_pressed = True 486 self.node.jump_pressed = False 487 488 # Throw punches when real close. 489 if dist < (1.6 if self._running else 1.2) and can_attack: 490 if random.random() < self.punchiness: 491 self.on_punch_press() 492 self.on_punch_release() 493 494 def on_punched(self, damage: int) -> None: 495 """ 496 Method override; sends ba.SpazBotPunchedMessage 497 to the current activity. 498 """ 499 ba.getactivity().handlemessage(SpazBotPunchedMessage(self, damage)) 500 501 def on_expire(self) -> None: 502 super().on_expire() 503 504 # We're being torn down; release our callback(s) so there's 505 # no chance of them keeping activities or other things alive. 506 self.update_callback = None 507 508 def handlemessage(self, msg: Any) -> Any: 509 # pylint: disable=too-many-branches 510 assert not self.expired 511 512 # Keep track of if we're being held and by who most recently. 513 if isinstance(msg, ba.PickedUpMessage): 514 super().handlemessage(msg) # Augment standard behavior. 515 self.held_count += 1 516 picked_up_by = msg.node.source_player 517 if picked_up_by: 518 self.last_player_held_by = picked_up_by 519 520 elif isinstance(msg, ba.DroppedMessage): 521 super().handlemessage(msg) # Augment standard behavior. 522 self.held_count -= 1 523 if self.held_count < 0: 524 print('ERROR: spaz held_count < 0') 525 526 # Let's count someone dropping us as an attack. 527 try: 528 if msg.node: 529 picked_up_by = msg.node.source_player 530 else: 531 picked_up_by = None 532 except Exception: 533 ba.print_exception('Error on SpazBot DroppedMessage.') 534 picked_up_by = None 535 536 if picked_up_by: 537 self.last_player_attacked_by = picked_up_by 538 self.last_attacked_time = ba.time() 539 self.last_attacked_type = ('picked_up', 'default') 540 541 elif isinstance(msg, ba.DieMessage): 542 543 # Report normal deaths for scoring purposes. 544 if not self._dead and not msg.immediate: 545 546 killerplayer: ba.Player | None 547 548 # If this guy was being held at the time of death, the 549 # holder is the killer. 550 if self.held_count > 0 and self.last_player_held_by: 551 killerplayer = self.last_player_held_by 552 else: 553 # If they were attacked by someone in the last few 554 # seconds that person's the killer. 555 # Otherwise it was a suicide. 556 if ( 557 self.last_player_attacked_by 558 and ba.time() - self.last_attacked_time < 4.0 559 ): 560 killerplayer = self.last_player_attacked_by 561 else: 562 killerplayer = None 563 activity = self._activity() 564 565 # (convert dead player refs to None) 566 if not killerplayer: 567 killerplayer = None 568 if activity is not None: 569 activity.handlemessage( 570 SpazBotDiedMessage(self, killerplayer, msg.how) 571 ) 572 super().handlemessage(msg) # Augment standard behavior. 573 574 # Keep track of the player who last hit us for point rewarding. 575 elif isinstance(msg, ba.HitMessage): 576 source_player = msg.get_source_player(ba.Player) 577 if source_player: 578 self.last_player_attacked_by = source_player 579 self.last_attacked_time = ba.time() 580 self.last_attacked_type = (msg.hit_type, msg.hit_subtype) 581 super().handlemessage(msg) 582 else: 583 super().handlemessage(msg)
A really dumb AI version of ba.Spaz.
Category: Bot Classes
Add these to a ba.BotSet to use them.
Note: currently the AI has no real ability to navigate obstacles and so should only be used on wide-open maps.
When a SpazBot is killed, it delivers a ba.SpazBotDiedMessage to the current activity.
When a SpazBot is punched, it delivers a ba.SpazBotPunchedMessage to the current activity.
111 def __init__(self) -> None: 112 """Instantiate a spaz-bot.""" 113 super().__init__( 114 color=self.color, 115 highlight=self.highlight, 116 character=self.character, 117 source_player=None, 118 start_invincible=False, 119 can_accept_powerups=False, 120 ) 121 122 # If you need to add custom behavior to a bot, set this to a callable 123 # which takes one arg (the bot) and returns False if the bot's normal 124 # update should be run and True if not. 125 self.update_callback: Callable[[SpazBot], Any] | None = None 126 activity = self.activity 127 assert isinstance(activity, ba.GameActivity) 128 self._map = weakref.ref(activity.map) 129 self.last_player_attacked_by: ba.Player | None = None 130 self.last_attacked_time = 0.0 131 self.last_attacked_type: tuple[str, str] | None = None 132 self.target_point_default: ba.Vec3 | None = None 133 self.held_count = 0 134 self.last_player_held_by: ba.Player | None = None 135 self.target_flag: Flag | None = None 136 self._charge_speed = 0.5 * ( 137 self.charge_speed_min + self.charge_speed_max 138 ) 139 self._lead_amount = 0.5 140 self._mode = 'wait' 141 self._charge_closing_in = False 142 self._last_charge_dist = 0.0 143 self._running = False 144 self._last_jump_time = 0.0 145 146 self._throw_release_time: float | None = None 147 self._have_dropped_throw_bomb: bool | None = None 148 self._player_pts: list[tuple[ba.Vec3, ba.Vec3]] | None = None 149 150 # These cooldowns didn't exist when these bots were calibrated, 151 # so take them out of the equation. 152 self._jump_cooldown = 0 153 self._pickup_cooldown = 0 154 self._fly_cooldown = 0 155 self._bomb_cooldown = 0 156 157 if self.start_cursed: 158 self.curse()
Instantiate a spaz-bot.
198 def set_player_points(self, pts: list[tuple[ba.Vec3, ba.Vec3]]) -> None: 199 """Provide the spaz-bot with the locations of its enemies.""" 200 self._player_pts = pts
Provide the spaz-bot with the locations of its enemies.
202 def update_ai(self) -> None: 203 """Should be called periodically to update the spaz' AI.""" 204 # pylint: disable=too-many-branches 205 # pylint: disable=too-many-statements 206 # pylint: disable=too-many-locals 207 if self.update_callback is not None: 208 if self.update_callback(self): 209 # Bot has been handled. 210 return 211 212 if not self.node: 213 return 214 215 pos = self.node.position 216 our_pos = ba.Vec3(pos[0], 0, pos[2]) 217 can_attack = True 218 219 target_pt_raw: ba.Vec3 | None 220 target_vel: ba.Vec3 | None 221 222 # If we're a flag-bearer, we're pretty simple-minded - just walk 223 # towards the flag and try to pick it up. 224 if self.target_flag: 225 if self.node.hold_node: 226 holding_flag = self.node.hold_node.getnodetype() == 'flag' 227 else: 228 holding_flag = False 229 230 # If we're holding the flag, just walk left. 231 if holding_flag: 232 # Just walk left. 233 self.node.move_left_right = -1.0 234 self.node.move_up_down = 0.0 235 236 # Otherwise try to go pick it up. 237 elif self.target_flag.node: 238 target_pt_raw = ba.Vec3(*self.target_flag.node.position) 239 diff = target_pt_raw - our_pos 240 diff = ba.Vec3(diff[0], 0, diff[2]) # Don't care about y. 241 dist = diff.length() 242 to_target = diff.normalized() 243 244 # If we're holding some non-flag item, drop it. 245 if self.node.hold_node: 246 self.node.pickup_pressed = True 247 self.node.pickup_pressed = False 248 return 249 250 # If we're a runner, run only when not super-near the flag. 251 if self.run and dist > 3.0: 252 self._running = True 253 self.node.run = 1.0 254 else: 255 self._running = False 256 self.node.run = 0.0 257 258 self.node.move_left_right = to_target.x 259 self.node.move_up_down = -to_target.z 260 if dist < 1.25: 261 self.node.pickup_pressed = True 262 self.node.pickup_pressed = False 263 return 264 265 # Not a flag-bearer. If we're holding anything but a bomb, drop it. 266 if self.node.hold_node: 267 holding_bomb = self.node.hold_node.getnodetype() in ['bomb', 'prop'] 268 if not holding_bomb: 269 self.node.pickup_pressed = True 270 self.node.pickup_pressed = False 271 return 272 273 target_pt_raw, target_vel = self._get_target_player_pt() 274 275 if target_pt_raw is None: 276 # Use default target if we've got one. 277 if self.target_point_default is not None: 278 target_pt_raw = self.target_point_default 279 target_vel = ba.Vec3(0, 0, 0) 280 can_attack = False 281 282 # With no target, we stop moving and drop whatever we're holding. 283 else: 284 self.node.move_left_right = 0 285 self.node.move_up_down = 0 286 if self.node.hold_node: 287 self.node.pickup_pressed = True 288 self.node.pickup_pressed = False 289 return 290 291 # We don't want height to come into play. 292 target_pt_raw[1] = 0.0 293 assert target_vel is not None 294 target_vel[1] = 0.0 295 296 dist_raw = (target_pt_raw - our_pos).length() 297 298 # Use a point out in front of them as real target. 299 # (more out in front the farther from us they are) 300 target_pt = ( 301 target_pt_raw + target_vel * dist_raw * 0.3 * self._lead_amount 302 ) 303 304 diff = target_pt - our_pos 305 dist = diff.length() 306 to_target = diff.normalized() 307 308 if self._mode == 'throw': 309 # We can only throw if alive and well. 310 if not self._dead and not self.node.knockout: 311 312 assert self._throw_release_time is not None 313 time_till_throw = self._throw_release_time - ba.time() 314 315 if not self.node.hold_node: 316 # If we haven't thrown yet, whip out the bomb. 317 if not self._have_dropped_throw_bomb: 318 self.drop_bomb() 319 self._have_dropped_throw_bomb = True 320 321 # Otherwise our lack of held node means we successfully 322 # released our bomb; lets retreat now. 323 else: 324 self._mode = 'flee' 325 326 # Oh crap, we're holding a bomb; better throw it. 327 elif time_till_throw <= 0.0: 328 # Jump and throw. 329 def _safe_pickup(node: ba.Node) -> None: 330 if node and self.node: 331 self.node.pickup_pressed = True 332 self.node.pickup_pressed = False 333 334 if dist > 5.0: 335 self.node.jump_pressed = True 336 self.node.jump_pressed = False 337 338 # Throws: 339 ba.timer(0.1, ba.Call(_safe_pickup, self.node)) 340 else: 341 # Throws: 342 ba.timer(0.1, ba.Call(_safe_pickup, self.node)) 343 344 if self.static: 345 if time_till_throw < 0.3: 346 speed = 1.0 347 elif time_till_throw < 0.7 and dist > 3.0: 348 speed = -1.0 # Whiplash for long throws. 349 else: 350 speed = 0.02 351 else: 352 if time_till_throw < 0.7: 353 # Right before throw charge full speed towards target. 354 speed = 1.0 355 else: 356 # Earlier we can hold or move backward for a whiplash. 357 speed = 0.0125 358 self.node.move_left_right = to_target.x * speed 359 self.node.move_up_down = to_target.z * -1.0 * speed 360 361 elif self._mode == 'charge': 362 if random.random() < 0.3: 363 self._charge_speed = random.uniform( 364 self.charge_speed_min, self.charge_speed_max 365 ) 366 367 # If we're a runner we run during charges *except when near 368 # an edge (otherwise we tend to fly off easily). 369 if self.run and dist_raw > self.run_dist_min: 370 self._lead_amount = 0.3 371 self._running = True 372 self.node.run = 1.0 373 else: 374 self._lead_amount = 0.01 375 self._running = False 376 self.node.run = 0.0 377 378 self.node.move_left_right = to_target.x * self._charge_speed 379 self.node.move_up_down = to_target.z * -1.0 * self._charge_speed 380 381 elif self._mode == 'wait': 382 # Every now and then, aim towards our target. 383 # Other than that, just stand there. 384 if ba.time(timeformat=ba.TimeFormat.MILLISECONDS) % 1234 < 100: 385 self.node.move_left_right = to_target.x * (400.0 / 33000) 386 self.node.move_up_down = to_target.z * (-400.0 / 33000) 387 else: 388 self.node.move_left_right = 0 389 self.node.move_up_down = 0 390 391 elif self._mode == 'flee': 392 # Even if we're a runner, only run till we get away from our 393 # target (if we keep running we tend to run off edges). 394 if self.run and dist < 3.0: 395 self._running = True 396 self.node.run = 1.0 397 else: 398 self._running = False 399 self.node.run = 0.0 400 self.node.move_left_right = to_target.x * -1.0 401 self.node.move_up_down = to_target.z 402 403 # We might wanna switch states unless we're doing a throw 404 # (in which case that's our sole concern). 405 if self._mode != 'throw': 406 407 # If we're currently charging, keep track of how far we are 408 # from our target. When this value increases it means our charge 409 # is over (ran by them or something). 410 if self._mode == 'charge': 411 if ( 412 self._charge_closing_in 413 and self._last_charge_dist < dist < 3.0 414 ): 415 self._charge_closing_in = False 416 self._last_charge_dist = dist 417 418 # If we have a clean shot, throw! 419 if ( 420 self.throw_dist_min <= dist < self.throw_dist_max 421 and random.random() < self.throwiness 422 and can_attack 423 ): 424 self._mode = 'throw' 425 self._lead_amount = ( 426 (0.4 + random.random() * 0.6) 427 if dist_raw > 4.0 428 else (0.1 + random.random() * 0.4) 429 ) 430 self._have_dropped_throw_bomb = False 431 self._throw_release_time = ba.time() + ( 432 1.0 / self.throw_rate 433 ) * (0.8 + 1.3 * random.random()) 434 435 # If we're static, always charge (which for us means barely move). 436 elif self.static: 437 self._mode = 'wait' 438 439 # If we're too close to charge (and aren't in the middle of an 440 # existing charge) run away. 441 elif dist < self.charge_dist_min and not self._charge_closing_in: 442 # ..unless we're near an edge, in which case we've got no 443 # choice but to charge. 444 if self.map.is_point_near_edge(our_pos, self._running): 445 if self._mode != 'charge': 446 self._mode = 'charge' 447 self._lead_amount = 0.2 448 self._charge_closing_in = True 449 self._last_charge_dist = dist 450 else: 451 self._mode = 'flee' 452 453 # We're within charging distance, backed against an edge, 454 # or farther than our max throw distance.. chaaarge! 455 elif ( 456 dist < self.charge_dist_max 457 or dist > self.throw_dist_max 458 or self.map.is_point_near_edge(our_pos, self._running) 459 ): 460 if self._mode != 'charge': 461 self._mode = 'charge' 462 self._lead_amount = 0.01 463 self._charge_closing_in = True 464 self._last_charge_dist = dist 465 466 # We're too close to throw but too far to charge - either run 467 # away or just chill if we're near an edge. 468 elif dist < self.throw_dist_min: 469 # Charge if either we're within charge range or 470 # cant retreat to throw. 471 self._mode = 'flee' 472 473 # Do some awesome jumps if we're running. 474 # FIXME: pylint: disable=too-many-boolean-expressions 475 if ( 476 self._running 477 and 1.2 < dist < 2.2 478 and ba.time() - self._last_jump_time > 1.0 479 ) or ( 480 self.bouncy 481 and ba.time() - self._last_jump_time > 0.4 482 and random.random() < 0.5 483 ): 484 self._last_jump_time = ba.time() 485 self.node.jump_pressed = True 486 self.node.jump_pressed = False 487 488 # Throw punches when real close. 489 if dist < (1.6 if self._running else 1.2) and can_attack: 490 if random.random() < self.punchiness: 491 self.on_punch_press() 492 self.on_punch_release()
Should be called periodically to update the spaz' AI.
494 def on_punched(self, damage: int) -> None: 495 """ 496 Method override; sends ba.SpazBotPunchedMessage 497 to the current activity. 498 """ 499 ba.getactivity().handlemessage(SpazBotPunchedMessage(self, damage))
Method override; sends ba.SpazBotPunchedMessage to the current activity.
501 def on_expire(self) -> None: 502 super().on_expire() 503 504 # We're being torn down; release our callback(s) so there's 505 # no chance of them keeping activities or other things alive. 506 self.update_callback = None
Called for remaining ba.Actor
s when their ba.Activity shuts down.
Actors can use this opportunity to clear callbacks or other references which have the potential of keeping the ba.Activity alive inadvertently (Activities can not exit cleanly while any Python references to them remain.)
Once an actor is expired (see ba.Actor.is_expired()) it should no longer perform any game-affecting operations (creating, modifying, or deleting nodes, media, timers, etc.) Attempts to do so will likely result in errors.
508 def handlemessage(self, msg: Any) -> Any: 509 # pylint: disable=too-many-branches 510 assert not self.expired 511 512 # Keep track of if we're being held and by who most recently. 513 if isinstance(msg, ba.PickedUpMessage): 514 super().handlemessage(msg) # Augment standard behavior. 515 self.held_count += 1 516 picked_up_by = msg.node.source_player 517 if picked_up_by: 518 self.last_player_held_by = picked_up_by 519 520 elif isinstance(msg, ba.DroppedMessage): 521 super().handlemessage(msg) # Augment standard behavior. 522 self.held_count -= 1 523 if self.held_count < 0: 524 print('ERROR: spaz held_count < 0') 525 526 # Let's count someone dropping us as an attack. 527 try: 528 if msg.node: 529 picked_up_by = msg.node.source_player 530 else: 531 picked_up_by = None 532 except Exception: 533 ba.print_exception('Error on SpazBot DroppedMessage.') 534 picked_up_by = None 535 536 if picked_up_by: 537 self.last_player_attacked_by = picked_up_by 538 self.last_attacked_time = ba.time() 539 self.last_attacked_type = ('picked_up', 'default') 540 541 elif isinstance(msg, ba.DieMessage): 542 543 # Report normal deaths for scoring purposes. 544 if not self._dead and not msg.immediate: 545 546 killerplayer: ba.Player | None 547 548 # If this guy was being held at the time of death, the 549 # holder is the killer. 550 if self.held_count > 0 and self.last_player_held_by: 551 killerplayer = self.last_player_held_by 552 else: 553 # If they were attacked by someone in the last few 554 # seconds that person's the killer. 555 # Otherwise it was a suicide. 556 if ( 557 self.last_player_attacked_by 558 and ba.time() - self.last_attacked_time < 4.0 559 ): 560 killerplayer = self.last_player_attacked_by 561 else: 562 killerplayer = None 563 activity = self._activity() 564 565 # (convert dead player refs to None) 566 if not killerplayer: 567 killerplayer = None 568 if activity is not None: 569 activity.handlemessage( 570 SpazBotDiedMessage(self, killerplayer, msg.how) 571 ) 572 super().handlemessage(msg) # Augment standard behavior. 573 574 # Keep track of the player who last hit us for point rewarding. 575 elif isinstance(msg, ba.HitMessage): 576 source_player = msg.get_source_player(ba.Player) 577 if source_player: 578 self.last_player_attacked_by = source_player 579 self.last_attacked_time = ba.time() 580 self.last_attacked_type = (msg.hit_type, msg.hit_subtype) 581 super().handlemessage(msg) 582 else: 583 super().handlemessage(msg)
General message handling; can be passed any message object.
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
586class BomberBot(SpazBot): 587 """A bot that throws regular bombs and occasionally punches. 588 589 category: Bot Classes 590 """ 591 592 character = 'Spaz' 593 punchiness = 0.3
A bot that throws regular bombs and occasionally punches.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
596class BomberBotLite(BomberBot): 597 """A less aggressive yellow version of ba.BomberBot. 598 599 category: Bot Classes 600 """ 601 602 color = LITE_BOT_COLOR 603 highlight = LITE_BOT_HIGHLIGHT 604 punchiness = 0.2 605 throw_rate = 0.7 606 throwiness = 0.1 607 charge_speed_min = 0.6 608 charge_speed_max = 0.6
A less aggressive yellow version of ba.BomberBot.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
611class BomberBotStaticLite(BomberBotLite): 612 """A less aggressive generally immobile weak version of ba.BomberBot. 613 614 category: Bot Classes 615 """ 616 617 static = True 618 throw_dist_min = 0.0
A less aggressive generally immobile weak version of ba.BomberBot.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
621class BomberBotStatic(BomberBot): 622 """A version of ba.BomberBot who generally stays in one place. 623 624 category: Bot Classes 625 """ 626 627 static = True 628 throw_dist_min = 0.0
A version of ba.BomberBot who generally stays in one place.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
631class BomberBotPro(BomberBot): 632 """A more powerful version of ba.BomberBot. 633 634 category: Bot Classes 635 """ 636 637 points_mult = 2 638 color = PRO_BOT_COLOR 639 highlight = PRO_BOT_HIGHLIGHT 640 default_bomb_count = 3 641 default_boxing_gloves = True 642 punchiness = 0.7 643 throw_rate = 1.3 644 run = True 645 run_dist_min = 6.0
A more powerful version of ba.BomberBot.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
648class BomberBotProShielded(BomberBotPro): 649 """A more powerful version of ba.BomberBot who starts with shields. 650 651 category: Bot Classes 652 """ 653 654 points_mult = 3 655 default_shields = True
A more powerful version of ba.BomberBot who starts with shields.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
658class BomberBotProStatic(BomberBotPro): 659 """A more powerful ba.BomberBot who generally stays in one place. 660 661 category: Bot Classes 662 """ 663 664 static = True 665 throw_dist_min = 0.0
A more powerful ba.BomberBot who generally stays in one place.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
668class BomberBotProStaticShielded(BomberBotProShielded): 669 """A powerful ba.BomberBot with shields who is generally immobile. 670 671 category: Bot Classes 672 """ 673 674 static = True 675 throw_dist_min = 0.0
A powerful ba.BomberBot with shields who is generally immobile.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
678class BrawlerBot(SpazBot): 679 """A bot who walks and punches things. 680 681 category: Bot Classes 682 """ 683 684 character = 'Kronk' 685 punchiness = 0.9 686 charge_dist_max = 9999.0 687 charge_speed_min = 1.0 688 charge_speed_max = 1.0 689 throw_dist_min = 9999 690 throw_dist_max = 9999
A bot who walks and punches things.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
693class BrawlerBotLite(BrawlerBot): 694 """A weaker version of ba.BrawlerBot. 695 696 category: Bot Classes 697 """ 698 699 color = LITE_BOT_COLOR 700 highlight = LITE_BOT_HIGHLIGHT 701 punchiness = 0.3 702 charge_speed_min = 0.6 703 charge_speed_max = 0.6
A weaker version of ba.BrawlerBot.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
706class BrawlerBotPro(BrawlerBot): 707 """A stronger version of ba.BrawlerBot. 708 709 category: Bot Classes 710 """ 711 712 color = PRO_BOT_COLOR 713 highlight = PRO_BOT_HIGHLIGHT 714 run = True 715 run_dist_min = 4.0 716 default_boxing_gloves = True 717 punchiness = 0.95 718 points_mult = 2
A stronger version of ba.BrawlerBot.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
721class BrawlerBotProShielded(BrawlerBotPro): 722 """A stronger version of ba.BrawlerBot who starts with shields. 723 724 category: Bot Classes 725 """ 726 727 default_shields = True 728 points_mult = 3
A stronger version of ba.BrawlerBot who starts with shields.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
731class ChargerBot(SpazBot): 732 """A speedy melee attack bot. 733 734 category: Bot Classes 735 """ 736 737 character = 'Snake Shadow' 738 punchiness = 1.0 739 run = True 740 charge_dist_min = 10.0 741 charge_dist_max = 9999.0 742 charge_speed_min = 1.0 743 charge_speed_max = 1.0 744 throw_dist_min = 9999 745 throw_dist_max = 9999 746 points_mult = 2
A speedy melee attack bot.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
749class BouncyBot(SpazBot): 750 """A speedy attacking melee bot that jumps constantly. 751 752 category: Bot Classes 753 """ 754 755 color = (1, 1, 1) 756 highlight = (1.0, 0.5, 0.5) 757 character = 'Easter Bunny' 758 punchiness = 1.0 759 run = True 760 bouncy = True 761 default_boxing_gloves = True 762 charge_dist_min = 10.0 763 charge_dist_max = 9999.0 764 charge_speed_min = 1.0 765 charge_speed_max = 1.0 766 throw_dist_min = 9999 767 throw_dist_max = 9999 768 points_mult = 2
A speedy attacking melee bot that jumps constantly.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
771class ChargerBotPro(ChargerBot): 772 """A stronger ba.ChargerBot. 773 774 category: Bot Classes 775 """ 776 777 color = PRO_BOT_COLOR 778 highlight = PRO_BOT_HIGHLIGHT 779 default_shields = True 780 default_boxing_gloves = True 781 points_mult = 3
A stronger ba.ChargerBot.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
784class ChargerBotProShielded(ChargerBotPro): 785 """A stronger ba.ChargerBot who starts with shields. 786 787 category: Bot Classes 788 """ 789 790 default_shields = True 791 points_mult = 4
A stronger ba.ChargerBot who starts with shields.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
794class TriggerBot(SpazBot): 795 """A slow moving bot with trigger bombs. 796 797 category: Bot Classes 798 """ 799 800 character = 'Zoe' 801 punchiness = 0.75 802 throwiness = 0.7 803 charge_dist_max = 1.0 804 charge_speed_min = 0.3 805 charge_speed_max = 0.5 806 throw_dist_min = 3.5 807 throw_dist_max = 5.5 808 default_bomb_type = 'impact' 809 points_mult = 2
A slow moving bot with trigger bombs.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
812class TriggerBotStatic(TriggerBot): 813 """A ba.TriggerBot who generally stays in one place. 814 815 category: Bot Classes 816 """ 817 818 static = True 819 throw_dist_min = 0.0
A ba.TriggerBot who generally stays in one place.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
822class TriggerBotPro(TriggerBot): 823 """A stronger version of ba.TriggerBot. 824 825 category: Bot Classes 826 """ 827 828 color = PRO_BOT_COLOR 829 highlight = PRO_BOT_HIGHLIGHT 830 default_bomb_count = 3 831 default_boxing_gloves = True 832 charge_speed_min = 1.0 833 charge_speed_max = 1.0 834 punchiness = 0.9 835 throw_rate = 1.3 836 run = True 837 run_dist_min = 6.0 838 points_mult = 3
A stronger version of ba.TriggerBot.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
841class TriggerBotProShielded(TriggerBotPro): 842 """A stronger version of ba.TriggerBot who starts with shields. 843 844 category: Bot Classes 845 """ 846 847 default_shields = True 848 points_mult = 4
A stronger version of ba.TriggerBot who starts with shields.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
851class StickyBot(SpazBot): 852 """A crazy bot who runs and throws sticky bombs. 853 854 category: Bot Classes 855 """ 856 857 character = 'Mel' 858 punchiness = 0.9 859 throwiness = 1.0 860 run = True 861 charge_dist_min = 4.0 862 charge_dist_max = 10.0 863 charge_speed_min = 1.0 864 charge_speed_max = 1.0 865 throw_dist_min = 0.0 866 throw_dist_max = 4.0 867 throw_rate = 2.0 868 default_bomb_type = 'sticky' 869 default_bomb_count = 3 870 points_mult = 3
A crazy bot who runs and throws sticky bombs.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
873class StickyBotStatic(StickyBot): 874 """A crazy bot who throws sticky-bombs but generally stays in one place. 875 876 category: Bot Classes 877 """ 878 879 static = True
A crazy bot who throws sticky-bombs but generally stays in one place.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
882class ExplodeyBot(SpazBot): 883 """A bot who runs and explodes in 5 seconds. 884 885 category: Bot Classes 886 """ 887 888 character = 'Jack Morgan' 889 run = True 890 charge_dist_min = 0.0 891 charge_dist_max = 9999 892 charge_speed_min = 1.0 893 charge_speed_max = 1.0 894 throw_dist_min = 9999 895 throw_dist_max = 9999 896 start_cursed = True 897 points_mult = 4
A bot who runs and explodes in 5 seconds.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
900class ExplodeyBotNoTimeLimit(ExplodeyBot): 901 """A bot who runs but does not explode on his own. 902 903 category: Bot Classes 904 """ 905 906 curse_time = None
A bot who runs but does not explode on his own.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
909class ExplodeyBotShielded(ExplodeyBot): 910 """A ba.ExplodeyBot who starts with shields. 911 912 category: Bot Classes 913 """ 914 915 default_shields = True 916 points_mult = 5
A ba.ExplodeyBot who starts with shields.
category: Bot Classes
Inherited Members
- bastd.actor.spaz.Spaz
- node
- exists
- add_dropped_bomb_callback
- is_alive
- set_score_text
- on_jump_press
- on_jump_release
- on_pickup_press
- on_pickup_release
- on_hold_position_press
- on_hold_position_release
- on_punch_press
- on_punch_release
- on_bomb_press
- on_bomb_release
- on_run
- on_fly_press
- on_fly_release
- on_move
- on_move_up_down
- on_move_left_right
- get_death_points
- curse
- equip_boxing_gloves
- equip_shields
- shield_decay
- drop_bomb
- set_land_mine_count
- curse_explode
- shatter
- set_bomb_count
- ba._actor.Actor
- autoretain
- expired
- activity
- getactivity
919class SpazBotSet: 920 """A container/controller for one or more ba.SpazBots. 921 922 category: Bot Classes 923 """ 924 925 def __init__(self) -> None: 926 """Create a bot-set.""" 927 928 # We spread our bots out over a few lists so we can update 929 # them in a staggered fashion. 930 self._bot_list_count = 5 931 self._bot_add_list = 0 932 self._bot_update_list = 0 933 self._bot_lists: list[list[SpazBot]] = [ 934 [] for _ in range(self._bot_list_count) 935 ] 936 self._spawn_sound = ba.getsound('spawn') 937 self._spawning_count = 0 938 self._bot_update_timer: ba.Timer | None = None 939 self.start_moving() 940 941 def __del__(self) -> None: 942 self.clear() 943 944 def spawn_bot( 945 self, 946 bot_type: type[SpazBot], 947 pos: Sequence[float], 948 spawn_time: float = 3.0, 949 on_spawn_call: Callable[[SpazBot], Any] | None = None, 950 ) -> None: 951 """Spawn a bot from this set.""" 952 from bastd.actor import spawner 953 954 spawner.Spawner( 955 pt=pos, 956 spawn_time=spawn_time, 957 send_spawn_message=False, 958 spawn_callback=ba.Call( 959 self._spawn_bot, bot_type, pos, on_spawn_call 960 ), 961 ) 962 self._spawning_count += 1 963 964 def _spawn_bot( 965 self, 966 bot_type: type[SpazBot], 967 pos: Sequence[float], 968 on_spawn_call: Callable[[SpazBot], Any] | None, 969 ) -> None: 970 spaz = bot_type() 971 ba.playsound(self._spawn_sound, position=pos) 972 assert spaz.node 973 spaz.node.handlemessage('flash') 974 spaz.node.is_area_of_interest = False 975 spaz.handlemessage(ba.StandMessage(pos, random.uniform(0, 360))) 976 self.add_bot(spaz) 977 self._spawning_count -= 1 978 if on_spawn_call is not None: 979 on_spawn_call(spaz) 980 981 def have_living_bots(self) -> bool: 982 """Return whether any bots in the set are alive or spawning.""" 983 return self._spawning_count > 0 or any( 984 any(b.is_alive() for b in l) for l in self._bot_lists 985 ) 986 987 def get_living_bots(self) -> list[SpazBot]: 988 """Get the living bots in the set.""" 989 bots: list[SpazBot] = [] 990 for botlist in self._bot_lists: 991 for bot in botlist: 992 if bot.is_alive(): 993 bots.append(bot) 994 return bots 995 996 def _update(self) -> None: 997 998 # Update one of our bot lists each time through. 999 # First off, remove no-longer-existing bots from the list. 1000 try: 1001 bot_list = self._bot_lists[self._bot_update_list] = [ 1002 b for b in self._bot_lists[self._bot_update_list] if b 1003 ] 1004 except Exception: 1005 bot_list = [] 1006 ba.print_exception( 1007 'Error updating bot list: ' 1008 + str(self._bot_lists[self._bot_update_list]) 1009 ) 1010 self._bot_update_list = ( 1011 self._bot_update_list + 1 1012 ) % self._bot_list_count 1013 1014 # Update our list of player points for the bots to use. 1015 player_pts = [] 1016 for player in ba.getactivity().players: 1017 assert isinstance(player, ba.Player) 1018 try: 1019 # TODO: could use abstracted player.position here so we 1020 # don't have to assume their actor type, but we have no 1021 # abstracted velocity as of yet. 1022 if player.is_alive(): 1023 assert isinstance(player.actor, Spaz) 1024 assert player.actor.node 1025 player_pts.append( 1026 ( 1027 ba.Vec3(player.actor.node.position), 1028 ba.Vec3(player.actor.node.velocity), 1029 ) 1030 ) 1031 except Exception: 1032 ba.print_exception('Error on bot-set _update.') 1033 1034 for bot in bot_list: 1035 bot.set_player_points(player_pts) 1036 bot.update_ai() 1037 1038 def clear(self) -> None: 1039 """Immediately clear out any bots in the set.""" 1040 1041 # Don't do this if the activity is shutting down or dead. 1042 activity = ba.getactivity(doraise=False) 1043 if activity is None or activity.expired: 1044 return 1045 1046 for i, bot_list in enumerate(self._bot_lists): 1047 for bot in bot_list: 1048 bot.handlemessage(ba.DieMessage(immediate=True)) 1049 self._bot_lists[i] = [] 1050 1051 def start_moving(self) -> None: 1052 """Start processing bot AI updates so they start doing their thing.""" 1053 self._bot_update_timer = ba.Timer( 1054 0.05, ba.WeakCall(self._update), repeat=True 1055 ) 1056 1057 def stop_moving(self) -> None: 1058 """Tell all bots to stop moving and stops updating their AI. 1059 1060 Useful when players have won and you want the 1061 enemy bots to just stand and look bewildered. 1062 """ 1063 self._bot_update_timer = None 1064 for botlist in self._bot_lists: 1065 for bot in botlist: 1066 if bot.node: 1067 bot.node.move_left_right = 0 1068 bot.node.move_up_down = 0 1069 1070 def celebrate(self, duration: float) -> None: 1071 """Tell all living bots in the set to celebrate momentarily. 1072 1073 Duration is given in seconds. 1074 """ 1075 msg = ba.CelebrateMessage(duration=duration) 1076 for botlist in self._bot_lists: 1077 for bot in botlist: 1078 if bot: 1079 bot.handlemessage(msg) 1080 1081 def final_celebrate(self) -> None: 1082 """Tell all bots in the set to stop what they were doing and celebrate. 1083 1084 Use this when the bots have won a game. 1085 """ 1086 self._bot_update_timer = None 1087 1088 # At this point stop doing anything but jumping and celebrating. 1089 for botlist in self._bot_lists: 1090 for bot in botlist: 1091 if bot: 1092 assert bot.node # (should exist if 'if bot' was True) 1093 bot.node.move_left_right = 0 1094 bot.node.move_up_down = 0 1095 ba.timer( 1096 0.5 * random.random(), 1097 ba.Call(bot.handlemessage, ba.CelebrateMessage()), 1098 ) 1099 jump_duration = random.randrange(400, 500) 1100 j = random.randrange(0, 200) 1101 for _i in range(10): 1102 bot.node.jump_pressed = True 1103 bot.node.jump_pressed = False 1104 j += jump_duration 1105 ba.timer( 1106 random.uniform(0.0, 1.0), 1107 ba.Call(bot.node.handlemessage, 'attack_sound'), 1108 ) 1109 ba.timer( 1110 random.uniform(1.0, 2.0), 1111 ba.Call(bot.node.handlemessage, 'attack_sound'), 1112 ) 1113 ba.timer( 1114 random.uniform(2.0, 3.0), 1115 ba.Call(bot.node.handlemessage, 'attack_sound'), 1116 ) 1117 1118 def add_bot(self, bot: SpazBot) -> None: 1119 """Add a ba.SpazBot instance to the set.""" 1120 self._bot_lists[self._bot_add_list].append(bot) 1121 self._bot_add_list = (self._bot_add_list + 1) % self._bot_list_count
A container/controller for one or more ba.SpazBots.
category: Bot Classes
925 def __init__(self) -> None: 926 """Create a bot-set.""" 927 928 # We spread our bots out over a few lists so we can update 929 # them in a staggered fashion. 930 self._bot_list_count = 5 931 self._bot_add_list = 0 932 self._bot_update_list = 0 933 self._bot_lists: list[list[SpazBot]] = [ 934 [] for _ in range(self._bot_list_count) 935 ] 936 self._spawn_sound = ba.getsound('spawn') 937 self._spawning_count = 0 938 self._bot_update_timer: ba.Timer | None = None 939 self.start_moving()
Create a bot-set.
944 def spawn_bot( 945 self, 946 bot_type: type[SpazBot], 947 pos: Sequence[float], 948 spawn_time: float = 3.0, 949 on_spawn_call: Callable[[SpazBot], Any] | None = None, 950 ) -> None: 951 """Spawn a bot from this set.""" 952 from bastd.actor import spawner 953 954 spawner.Spawner( 955 pt=pos, 956 spawn_time=spawn_time, 957 send_spawn_message=False, 958 spawn_callback=ba.Call( 959 self._spawn_bot, bot_type, pos, on_spawn_call 960 ), 961 ) 962 self._spawning_count += 1
Spawn a bot from this set.
981 def have_living_bots(self) -> bool: 982 """Return whether any bots in the set are alive or spawning.""" 983 return self._spawning_count > 0 or any( 984 any(b.is_alive() for b in l) for l in self._bot_lists 985 )
Return whether any bots in the set are alive or spawning.
987 def get_living_bots(self) -> list[SpazBot]: 988 """Get the living bots in the set.""" 989 bots: list[SpazBot] = [] 990 for botlist in self._bot_lists: 991 for bot in botlist: 992 if bot.is_alive(): 993 bots.append(bot) 994 return bots
Get the living bots in the set.
1038 def clear(self) -> None: 1039 """Immediately clear out any bots in the set.""" 1040 1041 # Don't do this if the activity is shutting down or dead. 1042 activity = ba.getactivity(doraise=False) 1043 if activity is None or activity.expired: 1044 return 1045 1046 for i, bot_list in enumerate(self._bot_lists): 1047 for bot in bot_list: 1048 bot.handlemessage(ba.DieMessage(immediate=True)) 1049 self._bot_lists[i] = []
Immediately clear out any bots in the set.
1051 def start_moving(self) -> None: 1052 """Start processing bot AI updates so they start doing their thing.""" 1053 self._bot_update_timer = ba.Timer( 1054 0.05, ba.WeakCall(self._update), repeat=True 1055 )
Start processing bot AI updates so they start doing their thing.
1057 def stop_moving(self) -> None: 1058 """Tell all bots to stop moving and stops updating their AI. 1059 1060 Useful when players have won and you want the 1061 enemy bots to just stand and look bewildered. 1062 """ 1063 self._bot_update_timer = None 1064 for botlist in self._bot_lists: 1065 for bot in botlist: 1066 if bot.node: 1067 bot.node.move_left_right = 0 1068 bot.node.move_up_down = 0
Tell all bots to stop moving and stops updating their AI.
Useful when players have won and you want the enemy bots to just stand and look bewildered.
1070 def celebrate(self, duration: float) -> None: 1071 """Tell all living bots in the set to celebrate momentarily. 1072 1073 Duration is given in seconds. 1074 """ 1075 msg = ba.CelebrateMessage(duration=duration) 1076 for botlist in self._bot_lists: 1077 for bot in botlist: 1078 if bot: 1079 bot.handlemessage(msg)
Tell all living bots in the set to celebrate momentarily.
Duration is given in seconds.
1081 def final_celebrate(self) -> None: 1082 """Tell all bots in the set to stop what they were doing and celebrate. 1083 1084 Use this when the bots have won a game. 1085 """ 1086 self._bot_update_timer = None 1087 1088 # At this point stop doing anything but jumping and celebrating. 1089 for botlist in self._bot_lists: 1090 for bot in botlist: 1091 if bot: 1092 assert bot.node # (should exist if 'if bot' was True) 1093 bot.node.move_left_right = 0 1094 bot.node.move_up_down = 0 1095 ba.timer( 1096 0.5 * random.random(), 1097 ba.Call(bot.handlemessage, ba.CelebrateMessage()), 1098 ) 1099 jump_duration = random.randrange(400, 500) 1100 j = random.randrange(0, 200) 1101 for _i in range(10): 1102 bot.node.jump_pressed = True 1103 bot.node.jump_pressed = False 1104 j += jump_duration 1105 ba.timer( 1106 random.uniform(0.0, 1.0), 1107 ba.Call(bot.node.handlemessage, 'attack_sound'), 1108 ) 1109 ba.timer( 1110 random.uniform(1.0, 2.0), 1111 ba.Call(bot.node.handlemessage, 'attack_sound'), 1112 ) 1113 ba.timer( 1114 random.uniform(2.0, 3.0), 1115 ba.Call(bot.node.handlemessage, 'attack_sound'), 1116 )
Tell all bots in the set to stop what they were doing and celebrate.
Use this when the bots have won a game.