bascenev1lib.game.kingofthehill

Defines the King of the Hill game.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Defines the King of the Hill game."""
  4
  5# ba_meta require api 9
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10import weakref
 11from enum import Enum
 12from typing import TYPE_CHECKING, override
 13
 14import bascenev1 as bs
 15
 16from bascenev1lib.actor.flag import Flag
 17from bascenev1lib.actor.playerspaz import PlayerSpaz
 18from bascenev1lib.actor.scoreboard import Scoreboard
 19from bascenev1lib.gameutils import SharedObjects
 20
 21if TYPE_CHECKING:
 22    from typing import Any, Sequence
 23
 24
 25class FlagState(Enum):
 26    """States our single flag can be in."""
 27
 28    NEW = 0
 29    UNCONTESTED = 1
 30    CONTESTED = 2
 31    HELD = 3
 32
 33
 34class Player(bs.Player['Team']):
 35    """Our player type for this game."""
 36
 37    def __init__(self) -> None:
 38        self.time_at_flag = 0
 39
 40
 41class Team(bs.Team[Player]):
 42    """Our team type for this game."""
 43
 44    def __init__(self, time_remaining: int) -> None:
 45        self.time_remaining = time_remaining
 46
 47
 48# ba_meta export bascenev1.GameActivity
 49class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]):
 50    """Game where a team wins by holding a 'hill' for a set amount of time."""
 51
 52    name = 'King of the Hill'
 53    description = 'Secure the flag for a set length of time.'
 54    available_settings = [
 55        bs.IntSetting(
 56            'Hold Time',
 57            min_value=10,
 58            default=30,
 59            increment=10,
 60        ),
 61        bs.IntChoiceSetting(
 62            'Time Limit',
 63            choices=[
 64                ('None', 0),
 65                ('1 Minute', 60),
 66                ('2 Minutes', 120),
 67                ('5 Minutes', 300),
 68                ('10 Minutes', 600),
 69                ('20 Minutes', 1200),
 70            ],
 71            default=0,
 72        ),
 73        bs.FloatChoiceSetting(
 74            'Respawn Times',
 75            choices=[
 76                ('Shorter', 0.25),
 77                ('Short', 0.5),
 78                ('Normal', 1.0),
 79                ('Long', 2.0),
 80                ('Longer', 4.0),
 81            ],
 82            default=1.0,
 83        ),
 84        bs.BoolSetting('Epic Mode', default=False),
 85    ]
 86    scoreconfig = bs.ScoreConfig(label='Time Held')
 87
 88    @override
 89    @classmethod
 90    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 91        return issubclass(sessiontype, bs.MultiTeamSession)
 92
 93    @override
 94    @classmethod
 95    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 96        assert bs.app.classic is not None
 97        return bs.app.classic.getmaps('king_of_the_hill')
 98
 99    def __init__(self, settings: dict):
100        super().__init__(settings)
101        shared = SharedObjects.get()
102        self._scoreboard = Scoreboard()
103        self._swipsound = bs.getsound('swip')
104        self._tick_sound = bs.getsound('tick')
105        self._countdownsounds = {
106            10: bs.getsound('announceTen'),
107            9: bs.getsound('announceNine'),
108            8: bs.getsound('announceEight'),
109            7: bs.getsound('announceSeven'),
110            6: bs.getsound('announceSix'),
111            5: bs.getsound('announceFive'),
112            4: bs.getsound('announceFour'),
113            3: bs.getsound('announceThree'),
114            2: bs.getsound('announceTwo'),
115            1: bs.getsound('announceOne'),
116        }
117        self._flag_pos: Sequence[float] | None = None
118        self._flag_state: FlagState | None = None
119        self._flag: Flag | None = None
120        self._flag_light: bs.Node | None = None
121        self._scoring_team: weakref.ref[Team] | None = None
122        self._hold_time = int(settings['Hold Time'])
123        self._time_limit = float(settings['Time Limit'])
124        self._epic_mode = bool(settings['Epic Mode'])
125        self._flag_region_material = bs.Material()
126        self._flag_region_material.add_actions(
127            conditions=('they_have_material', shared.player_material),
128            actions=(
129                ('modify_part_collision', 'collide', True),
130                ('modify_part_collision', 'physical', False),
131                (
132                    'call',
133                    'at_connect',
134                    bs.Call(self._handle_player_flag_region_collide, True),
135                ),
136                (
137                    'call',
138                    'at_disconnect',
139                    bs.Call(self._handle_player_flag_region_collide, False),
140                ),
141            ),
142        )
143
144        # Base class overrides.
145        self.slow_motion = self._epic_mode
146        self.default_music = (
147            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SCARY
148        )
149
150    @override
151    def get_instance_description(self) -> str | Sequence:
152        return 'Secure the flag for ${ARG1} seconds.', self._hold_time
153
154    @override
155    def get_instance_description_short(self) -> str | Sequence:
156        return 'secure the flag for ${ARG1} seconds', self._hold_time
157
158    @override
159    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
160        return Team(time_remaining=self._hold_time)
161
162    @override
163    def on_begin(self) -> None:
164        super().on_begin()
165        shared = SharedObjects.get()
166        self.setup_standard_time_limit(self._time_limit)
167        self.setup_standard_powerup_drops()
168        self._flag_pos = self.map.get_flag_position(None)
169        bs.timer(1.0, self._tick, repeat=True)
170        self._flag_state = FlagState.NEW
171        Flag.project_stand(self._flag_pos)
172        self._flag = Flag(
173            position=self._flag_pos, touchable=False, color=(1, 1, 1)
174        )
175        self._flag_light = bs.newnode(
176            'light',
177            attrs={
178                'position': self._flag_pos,
179                'intensity': 0.2,
180                'height_attenuated': False,
181                'radius': 0.4,
182                'color': (0.2, 0.2, 0.2),
183            },
184        )
185        # Flag region.
186        flagmats = [self._flag_region_material, shared.region_material]
187        bs.newnode(
188            'region',
189            attrs={
190                'position': self._flag_pos,
191                'scale': (1.8, 1.8, 1.8),
192                'type': 'sphere',
193                'materials': flagmats,
194            },
195        )
196        self._update_scoreboard()
197        self._update_flag_state()
198
199    def _tick(self) -> None:
200        self._update_flag_state()
201
202        # Give holding players points.
203        for player in self.players:
204            if player.time_at_flag > 0:
205                self.stats.player_scored(
206                    player, 3, screenmessage=False, display=False
207                )
208        if self._scoring_team is None:
209            scoring_team = None
210        else:
211            scoring_team = self._scoring_team()
212        if scoring_team:
213            if scoring_team.time_remaining > 0:
214                self._tick_sound.play()
215
216            scoring_team.time_remaining = max(
217                0, scoring_team.time_remaining - 1
218            )
219            self._update_scoreboard()
220            if scoring_team.time_remaining > 0:
221                assert self._flag is not None
222                self._flag.set_score_text(str(scoring_team.time_remaining))
223
224            # Announce numbers we have sounds for.
225            numsound = self._countdownsounds.get(scoring_team.time_remaining)
226            if numsound is not None:
227                numsound.play()
228
229            # winner
230            if scoring_team.time_remaining <= 0:
231                self.end_game()
232
233    @override
234    def end_game(self) -> None:
235        results = bs.GameResults()
236        for team in self.teams:
237            results.set_team_score(team, self._hold_time - team.time_remaining)
238        self.end(results=results, announce_delay=0)
239
240    def _update_flag_state(self) -> None:
241        holding_teams = set(
242            player.team for player in self.players if player.time_at_flag
243        )
244        prev_state = self._flag_state
245        assert self._flag_light
246        assert self._flag is not None
247        assert self._flag.node
248        if len(holding_teams) > 1:
249            self._flag_state = FlagState.CONTESTED
250            self._scoring_team = None
251            self._flag_light.color = (0.6, 0.6, 0.1)
252            self._flag.node.color = (1.0, 1.0, 0.4)
253        elif len(holding_teams) == 1:
254            holding_team = list(holding_teams)[0]
255            self._flag_state = FlagState.HELD
256            self._scoring_team = weakref.ref(holding_team)
257            self._flag_light.color = bs.normalized_color(holding_team.color)
258            self._flag.node.color = holding_team.color
259        else:
260            self._flag_state = FlagState.UNCONTESTED
261            self._scoring_team = None
262            self._flag_light.color = (0.2, 0.2, 0.2)
263            self._flag.node.color = (1, 1, 1)
264        if self._flag_state != prev_state:
265            self._swipsound.play()
266
267    def _handle_player_flag_region_collide(self, colliding: bool) -> None:
268        try:
269            spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
270        except bs.NotFoundError:
271            return
272
273        if not spaz.is_alive():
274            return
275
276        player = spaz.getplayer(Player, True)
277
278        # Different parts of us can collide so a single value isn't enough
279        # also don't count it if we're dead (flying heads shouldn't be able to
280        # win the game :-)
281        if colliding and player.is_alive():
282            player.time_at_flag += 1
283        else:
284            player.time_at_flag = max(0, player.time_at_flag - 1)
285
286        self._update_flag_state()
287
288    def _update_scoreboard(self) -> None:
289        for team in self.teams:
290            self._scoreboard.set_team_value(
291                team, team.time_remaining, self._hold_time, countdown=True
292            )
293
294    @override
295    def handlemessage(self, msg: Any) -> Any:
296        if isinstance(msg, bs.PlayerDiedMessage):
297            super().handlemessage(msg)  # Augment default.
298
299            # No longer can count as time_at_flag once dead.
300            player = msg.getplayer(Player)
301            player.time_at_flag = 0
302            self._update_flag_state()
303            self.respawn_player(player)
class FlagState(enum.Enum):
26class FlagState(Enum):
27    """States our single flag can be in."""
28
29    NEW = 0
30    UNCONTESTED = 1
31    CONTESTED = 2
32    HELD = 3

States our single flag can be in.

NEW = <FlagState.NEW: 0>
UNCONTESTED = <FlagState.UNCONTESTED: 1>
CONTESTED = <FlagState.CONTESTED: 2>
HELD = <FlagState.HELD: 3>
class Player(bascenev1._player.Player[ForwardRef('Team')]):
35class Player(bs.Player['Team']):
36    """Our player type for this game."""
37
38    def __init__(self) -> None:
39        self.time_at_flag = 0

Our player type for this game.

time_at_flag
class Team(bascenev1._team.Team[bascenev1lib.game.kingofthehill.Player]):
42class Team(bs.Team[Player]):
43    """Our team type for this game."""
44
45    def __init__(self, time_remaining: int) -> None:
46        self.time_remaining = time_remaining

Our team type for this game.

Team(time_remaining: int)
45    def __init__(self, time_remaining: int) -> None:
46        self.time_remaining = time_remaining
time_remaining
class KingOfTheHillGame(bascenev1._teamgame.TeamGameActivity[bascenev1lib.game.kingofthehill.Player, bascenev1lib.game.kingofthehill.Team]):
 50class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]):
 51    """Game where a team wins by holding a 'hill' for a set amount of time."""
 52
 53    name = 'King of the Hill'
 54    description = 'Secure the flag for a set length of time.'
 55    available_settings = [
 56        bs.IntSetting(
 57            'Hold Time',
 58            min_value=10,
 59            default=30,
 60            increment=10,
 61        ),
 62        bs.IntChoiceSetting(
 63            'Time Limit',
 64            choices=[
 65                ('None', 0),
 66                ('1 Minute', 60),
 67                ('2 Minutes', 120),
 68                ('5 Minutes', 300),
 69                ('10 Minutes', 600),
 70                ('20 Minutes', 1200),
 71            ],
 72            default=0,
 73        ),
 74        bs.FloatChoiceSetting(
 75            'Respawn Times',
 76            choices=[
 77                ('Shorter', 0.25),
 78                ('Short', 0.5),
 79                ('Normal', 1.0),
 80                ('Long', 2.0),
 81                ('Longer', 4.0),
 82            ],
 83            default=1.0,
 84        ),
 85        bs.BoolSetting('Epic Mode', default=False),
 86    ]
 87    scoreconfig = bs.ScoreConfig(label='Time Held')
 88
 89    @override
 90    @classmethod
 91    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 92        return issubclass(sessiontype, bs.MultiTeamSession)
 93
 94    @override
 95    @classmethod
 96    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 97        assert bs.app.classic is not None
 98        return bs.app.classic.getmaps('king_of_the_hill')
 99
100    def __init__(self, settings: dict):
101        super().__init__(settings)
102        shared = SharedObjects.get()
103        self._scoreboard = Scoreboard()
104        self._swipsound = bs.getsound('swip')
105        self._tick_sound = bs.getsound('tick')
106        self._countdownsounds = {
107            10: bs.getsound('announceTen'),
108            9: bs.getsound('announceNine'),
109            8: bs.getsound('announceEight'),
110            7: bs.getsound('announceSeven'),
111            6: bs.getsound('announceSix'),
112            5: bs.getsound('announceFive'),
113            4: bs.getsound('announceFour'),
114            3: bs.getsound('announceThree'),
115            2: bs.getsound('announceTwo'),
116            1: bs.getsound('announceOne'),
117        }
118        self._flag_pos: Sequence[float] | None = None
119        self._flag_state: FlagState | None = None
120        self._flag: Flag | None = None
121        self._flag_light: bs.Node | None = None
122        self._scoring_team: weakref.ref[Team] | None = None
123        self._hold_time = int(settings['Hold Time'])
124        self._time_limit = float(settings['Time Limit'])
125        self._epic_mode = bool(settings['Epic Mode'])
126        self._flag_region_material = bs.Material()
127        self._flag_region_material.add_actions(
128            conditions=('they_have_material', shared.player_material),
129            actions=(
130                ('modify_part_collision', 'collide', True),
131                ('modify_part_collision', 'physical', False),
132                (
133                    'call',
134                    'at_connect',
135                    bs.Call(self._handle_player_flag_region_collide, True),
136                ),
137                (
138                    'call',
139                    'at_disconnect',
140                    bs.Call(self._handle_player_flag_region_collide, False),
141                ),
142            ),
143        )
144
145        # Base class overrides.
146        self.slow_motion = self._epic_mode
147        self.default_music = (
148            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SCARY
149        )
150
151    @override
152    def get_instance_description(self) -> str | Sequence:
153        return 'Secure the flag for ${ARG1} seconds.', self._hold_time
154
155    @override
156    def get_instance_description_short(self) -> str | Sequence:
157        return 'secure the flag for ${ARG1} seconds', self._hold_time
158
159    @override
160    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
161        return Team(time_remaining=self._hold_time)
162
163    @override
164    def on_begin(self) -> None:
165        super().on_begin()
166        shared = SharedObjects.get()
167        self.setup_standard_time_limit(self._time_limit)
168        self.setup_standard_powerup_drops()
169        self._flag_pos = self.map.get_flag_position(None)
170        bs.timer(1.0, self._tick, repeat=True)
171        self._flag_state = FlagState.NEW
172        Flag.project_stand(self._flag_pos)
173        self._flag = Flag(
174            position=self._flag_pos, touchable=False, color=(1, 1, 1)
175        )
176        self._flag_light = bs.newnode(
177            'light',
178            attrs={
179                'position': self._flag_pos,
180                'intensity': 0.2,
181                'height_attenuated': False,
182                'radius': 0.4,
183                'color': (0.2, 0.2, 0.2),
184            },
185        )
186        # Flag region.
187        flagmats = [self._flag_region_material, shared.region_material]
188        bs.newnode(
189            'region',
190            attrs={
191                'position': self._flag_pos,
192                'scale': (1.8, 1.8, 1.8),
193                'type': 'sphere',
194                'materials': flagmats,
195            },
196        )
197        self._update_scoreboard()
198        self._update_flag_state()
199
200    def _tick(self) -> None:
201        self._update_flag_state()
202
203        # Give holding players points.
204        for player in self.players:
205            if player.time_at_flag > 0:
206                self.stats.player_scored(
207                    player, 3, screenmessage=False, display=False
208                )
209        if self._scoring_team is None:
210            scoring_team = None
211        else:
212            scoring_team = self._scoring_team()
213        if scoring_team:
214            if scoring_team.time_remaining > 0:
215                self._tick_sound.play()
216
217            scoring_team.time_remaining = max(
218                0, scoring_team.time_remaining - 1
219            )
220            self._update_scoreboard()
221            if scoring_team.time_remaining > 0:
222                assert self._flag is not None
223                self._flag.set_score_text(str(scoring_team.time_remaining))
224
225            # Announce numbers we have sounds for.
226            numsound = self._countdownsounds.get(scoring_team.time_remaining)
227            if numsound is not None:
228                numsound.play()
229
230            # winner
231            if scoring_team.time_remaining <= 0:
232                self.end_game()
233
234    @override
235    def end_game(self) -> None:
236        results = bs.GameResults()
237        for team in self.teams:
238            results.set_team_score(team, self._hold_time - team.time_remaining)
239        self.end(results=results, announce_delay=0)
240
241    def _update_flag_state(self) -> None:
242        holding_teams = set(
243            player.team for player in self.players if player.time_at_flag
244        )
245        prev_state = self._flag_state
246        assert self._flag_light
247        assert self._flag is not None
248        assert self._flag.node
249        if len(holding_teams) > 1:
250            self._flag_state = FlagState.CONTESTED
251            self._scoring_team = None
252            self._flag_light.color = (0.6, 0.6, 0.1)
253            self._flag.node.color = (1.0, 1.0, 0.4)
254        elif len(holding_teams) == 1:
255            holding_team = list(holding_teams)[0]
256            self._flag_state = FlagState.HELD
257            self._scoring_team = weakref.ref(holding_team)
258            self._flag_light.color = bs.normalized_color(holding_team.color)
259            self._flag.node.color = holding_team.color
260        else:
261            self._flag_state = FlagState.UNCONTESTED
262            self._scoring_team = None
263            self._flag_light.color = (0.2, 0.2, 0.2)
264            self._flag.node.color = (1, 1, 1)
265        if self._flag_state != prev_state:
266            self._swipsound.play()
267
268    def _handle_player_flag_region_collide(self, colliding: bool) -> None:
269        try:
270            spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
271        except bs.NotFoundError:
272            return
273
274        if not spaz.is_alive():
275            return
276
277        player = spaz.getplayer(Player, True)
278
279        # Different parts of us can collide so a single value isn't enough
280        # also don't count it if we're dead (flying heads shouldn't be able to
281        # win the game :-)
282        if colliding and player.is_alive():
283            player.time_at_flag += 1
284        else:
285            player.time_at_flag = max(0, player.time_at_flag - 1)
286
287        self._update_flag_state()
288
289    def _update_scoreboard(self) -> None:
290        for team in self.teams:
291            self._scoreboard.set_team_value(
292                team, team.time_remaining, self._hold_time, countdown=True
293            )
294
295    @override
296    def handlemessage(self, msg: Any) -> Any:
297        if isinstance(msg, bs.PlayerDiedMessage):
298            super().handlemessage(msg)  # Augment default.
299
300            # No longer can count as time_at_flag once dead.
301            player = msg.getplayer(Player)
302            player.time_at_flag = 0
303            self._update_flag_state()
304            self.respawn_player(player)

Game where a team wins by holding a 'hill' for a set amount of time.

KingOfTheHillGame(settings: dict)
100    def __init__(self, settings: dict):
101        super().__init__(settings)
102        shared = SharedObjects.get()
103        self._scoreboard = Scoreboard()
104        self._swipsound = bs.getsound('swip')
105        self._tick_sound = bs.getsound('tick')
106        self._countdownsounds = {
107            10: bs.getsound('announceTen'),
108            9: bs.getsound('announceNine'),
109            8: bs.getsound('announceEight'),
110            7: bs.getsound('announceSeven'),
111            6: bs.getsound('announceSix'),
112            5: bs.getsound('announceFive'),
113            4: bs.getsound('announceFour'),
114            3: bs.getsound('announceThree'),
115            2: bs.getsound('announceTwo'),
116            1: bs.getsound('announceOne'),
117        }
118        self._flag_pos: Sequence[float] | None = None
119        self._flag_state: FlagState | None = None
120        self._flag: Flag | None = None
121        self._flag_light: bs.Node | None = None
122        self._scoring_team: weakref.ref[Team] | None = None
123        self._hold_time = int(settings['Hold Time'])
124        self._time_limit = float(settings['Time Limit'])
125        self._epic_mode = bool(settings['Epic Mode'])
126        self._flag_region_material = bs.Material()
127        self._flag_region_material.add_actions(
128            conditions=('they_have_material', shared.player_material),
129            actions=(
130                ('modify_part_collision', 'collide', True),
131                ('modify_part_collision', 'physical', False),
132                (
133                    'call',
134                    'at_connect',
135                    bs.Call(self._handle_player_flag_region_collide, True),
136                ),
137                (
138                    'call',
139                    'at_disconnect',
140                    bs.Call(self._handle_player_flag_region_collide, False),
141                ),
142            ),
143        )
144
145        # Base class overrides.
146        self.slow_motion = self._epic_mode
147        self.default_music = (
148            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SCARY
149        )

Instantiate the Activity.

name = 'King of the Hill'
description = 'Secure the flag for a set length of time.'
available_settings = [IntSetting(name='Hold Time', default=30, min_value=10, max_value=9999, increment=10), IntChoiceSetting(name='Time Limit', default=0, choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)]), FloatChoiceSetting(name='Respawn Times', default=1.0, choices=[('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)]), BoolSetting(name='Epic Mode', default=False)]
scoreconfig = ScoreConfig(label='Time Held', scoretype=<ScoreType.POINTS: 'p'>, lower_is_better=False, none_is_winner=False, version='')
@override
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1.Session]) -> bool:
89    @override
90    @classmethod
91    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
92        return issubclass(sessiontype, bs.MultiTeamSession)

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

@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1.Session]) -> list[str]:
94    @override
95    @classmethod
96    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
97        assert bs.app.classic is not None
98        return bs.app.classic.getmaps('king_of_the_hill')

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.

slow_motion = False

If True, runs in slow motion and turns down sound pitch.

default_music = None
@override
def get_instance_description(self) -> Union[str, Sequence]:
151    @override
152    def get_instance_description(self) -> str | Sequence:
153        return 'Secure the flag for ${ARG1} seconds.', self._hold_time

Return a description for this game instance, in English.

This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'Score 3 goals.' in English

and can properly translate to 'Anota 3 goles.' in Spanish.

If we just returned the string 'Score 3 Goals' here, there would

have to be a translation entry for each specific number. ew.

return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

@override
def get_instance_description_short(self) -> Union[str, Sequence]:
155    @override
156    def get_instance_description_short(self) -> str | Sequence:
157        return 'secure the flag for ${ARG1} seconds', self._hold_time

Return a short description for this game instance in English.

This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'score 3 goals' in English

and can properly translate to 'anota 3 goles' in Spanish.

If we just returned the string 'score 3 goals' here, there would

have to be a translation entry for each specific number. ew.

return ['score ${ARG1} goals', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

@override
def create_team( self, sessionteam: bascenev1.SessionTeam) -> Team:
159    @override
160    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
161        return Team(time_remaining=self._hold_time)

Create the Team instance for this Activity.

Subclasses can override this if the activity's team class requires a custom constructor; otherwise it will be called with no args. Note that the team object should not be used at this point as it is not yet fully wired up; wait for on_team_join() for that.

@override
def on_begin(self) -> None:
163    @override
164    def on_begin(self) -> None:
165        super().on_begin()
166        shared = SharedObjects.get()
167        self.setup_standard_time_limit(self._time_limit)
168        self.setup_standard_powerup_drops()
169        self._flag_pos = self.map.get_flag_position(None)
170        bs.timer(1.0, self._tick, repeat=True)
171        self._flag_state = FlagState.NEW
172        Flag.project_stand(self._flag_pos)
173        self._flag = Flag(
174            position=self._flag_pos, touchable=False, color=(1, 1, 1)
175        )
176        self._flag_light = bs.newnode(
177            'light',
178            attrs={
179                'position': self._flag_pos,
180                'intensity': 0.2,
181                'height_attenuated': False,
182                'radius': 0.4,
183                'color': (0.2, 0.2, 0.2),
184            },
185        )
186        # Flag region.
187        flagmats = [self._flag_region_material, shared.region_material]
188        bs.newnode(
189            'region',
190            attrs={
191                'position': self._flag_pos,
192                'scale': (1.8, 1.8, 1.8),
193                'type': 'sphere',
194                'materials': flagmats,
195            },
196        )
197        self._update_scoreboard()
198        self._update_flag_state()

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 end_game(self) -> None:
234    @override
235    def end_game(self) -> None:
236        results = bs.GameResults()
237        for team in self.teams:
238            results.set_team_score(team, self._hold_time - team.time_remaining)
239        self.end(results=results, announce_delay=0)

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.

@override
def handlemessage(self, msg: Any) -> Any:
295    @override
296    def handlemessage(self, msg: Any) -> Any:
297        if isinstance(msg, bs.PlayerDiedMessage):
298            super().handlemessage(msg)  # Augment default.
299
300            # No longer can count as time_at_flag once dead.
301            player = msg.getplayer(Player)
302            player.time_at_flag = 0
303            self._update_flag_state()
304            self.respawn_player(player)

General message handling; can be passed any message object.