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.""" 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.""" 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. 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
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum