From 3a87cf48399ad53d5ccbbba01e0cb7677cdebcb5 Mon Sep 17 00:00:00 2001 From: rabbitboom <160723853+MW2917@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:36:14 +0000 Subject: [PATCH 1/5] Update minigames.json --- plugins/minigames.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index bf03d89..120cb65 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1044,6 +1044,19 @@ } } }, + "botsvsbots": { + "description": "Watch your team of bots fight the opposing team of bots!", + "external_url": "", + "authors": [ + { + "name": "rabbitboom", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } "handball": { "description": "Score some goals with handball", "external_url": "", @@ -1609,4 +1622,4 @@ } } } -} \ No newline at end of file +} From 71e996d27d141631abcf9dfca770acaa7d72d7c7 Mon Sep 17 00:00:00 2001 From: rabbitboom <160723853+MW2917@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:42:56 +0000 Subject: [PATCH 2/5] Add files via upload --- plugins/minigames/botsvsbots.py | 375 ++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 plugins/minigames/botsvsbots.py diff --git a/plugins/minigames/botsvsbots.py b/plugins/minigames/botsvsbots.py new file mode 100644 index 0000000..ffa903c --- /dev/null +++ b/plugins/minigames/botsvsbots.py @@ -0,0 +1,375 @@ +""" +Bots Fighting game. Pretty cool for if u want to bet with your mates, or let fate (randomess) decide on the winning team. + +-credits- +made by rabbitboom, aka Johnnyä»” (YouTube) or iCommerade (gaming world(???)) +original idea by Froshlee08 (hence a bot named in his honour) +shoutout to EraOS who gave me quite a lot of solution and Brotherboard who also helped. +""" + +# ba_meta require api 9 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING, override + + +import logging, random, weakref +import bascenev1 as bs +import efro.debug as Edebug + +from bascenev1lib.actor.spaz import Spaz, PunchHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.spazbot import ( + SpazBotDiedMessage, + SpazBotSet, + ChargerBot, + StickyBot, + SpazBot, + BrawlerBot, + TriggerBot, + BrawlerBotProShielded, + ChargerBotProShielded, + BouncyBot, + TriggerBotProShielded, + BomberBotProShielded, +) + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List + +class Player(bs.Player['Team']): + """Our player type for this game.""" + +class SpazBot2(SpazBot): + #custom bots that have custom behaviours requiring more msg and modules from spaz.py will have them imported here. + from bascenev1lib.actor.spaz import Spaz, PunchHitMessage + +class BouncyBotSemiLite(BouncyBot): + highlight = (1,1,0.8) + punchiness = 0.85 + run = False + default_boxing_gloves = False + charge_dist_min = 5 + points_mult = 1 + + +class FroshBot(SpazBot): + #Slightly beefier version of BomberBots, with slightly altered behaviour. + color = (0.13,0.13,0.13) + highlight = (0.2,1,1) + character = 'Bernard' + run = True + default_bomb_count = 2 + throw_rate = 0.8 + throwiness = 0.2 + punchiness = 0.85 + charge_dist_max = 3.0 + charge_speed_min = 0.3 + default_hitpoints = 1300 + +class FroshBotShielded(FroshBot): + default_shields = True + +class StickyBotShielded(StickyBot): + default_shields = True + +class IcePuncherBot(SpazBot2): + #A bot who can freeze anyone on punch and is immune to freezing. + color = (0,0,1) + highlight = (.2,.2,1) + character = 'Pascal' + run = True + punchiness = 0.8 + charge_dist_min = 6 + charge_dist_max = 9999 + charge_speed_min = 0.3 + charge_speed_max = 0.85 + throw_dist_min = 9999 + throw_dist_max = 9999 + + def handlemessage(self, msg): + #Add the ability to freeze anyone on punch. + if isinstance(msg, self.PunchHitMessage): + node = bs.getcollision().opposingnode + try: + node.handlemessage(bs.FreezeMessage()) + bs.getsound('freeze').play() + except Exception: + print('freeze failed.') + return super().handlemessage(msg) + elif isinstance(msg, bs.FreezeMessage): + pass #resistant to freezing. + else: + return super().handlemessage(msg) + +class IcePuncherBotShielded(IcePuncherBot): + default_shields = True + + +class GliderBot(BrawlerBot): + #A bot who can glide on terrain and navigate it safely as if it's ice skating. + color = (0.5,0.5,0.5) + highlight = (0,10,0) + character = 'B-9000' + charge_speed_min = 0.3 + charge_speed_max = 1.0 + + def __init__(self) -> None: + super().__init__() + self.node.hockey = True #Added the ability to move fast "like in hockey". + +class GliderBotShielded(GliderBot): + #Shielded version of GliderBot. + default_shields = True + + + +class TeamBotSet(SpazBotSet): + """Bots that can gather in a team and kill other bots.""" + + activity: BotsVSBotsGame + + def __init__ (self, team: Team) -> None: + super().__init__() + activity = bs.getactivity() + self._pro_bots = activity._pro_bots + self._bonus_bots = activity._bonus_bots + self._punching_bots_only = activity._punching_bots_only + self.spawn_time = activity.spawn_time + self._team = weakref.ref(team) + + + def colorforteam(self, spaz) -> None: + spaz.node.color = self.team.color + + + def yell(self) -> None: + for botlist in self._bot_lists: + for bot in botlist: + if bot: + assert bot.node + self.celebrate(self.spawn_time) + bs.Call(bot.node.handlemessage, 'jump_sound') + bs.timer(0, bs.Call(bot.node.handlemessage, 'attack_sound')) + + 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 = [] + try: + for n in bs.getnodes(): + if n.getnodetype() == 'spaz': + s = n.getdelegate(object) + if isinstance(s, SpazBot): + if not s in self.get_living_bots(): + if s.is_alive(): + player_pts.append(( + bs.Vec3(n.position), + bs.Vec3(n.velocity))) + except Exception: + logging.exception('Error on bot-set _update.') + + for bot in bot_list: + bot.set_player_points(player_pts) + bot.update_ai() + + + + def get_bot_type(self): + bot_types = [ + BrawlerBot, + ChargerBot, + BouncyBotSemiLite, + ] + if self._punching_bots_only == False: + bot_types += [ + SpazBot, + StickyBot, + ] + if self._pro_bots == True: + bot_types += [ + BrawlerBotProShielded, + ChargerBotProShielded, + BouncyBot, + ] + if self._punching_bots_only == False: + bot_types += [ + BomberBotProShielded, + ] + if self._bonus_bots == True: + bot_types += [ + IcePuncherBot, + GliderBot, + ] + if self._punching_bots_only == False: + bot_types += [ + FroshBot, + ] + if self._bonus_bots and self._pro_bots == True: + bot_types += [ + IcePuncherBotShielded, + GliderBotShielded, + ] + if self._punching_bots_only == False: + bot_types += [ + FroshBotShielded, + StickyBotShielded, + ] + return random.choice(bot_types) + + + + + @property + def team(self) -> Team: + """The bot's team.""" + return self._team() + + + +class Team(bs.Team[TeamBotSet]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + self.bots = TeamBotSet(self) + + + + + +# ba_meta export bascenev1.GameActivity +class BotsVSBotsGame(bs.TeamGameActivity[bs.Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Bots VS Bots' + description = 'Sit down and enjoy mobs from your team fight the opposing team for you. \n Feel free to enjoy some popcorn with it or place a bet with your friends.' #, self._score_to_win + + @override + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[bs.Setting]: + settings = [ + bs.IntSetting( + 'Bots Per Team', + min_value=5, + default=20, + increment=5, + ), + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Punchers only', default=False), + bs.BoolSetting('Pro bots', default=False), + bs.BoolSetting('Bonus bots', default=True), + ] + return settings + + @override + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @override + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._bots_per_team = int(settings['Bots Per Team']) + self._dingsound = bs.getsound('dingSmall') + self._punching_bots_only = bool(settings['Punchers only']) + self._pro_bots = bool(settings['Pro bots']) + self._bonus_bots = bool(settings['Bonus bots']) + self._epic_mode = bool(settings['Epic Mode']) + self._cheersound = bs.getsound('cheer') + self._marchsoundA = bs.getsound('footimpact01') + self._marchsoundB = bs.getsound('footimpact02') + self._marchsoundC = bs.getsound('footimpact03') + + # Base class overrides. + self.slow_motion = self._epic_mode + self.spawn_time = 1.6 if self._epic_mode else 4.0 + self.default_music = ( + bs.MusicType.TO_THE_DEATH + ) + + @override + def get_instance_description(self) -> str | Sequence: + return 'Enjoy the battle.' + @override + def get_instance_description_short(self) -> str | Sequence: + return 'Enjoy the battle.' + @override + def on_team_join(self, team: Team) -> None: + self.spawn_team(team) + + @override + def on_begin(self) -> None: + bs.TeamGameActivity.on_begin(self) + + def spawn_player(self, player: Player) -> None: + return None + + #def team_pois_set + + def spawn_team(self, team: Team) -> None: + camp = self.map.get_flag_position(team.id) + for i in range(self._bots_per_team): + team.bots.spawn_bot(team.bots.get_bot_type(), + (camp[0], camp[1] + 1.8, random.randrange(-4,5)), + self.spawn_time, team.bots.colorforteam) + bs.timer(float(self.spawn_time) + 1.7, team.bots.yell) + + + def update(self) -> None: + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0, self.end_game) + + def _get_living_teams(self) -> list[Team]: + return [ + team + for team in self.teams + if team.bots.have_living_bots() + ] + + + @override + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, SpazBotDiedMessage): + bs.pushcall(self.update) + + else: + return super().handlemessage(msg) + return None + + + + @override + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._cheersound.play() + for team in self.teams: + team.bots.final_celebrate() + results.set_team_score(team, len(team.bots.get_living_bots())) + self.end(results=results) From e63c5ffe61892d4009dd0a124d76780dfb17cbd7 Mon Sep 17 00:00:00 2001 From: MW2917 <160723853+MW2917@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:44:49 +0000 Subject: [PATCH 3/5] [ci] auto-format --- plugins/minigames/botsvsbots.py | 88 ++++++++++++++++----------------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/plugins/minigames/botsvsbots.py b/plugins/minigames/botsvsbots.py index ffa903c..c8cd7b0 100644 --- a/plugins/minigames/botsvsbots.py +++ b/plugins/minigames/botsvsbots.py @@ -15,7 +15,9 @@ from __future__ import annotations from typing import TYPE_CHECKING, override -import logging, random, weakref +import logging +import random +import weakref import bascenev1 as bs import efro.debug as Edebug @@ -40,15 +42,18 @@ from bascenev1lib.actor.spazbot import ( if TYPE_CHECKING: from typing import Any, Sequence, Optional, List + class Player(bs.Player['Team']): """Our player type for this game.""" + class SpazBot2(SpazBot): - #custom bots that have custom behaviours requiring more msg and modules from spaz.py will have them imported here. + # custom bots that have custom behaviours requiring more msg and modules from spaz.py will have them imported here. from bascenev1lib.actor.spaz import Spaz, PunchHitMessage + class BouncyBotSemiLite(BouncyBot): - highlight = (1,1,0.8) + highlight = (1, 1, 0.8) punchiness = 0.85 run = False default_boxing_gloves = False @@ -57,9 +62,9 @@ class BouncyBotSemiLite(BouncyBot): class FroshBot(SpazBot): - #Slightly beefier version of BomberBots, with slightly altered behaviour. - color = (0.13,0.13,0.13) - highlight = (0.2,1,1) + # Slightly beefier version of BomberBots, with slightly altered behaviour. + color = (0.13, 0.13, 0.13) + highlight = (0.2, 1, 1) character = 'Bernard' run = True default_bomb_count = 2 @@ -70,16 +75,19 @@ class FroshBot(SpazBot): charge_speed_min = 0.3 default_hitpoints = 1300 + class FroshBotShielded(FroshBot): default_shields = True + class StickyBotShielded(StickyBot): default_shields = True + class IcePuncherBot(SpazBot2): - #A bot who can freeze anyone on punch and is immune to freezing. - color = (0,0,1) - highlight = (.2,.2,1) + # A bot who can freeze anyone on punch and is immune to freezing. + color = (0, 0, 1) + highlight = (.2, .2, 1) character = 'Pascal' run = True punchiness = 0.8 @@ -91,7 +99,7 @@ class IcePuncherBot(SpazBot2): throw_dist_max = 9999 def handlemessage(self, msg): - #Add the ability to freeze anyone on punch. + # Add the ability to freeze anyone on punch. if isinstance(msg, self.PunchHitMessage): node = bs.getcollision().opposingnode try: @@ -101,38 +109,39 @@ class IcePuncherBot(SpazBot2): print('freeze failed.') return super().handlemessage(msg) elif isinstance(msg, bs.FreezeMessage): - pass #resistant to freezing. + pass # resistant to freezing. else: return super().handlemessage(msg) + class IcePuncherBotShielded(IcePuncherBot): default_shields = True class GliderBot(BrawlerBot): - #A bot who can glide on terrain and navigate it safely as if it's ice skating. - color = (0.5,0.5,0.5) - highlight = (0,10,0) + # A bot who can glide on terrain and navigate it safely as if it's ice skating. + color = (0.5, 0.5, 0.5) + highlight = (0, 10, 0) character = 'B-9000' charge_speed_min = 0.3 charge_speed_max = 1.0 def __init__(self) -> None: super().__init__() - self.node.hockey = True #Added the ability to move fast "like in hockey". + self.node.hockey = True # Added the ability to move fast "like in hockey". + class GliderBotShielded(GliderBot): - #Shielded version of GliderBot. + # Shielded version of GliderBot. default_shields = True - class TeamBotSet(SpazBotSet): """Bots that can gather in a team and kill other bots.""" activity: BotsVSBotsGame - def __init__ (self, team: Team) -> None: + def __init__(self, team: Team) -> None: super().__init__() activity = bs.getactivity() self._pro_bots = activity._pro_bots @@ -141,12 +150,10 @@ class TeamBotSet(SpazBotSet): self.spawn_time = activity.spawn_time self._team = weakref.ref(team) - def colorforteam(self, spaz) -> None: spaz.node.color = self.team.color - - def yell(self) -> None: + def yell(self) -> None: for botlist in self._bot_lists: for bot in botlist: if bot: @@ -191,14 +198,12 @@ class TeamBotSet(SpazBotSet): bot.set_player_points(player_pts) bot.update_ai() - - def get_bot_type(self): bot_types = [ BrawlerBot, ChargerBot, BouncyBotSemiLite, - ] + ] if self._punching_bots_only == False: bot_types += [ SpazBot, @@ -209,11 +214,11 @@ class TeamBotSet(SpazBotSet): BrawlerBotProShielded, ChargerBotProShielded, BouncyBot, - ] + ] if self._punching_bots_only == False: bot_types += [ BomberBotProShielded, - ] + ] if self._bonus_bots == True: bot_types += [ IcePuncherBot, @@ -222,7 +227,7 @@ class TeamBotSet(SpazBotSet): if self._punching_bots_only == False: bot_types += [ FroshBot, - ] + ] if self._bonus_bots and self._pro_bots == True: bot_types += [ IcePuncherBotShielded, @@ -232,11 +237,8 @@ class TeamBotSet(SpazBotSet): bot_types += [ FroshBotShielded, StickyBotShielded, - ] - return random.choice(bot_types) - - - + ] + return random.choice(bot_types) @property def team(self) -> Team: @@ -244,7 +246,6 @@ class TeamBotSet(SpazBotSet): return self._team() - class Team(bs.Team[TeamBotSet]): """Our team type for this game.""" @@ -252,16 +253,13 @@ class Team(bs.Team[TeamBotSet]): self.score = 0 self.bots = TeamBotSet(self) - - - # ba_meta export bascenev1.GameActivity class BotsVSBotsGame(bs.TeamGameActivity[bs.Player, Team]): """A game type based on acquiring kills.""" name = 'Bots VS Bots' - description = 'Sit down and enjoy mobs from your team fight the opposing team for you. \n Feel free to enjoy some popcorn with it or place a bet with your friends.' #, self._score_to_win + description = 'Sit down and enjoy mobs from your team fight the opposing team for you. \n Feel free to enjoy some popcorn with it or place a bet with your friends.' # , self._score_to_win @override @classmethod @@ -287,7 +285,7 @@ class BotsVSBotsGame(bs.TeamGameActivity[bs.Player, Team]): def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: return issubclass(sessiontype, bs.DualTeamSession) - @override + @override @classmethod def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: return ['Football Stadium'] @@ -315,9 +313,11 @@ class BotsVSBotsGame(bs.TeamGameActivity[bs.Player, Team]): @override def get_instance_description(self) -> str | Sequence: return 'Enjoy the battle.' + @override def get_instance_description_short(self) -> str | Sequence: return 'Enjoy the battle.' + @override def on_team_join(self, team: Team) -> None: self.spawn_team(team) @@ -328,17 +328,16 @@ class BotsVSBotsGame(bs.TeamGameActivity[bs.Player, Team]): def spawn_player(self, player: Player) -> None: return None - - #def team_pois_set + + # def team_pois_set def spawn_team(self, team: Team) -> None: camp = self.map.get_flag_position(team.id) for i in range(self._bots_per_team): team.bots.spawn_bot(team.bots.get_bot_type(), - (camp[0], camp[1] + 1.8, random.randrange(-4,5)), - self.spawn_time, team.bots.colorforteam) + (camp[0], camp[1] + 1.8, random.randrange(-4, 5)), + self.spawn_time, team.bots.colorforteam) bs.timer(float(self.spawn_time) + 1.7, team.bots.yell) - def update(self) -> None: if len(self._get_living_teams()) < 2: @@ -350,7 +349,6 @@ class BotsVSBotsGame(bs.TeamGameActivity[bs.Player, Team]): for team in self.teams if team.bots.have_living_bots() ] - @override def handlemessage(self, msg: Any) -> Any: @@ -360,8 +358,6 @@ class BotsVSBotsGame(bs.TeamGameActivity[bs.Player, Team]): else: return super().handlemessage(msg) return None - - @override def end_game(self) -> None: From 25b58cbfa842e37d3afd2607512a46624ebac1ea Mon Sep 17 00:00:00 2001 From: rabbitboom <160723853+MW2917@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:46:54 +0000 Subject: [PATCH 4/5] Update minigames.json --- plugins/minigames.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/minigames.json b/plugins/minigames.json index 120cb65..e35993b 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1057,6 +1057,7 @@ "versions": { "1.0.0": null } + }, "handball": { "description": "Score some goals with handball", "external_url": "", From 85465fe38a5437cd5fa9b8a95379ea034e4180a1 Mon Sep 17 00:00:00 2001 From: MW2917 <160723853+MW2917@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:47:18 +0000 Subject: [PATCH 5/5] [ci] apply-version-metadata --- plugins/minigames.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index e35993b..21faa67 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1055,7 +1055,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "25b58cb", + "released_on": "28-10-2025", + "md5sum": "bbe28558ce336f5f5d5d5a21305c5fd4" + } } }, "handball": { @@ -1623,4 +1628,4 @@ } } } -} +} \ No newline at end of file