Source code for bascenev1lib.actor.spazbot

# Released under the MIT License. See LICENSE for details.
#
"""Bot versions of Spaz."""
# pylint: disable=too-many-lines

from __future__ import annotations

import random
import weakref
import logging
from typing import TYPE_CHECKING, override

import bascenev1 as bs
from bascenev1lib.actor.spaz import Spaz

if TYPE_CHECKING:
    from typing import Any, Sequence, Callable
    from bascenev1lib.actor.flag import Flag

LITE_BOT_COLOR = (1.2, 0.9, 0.2)
LITE_BOT_HIGHLIGHT = (1.0, 0.5, 0.6)
DEFAULT_BOT_COLOR = (0.6, 0.6, 0.6)
DEFAULT_BOT_HIGHLIGHT = (0.1, 0.3, 0.1)
PRO_BOT_COLOR = (1.0, 0.2, 0.1)
PRO_BOT_HIGHLIGHT = (0.6, 0.1, 0.05)


[docs] class SpazBotPunchedMessage: """A message saying a bs.SpazBot got punched. Category: **Message Classes** """ spazbot: SpazBot """The bs.SpazBot that got punched.""" damage: int """How much damage was done to the SpazBot.""" def __init__(self, spazbot: SpazBot, damage: int): """Instantiate a message with the given values.""" self.spazbot = spazbot self.damage = damage
[docs] class SpazBotDiedMessage: """A message saying a bs.SpazBot has died. Category: **Message Classes** """ spazbot: SpazBot """The SpazBot that was killed.""" killerplayer: bs.Player | None """The bascenev1.Player that killed it (or None).""" how: bs.DeathType """The particular type of death.""" def __init__( self, spazbot: SpazBot, killerplayer: bs.Player | None, how: bs.DeathType, ): """Instantiate with given values.""" self.spazbot = spazbot self.killerplayer = killerplayer self.how = how
[docs] class SpazBot(Spaz): """A really dumb AI version of bs.Spaz. Category: **Bot Classes** Add these to a bs.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 bs.SpazBotDiedMessage to the current activity. When a SpazBot is punched, it delivers a bs.SpazBotPunchedMessage to the current activity. """ character = 'Spaz' punchiness = 0.5 throwiness = 0.7 static = False bouncy = False run = False charge_dist_min = 0.0 # When we can start a new charge. charge_dist_max = 2.0 # When we can start a new charge. run_dist_min = 0.0 # How close we can be to continue running. charge_speed_min = 0.4 charge_speed_max = 1.0 throw_dist_min = 5.0 throw_dist_max = 9.0 throw_rate = 1.0 default_bomb_type = 'normal' default_bomb_count = 3 start_cursed = False color = DEFAULT_BOT_COLOR highlight = DEFAULT_BOT_HIGHLIGHT def __init__(self) -> None: """Instantiate a spaz-bot.""" super().__init__( color=self.color, highlight=self.highlight, character=self.character, source_player=None, start_invincible=False, can_accept_powerups=False, ) # If you need to add custom behavior to a bot, set this to a callable # which takes one arg (the bot) and returns False if the bot's normal # update should be run and True if not. self.update_callback: Callable[[SpazBot], Any] | None = None activity = self.activity assert isinstance(activity, bs.GameActivity) self._map = weakref.ref(activity.map) self.last_player_attacked_by: bs.Player | None = None self.last_attacked_time = 0.0 self.last_attacked_type: tuple[str, str] | None = None self.target_point_default: bs.Vec3 | None = None self.held_count = 0 self.last_player_held_by: bs.Player | None = None self.target_flag: Flag | None = None self._charge_speed = 0.5 * ( self.charge_speed_min + self.charge_speed_max ) self._lead_amount = 0.5 self._mode = 'wait' self._charge_closing_in = False self._last_charge_dist = 0.0 self._running = False self._last_jump_time = 0.0 self._throw_release_time: float | None = None self._have_dropped_throw_bomb: bool | None = None self._player_pts: list[tuple[bs.Vec3, bs.Vec3]] | None = None # These cooldowns didn't exist when these bots were calibrated, # so take them out of the equation. self._jump_cooldown = 0 self._pickup_cooldown = 0 self._fly_cooldown = 0 self._bomb_cooldown = 0 if self.start_cursed: self.curse() @property def map(self) -> bs.Map: """The map this bot was created on.""" mval = self._map() assert mval is not None return mval def _get_target_player_pt(self) -> tuple[bs.Vec3 | None, bs.Vec3 | None]: """Returns the position and velocity of our target. Both values will be None in the case of no target. """ assert self.node botpt = bs.Vec3(self.node.position) closest_dist: float | None = None closest_vel: bs.Vec3 | None = None closest: bs.Vec3 | None = None assert self._player_pts is not None for plpt, plvel in self._player_pts: dist = (plpt - botpt).length() # Ignore player-points that are significantly below the bot # (keeps bots from following players off cliffs). if (closest_dist is None or dist < closest_dist) and ( plpt[1] > botpt[1] - 5.0 ): closest_dist = dist closest_vel = plvel closest = plpt if closest_dist is not None: assert closest_vel is not None assert closest is not None return ( bs.Vec3(closest[0], closest[1], closest[2]), bs.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]), ) return None, None
[docs] def set_player_points(self, pts: list[tuple[bs.Vec3, bs.Vec3]]) -> None: """Provide the spaz-bot with the locations of its enemies.""" self._player_pts = pts
[docs] def update_ai(self) -> None: """Should be called periodically to update the spaz' AI.""" # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-locals if self.update_callback is not None: if self.update_callback(self): # Bot has been handled. return if not self.node: return pos = self.node.position our_pos = bs.Vec3(pos[0], 0, pos[2]) can_attack = True target_pt_raw: bs.Vec3 | None target_vel: bs.Vec3 | None # If we're a flag-bearer, we're pretty simple-minded - just walk # towards the flag and try to pick it up. if self.target_flag: if self.node.hold_node: holding_flag = self.node.hold_node.getnodetype() == 'flag' else: holding_flag = False # If we're holding the flag, just walk left. if holding_flag: # Just walk left. self.node.move_left_right = -1.0 self.node.move_up_down = 0.0 # Otherwise try to go pick it up. elif self.target_flag.node: target_pt_raw = bs.Vec3(*self.target_flag.node.position) diff = target_pt_raw - our_pos diff = bs.Vec3(diff[0], 0, diff[2]) # Don't care about y. dist = diff.length() to_target = diff.normalized() # If we're holding some non-flag item, drop it. if self.node.hold_node: self.node.pickup_pressed = True self.node.pickup_pressed = False return # If we're a runner, run only when not super-near the flag. if self.run and dist > 3.0: self._running = True self.node.run = 1.0 else: self._running = False self.node.run = 0.0 self.node.move_left_right = to_target.x self.node.move_up_down = -to_target.z if dist < 1.25: self.node.pickup_pressed = True self.node.pickup_pressed = False return # Not a flag-bearer. If we're holding anything but a bomb, drop it. if self.node.hold_node: holding_bomb = self.node.hold_node.getnodetype() in ['bomb', 'prop'] if not holding_bomb: self.node.pickup_pressed = True self.node.pickup_pressed = False return target_pt_raw, target_vel = self._get_target_player_pt() if target_pt_raw is None: # Use default target if we've got one. if self.target_point_default is not None: target_pt_raw = self.target_point_default target_vel = bs.Vec3(0, 0, 0) can_attack = False # With no target, we stop moving and drop whatever we're holding. else: self.node.move_left_right = 0 self.node.move_up_down = 0 if self.node.hold_node: self.node.pickup_pressed = True self.node.pickup_pressed = False return # We don't want height to come into play. target_pt_raw[1] = 0.0 assert target_vel is not None target_vel[1] = 0.0 dist_raw = (target_pt_raw - our_pos).length() # Use a point out in front of them as real target. # (more out in front the farther from us they are) target_pt = ( target_pt_raw + target_vel * dist_raw * 0.3 * self._lead_amount ) diff = target_pt - our_pos dist = diff.length() to_target = diff.normalized() if self._mode == 'throw': # We can only throw if alive and well. if not self._dead and not self.node.knockout: assert self._throw_release_time is not None time_till_throw = self._throw_release_time - bs.time() if not self.node.hold_node: # If we haven't thrown yet, whip out the bomb. if not self._have_dropped_throw_bomb: self.drop_bomb() self._have_dropped_throw_bomb = True # Otherwise our lack of held node means we successfully # released our bomb; lets retreat now. else: self._mode = 'flee' # Oh crap, we're holding a bomb; better throw it. elif time_till_throw <= 0.0: # Jump and throw. def _safe_pickup(node: bs.Node) -> None: if node and self.node: self.node.pickup_pressed = True self.node.pickup_pressed = False if dist > 5.0: self.node.jump_pressed = True self.node.jump_pressed = False # Throws: bs.timer(0.1, bs.Call(_safe_pickup, self.node)) else: # Throws: bs.timer(0.1, bs.Call(_safe_pickup, self.node)) if self.static: if time_till_throw < 0.3: speed = 1.0 elif time_till_throw < 0.7 and dist > 3.0: speed = -1.0 # Whiplash for long throws. else: speed = 0.02 else: if time_till_throw < 0.7: # Right before throw charge full speed towards target. speed = 1.0 else: # Earlier we can hold or move backward for a whiplash. speed = 0.0125 self.node.move_left_right = to_target.x * speed self.node.move_up_down = to_target.z * -1.0 * speed elif self._mode == 'charge': if random.random() < 0.3: self._charge_speed = random.uniform( self.charge_speed_min, self.charge_speed_max ) # If we're a runner we run during charges *except when near # an edge (otherwise we tend to fly off easily). if self.run and dist_raw > self.run_dist_min: self._lead_amount = 0.3 self._running = True self.node.run = 1.0 else: self._lead_amount = 0.01 self._running = False self.node.run = 0.0 self.node.move_left_right = to_target.x * self._charge_speed self.node.move_up_down = to_target.z * -1.0 * self._charge_speed elif self._mode == 'wait': # Every now and then, aim towards our target. # Other than that, just stand there. if int(bs.time() * 1000.0) % 1234 < 100: self.node.move_left_right = to_target.x * (400.0 / 33000) self.node.move_up_down = to_target.z * (-400.0 / 33000) else: self.node.move_left_right = 0 self.node.move_up_down = 0 elif self._mode == 'flee': # Even if we're a runner, only run till we get away from our # target (if we keep running we tend to run off edges). if self.run and dist < 3.0: self._running = True self.node.run = 1.0 else: self._running = False self.node.run = 0.0 self.node.move_left_right = to_target.x * -1.0 self.node.move_up_down = to_target.z # We might wanna switch states unless we're doing a throw # (in which case that's our sole concern). if self._mode != 'throw': # If we're currently charging, keep track of how far we are # from our target. When this value increases it means our charge # is over (ran by them or something). if self._mode == 'charge': if ( self._charge_closing_in and self._last_charge_dist < dist < 3.0 ): self._charge_closing_in = False self._last_charge_dist = dist # If we have a clean shot, throw! if ( self.throw_dist_min <= dist < self.throw_dist_max and random.random() < self.throwiness and can_attack ): self._mode = 'throw' self._lead_amount = ( (0.4 + random.random() * 0.6) if dist_raw > 4.0 else (0.1 + random.random() * 0.4) ) self._have_dropped_throw_bomb = False self._throw_release_time = bs.time() + ( 1.0 / self.throw_rate ) * (0.8 + 1.3 * random.random()) # If we're static, always charge (which for us means barely move). elif self.static: self._mode = 'wait' # If we're too close to charge (and aren't in the middle of an # existing charge) run away. elif dist < self.charge_dist_min and not self._charge_closing_in: # ..unless we're near an edge, in which case we've got no # choice but to charge. if self.map.is_point_near_edge(our_pos, self._running): if self._mode != 'charge': self._mode = 'charge' self._lead_amount = 0.2 self._charge_closing_in = True self._last_charge_dist = dist else: self._mode = 'flee' # We're within charging distance, backed against an edge, # or farther than our max throw distance.. chaaarge! elif ( dist < self.charge_dist_max or dist > self.throw_dist_max or self.map.is_point_near_edge(our_pos, self._running) ): if self._mode != 'charge': self._mode = 'charge' self._lead_amount = 0.01 self._charge_closing_in = True self._last_charge_dist = dist # We're too close to throw but too far to charge - either run # away or just chill if we're near an edge. elif dist < self.throw_dist_min: # Charge if either we're within charge range or # cant retreat to throw. self._mode = 'flee' # Do some awesome jumps if we're running. # FIXME: pylint: disable=too-many-boolean-expressions if ( self._running and 1.2 < dist < 2.2 and bs.time() - self._last_jump_time > 1.0 ) or ( self.bouncy and bs.time() - self._last_jump_time > 0.4 and random.random() < 0.5 ): self._last_jump_time = bs.time() self.node.jump_pressed = True self.node.jump_pressed = False # Throw punches when real close. if dist < (1.6 if self._running else 1.2) and can_attack: if random.random() < self.punchiness: self.on_punch_press() self.on_punch_release()
[docs] @override def on_punched(self, damage: int) -> None: """ Method override; sends bs.SpazBotPunchedMessage to the current activity. """ bs.getactivity().handlemessage(SpazBotPunchedMessage(self, damage))
[docs] @override def on_expire(self) -> None: super().on_expire() # We're being torn down; release our callback(s) so there's # no chance of them keeping activities or other things alive. self.update_callback = None
[docs] @override def handlemessage(self, msg: Any) -> Any: # pylint: disable=too-many-branches assert not self.expired # Keep track of if we're being held and by who most recently. if isinstance(msg, bs.PickedUpMessage): super().handlemessage(msg) # Augment standard behavior. self.held_count += 1 picked_up_by = msg.node.source_player if picked_up_by: self.last_player_held_by = picked_up_by elif isinstance(msg, bs.DroppedMessage): super().handlemessage(msg) # Augment standard behavior. self.held_count -= 1 if self.held_count < 0: print('ERROR: spaz held_count < 0') # Let's count someone dropping us as an attack. try: if msg.node: picked_up_by = msg.node.source_player else: picked_up_by = None except Exception: logging.exception('Error on SpazBot DroppedMessage.') picked_up_by = None if picked_up_by: self.last_player_attacked_by = picked_up_by self.last_attacked_time = bs.time() self.last_attacked_type = ('picked_up', 'default') elif isinstance(msg, bs.DieMessage): # Report normal deaths for scoring purposes. if not self._dead and not msg.immediate: killerplayer: bs.Player | None # If this guy was being held at the time of death, the # holder is the killer. if self.held_count > 0 and self.last_player_held_by: killerplayer = self.last_player_held_by else: # If they were attacked by someone in the last few # seconds that person's the killer. # Otherwise it was a suicide. if ( self.last_player_attacked_by and bs.time() - self.last_attacked_time < 4.0 ): killerplayer = self.last_player_attacked_by else: killerplayer = None activity = self._activity() # (convert dead player refs to None) if not killerplayer: killerplayer = None if activity is not None: activity.handlemessage( SpazBotDiedMessage(self, killerplayer, msg.how) ) super().handlemessage(msg) # Augment standard behavior. # Keep track of the player who last hit us for point rewarding. elif isinstance(msg, bs.HitMessage): source_player = msg.get_source_player(bs.Player) if source_player: self.last_player_attacked_by = source_player self.last_attacked_time = bs.time() self.last_attacked_type = (msg.hit_type, msg.hit_subtype) super().handlemessage(msg) else: super().handlemessage(msg)
[docs] class BomberBot(SpazBot): """A bot that throws regular bombs and occasionally punches. category: Bot Classes """ character = 'Spaz' punchiness = 0.3
[docs] class BomberBotLite(BomberBot): """A less aggressive yellow version of bs.BomberBot. category: Bot Classes """ color = LITE_BOT_COLOR highlight = LITE_BOT_HIGHLIGHT punchiness = 0.2 throw_rate = 0.7 throwiness = 0.1 charge_speed_min = 0.6 charge_speed_max = 0.6
[docs] class BomberBotStaticLite(BomberBotLite): """A less aggressive generally immobile weak version of bs.BomberBot. category: Bot Classes """ static = True throw_dist_min = 0.0
[docs] class BomberBotStatic(BomberBot): """A version of bs.BomberBot who generally stays in one place. category: Bot Classes """ static = True throw_dist_min = 0.0
[docs] class BomberBotPro(BomberBot): """A more powerful version of bs.BomberBot. category: Bot Classes """ points_mult = 2 color = PRO_BOT_COLOR highlight = PRO_BOT_HIGHLIGHT default_bomb_count = 3 default_boxing_gloves = True punchiness = 0.7 throw_rate = 1.3 run = True run_dist_min = 6.0
[docs] class BomberBotProShielded(BomberBotPro): """A more powerful version of bs.BomberBot who starts with shields. category: Bot Classes """ points_mult = 3 default_shields = True
[docs] class BomberBotProStatic(BomberBotPro): """A more powerful bs.BomberBot who generally stays in one place. category: Bot Classes """ static = True throw_dist_min = 0.0
[docs] class BomberBotProStaticShielded(BomberBotProShielded): """A powerful bs.BomberBot with shields who is generally immobile. category: Bot Classes """ static = True throw_dist_min = 0.0
[docs] class BrawlerBot(SpazBot): """A bot who walks and punches things. category: Bot Classes """ character = 'Kronk' punchiness = 0.9 charge_dist_max = 9999.0 charge_speed_min = 1.0 charge_speed_max = 1.0 throw_dist_min = 9999 throw_dist_max = 9999
[docs] class BrawlerBotLite(BrawlerBot): """A weaker version of bs.BrawlerBot. category: Bot Classes """ color = LITE_BOT_COLOR highlight = LITE_BOT_HIGHLIGHT punchiness = 0.3 charge_speed_min = 0.6 charge_speed_max = 0.6
[docs] class BrawlerBotPro(BrawlerBot): """A stronger version of bs.BrawlerBot. category: Bot Classes """ color = PRO_BOT_COLOR highlight = PRO_BOT_HIGHLIGHT run = True run_dist_min = 4.0 default_boxing_gloves = True punchiness = 0.95 points_mult = 2
[docs] class BrawlerBotProShielded(BrawlerBotPro): """A stronger version of bs.BrawlerBot who starts with shields. category: Bot Classes """ default_shields = True points_mult = 3
[docs] class ChargerBot(SpazBot): """A speedy melee attack bot. category: Bot Classes """ character = 'Snake Shadow' punchiness = 1.0 run = True charge_dist_min = 10.0 charge_dist_max = 9999.0 charge_speed_min = 1.0 charge_speed_max = 1.0 throw_dist_min = 9999 throw_dist_max = 9999 points_mult = 2
[docs] class BouncyBot(SpazBot): """A speedy attacking melee bot that jumps constantly. category: Bot Classes """ color = (1, 1, 1) highlight = (1.0, 0.5, 0.5) character = 'Easter Bunny' punchiness = 1.0 run = True bouncy = True default_boxing_gloves = True charge_dist_min = 10.0 charge_dist_max = 9999.0 charge_speed_min = 1.0 charge_speed_max = 1.0 throw_dist_min = 9999 throw_dist_max = 9999 points_mult = 2
[docs] class ChargerBotPro(ChargerBot): """A stronger bs.ChargerBot. category: Bot Classes """ color = PRO_BOT_COLOR highlight = PRO_BOT_HIGHLIGHT default_boxing_gloves = True points_mult = 3
[docs] class ChargerBotProShielded(ChargerBotPro): """A stronger bs.ChargerBot who starts with shields. category: Bot Classes """ default_shields = True points_mult = 4
[docs] class TriggerBot(SpazBot): """A slow moving bot with trigger bombs. category: Bot Classes """ character = 'Zoe' punchiness = 0.75 throwiness = 0.7 charge_dist_max = 1.0 charge_speed_min = 0.3 charge_speed_max = 0.5 throw_dist_min = 3.5 throw_dist_max = 5.5 default_bomb_type = 'impact' points_mult = 2
[docs] class TriggerBotStatic(TriggerBot): """A bs.TriggerBot who generally stays in one place. category: Bot Classes """ static = True throw_dist_min = 0.0
[docs] class TriggerBotPro(TriggerBot): """A stronger version of bs.TriggerBot. category: Bot Classes """ color = PRO_BOT_COLOR highlight = PRO_BOT_HIGHLIGHT default_bomb_count = 3 default_boxing_gloves = True charge_speed_min = 1.0 charge_speed_max = 1.0 punchiness = 0.9 throw_rate = 1.3 run = True run_dist_min = 6.0 points_mult = 3
[docs] class TriggerBotProShielded(TriggerBotPro): """A stronger version of bs.TriggerBot who starts with shields. category: Bot Classes """ default_shields = True points_mult = 4
[docs] class StickyBot(SpazBot): """A crazy bot who runs and throws sticky bombs. category: Bot Classes """ character = 'Mel' punchiness = 0.9 throwiness = 1.0 run = True charge_dist_min = 4.0 charge_dist_max = 10.0 charge_speed_min = 1.0 charge_speed_max = 1.0 throw_dist_min = 0.0 throw_dist_max = 4.0 throw_rate = 2.0 default_bomb_type = 'sticky' default_bomb_count = 3 points_mult = 3
[docs] class StickyBotStatic(StickyBot): """A crazy bot who throws sticky-bombs but generally stays in one place. category: Bot Classes """ static = True
[docs] class ExplodeyBot(SpazBot): """A bot who runs and explodes in 5 seconds. category: Bot Classes """ character = 'Jack Morgan' run = True charge_dist_min = 0.0 charge_dist_max = 9999 charge_speed_min = 1.0 charge_speed_max = 1.0 throw_dist_min = 9999 throw_dist_max = 9999 start_cursed = True points_mult = 4
[docs] class ExplodeyBotNoTimeLimit(ExplodeyBot): """A bot who runs but does not explode on his own. category: Bot Classes """ curse_time = None
[docs] class ExplodeyBotShielded(ExplodeyBot): """A bs.ExplodeyBot who starts with shields. category: Bot Classes """ default_shields = True points_mult = 5
[docs] class SpazBotSet: """A container/controller for one or more bs.SpazBots. category: Bot Classes """ def __init__(self) -> None: """Create a bot-set.""" # We spread our bots out over a few lists so we can update # them in a staggered fashion. self._bot_list_count = 5 self._bot_add_list = 0 self._bot_update_list = 0 self._bot_lists: list[list[SpazBot]] = [ [] for _ in range(self._bot_list_count) ] self._spawn_sound = bs.getsound('spawn') self._spawning_count = 0 self._bot_update_timer: bs.Timer | None = None self.start_moving() def __del__(self) -> None: self.clear()
[docs] def spawn_bot( self, bot_type: type[SpazBot], pos: Sequence[float], spawn_time: float = 3.0, on_spawn_call: Callable[[SpazBot], Any] | None = None, ) -> None: """Spawn a bot from this set.""" from bascenev1lib.actor.spawner import Spawner Spawner( pt=pos, spawn_time=spawn_time, send_spawn_message=False, spawn_callback=bs.Call( self._spawn_bot, bot_type, pos, on_spawn_call ), ) self._spawning_count += 1
def _spawn_bot( self, bot_type: type[SpazBot], pos: Sequence[float], on_spawn_call: Callable[[SpazBot], Any] | None, ) -> None: spaz = bot_type() self._spawn_sound.play(position=pos) assert spaz.node spaz.node.handlemessage('flash') spaz.node.is_area_of_interest = False spaz.handlemessage(bs.StandMessage(pos, random.uniform(0, 360))) self.add_bot(spaz) self._spawning_count -= 1 if on_spawn_call is not None: on_spawn_call(spaz)
[docs] def have_living_bots(self) -> bool: """Return whether any bots in the set are alive or spawning.""" return self._spawning_count > 0 or any( any(b.is_alive() for b in l) for l in self._bot_lists )
[docs] def get_living_bots(self) -> list[SpazBot]: """Get the living bots in the set.""" bots: list[SpazBot] = [] for botlist in self._bot_lists: for bot in botlist: if bot.is_alive(): bots.append(bot) return bots
def _update(self) -> None: # Update one of our bot lists each time through. # First off, remove no-longer-existing bots from the list. try: bot_list = self._bot_lists[self._bot_update_list] = [ b for b in self._bot_lists[self._bot_update_list] if b ] except Exception: bot_list = [] logging.exception( 'Error updating bot list: %s', self._bot_lists[self._bot_update_list], ) self._bot_update_list = ( self._bot_update_list + 1 ) % self._bot_list_count # Update our list of player points for the bots to use. player_pts = [] for player in bs.getactivity().players: assert isinstance(player, bs.Player) try: # TODO: could use abstracted player.position here so we # don't have to assume their actor type, but we have no # abstracted velocity as of yet. if player.is_alive(): assert isinstance(player.actor, Spaz) assert player.actor.node player_pts.append( ( bs.Vec3(player.actor.node.position), bs.Vec3(player.actor.node.velocity), ) ) except Exception: logging.exception('Error on bot-set _update.') for bot in bot_list: bot.set_player_points(player_pts) bot.update_ai()
[docs] def clear(self) -> None: """Immediately clear out any bots in the set.""" # Don't do this if the activity is shutting down or dead. activity = bs.getactivity(doraise=False) if activity is None or activity.expired: return for i, bot_list in enumerate(self._bot_lists): for bot in bot_list: bot.handlemessage(bs.DieMessage(immediate=True)) self._bot_lists[i] = []
[docs] def start_moving(self) -> None: """Start processing bot AI updates so they start doing their thing.""" self._bot_update_timer = bs.Timer( 0.05, bs.WeakCall(self._update), repeat=True )
[docs] def stop_moving(self) -> None: """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. """ self._bot_update_timer = None for botlist in self._bot_lists: for bot in botlist: if bot.node: bot.node.move_left_right = 0 bot.node.move_up_down = 0
[docs] def celebrate(self, duration: float) -> None: """Tell all living bots in the set to celebrate momentarily. Duration is given in seconds. """ msg = bs.CelebrateMessage(duration=duration) for botlist in self._bot_lists: for bot in botlist: if bot: bot.handlemessage(msg)
[docs] def final_celebrate(self) -> None: """Tell all bots in the set to stop what they were doing and celebrate. Use this when the bots have won a game. """ self._bot_update_timer = None # At this point stop doing anything but jumping and celebrating. for botlist in self._bot_lists: for bot in botlist: if bot: assert bot.node # (should exist if 'if bot' was True) bot.node.move_left_right = 0 bot.node.move_up_down = 0 bs.timer( 0.5 * random.random(), bs.Call(bot.handlemessage, bs.CelebrateMessage()), ) jump_duration = random.randrange(400, 500) j = random.randrange(0, 200) for _i in range(10): bot.node.jump_pressed = True bot.node.jump_pressed = False j += jump_duration bs.timer( random.uniform(0.0, 1.0), bs.Call(bot.node.handlemessage, 'attack_sound'), ) bs.timer( random.uniform(1.0, 2.0), bs.Call(bot.node.handlemessage, 'attack_sound'), ) bs.timer( random.uniform(2.0, 3.0), bs.Call(bot.node.handlemessage, 'attack_sound'), )
[docs] def add_bot(self, bot: SpazBot) -> None: """Add a bs.SpazBot instance to the set.""" self._bot_lists[self._bot_add_list].append(bot) self._bot_add_list = (self._bot_add_list + 1) % self._bot_list_count