pacai.capture.gamestate

  1import random
  2import typing
  3
  4import PIL.Image
  5
  6import pacai.core.action
  7import pacai.core.font
  8import pacai.core.gamestate
  9import pacai.core.spritesheet
 10import pacai.pacman.board
 11import pacai.pacman.gamestate
 12
 13TEAM_MODIFIERS: tuple[int, int] = (-1, 1)
 14""" The different modifiers for the two teams: evens/west and odds/east. """
 15
 16FOOD_POINTS: int = 10
 17""" Points for eating food. """
 18
 19CAPSULE_POINTS: int = 0
 20""" Points for eating a power capsule. """
 21
 22KILL_GHOST_POINTS: int = 0
 23""" Points for eating a scared ghost. """
 24
 25KILL_PACMAN_POINTS: int = 0
 26""" Points for eating a Pac-Man. """
 27
 28PREFIX_MARKERS: set[pacai.core.board.Marker] = {
 29    pacai.core.board.MARKER_WALL,
 30    pacai.pacman.board.MARKER_PELLET,
 31    pacai.pacman.board.MARKER_CAPSULE,
 32    pacai.pacman.board.MARKER_SCARED_GHOST,
 33}
 34""" These markers get prefixed with -/+ when searching for sprites. """
 35
 36GHOST_MARKER_PREFIX: str = 'G'
 37""" Prefix (non-scared) ghost markers with this. """
 38
 39SCORE_COLOR_ZERO: tuple[int, int, int] = (255, 255, 255)
 40SCORE_COLOR_NEGATIVE: tuple[int, int, int] = (229, 0, 0)
 41SCORE_COLOR_POSITIVE: tuple[int, int, int] = (0, 66, 255)
 42
 43class GameState(pacai.pacman.gamestate.GameState):
 44    """
 45    A game state specific to a game of Capture.
 46    Derived from the Pac-Man game state.
 47
 48    A common concept in this game is that the board is divided in half between two teams.
 49    The even team (with even agent indexes) start in the west and uses a -1 modifier.
 50    The odd team (with odd agent indexes) start in the east and uses a +1 modifier.
 51    """
 52
 53    def _team_modifier(self, agent_index: int = -1) -> int:
 54        """ Return -1 or +1 depending on if the even or odd team (respectively) is currently active. """
 55
 56        if (agent_index == -1):
 57            agent_index = self.agent_index
 58
 59        return ((agent_index % 2) * 2) - 1
 60
 61    def _team_side(self, agent_index: int = -1, position: pacai.core.board.Position | None = None) -> int:
 62        """
 63        Return -1 or +1 depending on which side of the board this position is on.
 64        If no position is provided, the agent's position will be used.
 65        If no agent index is provided, the current agent index will be used.
 66        """
 67
 68        if (position is None):
 69            if (agent_index == -1):
 70                agent_index = self.agent_index
 71
 72            position = self.get_agent_position(agent_index = agent_index)
 73
 74        if (position is None):
 75            raise ValueError("Could not find position.")
 76
 77        if (position.col < (self.board.width / 2)):
 78            return -1
 79
 80        return 1
 81
 82    def _team_agent_indexes(self, team_modifier: int) -> list[int]:
 83        """ Get the agent indexes for the current team. """
 84
 85        return [agent_index for agent_index in self.get_agent_indexes() if (self._team_modifier(agent_index) == team_modifier)]
 86
 87    def get_normalized_score(self, agent_index: int = -1) -> float:
 88        """
 89        Get the score of this state normalized according to the team asking for it.
 90        Higher numbers are always "better".
 91        """
 92
 93        return self._team_modifier(agent_index) * self.score
 94
 95    def is_ghost(self, agent_index: int = -1) -> bool:
 96        """ Check if this agent is currently in "ghost mode", i.e., on their own side of the board. """
 97
 98        if (agent_index == -1):
 99            agent_index = self.agent_index
100
101        position = self.get_agent_position(agent_index = agent_index)
102
103        if (position is None):
104            return False
105
106        return (self._team_side(agent_index = agent_index) == self._team_modifier(agent_index = agent_index))
107
108    def is_pacman(self, agent_index: int = -1) -> bool:
109        """ Check if this agent is currently in "Pac-Man mode", i.e., on the opponent's side of the board. """
110
111        if (agent_index == -1):
112            agent_index = self.agent_index
113
114        position = self.get_agent_position(agent_index = agent_index)
115
116        if (position is None):
117            return False
118
119        return (self._team_side(agent_index = agent_index) != self._team_modifier(agent_index = agent_index))
120
121    def is_scared(self, agent_index: int = -1) -> bool:
122        # Only ghosts can be scared.
123        if (not self.is_ghost(agent_index)):
124            return False
125
126        return super().is_scared(agent_index = agent_index)
127
128    def process_agent_timeout(self, agent_index: int) -> None:
129        # Treat timeouts like crashes.
130        self.process_agent_crash(agent_index)
131
132    def process_agent_crash(self, agent_index: int) -> None:
133        super().process_agent_crash(agent_index)
134
135        # Set the score in favor of the non-crashing team.
136        self.score = -1 * self._team_modifier(agent_index) * pacai.pacman.gamestate.CRASH_POINTS
137
138    def get_legal_actions(self, position: pacai.core.board.Position | None = None) -> list[pacai.core.action.Action]:
139        if (position is None):
140            position = self.get_agent_position(self.agent_index)
141
142        # Dead agents (agents not on the board) can only stop (when they will respawn).
143        if (position is None):
144            return [pacai.core.action.STOP]
145
146        # Call directly into pacai.core.gamestate.GameState because Pac-Man has special actions for ghosts.
147        return pacai.core.gamestate.GameState.get_legal_actions(self, position)
148
149    def food_count(self, team_modifier: int = 0, agent_index: int = -1) -> int:
150        """
151        Get the count of all food currently on the board for the specified team.
152        If no team is specified, use the given agent's team.
153        If no agent is specified, use the current agent.
154        """
155
156        return len(self.get_food(team_modifier = team_modifier, agent_index = agent_index))
157
158    def get_food(self, team_modifier: int = 0, agent_index: int = -1) -> set[pacai.core.board.Position]:
159        """
160        Get the positions of all food currently on the board for the specific team.
161        This refers to food that the given team can eat (which lives on the opponents side).
162        If no team is specified, use the given agent's team.
163        If no agent is specified, use the current agent.
164        """
165
166        if (team_modifier == 0):
167            if (agent_index == -1):
168                agent_index = self.agent_index
169
170            team_modifier = self._team_modifier(agent_index)
171
172        team_food = set()
173        for position in self.board.get_marker_positions(pacai.pacman.board.MARKER_PELLET):
174            # If the food does not belong to this team, they can eat it.
175            if (self._team_side(position = position) != team_modifier):
176                team_food.add(position)
177
178        return team_food
179
180    def get_team_positions(self, team_modifier: int) -> dict[int, pacai.core.board.Position]:
181        """ Get the position of all agents on the board belonging to the given team. """
182
183        agents = {}
184
185        for (agent_index, agent_position) in self.get_agent_positions().items():
186            if (team_modifier != self._team_modifier(agent_index)):
187                continue
188
189            if (agent_position is None):
190                continue
191
192            agents[agent_index] = agent_position
193
194        return agents
195
196    def get_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
197        """ Get the position of all allies currently on the board. """
198
199        if (agent_index == -1):
200            agent_index = self.agent_index
201
202        team_modifier = self._team_modifier(agent_index)
203
204        agents = self.get_team_positions(team_modifier)
205
206        # Remove self.
207        agents.pop(agent_index, None)
208
209        return agents
210
211    def get_scared_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
212        """ Get the position of all scared allies currently on the board. """
213
214        allies = self.get_ally_positions(agent_index = agent_index)
215        return {index: position for (index, position) in allies.items() if self.is_scared(index)}
216
217    def get_nonscared_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
218        """ Get the position of all non-scared allies currently on the board. """
219
220        allies = self.get_ally_positions(agent_index = agent_index)
221        return {index: position for (index, position) in allies.items() if not self.is_scared(index)}
222
223    def get_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
224        """ Get the position of all opponents currently on the board. """
225
226        if (agent_index == -1):
227            agent_index = self.agent_index
228
229        team_modifier = self._team_modifier(agent_index)
230
231        return self.get_team_positions(-team_modifier)
232
233    def get_scared_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
234        """ Get the position of all scared opponents currently on the board. """
235
236        opponents = self.get_opponent_positions(agent_index = agent_index)
237        return {index: position for (index, position) in opponents.items() if self.is_scared(index)}
238
239    def get_nonscared_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
240        """ Get the position of all non-scared opponents currently on the board. """
241
242        opponents = self.get_opponent_positions(agent_index = agent_index)
243        return {index: position for (index, position) in opponents.items() if not self.is_scared(index)}
244
245    def get_invader_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
246        """ Get the position of all invading opponents (opponents on your side of the board). """
247
248        opponents = self.get_opponent_positions(agent_index = agent_index)
249        return {index: position for (index, position) in opponents.items() if self.is_pacman(index)}
250
251    def game_complete(self) -> list[int]:
252        # A side wins if there is no food left for them to eat.
253        for team_modifier in TEAM_MODIFIERS:
254            if (self.food_count(team_modifier = team_modifier) == 0):
255                return self._team_agent_indexes(team_modifier)
256
257        return []
258
259    def sprite_lookup(self,
260            sprite_sheet: pacai.core.spritesheet.SpriteSheet,
261            position: pacai.core.board.Position,
262            marker: pacai.core.board.Marker | None = None,
263            action: pacai.core.action.Action | None = None,
264            adjacency: pacai.core.board.AdjacencyString | None = None,
265            animation_key: str | None = None,
266            ) -> PIL.Image.Image:
267        # If the agent in on their own side, they should be a ghost (maybe a scared one).
268        if ((marker is not None) and (marker.is_agent()) and (self.is_ghost(marker.get_agent_index()))):
269            if (self.is_scared(marker.get_agent_index())):
270                marker = pacai.pacman.board.MARKER_SCARED_GHOST
271            else:
272                marker = pacai.core.board.Marker(GHOST_MARKER_PREFIX + str(marker))
273
274        # Prefix specific markers with a -/+ depending on the side it is on.
275        if (marker in PREFIX_MARKERS):
276            prefix = '-'
277            if (self._team_side(position = position) > 0):
278                prefix = '+'
279
280            marker = pacai.core.board.Marker(prefix + str(marker))
281
282        return pacai.core.gamestate.GameState.sprite_lookup(self,
283                sprite_sheet, position,
284                marker = marker, action = action, adjacency = adjacency, animation_key = animation_key)
285
286    def get_footer_text(self) -> pacai.core.font.Text | None:
287        text = super().get_footer_text()
288
289        # Color the text based on who's winning.
290        if (text is not None):
291            if (self.score < 0.0):
292                text.color = SCORE_COLOR_NEGATIVE
293            elif (self.score > 0.0):
294                text.color = SCORE_COLOR_POSITIVE
295            else:
296                text.color = SCORE_COLOR_ZERO
297
298        return text
299
300    def process_turn(self,  # pylint: disable=too-many-statements
301            action: pacai.core.action.Action,
302            rng: random.Random | None = None,
303            **kwargs: typing.Any) -> None:
304        agent_marker = pacai.core.board.Marker(str(self.agent_index))
305        team_modifier = self._team_modifier()
306
307        # Compute the agent's new position.
308        old_position = self.get_agent_position()
309        if (old_position is None):
310            # The agent is currently dead and needs to respawn.
311            new_position = self.board.get_agent_initial_position(self.agent_index)
312            if (new_position is None):
313                raise ValueError(f"Cannot find initial position for agent {self.agent_index}.")
314
315            self.board.place_marker(agent_marker, new_position)
316        else:
317            new_position = old_position.apply_action(action)
318
319        # Get all the markers that we are moving into.
320        interaction_markers = set()
321        if (old_position != new_position):
322            interaction_markers = self.board.get(new_position)
323
324            # Since we are moving, pickup the agent from their current location and move them to their new location.
325            if (old_position is not None):
326                self.board.remove_marker(agent_marker, old_position)
327                self.board.place_marker(agent_marker, new_position)
328
329        died = False
330
331        # Process actions for all the markers we are moving onto.
332        for interaction_marker in interaction_markers:
333            if (interaction_marker == pacai.pacman.board.MARKER_PELLET):
334                # Ignore our own food.
335                if (team_modifier == self._team_side(position = new_position)):
336                    continue
337
338                # Eat a food pellet.
339                self.board.remove_marker(interaction_marker, new_position)
340                self.score += team_modifier * FOOD_POINTS
341
342                if (self.food_count(team_modifier = team_modifier) == 0):
343                    self.game_over = True
344            elif (interaction_marker == pacai.pacman.board.MARKER_CAPSULE):
345                # Ignore our own capsules.
346                if (team_modifier == self._team_side(position = new_position)):
347                    continue
348
349                # Eat a power capsule, scare all enemy ghosts.
350                self.board.remove_marker(interaction_marker, new_position)
351                self.score += team_modifier * CAPSULE_POINTS
352
353                # Scare all enemies.
354                for agent_index in self._team_agent_indexes(-team_modifier):
355                    self.scared_timers[agent_index] = pacai.pacman.gamestate.SCARED_TIME
356            elif (interaction_marker.is_agent()):
357                other_agent_index = interaction_marker.get_agent_index()
358                other_team_modifier = self._team_modifier(other_agent_index)
359
360                if (team_modifier == other_team_modifier):
361                    # This is an agent on our team, nothing to do.
362                    continue
363
364                # Check if anyone is scared.
365                self_scared = self.is_scared(self.agent_index)
366                other_scared = self.is_scared(other_agent_index)
367
368                # Check who is a ghost (agent's on their own side are a ghost).
369                self_ghost = self.is_ghost(self.agent_index)
370                other_ghost = self.is_ghost(other_agent_index)
371
372                # Check who was eaten (and remove them), what team did the eating, and the points that should be awarded.
373                eating_team_modifier = 0
374                points = 0
375
376                if ((self_scared and self_ghost) or ((not other_scared) and other_ghost)):
377                    # We got eaten, but our marker is already off the board.
378                    died = True
379                    self._kill_agent(self.agent_index)
380
381                    eating_team_modifier = other_team_modifier
382
383                    if (self_scared):
384                        points = KILL_GHOST_POINTS
385                    else:
386                        points = KILL_PACMAN_POINTS
387                else:
388                    # If we ate the opponent, remove them from the board.
389                    self.board.remove_marker(interaction_marker, new_position)
390                    self._kill_agent(other_agent_index)
391
392                    eating_team_modifier = team_modifier
393
394                    if (other_scared):
395                        points = KILL_GHOST_POINTS
396                    else:
397                        points = KILL_PACMAN_POINTS
398
399                self.score += (eating_team_modifier * points)
400
401        # The current agent has died, remove their marker.
402        if (died):
403            self.board.remove_marker(agent_marker, new_position)
404
405        # Decrement the scared timer.
406        if (self.agent_index in self.scared_timers):
407            self.scared_timers[self.agent_index] -= 1
408
409            if (self.scared_timers[self.agent_index] <= 0):
410                self._stop_scared(self.agent_index)
411
412    def _kill_agent(self, agent_index: int) -> None:
413        """ Set the non-board state for killing an agent. """
414
415        # Reset the last action.
416        if (agent_index not in self.agent_actions):
417            self.agent_actions[agent_index] = []
418
419        self.agent_actions[agent_index].append(pacai.core.action.STOP)
420
421        # The agent is no longer scared.
422        self._stop_scared(agent_index)
423
424    def _stop_scared(self, agent_index: int) -> None:
425        """ Stop an agent from being scared. """
426
427        if (agent_index in self.scared_timers):
428            del self.scared_timers[agent_index]
TEAM_MODIFIERS: tuple[int, int] = (-1, 1)

The different modifiers for the two teams: evens/west and odds/east.

FOOD_POINTS: int = 10

Points for eating food.

CAPSULE_POINTS: int = 0

Points for eating a power capsule.

KILL_GHOST_POINTS: int = 0

Points for eating a scared ghost.

KILL_PACMAN_POINTS: int = 0

Points for eating a Pac-Man.

PREFIX_MARKERS: set[pacai.core.board.Marker] = {'!', 'o', '.', '%'}

These markers get prefixed with -/+ when searching for sprites.

GHOST_MARKER_PREFIX: str = 'G'

Prefix (non-scared) ghost markers with this.

SCORE_COLOR_ZERO: tuple[int, int, int] = (255, 255, 255)
SCORE_COLOR_NEGATIVE: tuple[int, int, int] = (229, 0, 0)
SCORE_COLOR_POSITIVE: tuple[int, int, int] = (0, 66, 255)
class GameState(pacai.pacman.gamestate.GameState):
 44class GameState(pacai.pacman.gamestate.GameState):
 45    """
 46    A game state specific to a game of Capture.
 47    Derived from the Pac-Man game state.
 48
 49    A common concept in this game is that the board is divided in half between two teams.
 50    The even team (with even agent indexes) start in the west and uses a -1 modifier.
 51    The odd team (with odd agent indexes) start in the east and uses a +1 modifier.
 52    """
 53
 54    def _team_modifier(self, agent_index: int = -1) -> int:
 55        """ Return -1 or +1 depending on if the even or odd team (respectively) is currently active. """
 56
 57        if (agent_index == -1):
 58            agent_index = self.agent_index
 59
 60        return ((agent_index % 2) * 2) - 1
 61
 62    def _team_side(self, agent_index: int = -1, position: pacai.core.board.Position | None = None) -> int:
 63        """
 64        Return -1 or +1 depending on which side of the board this position is on.
 65        If no position is provided, the agent's position will be used.
 66        If no agent index is provided, the current agent index will be used.
 67        """
 68
 69        if (position is None):
 70            if (agent_index == -1):
 71                agent_index = self.agent_index
 72
 73            position = self.get_agent_position(agent_index = agent_index)
 74
 75        if (position is None):
 76            raise ValueError("Could not find position.")
 77
 78        if (position.col < (self.board.width / 2)):
 79            return -1
 80
 81        return 1
 82
 83    def _team_agent_indexes(self, team_modifier: int) -> list[int]:
 84        """ Get the agent indexes for the current team. """
 85
 86        return [agent_index for agent_index in self.get_agent_indexes() if (self._team_modifier(agent_index) == team_modifier)]
 87
 88    def get_normalized_score(self, agent_index: int = -1) -> float:
 89        """
 90        Get the score of this state normalized according to the team asking for it.
 91        Higher numbers are always "better".
 92        """
 93
 94        return self._team_modifier(agent_index) * self.score
 95
 96    def is_ghost(self, agent_index: int = -1) -> bool:
 97        """ Check if this agent is currently in "ghost mode", i.e., on their own side of the board. """
 98
 99        if (agent_index == -1):
100            agent_index = self.agent_index
101
102        position = self.get_agent_position(agent_index = agent_index)
103
104        if (position is None):
105            return False
106
107        return (self._team_side(agent_index = agent_index) == self._team_modifier(agent_index = agent_index))
108
109    def is_pacman(self, agent_index: int = -1) -> bool:
110        """ Check if this agent is currently in "Pac-Man mode", i.e., on the opponent's side of the board. """
111
112        if (agent_index == -1):
113            agent_index = self.agent_index
114
115        position = self.get_agent_position(agent_index = agent_index)
116
117        if (position is None):
118            return False
119
120        return (self._team_side(agent_index = agent_index) != self._team_modifier(agent_index = agent_index))
121
122    def is_scared(self, agent_index: int = -1) -> bool:
123        # Only ghosts can be scared.
124        if (not self.is_ghost(agent_index)):
125            return False
126
127        return super().is_scared(agent_index = agent_index)
128
129    def process_agent_timeout(self, agent_index: int) -> None:
130        # Treat timeouts like crashes.
131        self.process_agent_crash(agent_index)
132
133    def process_agent_crash(self, agent_index: int) -> None:
134        super().process_agent_crash(agent_index)
135
136        # Set the score in favor of the non-crashing team.
137        self.score = -1 * self._team_modifier(agent_index) * pacai.pacman.gamestate.CRASH_POINTS
138
139    def get_legal_actions(self, position: pacai.core.board.Position | None = None) -> list[pacai.core.action.Action]:
140        if (position is None):
141            position = self.get_agent_position(self.agent_index)
142
143        # Dead agents (agents not on the board) can only stop (when they will respawn).
144        if (position is None):
145            return [pacai.core.action.STOP]
146
147        # Call directly into pacai.core.gamestate.GameState because Pac-Man has special actions for ghosts.
148        return pacai.core.gamestate.GameState.get_legal_actions(self, position)
149
150    def food_count(self, team_modifier: int = 0, agent_index: int = -1) -> int:
151        """
152        Get the count of all food currently on the board for the specified team.
153        If no team is specified, use the given agent's team.
154        If no agent is specified, use the current agent.
155        """
156
157        return len(self.get_food(team_modifier = team_modifier, agent_index = agent_index))
158
159    def get_food(self, team_modifier: int = 0, agent_index: int = -1) -> set[pacai.core.board.Position]:
160        """
161        Get the positions of all food currently on the board for the specific team.
162        This refers to food that the given team can eat (which lives on the opponents side).
163        If no team is specified, use the given agent's team.
164        If no agent is specified, use the current agent.
165        """
166
167        if (team_modifier == 0):
168            if (agent_index == -1):
169                agent_index = self.agent_index
170
171            team_modifier = self._team_modifier(agent_index)
172
173        team_food = set()
174        for position in self.board.get_marker_positions(pacai.pacman.board.MARKER_PELLET):
175            # If the food does not belong to this team, they can eat it.
176            if (self._team_side(position = position) != team_modifier):
177                team_food.add(position)
178
179        return team_food
180
181    def get_team_positions(self, team_modifier: int) -> dict[int, pacai.core.board.Position]:
182        """ Get the position of all agents on the board belonging to the given team. """
183
184        agents = {}
185
186        for (agent_index, agent_position) in self.get_agent_positions().items():
187            if (team_modifier != self._team_modifier(agent_index)):
188                continue
189
190            if (agent_position is None):
191                continue
192
193            agents[agent_index] = agent_position
194
195        return agents
196
197    def get_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
198        """ Get the position of all allies currently on the board. """
199
200        if (agent_index == -1):
201            agent_index = self.agent_index
202
203        team_modifier = self._team_modifier(agent_index)
204
205        agents = self.get_team_positions(team_modifier)
206
207        # Remove self.
208        agents.pop(agent_index, None)
209
210        return agents
211
212    def get_scared_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
213        """ Get the position of all scared allies currently on the board. """
214
215        allies = self.get_ally_positions(agent_index = agent_index)
216        return {index: position for (index, position) in allies.items() if self.is_scared(index)}
217
218    def get_nonscared_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
219        """ Get the position of all non-scared allies currently on the board. """
220
221        allies = self.get_ally_positions(agent_index = agent_index)
222        return {index: position for (index, position) in allies.items() if not self.is_scared(index)}
223
224    def get_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
225        """ Get the position of all opponents currently on the board. """
226
227        if (agent_index == -1):
228            agent_index = self.agent_index
229
230        team_modifier = self._team_modifier(agent_index)
231
232        return self.get_team_positions(-team_modifier)
233
234    def get_scared_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
235        """ Get the position of all scared opponents currently on the board. """
236
237        opponents = self.get_opponent_positions(agent_index = agent_index)
238        return {index: position for (index, position) in opponents.items() if self.is_scared(index)}
239
240    def get_nonscared_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
241        """ Get the position of all non-scared opponents currently on the board. """
242
243        opponents = self.get_opponent_positions(agent_index = agent_index)
244        return {index: position for (index, position) in opponents.items() if not self.is_scared(index)}
245
246    def get_invader_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
247        """ Get the position of all invading opponents (opponents on your side of the board). """
248
249        opponents = self.get_opponent_positions(agent_index = agent_index)
250        return {index: position for (index, position) in opponents.items() if self.is_pacman(index)}
251
252    def game_complete(self) -> list[int]:
253        # A side wins if there is no food left for them to eat.
254        for team_modifier in TEAM_MODIFIERS:
255            if (self.food_count(team_modifier = team_modifier) == 0):
256                return self._team_agent_indexes(team_modifier)
257
258        return []
259
260    def sprite_lookup(self,
261            sprite_sheet: pacai.core.spritesheet.SpriteSheet,
262            position: pacai.core.board.Position,
263            marker: pacai.core.board.Marker | None = None,
264            action: pacai.core.action.Action | None = None,
265            adjacency: pacai.core.board.AdjacencyString | None = None,
266            animation_key: str | None = None,
267            ) -> PIL.Image.Image:
268        # If the agent in on their own side, they should be a ghost (maybe a scared one).
269        if ((marker is not None) and (marker.is_agent()) and (self.is_ghost(marker.get_agent_index()))):
270            if (self.is_scared(marker.get_agent_index())):
271                marker = pacai.pacman.board.MARKER_SCARED_GHOST
272            else:
273                marker = pacai.core.board.Marker(GHOST_MARKER_PREFIX + str(marker))
274
275        # Prefix specific markers with a -/+ depending on the side it is on.
276        if (marker in PREFIX_MARKERS):
277            prefix = '-'
278            if (self._team_side(position = position) > 0):
279                prefix = '+'
280
281            marker = pacai.core.board.Marker(prefix + str(marker))
282
283        return pacai.core.gamestate.GameState.sprite_lookup(self,
284                sprite_sheet, position,
285                marker = marker, action = action, adjacency = adjacency, animation_key = animation_key)
286
287    def get_footer_text(self) -> pacai.core.font.Text | None:
288        text = super().get_footer_text()
289
290        # Color the text based on who's winning.
291        if (text is not None):
292            if (self.score < 0.0):
293                text.color = SCORE_COLOR_NEGATIVE
294            elif (self.score > 0.0):
295                text.color = SCORE_COLOR_POSITIVE
296            else:
297                text.color = SCORE_COLOR_ZERO
298
299        return text
300
301    def process_turn(self,  # pylint: disable=too-many-statements
302            action: pacai.core.action.Action,
303            rng: random.Random | None = None,
304            **kwargs: typing.Any) -> None:
305        agent_marker = pacai.core.board.Marker(str(self.agent_index))
306        team_modifier = self._team_modifier()
307
308        # Compute the agent's new position.
309        old_position = self.get_agent_position()
310        if (old_position is None):
311            # The agent is currently dead and needs to respawn.
312            new_position = self.board.get_agent_initial_position(self.agent_index)
313            if (new_position is None):
314                raise ValueError(f"Cannot find initial position for agent {self.agent_index}.")
315
316            self.board.place_marker(agent_marker, new_position)
317        else:
318            new_position = old_position.apply_action(action)
319
320        # Get all the markers that we are moving into.
321        interaction_markers = set()
322        if (old_position != new_position):
323            interaction_markers = self.board.get(new_position)
324
325            # Since we are moving, pickup the agent from their current location and move them to their new location.
326            if (old_position is not None):
327                self.board.remove_marker(agent_marker, old_position)
328                self.board.place_marker(agent_marker, new_position)
329
330        died = False
331
332        # Process actions for all the markers we are moving onto.
333        for interaction_marker in interaction_markers:
334            if (interaction_marker == pacai.pacman.board.MARKER_PELLET):
335                # Ignore our own food.
336                if (team_modifier == self._team_side(position = new_position)):
337                    continue
338
339                # Eat a food pellet.
340                self.board.remove_marker(interaction_marker, new_position)
341                self.score += team_modifier * FOOD_POINTS
342
343                if (self.food_count(team_modifier = team_modifier) == 0):
344                    self.game_over = True
345            elif (interaction_marker == pacai.pacman.board.MARKER_CAPSULE):
346                # Ignore our own capsules.
347                if (team_modifier == self._team_side(position = new_position)):
348                    continue
349
350                # Eat a power capsule, scare all enemy ghosts.
351                self.board.remove_marker(interaction_marker, new_position)
352                self.score += team_modifier * CAPSULE_POINTS
353
354                # Scare all enemies.
355                for agent_index in self._team_agent_indexes(-team_modifier):
356                    self.scared_timers[agent_index] = pacai.pacman.gamestate.SCARED_TIME
357            elif (interaction_marker.is_agent()):
358                other_agent_index = interaction_marker.get_agent_index()
359                other_team_modifier = self._team_modifier(other_agent_index)
360
361                if (team_modifier == other_team_modifier):
362                    # This is an agent on our team, nothing to do.
363                    continue
364
365                # Check if anyone is scared.
366                self_scared = self.is_scared(self.agent_index)
367                other_scared = self.is_scared(other_agent_index)
368
369                # Check who is a ghost (agent's on their own side are a ghost).
370                self_ghost = self.is_ghost(self.agent_index)
371                other_ghost = self.is_ghost(other_agent_index)
372
373                # Check who was eaten (and remove them), what team did the eating, and the points that should be awarded.
374                eating_team_modifier = 0
375                points = 0
376
377                if ((self_scared and self_ghost) or ((not other_scared) and other_ghost)):
378                    # We got eaten, but our marker is already off the board.
379                    died = True
380                    self._kill_agent(self.agent_index)
381
382                    eating_team_modifier = other_team_modifier
383
384                    if (self_scared):
385                        points = KILL_GHOST_POINTS
386                    else:
387                        points = KILL_PACMAN_POINTS
388                else:
389                    # If we ate the opponent, remove them from the board.
390                    self.board.remove_marker(interaction_marker, new_position)
391                    self._kill_agent(other_agent_index)
392
393                    eating_team_modifier = team_modifier
394
395                    if (other_scared):
396                        points = KILL_GHOST_POINTS
397                    else:
398                        points = KILL_PACMAN_POINTS
399
400                self.score += (eating_team_modifier * points)
401
402        # The current agent has died, remove their marker.
403        if (died):
404            self.board.remove_marker(agent_marker, new_position)
405
406        # Decrement the scared timer.
407        if (self.agent_index in self.scared_timers):
408            self.scared_timers[self.agent_index] -= 1
409
410            if (self.scared_timers[self.agent_index] <= 0):
411                self._stop_scared(self.agent_index)
412
413    def _kill_agent(self, agent_index: int) -> None:
414        """ Set the non-board state for killing an agent. """
415
416        # Reset the last action.
417        if (agent_index not in self.agent_actions):
418            self.agent_actions[agent_index] = []
419
420        self.agent_actions[agent_index].append(pacai.core.action.STOP)
421
422        # The agent is no longer scared.
423        self._stop_scared(agent_index)
424
425    def _stop_scared(self, agent_index: int) -> None:
426        """ Stop an agent from being scared. """
427
428        if (agent_index in self.scared_timers):
429            del self.scared_timers[agent_index]

A game state specific to a game of Capture. Derived from the Pac-Man game state.

A common concept in this game is that the board is divided in half between two teams. The even team (with even agent indexes) start in the west and uses a -1 modifier. The odd team (with odd agent indexes) start in the east and uses a +1 modifier.

def get_normalized_score(self, agent_index: int = -1) -> float:
88    def get_normalized_score(self, agent_index: int = -1) -> float:
89        """
90        Get the score of this state normalized according to the team asking for it.
91        Higher numbers are always "better".
92        """
93
94        return self._team_modifier(agent_index) * self.score

Get the score of this state normalized according to the team asking for it. Higher numbers are always "better".

def is_ghost(self, agent_index: int = -1) -> bool:
 96    def is_ghost(self, agent_index: int = -1) -> bool:
 97        """ Check if this agent is currently in "ghost mode", i.e., on their own side of the board. """
 98
 99        if (agent_index == -1):
100            agent_index = self.agent_index
101
102        position = self.get_agent_position(agent_index = agent_index)
103
104        if (position is None):
105            return False
106
107        return (self._team_side(agent_index = agent_index) == self._team_modifier(agent_index = agent_index))

Check if this agent is currently in "ghost mode", i.e., on their own side of the board.

def is_pacman(self, agent_index: int = -1) -> bool:
109    def is_pacman(self, agent_index: int = -1) -> bool:
110        """ Check if this agent is currently in "Pac-Man mode", i.e., on the opponent's side of the board. """
111
112        if (agent_index == -1):
113            agent_index = self.agent_index
114
115        position = self.get_agent_position(agent_index = agent_index)
116
117        if (position is None):
118            return False
119
120        return (self._team_side(agent_index = agent_index) != self._team_modifier(agent_index = agent_index))

Check if this agent is currently in "Pac-Man mode", i.e., on the opponent's side of the board.

def is_scared(self, agent_index: int = -1) -> bool:
122    def is_scared(self, agent_index: int = -1) -> bool:
123        # Only ghosts can be scared.
124        if (not self.is_ghost(agent_index)):
125            return False
126
127        return super().is_scared(agent_index = agent_index)

Check if the given agent (or the current agent) is currently scared.

def process_agent_timeout(self, agent_index: int) -> None:
129    def process_agent_timeout(self, agent_index: int) -> None:
130        # Treat timeouts like crashes.
131        self.process_agent_crash(agent_index)

Notify the state that the given agent has timed out. The state should make any updates and set the end of game information.

def process_agent_crash(self, agent_index: int) -> None:
133    def process_agent_crash(self, agent_index: int) -> None:
134        super().process_agent_crash(agent_index)
135
136        # Set the score in favor of the non-crashing team.
137        self.score = -1 * self._team_modifier(agent_index) * pacai.pacman.gamestate.CRASH_POINTS

Notify the state that the given agent has crashed. The state should make any updates and set the end of game information.

def food_count(self, team_modifier: int = 0, agent_index: int = -1) -> int:
150    def food_count(self, team_modifier: int = 0, agent_index: int = -1) -> int:
151        """
152        Get the count of all food currently on the board for the specified team.
153        If no team is specified, use the given agent's team.
154        If no agent is specified, use the current agent.
155        """
156
157        return len(self.get_food(team_modifier = team_modifier, agent_index = agent_index))

Get the count of all food currently on the board for the specified team. If no team is specified, use the given agent's team. If no agent is specified, use the current agent.

def get_food( self, team_modifier: int = 0, agent_index: int = -1) -> set[pacai.core.board.Position]:
159    def get_food(self, team_modifier: int = 0, agent_index: int = -1) -> set[pacai.core.board.Position]:
160        """
161        Get the positions of all food currently on the board for the specific team.
162        This refers to food that the given team can eat (which lives on the opponents side).
163        If no team is specified, use the given agent's team.
164        If no agent is specified, use the current agent.
165        """
166
167        if (team_modifier == 0):
168            if (agent_index == -1):
169                agent_index = self.agent_index
170
171            team_modifier = self._team_modifier(agent_index)
172
173        team_food = set()
174        for position in self.board.get_marker_positions(pacai.pacman.board.MARKER_PELLET):
175            # If the food does not belong to this team, they can eat it.
176            if (self._team_side(position = position) != team_modifier):
177                team_food.add(position)
178
179        return team_food

Get the positions of all food currently on the board for the specific team. This refers to food that the given team can eat (which lives on the opponents side). If no team is specified, use the given agent's team. If no agent is specified, use the current agent.

def get_team_positions(self, team_modifier: int) -> dict[int, pacai.core.board.Position]:
181    def get_team_positions(self, team_modifier: int) -> dict[int, pacai.core.board.Position]:
182        """ Get the position of all agents on the board belonging to the given team. """
183
184        agents = {}
185
186        for (agent_index, agent_position) in self.get_agent_positions().items():
187            if (team_modifier != self._team_modifier(agent_index)):
188                continue
189
190            if (agent_position is None):
191                continue
192
193            agents[agent_index] = agent_position
194
195        return agents

Get the position of all agents on the board belonging to the given team.

def get_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
197    def get_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
198        """ Get the position of all allies currently on the board. """
199
200        if (agent_index == -1):
201            agent_index = self.agent_index
202
203        team_modifier = self._team_modifier(agent_index)
204
205        agents = self.get_team_positions(team_modifier)
206
207        # Remove self.
208        agents.pop(agent_index, None)
209
210        return agents

Get the position of all allies currently on the board.

def get_scared_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
212    def get_scared_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
213        """ Get the position of all scared allies currently on the board. """
214
215        allies = self.get_ally_positions(agent_index = agent_index)
216        return {index: position for (index, position) in allies.items() if self.is_scared(index)}

Get the position of all scared allies currently on the board.

def get_nonscared_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
218    def get_nonscared_ally_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
219        """ Get the position of all non-scared allies currently on the board. """
220
221        allies = self.get_ally_positions(agent_index = agent_index)
222        return {index: position for (index, position) in allies.items() if not self.is_scared(index)}

Get the position of all non-scared allies currently on the board.

def get_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
224    def get_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
225        """ Get the position of all opponents currently on the board. """
226
227        if (agent_index == -1):
228            agent_index = self.agent_index
229
230        team_modifier = self._team_modifier(agent_index)
231
232        return self.get_team_positions(-team_modifier)

Get the position of all opponents currently on the board.

def get_scared_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
234    def get_scared_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
235        """ Get the position of all scared opponents currently on the board. """
236
237        opponents = self.get_opponent_positions(agent_index = agent_index)
238        return {index: position for (index, position) in opponents.items() if self.is_scared(index)}

Get the position of all scared opponents currently on the board.

def get_nonscared_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
240    def get_nonscared_opponent_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
241        """ Get the position of all non-scared opponents currently on the board. """
242
243        opponents = self.get_opponent_positions(agent_index = agent_index)
244        return {index: position for (index, position) in opponents.items() if not self.is_scared(index)}

Get the position of all non-scared opponents currently on the board.

def get_invader_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
246    def get_invader_positions(self, agent_index: int = -1) -> dict[int, pacai.core.board.Position]:
247        """ Get the position of all invading opponents (opponents on your side of the board). """
248
249        opponents = self.get_opponent_positions(agent_index = agent_index)
250        return {index: position for (index, position) in opponents.items() if self.is_pacman(index)}

Get the position of all invading opponents (opponents on your side of the board).

def game_complete(self) -> list[int]:
252    def game_complete(self) -> list[int]:
253        # A side wins if there is no food left for them to eat.
254        for team_modifier in TEAM_MODIFIERS:
255            if (self.food_count(team_modifier = team_modifier) == 0):
256                return self._team_agent_indexes(team_modifier)
257
258        return []

Indicate that the game has ended. The state should take any final actions and return the indexes of the winning agents (if any).

def sprite_lookup( self, sprite_sheet: pacai.core.spritesheet.SpriteSheet, position: pacai.core.board.Position, marker: pacai.core.board.Marker | None = None, action: pacai.core.action.Action | None = None, adjacency: pacai.core.board.AdjacencyString | None = None, animation_key: str | None = None) -> PIL.Image.Image:
260    def sprite_lookup(self,
261            sprite_sheet: pacai.core.spritesheet.SpriteSheet,
262            position: pacai.core.board.Position,
263            marker: pacai.core.board.Marker | None = None,
264            action: pacai.core.action.Action | None = None,
265            adjacency: pacai.core.board.AdjacencyString | None = None,
266            animation_key: str | None = None,
267            ) -> PIL.Image.Image:
268        # If the agent in on their own side, they should be a ghost (maybe a scared one).
269        if ((marker is not None) and (marker.is_agent()) and (self.is_ghost(marker.get_agent_index()))):
270            if (self.is_scared(marker.get_agent_index())):
271                marker = pacai.pacman.board.MARKER_SCARED_GHOST
272            else:
273                marker = pacai.core.board.Marker(GHOST_MARKER_PREFIX + str(marker))
274
275        # Prefix specific markers with a -/+ depending on the side it is on.
276        if (marker in PREFIX_MARKERS):
277            prefix = '-'
278            if (self._team_side(position = position) > 0):
279                prefix = '+'
280
281            marker = pacai.core.board.Marker(prefix + str(marker))
282
283        return pacai.core.gamestate.GameState.sprite_lookup(self,
284                sprite_sheet, position,
285                marker = marker, action = action, adjacency = adjacency, animation_key = animation_key)

Lookup the proper sprite for a situation. By default this just calls into the sprite sheet, but children may override for more expressive functionality.

def process_turn( self, action: pacai.core.action.Action, rng: random.Random | None = None, **kwargs: Any) -> None:
301    def process_turn(self,  # pylint: disable=too-many-statements
302            action: pacai.core.action.Action,
303            rng: random.Random | None = None,
304            **kwargs: typing.Any) -> None:
305        agent_marker = pacai.core.board.Marker(str(self.agent_index))
306        team_modifier = self._team_modifier()
307
308        # Compute the agent's new position.
309        old_position = self.get_agent_position()
310        if (old_position is None):
311            # The agent is currently dead and needs to respawn.
312            new_position = self.board.get_agent_initial_position(self.agent_index)
313            if (new_position is None):
314                raise ValueError(f"Cannot find initial position for agent {self.agent_index}.")
315
316            self.board.place_marker(agent_marker, new_position)
317        else:
318            new_position = old_position.apply_action(action)
319
320        # Get all the markers that we are moving into.
321        interaction_markers = set()
322        if (old_position != new_position):
323            interaction_markers = self.board.get(new_position)
324
325            # Since we are moving, pickup the agent from their current location and move them to their new location.
326            if (old_position is not None):
327                self.board.remove_marker(agent_marker, old_position)
328                self.board.place_marker(agent_marker, new_position)
329
330        died = False
331
332        # Process actions for all the markers we are moving onto.
333        for interaction_marker in interaction_markers:
334            if (interaction_marker == pacai.pacman.board.MARKER_PELLET):
335                # Ignore our own food.
336                if (team_modifier == self._team_side(position = new_position)):
337                    continue
338
339                # Eat a food pellet.
340                self.board.remove_marker(interaction_marker, new_position)
341                self.score += team_modifier * FOOD_POINTS
342
343                if (self.food_count(team_modifier = team_modifier) == 0):
344                    self.game_over = True
345            elif (interaction_marker == pacai.pacman.board.MARKER_CAPSULE):
346                # Ignore our own capsules.
347                if (team_modifier == self._team_side(position = new_position)):
348                    continue
349
350                # Eat a power capsule, scare all enemy ghosts.
351                self.board.remove_marker(interaction_marker, new_position)
352                self.score += team_modifier * CAPSULE_POINTS
353
354                # Scare all enemies.
355                for agent_index in self._team_agent_indexes(-team_modifier):
356                    self.scared_timers[agent_index] = pacai.pacman.gamestate.SCARED_TIME
357            elif (interaction_marker.is_agent()):
358                other_agent_index = interaction_marker.get_agent_index()
359                other_team_modifier = self._team_modifier(other_agent_index)
360
361                if (team_modifier == other_team_modifier):
362                    # This is an agent on our team, nothing to do.
363                    continue
364
365                # Check if anyone is scared.
366                self_scared = self.is_scared(self.agent_index)
367                other_scared = self.is_scared(other_agent_index)
368
369                # Check who is a ghost (agent's on their own side are a ghost).
370                self_ghost = self.is_ghost(self.agent_index)
371                other_ghost = self.is_ghost(other_agent_index)
372
373                # Check who was eaten (and remove them), what team did the eating, and the points that should be awarded.
374                eating_team_modifier = 0
375                points = 0
376
377                if ((self_scared and self_ghost) or ((not other_scared) and other_ghost)):
378                    # We got eaten, but our marker is already off the board.
379                    died = True
380                    self._kill_agent(self.agent_index)
381
382                    eating_team_modifier = other_team_modifier
383
384                    if (self_scared):
385                        points = KILL_GHOST_POINTS
386                    else:
387                        points = KILL_PACMAN_POINTS
388                else:
389                    # If we ate the opponent, remove them from the board.
390                    self.board.remove_marker(interaction_marker, new_position)
391                    self._kill_agent(other_agent_index)
392
393                    eating_team_modifier = team_modifier
394
395                    if (other_scared):
396                        points = KILL_GHOST_POINTS
397                    else:
398                        points = KILL_PACMAN_POINTS
399
400                self.score += (eating_team_modifier * points)
401
402        # The current agent has died, remove their marker.
403        if (died):
404            self.board.remove_marker(agent_marker, new_position)
405
406        # Decrement the scared timer.
407        if (self.agent_index in self.scared_timers):
408            self.scared_timers[self.agent_index] -= 1
409
410            if (self.scared_timers[self.agent_index] <= 0):
411                self._stop_scared(self.agent_index)

Process the current agent's turn with the given action. This may modify the current state. To get a copy of a potential successor state, use generate_successor().