bascenev1lib.game.ninjafight

Provides Ninja Fight mini-game.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides Ninja Fight mini-game."""
  4
  5# ba_meta require api 9
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10import random
 11from typing import TYPE_CHECKING, override
 12
 13import bascenev1 as bs
 14
 15from bascenev1lib.actor.spazbot import (
 16    SpazBotSet,
 17    ChargerBot,
 18    SpazBotDiedMessage,
 19)
 20from bascenev1lib.actor.onscreentimer import OnScreenTimer
 21
 22if TYPE_CHECKING:
 23    from typing import Any
 24
 25
 26class Player(bs.Player['Team']):
 27    """Our player type for this game."""
 28
 29
 30class Team(bs.Team[Player]):
 31    """Our team type for this game."""
 32
 33
 34# ba_meta export bascenev1.GameActivity
 35class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
 36    """
 37    A co-op game where you try to defeat a group
 38    of Ninjas as fast as possible
 39    """
 40
 41    name = 'Ninja Fight'
 42    description = 'How fast can you defeat the ninjas?'
 43    scoreconfig = bs.ScoreConfig(
 44        label='Time', scoretype=bs.ScoreType.MILLISECONDS, lower_is_better=True
 45    )
 46    default_music = bs.MusicType.TO_THE_DEATH
 47
 48    @override
 49    @classmethod
 50    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 51        # For now we're hard-coding spawn positions and whatnot
 52        # so we need to be sure to specify that we only support
 53        # a specific map.
 54        return ['Courtyard']
 55
 56    @override
 57    @classmethod
 58    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 59        # We currently support Co-Op only.
 60        return issubclass(sessiontype, bs.CoopSession)
 61
 62    # In the constructor we should load any media we need/etc.
 63    # ...but not actually create anything yet.
 64    def __init__(self, settings: dict):
 65        super().__init__(settings)
 66        self._winsound = bs.getsound('score')
 67        self._won = False
 68        self._timer: OnScreenTimer | None = None
 69        self._bots = SpazBotSet()
 70        self._preset = str(settings['preset'])
 71
 72    # Called when our game actually begins.
 73    @override
 74    def on_begin(self) -> None:
 75        super().on_begin()
 76        is_pro = self._preset == 'pro'
 77
 78        # In pro mode there's no powerups.
 79        if not is_pro:
 80            self.setup_standard_powerup_drops()
 81
 82        # Make our on-screen timer and start it roughly when our bots appear.
 83        self._timer = OnScreenTimer()
 84        bs.timer(4.0, self._timer.start)
 85
 86        # Spawn some baddies.
 87        bs.timer(
 88            1.0,
 89            lambda: self._bots.spawn_bot(
 90                ChargerBot, pos=(3, 3, -2), spawn_time=3.0
 91            ),
 92        )
 93        bs.timer(
 94            2.0,
 95            lambda: self._bots.spawn_bot(
 96                ChargerBot, pos=(-3, 3, -2), spawn_time=3.0
 97            ),
 98        )
 99        bs.timer(
100            3.0,
101            lambda: self._bots.spawn_bot(
102                ChargerBot, pos=(5, 3, -2), spawn_time=3.0
103            ),
104        )
105        bs.timer(
106            4.0,
107            lambda: self._bots.spawn_bot(
108                ChargerBot, pos=(-5, 3, -2), spawn_time=3.0
109            ),
110        )
111
112        # Add some extras for multiplayer or pro mode.
113        assert self.initialplayerinfos is not None
114        if len(self.initialplayerinfos) > 2 or is_pro:
115            bs.timer(
116                5.0,
117                lambda: self._bots.spawn_bot(
118                    ChargerBot, pos=(0, 3, -5), spawn_time=3.0
119                ),
120            )
121        if len(self.initialplayerinfos) > 3 or is_pro:
122            bs.timer(
123                6.0,
124                lambda: self._bots.spawn_bot(
125                    ChargerBot, pos=(0, 3, 1), spawn_time=3.0
126                ),
127            )
128
129    # Called for each spawning player.
130    @override
131    def spawn_player(self, player: Player) -> bs.Actor:
132        # Let's spawn close to the center.
133        spawn_center = (0, 3, -2)
134        pos = (
135            spawn_center[0] + random.uniform(-1.5, 1.5),
136            spawn_center[1],
137            spawn_center[2] + random.uniform(-1.5, 1.5),
138        )
139        return self.spawn_player_spaz(player, position=pos)
140
141    def _check_if_won(self) -> None:
142        # Simply end the game if there's no living bots.
143        # FIXME: Should also make sure all bots have been spawned;
144        #  if spawning is spread out enough that we're able to kill
145        #  all living bots before the next spawns, it would incorrectly
146        #  count as a win.
147        if not self._bots.have_living_bots():
148            self._won = True
149            self.end_game()
150
151    # Called for miscellaneous messages.
152    @override
153    def handlemessage(self, msg: Any) -> Any:
154        # A player has died.
155        if isinstance(msg, bs.PlayerDiedMessage):
156            super().handlemessage(msg)  # Augment standard behavior.
157            self.respawn_player(msg.getplayer(Player))
158
159        # A spaz-bot has died.
160        elif isinstance(msg, SpazBotDiedMessage):
161            # Unfortunately the bot-set will always tell us there are living
162            # bots if we ask here (the currently-dying bot isn't officially
163            # marked dead yet) ..so lets push a call into the event loop to
164            # check once this guy has finished dying.
165            bs.pushcall(self._check_if_won)
166
167        # Let the base class handle anything we don't.
168        else:
169            return super().handlemessage(msg)
170        return None
171
172    # When this is called, we should fill out results and end the game
173    # *regardless* of whether is has been won. (this may be called due
174    # to a tournament ending or other external reason).
175    @override
176    def end_game(self) -> None:
177        # Stop our on-screen timer so players can see what they got.
178        assert self._timer is not None
179        self._timer.stop()
180
181        results = bs.GameResults()
182
183        # If we won, set our score to the elapsed time in milliseconds.
184        # (there should just be 1 team here since this is co-op).
185        # ..if we didn't win, leave scores as default (None) which means
186        # we lost.
187        if self._won:
188            elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0)
189            bs.cameraflash()
190            self._winsound.play()
191            for team in self.teams:
192                for player in team.players:
193                    if player.actor:
194                        player.actor.handlemessage(bs.CelebrateMessage())
195                results.set_team_score(team, elapsed_time_ms)
196
197        # Ends the activity.
198        self.end(results)
class Player(bascenev1._player.Player[ForwardRef('Team')]):
27class Player(bs.Player['Team']):
28    """Our player type for this game."""

Our player type for this game.

class Team(bascenev1._team.Team[bascenev1lib.game.ninjafight.Player]):
31class Team(bs.Team[Player]):
32    """Our team type for this game."""

Our team type for this game.

class NinjaFightGame(bascenev1._teamgame.TeamGameActivity[bascenev1lib.game.ninjafight.Player, bascenev1lib.game.ninjafight.Team]):
 36class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
 37    """
 38    A co-op game where you try to defeat a group
 39    of Ninjas as fast as possible
 40    """
 41
 42    name = 'Ninja Fight'
 43    description = 'How fast can you defeat the ninjas?'
 44    scoreconfig = bs.ScoreConfig(
 45        label='Time', scoretype=bs.ScoreType.MILLISECONDS, lower_is_better=True
 46    )
 47    default_music = bs.MusicType.TO_THE_DEATH
 48
 49    @override
 50    @classmethod
 51    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 52        # For now we're hard-coding spawn positions and whatnot
 53        # so we need to be sure to specify that we only support
 54        # a specific map.
 55        return ['Courtyard']
 56
 57    @override
 58    @classmethod
 59    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 60        # We currently support Co-Op only.
 61        return issubclass(sessiontype, bs.CoopSession)
 62
 63    # In the constructor we should load any media we need/etc.
 64    # ...but not actually create anything yet.
 65    def __init__(self, settings: dict):
 66        super().__init__(settings)
 67        self._winsound = bs.getsound('score')
 68        self._won = False
 69        self._timer: OnScreenTimer | None = None
 70        self._bots = SpazBotSet()
 71        self._preset = str(settings['preset'])
 72
 73    # Called when our game actually begins.
 74    @override
 75    def on_begin(self) -> None:
 76        super().on_begin()
 77        is_pro = self._preset == 'pro'
 78
 79        # In pro mode there's no powerups.
 80        if not is_pro:
 81            self.setup_standard_powerup_drops()
 82
 83        # Make our on-screen timer and start it roughly when our bots appear.
 84        self._timer = OnScreenTimer()
 85        bs.timer(4.0, self._timer.start)
 86
 87        # Spawn some baddies.
 88        bs.timer(
 89            1.0,
 90            lambda: self._bots.spawn_bot(
 91                ChargerBot, pos=(3, 3, -2), spawn_time=3.0
 92            ),
 93        )
 94        bs.timer(
 95            2.0,
 96            lambda: self._bots.spawn_bot(
 97                ChargerBot, pos=(-3, 3, -2), spawn_time=3.0
 98            ),
 99        )
100        bs.timer(
101            3.0,
102            lambda: self._bots.spawn_bot(
103                ChargerBot, pos=(5, 3, -2), spawn_time=3.0
104            ),
105        )
106        bs.timer(
107            4.0,
108            lambda: self._bots.spawn_bot(
109                ChargerBot, pos=(-5, 3, -2), spawn_time=3.0
110            ),
111        )
112
113        # Add some extras for multiplayer or pro mode.
114        assert self.initialplayerinfos is not None
115        if len(self.initialplayerinfos) > 2 or is_pro:
116            bs.timer(
117                5.0,
118                lambda: self._bots.spawn_bot(
119                    ChargerBot, pos=(0, 3, -5), spawn_time=3.0
120                ),
121            )
122        if len(self.initialplayerinfos) > 3 or is_pro:
123            bs.timer(
124                6.0,
125                lambda: self._bots.spawn_bot(
126                    ChargerBot, pos=(0, 3, 1), spawn_time=3.0
127                ),
128            )
129
130    # Called for each spawning player.
131    @override
132    def spawn_player(self, player: Player) -> bs.Actor:
133        # Let's spawn close to the center.
134        spawn_center = (0, 3, -2)
135        pos = (
136            spawn_center[0] + random.uniform(-1.5, 1.5),
137            spawn_center[1],
138            spawn_center[2] + random.uniform(-1.5, 1.5),
139        )
140        return self.spawn_player_spaz(player, position=pos)
141
142    def _check_if_won(self) -> None:
143        # Simply end the game if there's no living bots.
144        # FIXME: Should also make sure all bots have been spawned;
145        #  if spawning is spread out enough that we're able to kill
146        #  all living bots before the next spawns, it would incorrectly
147        #  count as a win.
148        if not self._bots.have_living_bots():
149            self._won = True
150            self.end_game()
151
152    # Called for miscellaneous messages.
153    @override
154    def handlemessage(self, msg: Any) -> Any:
155        # A player has died.
156        if isinstance(msg, bs.PlayerDiedMessage):
157            super().handlemessage(msg)  # Augment standard behavior.
158            self.respawn_player(msg.getplayer(Player))
159
160        # A spaz-bot has died.
161        elif isinstance(msg, SpazBotDiedMessage):
162            # Unfortunately the bot-set will always tell us there are living
163            # bots if we ask here (the currently-dying bot isn't officially
164            # marked dead yet) ..so lets push a call into the event loop to
165            # check once this guy has finished dying.
166            bs.pushcall(self._check_if_won)
167
168        # Let the base class handle anything we don't.
169        else:
170            return super().handlemessage(msg)
171        return None
172
173    # When this is called, we should fill out results and end the game
174    # *regardless* of whether is has been won. (this may be called due
175    # to a tournament ending or other external reason).
176    @override
177    def end_game(self) -> None:
178        # Stop our on-screen timer so players can see what they got.
179        assert self._timer is not None
180        self._timer.stop()
181
182        results = bs.GameResults()
183
184        # If we won, set our score to the elapsed time in milliseconds.
185        # (there should just be 1 team here since this is co-op).
186        # ..if we didn't win, leave scores as default (None) which means
187        # we lost.
188        if self._won:
189            elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0)
190            bs.cameraflash()
191            self._winsound.play()
192            for team in self.teams:
193                for player in team.players:
194                    if player.actor:
195                        player.actor.handlemessage(bs.CelebrateMessage())
196                results.set_team_score(team, elapsed_time_ms)
197
198        # Ends the activity.
199        self.end(results)

A co-op game where you try to defeat a group of Ninjas as fast as possible

NinjaFightGame(settings: dict)
65    def __init__(self, settings: dict):
66        super().__init__(settings)
67        self._winsound = bs.getsound('score')
68        self._won = False
69        self._timer: OnScreenTimer | None = None
70        self._bots = SpazBotSet()
71        self._preset = str(settings['preset'])

Instantiate the Activity.

name = 'Ninja Fight'
description = 'How fast can you defeat the ninjas?'
scoreconfig = ScoreConfig(label='Time', scoretype=<ScoreType.MILLISECONDS: 'ms'>, lower_is_better=True, none_is_winner=False, version='')
default_music = <MusicType.TO_THE_DEATH: 'ToTheDeath'>
@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1.Session]) -> list[str]:
49    @override
50    @classmethod
51    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
52        # For now we're hard-coding spawn positions and whatnot
53        # so we need to be sure to specify that we only support
54        # a specific map.
55        return ['Courtyard']

Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given bascenev1.Session type.

@override
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1.Session]) -> bool:
57    @override
58    @classmethod
59    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
60        # We currently support Co-Op only.
61        return issubclass(sessiontype, bs.CoopSession)

Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.

@override
def on_begin(self) -> None:
 74    @override
 75    def on_begin(self) -> None:
 76        super().on_begin()
 77        is_pro = self._preset == 'pro'
 78
 79        # In pro mode there's no powerups.
 80        if not is_pro:
 81            self.setup_standard_powerup_drops()
 82
 83        # Make our on-screen timer and start it roughly when our bots appear.
 84        self._timer = OnScreenTimer()
 85        bs.timer(4.0, self._timer.start)
 86
 87        # Spawn some baddies.
 88        bs.timer(
 89            1.0,
 90            lambda: self._bots.spawn_bot(
 91                ChargerBot, pos=(3, 3, -2), spawn_time=3.0
 92            ),
 93        )
 94        bs.timer(
 95            2.0,
 96            lambda: self._bots.spawn_bot(
 97                ChargerBot, pos=(-3, 3, -2), spawn_time=3.0
 98            ),
 99        )
100        bs.timer(
101            3.0,
102            lambda: self._bots.spawn_bot(
103                ChargerBot, pos=(5, 3, -2), spawn_time=3.0
104            ),
105        )
106        bs.timer(
107            4.0,
108            lambda: self._bots.spawn_bot(
109                ChargerBot, pos=(-5, 3, -2), spawn_time=3.0
110            ),
111        )
112
113        # Add some extras for multiplayer or pro mode.
114        assert self.initialplayerinfos is not None
115        if len(self.initialplayerinfos) > 2 or is_pro:
116            bs.timer(
117                5.0,
118                lambda: self._bots.spawn_bot(
119                    ChargerBot, pos=(0, 3, -5), spawn_time=3.0
120                ),
121            )
122        if len(self.initialplayerinfos) > 3 or is_pro:
123            bs.timer(
124                6.0,
125                lambda: self._bots.spawn_bot(
126                    ChargerBot, pos=(0, 3, 1), spawn_time=3.0
127                ),
128            )

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

@override
def spawn_player( self, player: Player) -> bascenev1.Actor:
131    @override
132    def spawn_player(self, player: Player) -> bs.Actor:
133        # Let's spawn close to the center.
134        spawn_center = (0, 3, -2)
135        pos = (
136            spawn_center[0] + random.uniform(-1.5, 1.5),
137            spawn_center[1],
138            spawn_center[2] + random.uniform(-1.5, 1.5),
139        )
140        return self.spawn_player_spaz(player, position=pos)

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

@override
def handlemessage(self, msg: Any) -> Any:
153    @override
154    def handlemessage(self, msg: Any) -> Any:
155        # A player has died.
156        if isinstance(msg, bs.PlayerDiedMessage):
157            super().handlemessage(msg)  # Augment standard behavior.
158            self.respawn_player(msg.getplayer(Player))
159
160        # A spaz-bot has died.
161        elif isinstance(msg, SpazBotDiedMessage):
162            # Unfortunately the bot-set will always tell us there are living
163            # bots if we ask here (the currently-dying bot isn't officially
164            # marked dead yet) ..so lets push a call into the event loop to
165            # check once this guy has finished dying.
166            bs.pushcall(self._check_if_won)
167
168        # Let the base class handle anything we don't.
169        else:
170            return super().handlemessage(msg)
171        return None

General message handling; can be passed any message object.

@override
def end_game(self) -> None:
176    @override
177    def end_game(self) -> None:
178        # Stop our on-screen timer so players can see what they got.
179        assert self._timer is not None
180        self._timer.stop()
181
182        results = bs.GameResults()
183
184        # If we won, set our score to the elapsed time in milliseconds.
185        # (there should just be 1 team here since this is co-op).
186        # ..if we didn't win, leave scores as default (None) which means
187        # we lost.
188        if self._won:
189            elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0)
190            bs.cameraflash()
191            self._winsound.play()
192            for team in self.teams:
193                for player in team.players:
194                    if player.actor:
195                        player.actor.handlemessage(bs.CelebrateMessage())
196                results.set_team_score(team, elapsed_time_ms)
197
198        # Ends the activity.
199        self.end(results)

Tell the game to wrap up and call bascenev1.Activity.end().

This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.