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]
The different modifiers for the two teams: evens/west and odds/east.
Points for eating food.
Points for eating a power capsule.
Points for eating a scared ghost.
Points for eating a Pac-Man.
These markers get prefixed with -/+ when searching for sprites.
Prefix (non-scared) ghost markers with this.
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.
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".
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.
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.
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.
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.
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.
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)
Get the moves that the current agent is allowed to make. Stopping is generally always considered a legal action (unless a game re-defines this behavior).
If a position is provided, it will override the current agent's position.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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.
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().
Inherited Members
- pacai.pacman.gamestate.GameState
- GameState
- scared_timers
- copy
- compute_move_delay
- get_ghost_positions
- get_scared_ghost_positions
- get_nonscared_ghost_positions
- pacai.core.gamestate.GameState
- board
- seed
- agent_index
- last_agent_index
- game_over
- agent_actions
- score
- turn_count
- move_delays
- tickets
- game_start
- agents_game_start
- get_num_agents
- get_agent_indexes
- get_agent_positions
- get_agent_position
- get_agent_actions
- get_last_agent_action
- get_reverse_action
- generate_successor
- process_game_timeout
- process_turn_full
- get_next_agent_index
- skip_draw
- get_static_positions
- get_static_text
- get_nonstatic_text
- to_dict
- from_dict