pacai.core.ui
1import abc 2import argparse 3import os 4import time 5import typing 6 7import PIL.Image 8import PIL.ImageDraw 9import PIL.ImageFont 10import edq.util.time 11 12import pacai.core.action 13import pacai.core.board 14import pacai.core.font 15import pacai.core.gamestate 16import pacai.core.spritesheet 17import pacai.util.alias 18import pacai.util.reflection 19 20DEFAULT_FPS: int = 15 21 22DEFAULT_ANIMATION_FPS: int = 15 23DEFAULT_ANIMATION_SKIP_FRAMES: int = 1 24MIN_ANIMATION_FPS: int = 1 25DEFAULT_ANIMATION_OPTIMIZE: bool = False 26 27DEFAULT_SPRITE_SHEET: str = 'generic' 28 29ANIMATION_KEY: str = 'UI.draw_image' 30 31ANIMATION_EXTS: list[str] = ['.gif', '.webp'] 32""" The allowed extensions for animation files. """ 33 34WASD_CHAR_MAPPING: dict[str, pacai.core.action.Action] = { 35 'w': pacai.core.action.NORTH, 36 'W': pacai.core.action.NORTH, 37 'ArrowUp': pacai.core.action.NORTH, 38 39 'a': pacai.core.action.WEST, 40 'A': pacai.core.action.WEST, 41 'ArrowLeft': pacai.core.action.WEST, 42 43 's': pacai.core.action.SOUTH, 44 'S': pacai.core.action.SOUTH, 45 'ArrowDown': pacai.core.action.SOUTH, 46 47 'd': pacai.core.action.EAST, 48 'D': pacai.core.action.EAST, 49 'ArrowRight': pacai.core.action.EAST, 50 51 ' ': pacai.core.action.STOP, 52 'space': pacai.core.action.STOP, 53 'Space': pacai.core.action.STOP, 54 'SPACE': pacai.core.action.STOP, 55} 56""" A character to action mapping using the common WASD scheme. """ 57 58ARROW_CHAR_MAPPING: dict[str, pacai.core.action.Action] = { 59 'Up': pacai.core.action.NORTH, 60 'ArrowUp': pacai.core.action.NORTH, 61 62 'Left': pacai.core.action.WEST, 63 'ArrowLeft': pacai.core.action.WEST, 64 65 'Down': pacai.core.action.SOUTH, 66 'ArrowDown': pacai.core.action.SOUTH, 67 68 'Right': pacai.core.action.EAST, 69 'ArrowRight': pacai.core.action.EAST, 70 71 ' ': pacai.core.action.STOP, 72 'space': pacai.core.action.STOP, 73 'Space': pacai.core.action.STOP, 74 'SPACE': pacai.core.action.STOP, 75} 76""" A character to action mapping using the arrow keys. """ 77 78DUAL_CHAR_MAPPING: dict[str, pacai.core.action.Action] = WASD_CHAR_MAPPING | ARROW_CHAR_MAPPING 79""" A character to action mapping that uses both WASD_CHAR_MAPPING and ARROW_CHAR_MAPPING. """ 80 81class UserInputDevice(abc.ABC): 82 """ 83 This class provides a way for users to convey inputs through a UI. 84 Not all UIs will support user input. 85 """ 86 87 @abc.abstractmethod 88 def get_inputs(self) -> list[pacai.core.action.Action]: 89 """ 90 Get any inputs that have occurred since the last call to this method. 91 This method is responsible for not returning the same input instance in subsequent calls. 92 The last input in the returned list should be the most recent input. 93 """ 94 95 def close(self) -> None: 96 """ Close the user input channel and release all owned resources. """ 97 98class UI(abc.ABC): 99 """ 100 UIs represent the basic way that a game interacts with the user, 101 by displaying the state of the game and taking input from the user (if applicable). 102 """ 103 104 def __init__(self, 105 user_input_device: UserInputDevice | None = None, 106 fps: int = DEFAULT_FPS, 107 animation_path: str | None = None, 108 animation_optimize: bool = DEFAULT_ANIMATION_OPTIMIZE, 109 animation_fps: int = DEFAULT_ANIMATION_FPS, 110 animation_skip_frames: int = DEFAULT_ANIMATION_SKIP_FRAMES, 111 sprite_sheet_path: str = DEFAULT_SPRITE_SHEET, 112 font_path: str = pacai.core.font.DEFAULT_FONT_PATH, 113 **kwargs: typing.Any) -> None: 114 self._user_input_device: UserInputDevice | None = user_input_device 115 """ The device to use to get user input. """ 116 117 self._fps: int = fps 118 """ 119 The desired frames per second this game will be displayed at. 120 Zero or lower values will be ignored. 121 This is just a suggestion that the game will try an accommodate. 122 Not all UIs will observe fps. 123 """ 124 125 self._last_fps_wait: edq.util.time.Timestamp | None = None 126 """ 127 Keep track of the last time the UI waited to adjust the fps. 128 We need this information to compute the next wait time. 129 """ 130 131 self._update_count: int = 0 132 """ Keep track of the number of times update() has been called. """ 133 134 self._animation_path: str | None = animation_path 135 """ If specified, create a animation and write it to this location after the game completes. """ 136 137 if (self._animation_path is not None): 138 if (os.path.splitext(self._animation_path)[-1] not in ANIMATION_EXTS): 139 raise ValueError(f"Animation path must have one of the following extensions {ANIMATION_EXTS}, found '{self._animation_path}'.") 140 141 self._animation_optimize: bool = animation_optimize 142 """ Optimize the animation output to reduce file size. """ 143 144 self._animation_fps: int = max(MIN_ANIMATION_FPS, animation_fps) 145 """ The frame rate for the animation. """ 146 147 self._animation_skip_frames: int = max(1, animation_skip_frames) 148 """ 149 Skip this many frames between drawing animation frames. 150 This can help speed up animation creation by leaving out less important frames. 151 For example, this can be set to the number of agents to only draw frames after all agents have moved. 152 """ 153 154 self._animation_frames: list[PIL.Image.Image] = [] 155 """ The frames for the animation (one per call to update(). """ 156 157 self._static_base_image: PIL.Image.Image | None = None 158 """ 159 Cache an image that has all of the static (non-changing) elements (like walls) drawn. 160 This can be reused as the base image every time we draw an image. 161 """ 162 163 # Only load sprites (and fonts) if we need them. 164 sprite_sheet = None 165 fonts = {} 166 if (self.requires_sprites() or (self._animation_path is not None)): 167 sprite_sheet = pacai.core.spritesheet.load(sprite_sheet_path) 168 169 for font_size in pacai.core.font.FontSize: 170 fonts[font_size] = PIL.ImageFont.truetype(font_path, int(sprite_sheet.height * font_size.value)) 171 172 self._sprite_sheet: pacai.core.spritesheet.SpriteSheet | None = sprite_sheet 173 """ The sprite sheet to use for this UI. """ 174 175 self._fonts: dict[pacai.core.font.FontSize, PIL.ImageFont.FreeTypeFont] = fonts 176 """ The available fonts indexed by size. """ 177 178 self._image_cache: dict[int, PIL.Image.Image] = {} 179 """ Cache images (by game state turn count) to avoid redrawing images. """ 180 181 self._highlights: dict[pacai.core.board.Position, float] = {} 182 """ The current set of board highlights. """ 183 184 def update(self, 185 state: pacai.core.gamestate.GameState, 186 force_draw_image: bool = False, 187 board_highlights: list[pacai.core.board.Highlight] | None = None, 188 ) -> None: 189 """ 190 Update the UI with the current state of the game. 191 This is the main entry point for the game into the UI. 192 """ 193 194 self.wait_for_fps() 195 196 if (board_highlights is None): 197 board_highlights = [] 198 199 for board_highlight in board_highlights: 200 intensity = board_highlight.get_float_intensity() 201 if (intensity is None): 202 self._highlights.pop(board_highlight.position, None) 203 else: 204 self._highlights[board_highlight.position] = intensity 205 206 if ((self._animation_path is not None) and (force_draw_image or (self._update_count % self._animation_skip_frames == 0))): 207 image = self.draw_image(state) 208 self._animation_frames.append(image) 209 210 self.draw(state) 211 212 self._update_count += 1 213 214 def game_start(self, 215 initial_state: pacai.core.gamestate.GameState, 216 board_highlights: list[pacai.core.board.Highlight] | None = None, 217 ) -> None: 218 """ Initialize the UI with the game's initial state. """ 219 220 self.update(initial_state, board_highlights = board_highlights, force_draw_image = True) 221 222 def game_complete(self, 223 final_state: pacai.core.gamestate.GameState, 224 board_highlights: list[pacai.core.board.Highlight] | None = None, 225 ) -> None: 226 """ Update the UI with the game's final state. """ 227 228 self.update(final_state, board_highlights = board_highlights, force_draw_image = True) 229 230 # Write the animation. 231 if ((self._animation_path is not None) and (len(self._animation_frames) > 0)): 232 ms_per_frame = int(1.0 / self._animation_fps * 1000.0) 233 234 options = { 235 'save_all': True, 236 'append_images': self._animation_frames, 237 'duration': ms_per_frame, 238 'loop': 0, 239 'optimize': False, 240 'minimize_size': False, 241 } 242 243 if (self._animation_optimize): 244 options['optimize'] = True 245 options['minimize_size'] = True 246 247 self._animation_frames[0].save(self._animation_path, None, **options) 248 249 def wait_for_fps(self) -> None: 250 """ 251 Wait/Sleep for long enough to get close to the desired FPS. 252 Not all UIs will provide a real implementation for this method. 253 """ 254 255 # No FPS limit is in place. 256 if (self._fps <= 0): 257 return 258 259 # This is the first wait request, we don't have enough information yet. 260 if (self._last_fps_wait is None): 261 self._last_fps_wait = edq.util.time.Timestamp.now() 262 return 263 264 last_time = self._last_fps_wait 265 now = edq.util.time.Timestamp.now() 266 267 duration = now.sub(last_time) 268 269 # Get the ideal number of milliseconds between frames. 270 ideal_time_between_frames_ms = 1000.0 / self._fps 271 272 # Get the wait time by comparing how long it has been since the last wait, 273 # with the ideal wait between frames. 274 wait_time_ms = ideal_time_between_frames_ms - duration.to_msecs() 275 if (wait_time_ms > 0): 276 self.sleep(int(wait_time_ms)) 277 278 # Mark the time this method completed. 279 self._last_fps_wait = edq.util.time.Timestamp.now() 280 281 def requires_sprites(self) -> bool: 282 """ Check if this specific UI needs sprites or sprite sheets. """ 283 284 return True 285 286 def sleep(self, sleep_time_ms: int) -> None: 287 """ 288 Sleep for the specified number of ms. 289 This is in a method so children can override with any more UI-specific sleep procedures. 290 """ 291 292 time.sleep(sleep_time_ms / 1000.0) 293 294 def close(self) -> None: 295 """ Close the UI and release all owned resources. """ 296 297 if (self._user_input_device is not None): 298 self._user_input_device.close() 299 300 def get_user_inputs(self) -> list[pacai.core.action.Action]: 301 """ 302 If a user input device is available, 303 get the inputs via UserInputDevice.get_inputs(). 304 If no device is available, return an empty list. 305 """ 306 307 if (self._user_input_device is None): 308 return [] 309 310 return self._user_input_device.get_inputs() 311 312 def draw_image(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> PIL.Image.Image: 313 """ 314 Visualize the state of the game as an image. 315 This method is typically used for rendering the game to an animation. 316 each call to this method is one frame in the animation. 317 """ 318 319 if (self._sprite_sheet is None): 320 raise ValueError("Cannot draw images without a sprite sheet.") 321 322 # First, check the cache for the image. 323 if (state.turn_count in self._image_cache): 324 return self._image_cache[state.turn_count] 325 326 image = self._get_static_image(state, **kwargs) 327 328 canvas = PIL.ImageDraw.Draw(image) 329 330 # Draw highlights. 331 for (position, base_intensity) in self._highlights.items(): 332 start_coord = self._position_to_image_coords(position) 333 end_coord = self._position_to_image_coords(position.add(pacai.core.board.Position(1, 1))) 334 335 # Don't let the intensity go to zero. 336 intensity = 0.10 + (0.9 * base_intensity) 337 338 highlight_color = ( 339 int(self._sprite_sheet.highlight[0] * intensity), 340 int(self._sprite_sheet.highlight[1] * intensity), 341 int(self._sprite_sheet.highlight[2] * intensity), 342 ) 343 344 canvas.rectangle([start_coord, end_coord], fill = tuple(highlight_color)) 345 346 # Draw non-agent (non-wall) markers. 347 for (marker, positions) in state.board._nonwall_objects.items(): 348 if (marker.is_agent()): 349 continue 350 351 for position in positions: 352 if (state.skip_draw(marker, position, static = False)): 353 continue 354 355 sprite = self._get_sprite(state, position, marker = marker, animation_key = ANIMATION_KEY) 356 self._place_sprite(position, sprite, image) 357 358 # Draw non-static text. 359 self._draw_position_text(state.get_nonstatic_text(), image) 360 361 # Draw agent markers. 362 for (marker, positions) in state.board._nonwall_objects.items(): 363 if (not marker.is_agent()): 364 continue 365 366 for position in positions: 367 if (state.skip_draw(marker, position, static = False)): 368 continue 369 370 last_action = state.get_last_agent_action(marker.get_agent_index()) 371 sprite = self._get_sprite(state, position, marker = marker, action = last_action, animation_key = ANIMATION_KEY) 372 self._place_sprite(position, sprite, image) 373 374 # Draw the footer (usually the score). 375 footer_text = state.get_footer_text() 376 if (footer_text is not None): 377 (base_x, base_y) = self._position_to_image_coords(pacai.core.board.Position(state.board.height, 0)) 378 self._draw_text(footer_text, base_x, base_y, canvas) 379 380 # Store this image in the cache. 381 self._image_cache[state.turn_count] = image 382 383 return image 384 385 def _get_font(self, size: pacai.core.font.FontSize) -> PIL.ImageFont.FreeTypeFont: 386 font = self._fonts.get(size, None) 387 if (font is None): 388 raise ValueError("Font has not been loaded.") 389 390 return font 391 392 def _get_static_image(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> PIL.Image.Image: 393 """ 394 Get the base image that only contains static objects. 395 This method will handle caching the base static image. 396 """ 397 398 if (self._sprite_sheet is None): 399 raise ValueError("Cannot draw images without a sprite sheet.") 400 401 # Check the cache. 402 if (self._static_base_image is not None): 403 return self._static_base_image.copy() 404 405 # Height is +1 to leave room for the score. 406 size = ( 407 state.board.width * self._sprite_sheet.width, 408 (state.board.height + 1) * self._sprite_sheet.height, 409 ) 410 411 # Add in an alpha channel to the background. 412 background_color = list(self._sprite_sheet.background) 413 background_color.append(255) 414 415 image = PIL.Image.new('RGB', size, tuple(background_color)) 416 417 # Draw wall markers. 418 for position in state.board.get_walls(): 419 if (state.skip_draw(pacai.core.board.MARKER_WALL, position, static = True)): 420 continue 421 422 adjacency = state.board.get_adjacent_walls(position) 423 sprite = self._get_sprite(state, position, marker = pacai.core.board.MARKER_WALL, adjacency = adjacency, animation_key = ANIMATION_KEY) 424 self._place_sprite(position, sprite, image) 425 426 # Draw an additional static markers. 427 for position in state.get_static_positions(): 428 for marker in state.board.get(position): 429 if (state.skip_draw(marker, position, static = True)): 430 continue 431 432 sprite = self._get_sprite(state, position, marker = marker, animation_key = ANIMATION_KEY) 433 self._place_sprite(position, sprite, image) 434 435 # Draw static text. 436 self._draw_position_text(state.get_static_text(), image) 437 438 # Cache the image. 439 self._static_base_image = image.copy() 440 441 return image 442 443 def _draw_position_text(self, board_texts: list[pacai.core.font.BoardText], image: PIL.Image.Image) -> None: 444 """ Draw text on a board position. """ 445 446 if (len(board_texts) == 0): 447 return 448 449 canvas = PIL.ImageDraw.Draw(image) 450 for board_text in board_texts: 451 # Base positions start in the upper left. 452 (base_x, base_y) = self._position_to_image_coords(board_text.position) 453 454 self._draw_text(board_text, base_x, base_y, canvas) 455 456 def _draw_text(self, 457 text: pacai.core.font.Text, 458 base_x: int, base_y: int, 459 canvas: PIL.ImageDraw.ImageDraw, 460 ) -> None: 461 """ Draw text to the board. """ 462 463 if (self._sprite_sheet is None): 464 raise ValueError("Cannot draw text without a sprite sheet.") 465 466 # Compute alignment offsets. 467 vertical_offset = self._sprite_sheet.height * text.vertical_align.value 468 horizontal_offset = self._sprite_sheet.width * text.horizontal_align.value 469 470 y = base_y + vertical_offset 471 x = base_x + horizontal_offset 472 473 color = text.color 474 if (color is None): 475 color = self._sprite_sheet.text 476 477 canvas.text((x, y), text.text, color, 478 self._get_font(text.size), 479 anchor = text.anchor, 480 align = 'center') 481 482 def _get_sprite(self, state: pacai.core.gamestate.GameState, position: pacai.core.board.Position, **kwargs: typing.Any) -> PIL.Image.Image: 483 """ Get the requested sprite. """ 484 485 if (self._sprite_sheet is None): 486 raise ValueError("Sprites are not loaded in this UI.") 487 488 return state.sprite_lookup(self._sprite_sheet, position, **kwargs) 489 490 def _place_sprite(self, position: pacai.core.board.Position, sprite: PIL.Image.Image, image: PIL.Image.Image) -> None: 491 image_coordinates = self._position_to_image_coords(position) 492 493 # Overlay the sprite onto the image. 494 # Note that the same image is used as the mask, since sprites will usually have alpha channels 495 # (so the transparent parts will not get drawn). 496 image.paste(sprite, image_coordinates, sprite) 497 498 def _position_to_image_coords(self, position: pacai.core.board.Position) -> tuple[int, int]: 499 """ 500 Get the image coordinates (in pixels) for this position. 501 Returns: (x, y). 502 """ 503 504 if (self._sprite_sheet is None): 505 raise ValueError("Sprites are not loaded.") 506 507 return self._sprite_sheet.position_to_pixels(position) 508 509 @abc.abstractmethod 510 def draw(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> None: 511 """ 512 Visualize the state of the game to the UI. 513 This is the typically the main override point for children. 514 Note that how this method visualizes the game completely unrelated 515 to how the draw_image() method works. 516 draw() will render to whatever the specific UI for the child class is, 517 while draw_image() specifically creates an image which will be used for animations. 518 If the child UI is also image-based than it can leverage draw_image(), 519 but there is no requirement to do that. 520 """ 521 522def set_cli_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: 523 """ 524 Set common CLI arguments. 525 This is a sibling to init_from_args(), as the arguments set here can be interpreted there. 526 """ 527 528 parser.add_argument('--ui', dest = 'ui', 529 action = 'store', type = str, default = pacai.util.alias.UI_WEB.short, 530 help = ('Set the UI/graphics to use (default: %(default)s).' 531 + ' Builtin options:' 532 + f' `{pacai.util.alias.UI_NULL.short}` (`{pacai.util.alias.UI_NULL.long}`)' 533 + ' -- Do not show any ui/graphics (best if you want to run fast and just need the result),' 534 + f' `{pacai.util.alias.UI_STDIO.short}` (`{pacai.util.alias.UI_STDIO.long}`)' 535 + ' -- Use stdin/stdout from the terminal,' 536 + f' `{pacai.util.alias.UI_TK.short}` (`{pacai.util.alias.UI_TK.long}`)' 537 + ' -- Use Tk/tkinter (must already be installed) to open a window,' 538 + f' `{pacai.util.alias.UI_WEB.short}` (`{pacai.util.alias.UI_WEB.long}`)' 539 + ' -- Launch a browser window (default).')) 540 541 parser.add_argument('--show-training-ui', dest = 'show_training_ui', 542 action = 'store_true', default = False, 543 help = 'Show the specified UI (--ui) for training epochs/games. Otherwise, a null UI will be used (default: %(default)s).') 544 545 parser.add_argument('--fps', dest = 'fps', 546 action = 'store', type = int, default = DEFAULT_FPS, 547 help = ('Set the visual speed (frames per second) for UIs (default: %(default)s).' 548 + ' Lower values are slower, and higher values are faster.')) 549 550 parser.add_argument('--animation-path', dest = 'animation_path', 551 action = 'store', type = str, default = None, 552 help = ('If specified, store an animated recording of the game at the specified location.' 553 + f" This path must have one of the following extensions: {ANIMATION_EXTS}.")) 554 555 parser.add_argument('--animation-fps', dest = 'animation_fps', 556 action = 'store', type = int, default = DEFAULT_ANIMATION_FPS, 557 help = 'Set the fps of the animation (default: %(default)s).') 558 559 parser.add_argument('--animation-skip-frames', dest = 'animation_skip_frames', 560 action = 'store', type = int, default = DEFAULT_ANIMATION_SKIP_FRAMES, 561 help = ('Only include every X frames in the animation.' 562 + ' The default (1) means that every frame will be included.' 563 + ' Using higher values can reduce the animations size and processing time' 564 + ' (since there are fewer frames).')) 565 566 parser.add_argument('--animation-optimize', dest = 'animation_optimize', 567 action = 'store_true', default = DEFAULT_ANIMATION_OPTIMIZE, 568 help = 'Optimize the animation to reduce file size (will take longer) (default: %(default)s).') 569 570 return parser 571 572def init_from_args( 573 args: argparse.Namespace, 574 num_uis: int = 0, 575 null_out_uis: int = 0, 576 additional_args: dict | None = None, 577 ) -> argparse.Namespace: 578 """ 579 Take in args from a parser that was passed to set_cli_args(), 580 and initialize the proper components. 581 Constructed UIs will be placed `args._uis`. 582 If `num_uis` is not provided (or <= 0), 583 then `args.num_games` + `args.num_training` will be used. 584 If `null_out_uis` is > 0, then at most that number of UIs (starting at the beginning) 585 will be converted to null UIs. 586 This will not change the total number of UIs, just null out the first number of UIs. 587 """ 588 589 ui_args = { 590 'fps': args.fps, 591 'animation_path': args.animation_path, 592 'animation_fps': args.animation_fps, 593 'animation_skip_frames': args.animation_skip_frames, 594 'animation_optimize': args.animation_optimize, 595 } 596 597 if (additional_args is not None): 598 ui_args.update(additional_args) 599 600 if (num_uis <= 0): 601 num_uis = args.num_games + args.num_training 602 603 uis = [] 604 for i in range(num_uis): 605 ui_name = args.ui 606 if (i < null_out_uis): 607 ui_name = pacai.util.alias.UI_NULL.long 608 609 uis.append(pacai.util.reflection.new_object(ui_name, **ui_args)) 610 611 setattr(args, '_uis', uis) 612 613 return args
The allowed extensions for animation files.
A character to action mapping using the common WASD scheme.
A character to action mapping using the arrow keys.
A character to action mapping that uses both WASD_CHAR_MAPPING and ARROW_CHAR_MAPPING.
82class UserInputDevice(abc.ABC): 83 """ 84 This class provides a way for users to convey inputs through a UI. 85 Not all UIs will support user input. 86 """ 87 88 @abc.abstractmethod 89 def get_inputs(self) -> list[pacai.core.action.Action]: 90 """ 91 Get any inputs that have occurred since the last call to this method. 92 This method is responsible for not returning the same input instance in subsequent calls. 93 The last input in the returned list should be the most recent input. 94 """ 95 96 def close(self) -> None: 97 """ Close the user input channel and release all owned resources. """
This class provides a way for users to convey inputs through a UI. Not all UIs will support user input.
88 @abc.abstractmethod 89 def get_inputs(self) -> list[pacai.core.action.Action]: 90 """ 91 Get any inputs that have occurred since the last call to this method. 92 This method is responsible for not returning the same input instance in subsequent calls. 93 The last input in the returned list should be the most recent input. 94 """
Get any inputs that have occurred since the last call to this method. This method is responsible for not returning the same input instance in subsequent calls. The last input in the returned list should be the most recent input.
99class UI(abc.ABC): 100 """ 101 UIs represent the basic way that a game interacts with the user, 102 by displaying the state of the game and taking input from the user (if applicable). 103 """ 104 105 def __init__(self, 106 user_input_device: UserInputDevice | None = None, 107 fps: int = DEFAULT_FPS, 108 animation_path: str | None = None, 109 animation_optimize: bool = DEFAULT_ANIMATION_OPTIMIZE, 110 animation_fps: int = DEFAULT_ANIMATION_FPS, 111 animation_skip_frames: int = DEFAULT_ANIMATION_SKIP_FRAMES, 112 sprite_sheet_path: str = DEFAULT_SPRITE_SHEET, 113 font_path: str = pacai.core.font.DEFAULT_FONT_PATH, 114 **kwargs: typing.Any) -> None: 115 self._user_input_device: UserInputDevice | None = user_input_device 116 """ The device to use to get user input. """ 117 118 self._fps: int = fps 119 """ 120 The desired frames per second this game will be displayed at. 121 Zero or lower values will be ignored. 122 This is just a suggestion that the game will try an accommodate. 123 Not all UIs will observe fps. 124 """ 125 126 self._last_fps_wait: edq.util.time.Timestamp | None = None 127 """ 128 Keep track of the last time the UI waited to adjust the fps. 129 We need this information to compute the next wait time. 130 """ 131 132 self._update_count: int = 0 133 """ Keep track of the number of times update() has been called. """ 134 135 self._animation_path: str | None = animation_path 136 """ If specified, create a animation and write it to this location after the game completes. """ 137 138 if (self._animation_path is not None): 139 if (os.path.splitext(self._animation_path)[-1] not in ANIMATION_EXTS): 140 raise ValueError(f"Animation path must have one of the following extensions {ANIMATION_EXTS}, found '{self._animation_path}'.") 141 142 self._animation_optimize: bool = animation_optimize 143 """ Optimize the animation output to reduce file size. """ 144 145 self._animation_fps: int = max(MIN_ANIMATION_FPS, animation_fps) 146 """ The frame rate for the animation. """ 147 148 self._animation_skip_frames: int = max(1, animation_skip_frames) 149 """ 150 Skip this many frames between drawing animation frames. 151 This can help speed up animation creation by leaving out less important frames. 152 For example, this can be set to the number of agents to only draw frames after all agents have moved. 153 """ 154 155 self._animation_frames: list[PIL.Image.Image] = [] 156 """ The frames for the animation (one per call to update(). """ 157 158 self._static_base_image: PIL.Image.Image | None = None 159 """ 160 Cache an image that has all of the static (non-changing) elements (like walls) drawn. 161 This can be reused as the base image every time we draw an image. 162 """ 163 164 # Only load sprites (and fonts) if we need them. 165 sprite_sheet = None 166 fonts = {} 167 if (self.requires_sprites() or (self._animation_path is not None)): 168 sprite_sheet = pacai.core.spritesheet.load(sprite_sheet_path) 169 170 for font_size in pacai.core.font.FontSize: 171 fonts[font_size] = PIL.ImageFont.truetype(font_path, int(sprite_sheet.height * font_size.value)) 172 173 self._sprite_sheet: pacai.core.spritesheet.SpriteSheet | None = sprite_sheet 174 """ The sprite sheet to use for this UI. """ 175 176 self._fonts: dict[pacai.core.font.FontSize, PIL.ImageFont.FreeTypeFont] = fonts 177 """ The available fonts indexed by size. """ 178 179 self._image_cache: dict[int, PIL.Image.Image] = {} 180 """ Cache images (by game state turn count) to avoid redrawing images. """ 181 182 self._highlights: dict[pacai.core.board.Position, float] = {} 183 """ The current set of board highlights. """ 184 185 def update(self, 186 state: pacai.core.gamestate.GameState, 187 force_draw_image: bool = False, 188 board_highlights: list[pacai.core.board.Highlight] | None = None, 189 ) -> None: 190 """ 191 Update the UI with the current state of the game. 192 This is the main entry point for the game into the UI. 193 """ 194 195 self.wait_for_fps() 196 197 if (board_highlights is None): 198 board_highlights = [] 199 200 for board_highlight in board_highlights: 201 intensity = board_highlight.get_float_intensity() 202 if (intensity is None): 203 self._highlights.pop(board_highlight.position, None) 204 else: 205 self._highlights[board_highlight.position] = intensity 206 207 if ((self._animation_path is not None) and (force_draw_image or (self._update_count % self._animation_skip_frames == 0))): 208 image = self.draw_image(state) 209 self._animation_frames.append(image) 210 211 self.draw(state) 212 213 self._update_count += 1 214 215 def game_start(self, 216 initial_state: pacai.core.gamestate.GameState, 217 board_highlights: list[pacai.core.board.Highlight] | None = None, 218 ) -> None: 219 """ Initialize the UI with the game's initial state. """ 220 221 self.update(initial_state, board_highlights = board_highlights, force_draw_image = True) 222 223 def game_complete(self, 224 final_state: pacai.core.gamestate.GameState, 225 board_highlights: list[pacai.core.board.Highlight] | None = None, 226 ) -> None: 227 """ Update the UI with the game's final state. """ 228 229 self.update(final_state, board_highlights = board_highlights, force_draw_image = True) 230 231 # Write the animation. 232 if ((self._animation_path is not None) and (len(self._animation_frames) > 0)): 233 ms_per_frame = int(1.0 / self._animation_fps * 1000.0) 234 235 options = { 236 'save_all': True, 237 'append_images': self._animation_frames, 238 'duration': ms_per_frame, 239 'loop': 0, 240 'optimize': False, 241 'minimize_size': False, 242 } 243 244 if (self._animation_optimize): 245 options['optimize'] = True 246 options['minimize_size'] = True 247 248 self._animation_frames[0].save(self._animation_path, None, **options) 249 250 def wait_for_fps(self) -> None: 251 """ 252 Wait/Sleep for long enough to get close to the desired FPS. 253 Not all UIs will provide a real implementation for this method. 254 """ 255 256 # No FPS limit is in place. 257 if (self._fps <= 0): 258 return 259 260 # This is the first wait request, we don't have enough information yet. 261 if (self._last_fps_wait is None): 262 self._last_fps_wait = edq.util.time.Timestamp.now() 263 return 264 265 last_time = self._last_fps_wait 266 now = edq.util.time.Timestamp.now() 267 268 duration = now.sub(last_time) 269 270 # Get the ideal number of milliseconds between frames. 271 ideal_time_between_frames_ms = 1000.0 / self._fps 272 273 # Get the wait time by comparing how long it has been since the last wait, 274 # with the ideal wait between frames. 275 wait_time_ms = ideal_time_between_frames_ms - duration.to_msecs() 276 if (wait_time_ms > 0): 277 self.sleep(int(wait_time_ms)) 278 279 # Mark the time this method completed. 280 self._last_fps_wait = edq.util.time.Timestamp.now() 281 282 def requires_sprites(self) -> bool: 283 """ Check if this specific UI needs sprites or sprite sheets. """ 284 285 return True 286 287 def sleep(self, sleep_time_ms: int) -> None: 288 """ 289 Sleep for the specified number of ms. 290 This is in a method so children can override with any more UI-specific sleep procedures. 291 """ 292 293 time.sleep(sleep_time_ms / 1000.0) 294 295 def close(self) -> None: 296 """ Close the UI and release all owned resources. """ 297 298 if (self._user_input_device is not None): 299 self._user_input_device.close() 300 301 def get_user_inputs(self) -> list[pacai.core.action.Action]: 302 """ 303 If a user input device is available, 304 get the inputs via UserInputDevice.get_inputs(). 305 If no device is available, return an empty list. 306 """ 307 308 if (self._user_input_device is None): 309 return [] 310 311 return self._user_input_device.get_inputs() 312 313 def draw_image(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> PIL.Image.Image: 314 """ 315 Visualize the state of the game as an image. 316 This method is typically used for rendering the game to an animation. 317 each call to this method is one frame in the animation. 318 """ 319 320 if (self._sprite_sheet is None): 321 raise ValueError("Cannot draw images without a sprite sheet.") 322 323 # First, check the cache for the image. 324 if (state.turn_count in self._image_cache): 325 return self._image_cache[state.turn_count] 326 327 image = self._get_static_image(state, **kwargs) 328 329 canvas = PIL.ImageDraw.Draw(image) 330 331 # Draw highlights. 332 for (position, base_intensity) in self._highlights.items(): 333 start_coord = self._position_to_image_coords(position) 334 end_coord = self._position_to_image_coords(position.add(pacai.core.board.Position(1, 1))) 335 336 # Don't let the intensity go to zero. 337 intensity = 0.10 + (0.9 * base_intensity) 338 339 highlight_color = ( 340 int(self._sprite_sheet.highlight[0] * intensity), 341 int(self._sprite_sheet.highlight[1] * intensity), 342 int(self._sprite_sheet.highlight[2] * intensity), 343 ) 344 345 canvas.rectangle([start_coord, end_coord], fill = tuple(highlight_color)) 346 347 # Draw non-agent (non-wall) markers. 348 for (marker, positions) in state.board._nonwall_objects.items(): 349 if (marker.is_agent()): 350 continue 351 352 for position in positions: 353 if (state.skip_draw(marker, position, static = False)): 354 continue 355 356 sprite = self._get_sprite(state, position, marker = marker, animation_key = ANIMATION_KEY) 357 self._place_sprite(position, sprite, image) 358 359 # Draw non-static text. 360 self._draw_position_text(state.get_nonstatic_text(), image) 361 362 # Draw agent markers. 363 for (marker, positions) in state.board._nonwall_objects.items(): 364 if (not marker.is_agent()): 365 continue 366 367 for position in positions: 368 if (state.skip_draw(marker, position, static = False)): 369 continue 370 371 last_action = state.get_last_agent_action(marker.get_agent_index()) 372 sprite = self._get_sprite(state, position, marker = marker, action = last_action, animation_key = ANIMATION_KEY) 373 self._place_sprite(position, sprite, image) 374 375 # Draw the footer (usually the score). 376 footer_text = state.get_footer_text() 377 if (footer_text is not None): 378 (base_x, base_y) = self._position_to_image_coords(pacai.core.board.Position(state.board.height, 0)) 379 self._draw_text(footer_text, base_x, base_y, canvas) 380 381 # Store this image in the cache. 382 self._image_cache[state.turn_count] = image 383 384 return image 385 386 def _get_font(self, size: pacai.core.font.FontSize) -> PIL.ImageFont.FreeTypeFont: 387 font = self._fonts.get(size, None) 388 if (font is None): 389 raise ValueError("Font has not been loaded.") 390 391 return font 392 393 def _get_static_image(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> PIL.Image.Image: 394 """ 395 Get the base image that only contains static objects. 396 This method will handle caching the base static image. 397 """ 398 399 if (self._sprite_sheet is None): 400 raise ValueError("Cannot draw images without a sprite sheet.") 401 402 # Check the cache. 403 if (self._static_base_image is not None): 404 return self._static_base_image.copy() 405 406 # Height is +1 to leave room for the score. 407 size = ( 408 state.board.width * self._sprite_sheet.width, 409 (state.board.height + 1) * self._sprite_sheet.height, 410 ) 411 412 # Add in an alpha channel to the background. 413 background_color = list(self._sprite_sheet.background) 414 background_color.append(255) 415 416 image = PIL.Image.new('RGB', size, tuple(background_color)) 417 418 # Draw wall markers. 419 for position in state.board.get_walls(): 420 if (state.skip_draw(pacai.core.board.MARKER_WALL, position, static = True)): 421 continue 422 423 adjacency = state.board.get_adjacent_walls(position) 424 sprite = self._get_sprite(state, position, marker = pacai.core.board.MARKER_WALL, adjacency = adjacency, animation_key = ANIMATION_KEY) 425 self._place_sprite(position, sprite, image) 426 427 # Draw an additional static markers. 428 for position in state.get_static_positions(): 429 for marker in state.board.get(position): 430 if (state.skip_draw(marker, position, static = True)): 431 continue 432 433 sprite = self._get_sprite(state, position, marker = marker, animation_key = ANIMATION_KEY) 434 self._place_sprite(position, sprite, image) 435 436 # Draw static text. 437 self._draw_position_text(state.get_static_text(), image) 438 439 # Cache the image. 440 self._static_base_image = image.copy() 441 442 return image 443 444 def _draw_position_text(self, board_texts: list[pacai.core.font.BoardText], image: PIL.Image.Image) -> None: 445 """ Draw text on a board position. """ 446 447 if (len(board_texts) == 0): 448 return 449 450 canvas = PIL.ImageDraw.Draw(image) 451 for board_text in board_texts: 452 # Base positions start in the upper left. 453 (base_x, base_y) = self._position_to_image_coords(board_text.position) 454 455 self._draw_text(board_text, base_x, base_y, canvas) 456 457 def _draw_text(self, 458 text: pacai.core.font.Text, 459 base_x: int, base_y: int, 460 canvas: PIL.ImageDraw.ImageDraw, 461 ) -> None: 462 """ Draw text to the board. """ 463 464 if (self._sprite_sheet is None): 465 raise ValueError("Cannot draw text without a sprite sheet.") 466 467 # Compute alignment offsets. 468 vertical_offset = self._sprite_sheet.height * text.vertical_align.value 469 horizontal_offset = self._sprite_sheet.width * text.horizontal_align.value 470 471 y = base_y + vertical_offset 472 x = base_x + horizontal_offset 473 474 color = text.color 475 if (color is None): 476 color = self._sprite_sheet.text 477 478 canvas.text((x, y), text.text, color, 479 self._get_font(text.size), 480 anchor = text.anchor, 481 align = 'center') 482 483 def _get_sprite(self, state: pacai.core.gamestate.GameState, position: pacai.core.board.Position, **kwargs: typing.Any) -> PIL.Image.Image: 484 """ Get the requested sprite. """ 485 486 if (self._sprite_sheet is None): 487 raise ValueError("Sprites are not loaded in this UI.") 488 489 return state.sprite_lookup(self._sprite_sheet, position, **kwargs) 490 491 def _place_sprite(self, position: pacai.core.board.Position, sprite: PIL.Image.Image, image: PIL.Image.Image) -> None: 492 image_coordinates = self._position_to_image_coords(position) 493 494 # Overlay the sprite onto the image. 495 # Note that the same image is used as the mask, since sprites will usually have alpha channels 496 # (so the transparent parts will not get drawn). 497 image.paste(sprite, image_coordinates, sprite) 498 499 def _position_to_image_coords(self, position: pacai.core.board.Position) -> tuple[int, int]: 500 """ 501 Get the image coordinates (in pixels) for this position. 502 Returns: (x, y). 503 """ 504 505 if (self._sprite_sheet is None): 506 raise ValueError("Sprites are not loaded.") 507 508 return self._sprite_sheet.position_to_pixels(position) 509 510 @abc.abstractmethod 511 def draw(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> None: 512 """ 513 Visualize the state of the game to the UI. 514 This is the typically the main override point for children. 515 Note that how this method visualizes the game completely unrelated 516 to how the draw_image() method works. 517 draw() will render to whatever the specific UI for the child class is, 518 while draw_image() specifically creates an image which will be used for animations. 519 If the child UI is also image-based than it can leverage draw_image(), 520 but there is no requirement to do that. 521 """
UIs represent the basic way that a game interacts with the user, by displaying the state of the game and taking input from the user (if applicable).
185 def update(self, 186 state: pacai.core.gamestate.GameState, 187 force_draw_image: bool = False, 188 board_highlights: list[pacai.core.board.Highlight] | None = None, 189 ) -> None: 190 """ 191 Update the UI with the current state of the game. 192 This is the main entry point for the game into the UI. 193 """ 194 195 self.wait_for_fps() 196 197 if (board_highlights is None): 198 board_highlights = [] 199 200 for board_highlight in board_highlights: 201 intensity = board_highlight.get_float_intensity() 202 if (intensity is None): 203 self._highlights.pop(board_highlight.position, None) 204 else: 205 self._highlights[board_highlight.position] = intensity 206 207 if ((self._animation_path is not None) and (force_draw_image or (self._update_count % self._animation_skip_frames == 0))): 208 image = self.draw_image(state) 209 self._animation_frames.append(image) 210 211 self.draw(state) 212 213 self._update_count += 1
Update the UI with the current state of the game. This is the main entry point for the game into the UI.
215 def game_start(self, 216 initial_state: pacai.core.gamestate.GameState, 217 board_highlights: list[pacai.core.board.Highlight] | None = None, 218 ) -> None: 219 """ Initialize the UI with the game's initial state. """ 220 221 self.update(initial_state, board_highlights = board_highlights, force_draw_image = True)
Initialize the UI with the game's initial state.
223 def game_complete(self, 224 final_state: pacai.core.gamestate.GameState, 225 board_highlights: list[pacai.core.board.Highlight] | None = None, 226 ) -> None: 227 """ Update the UI with the game's final state. """ 228 229 self.update(final_state, board_highlights = board_highlights, force_draw_image = True) 230 231 # Write the animation. 232 if ((self._animation_path is not None) and (len(self._animation_frames) > 0)): 233 ms_per_frame = int(1.0 / self._animation_fps * 1000.0) 234 235 options = { 236 'save_all': True, 237 'append_images': self._animation_frames, 238 'duration': ms_per_frame, 239 'loop': 0, 240 'optimize': False, 241 'minimize_size': False, 242 } 243 244 if (self._animation_optimize): 245 options['optimize'] = True 246 options['minimize_size'] = True 247 248 self._animation_frames[0].save(self._animation_path, None, **options)
Update the UI with the game's final state.
250 def wait_for_fps(self) -> None: 251 """ 252 Wait/Sleep for long enough to get close to the desired FPS. 253 Not all UIs will provide a real implementation for this method. 254 """ 255 256 # No FPS limit is in place. 257 if (self._fps <= 0): 258 return 259 260 # This is the first wait request, we don't have enough information yet. 261 if (self._last_fps_wait is None): 262 self._last_fps_wait = edq.util.time.Timestamp.now() 263 return 264 265 last_time = self._last_fps_wait 266 now = edq.util.time.Timestamp.now() 267 268 duration = now.sub(last_time) 269 270 # Get the ideal number of milliseconds between frames. 271 ideal_time_between_frames_ms = 1000.0 / self._fps 272 273 # Get the wait time by comparing how long it has been since the last wait, 274 # with the ideal wait between frames. 275 wait_time_ms = ideal_time_between_frames_ms - duration.to_msecs() 276 if (wait_time_ms > 0): 277 self.sleep(int(wait_time_ms)) 278 279 # Mark the time this method completed. 280 self._last_fps_wait = edq.util.time.Timestamp.now()
Wait/Sleep for long enough to get close to the desired FPS. Not all UIs will provide a real implementation for this method.
282 def requires_sprites(self) -> bool: 283 """ Check if this specific UI needs sprites or sprite sheets. """ 284 285 return True
Check if this specific UI needs sprites or sprite sheets.
287 def sleep(self, sleep_time_ms: int) -> None: 288 """ 289 Sleep for the specified number of ms. 290 This is in a method so children can override with any more UI-specific sleep procedures. 291 """ 292 293 time.sleep(sleep_time_ms / 1000.0)
Sleep for the specified number of ms. This is in a method so children can override with any more UI-specific sleep procedures.
295 def close(self) -> None: 296 """ Close the UI and release all owned resources. """ 297 298 if (self._user_input_device is not None): 299 self._user_input_device.close()
Close the UI and release all owned resources.
301 def get_user_inputs(self) -> list[pacai.core.action.Action]: 302 """ 303 If a user input device is available, 304 get the inputs via UserInputDevice.get_inputs(). 305 If no device is available, return an empty list. 306 """ 307 308 if (self._user_input_device is None): 309 return [] 310 311 return self._user_input_device.get_inputs()
If a user input device is available, get the inputs via UserInputDevice.get_inputs(). If no device is available, return an empty list.
313 def draw_image(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> PIL.Image.Image: 314 """ 315 Visualize the state of the game as an image. 316 This method is typically used for rendering the game to an animation. 317 each call to this method is one frame in the animation. 318 """ 319 320 if (self._sprite_sheet is None): 321 raise ValueError("Cannot draw images without a sprite sheet.") 322 323 # First, check the cache for the image. 324 if (state.turn_count in self._image_cache): 325 return self._image_cache[state.turn_count] 326 327 image = self._get_static_image(state, **kwargs) 328 329 canvas = PIL.ImageDraw.Draw(image) 330 331 # Draw highlights. 332 for (position, base_intensity) in self._highlights.items(): 333 start_coord = self._position_to_image_coords(position) 334 end_coord = self._position_to_image_coords(position.add(pacai.core.board.Position(1, 1))) 335 336 # Don't let the intensity go to zero. 337 intensity = 0.10 + (0.9 * base_intensity) 338 339 highlight_color = ( 340 int(self._sprite_sheet.highlight[0] * intensity), 341 int(self._sprite_sheet.highlight[1] * intensity), 342 int(self._sprite_sheet.highlight[2] * intensity), 343 ) 344 345 canvas.rectangle([start_coord, end_coord], fill = tuple(highlight_color)) 346 347 # Draw non-agent (non-wall) markers. 348 for (marker, positions) in state.board._nonwall_objects.items(): 349 if (marker.is_agent()): 350 continue 351 352 for position in positions: 353 if (state.skip_draw(marker, position, static = False)): 354 continue 355 356 sprite = self._get_sprite(state, position, marker = marker, animation_key = ANIMATION_KEY) 357 self._place_sprite(position, sprite, image) 358 359 # Draw non-static text. 360 self._draw_position_text(state.get_nonstatic_text(), image) 361 362 # Draw agent markers. 363 for (marker, positions) in state.board._nonwall_objects.items(): 364 if (not marker.is_agent()): 365 continue 366 367 for position in positions: 368 if (state.skip_draw(marker, position, static = False)): 369 continue 370 371 last_action = state.get_last_agent_action(marker.get_agent_index()) 372 sprite = self._get_sprite(state, position, marker = marker, action = last_action, animation_key = ANIMATION_KEY) 373 self._place_sprite(position, sprite, image) 374 375 # Draw the footer (usually the score). 376 footer_text = state.get_footer_text() 377 if (footer_text is not None): 378 (base_x, base_y) = self._position_to_image_coords(pacai.core.board.Position(state.board.height, 0)) 379 self._draw_text(footer_text, base_x, base_y, canvas) 380 381 # Store this image in the cache. 382 self._image_cache[state.turn_count] = image 383 384 return image
Visualize the state of the game as an image. This method is typically used for rendering the game to an animation. each call to this method is one frame in the animation.
510 @abc.abstractmethod 511 def draw(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> None: 512 """ 513 Visualize the state of the game to the UI. 514 This is the typically the main override point for children. 515 Note that how this method visualizes the game completely unrelated 516 to how the draw_image() method works. 517 draw() will render to whatever the specific UI for the child class is, 518 while draw_image() specifically creates an image which will be used for animations. 519 If the child UI is also image-based than it can leverage draw_image(), 520 but there is no requirement to do that. 521 """
Visualize the state of the game to the UI. This is the typically the main override point for children. Note that how this method visualizes the game completely unrelated to how the draw_image() method works. draw() will render to whatever the specific UI for the child class is, while draw_image() specifically creates an image which will be used for animations. If the child UI is also image-based than it can leverage draw_image(), but there is no requirement to do that.
523def set_cli_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: 524 """ 525 Set common CLI arguments. 526 This is a sibling to init_from_args(), as the arguments set here can be interpreted there. 527 """ 528 529 parser.add_argument('--ui', dest = 'ui', 530 action = 'store', type = str, default = pacai.util.alias.UI_WEB.short, 531 help = ('Set the UI/graphics to use (default: %(default)s).' 532 + ' Builtin options:' 533 + f' `{pacai.util.alias.UI_NULL.short}` (`{pacai.util.alias.UI_NULL.long}`)' 534 + ' -- Do not show any ui/graphics (best if you want to run fast and just need the result),' 535 + f' `{pacai.util.alias.UI_STDIO.short}` (`{pacai.util.alias.UI_STDIO.long}`)' 536 + ' -- Use stdin/stdout from the terminal,' 537 + f' `{pacai.util.alias.UI_TK.short}` (`{pacai.util.alias.UI_TK.long}`)' 538 + ' -- Use Tk/tkinter (must already be installed) to open a window,' 539 + f' `{pacai.util.alias.UI_WEB.short}` (`{pacai.util.alias.UI_WEB.long}`)' 540 + ' -- Launch a browser window (default).')) 541 542 parser.add_argument('--show-training-ui', dest = 'show_training_ui', 543 action = 'store_true', default = False, 544 help = 'Show the specified UI (--ui) for training epochs/games. Otherwise, a null UI will be used (default: %(default)s).') 545 546 parser.add_argument('--fps', dest = 'fps', 547 action = 'store', type = int, default = DEFAULT_FPS, 548 help = ('Set the visual speed (frames per second) for UIs (default: %(default)s).' 549 + ' Lower values are slower, and higher values are faster.')) 550 551 parser.add_argument('--animation-path', dest = 'animation_path', 552 action = 'store', type = str, default = None, 553 help = ('If specified, store an animated recording of the game at the specified location.' 554 + f" This path must have one of the following extensions: {ANIMATION_EXTS}.")) 555 556 parser.add_argument('--animation-fps', dest = 'animation_fps', 557 action = 'store', type = int, default = DEFAULT_ANIMATION_FPS, 558 help = 'Set the fps of the animation (default: %(default)s).') 559 560 parser.add_argument('--animation-skip-frames', dest = 'animation_skip_frames', 561 action = 'store', type = int, default = DEFAULT_ANIMATION_SKIP_FRAMES, 562 help = ('Only include every X frames in the animation.' 563 + ' The default (1) means that every frame will be included.' 564 + ' Using higher values can reduce the animations size and processing time' 565 + ' (since there are fewer frames).')) 566 567 parser.add_argument('--animation-optimize', dest = 'animation_optimize', 568 action = 'store_true', default = DEFAULT_ANIMATION_OPTIMIZE, 569 help = 'Optimize the animation to reduce file size (will take longer) (default: %(default)s).') 570 571 return parser
Set common CLI arguments. This is a sibling to init_from_args(), as the arguments set here can be interpreted there.
573def init_from_args( 574 args: argparse.Namespace, 575 num_uis: int = 0, 576 null_out_uis: int = 0, 577 additional_args: dict | None = None, 578 ) -> argparse.Namespace: 579 """ 580 Take in args from a parser that was passed to set_cli_args(), 581 and initialize the proper components. 582 Constructed UIs will be placed `args._uis`. 583 If `num_uis` is not provided (or <= 0), 584 then `args.num_games` + `args.num_training` will be used. 585 If `null_out_uis` is > 0, then at most that number of UIs (starting at the beginning) 586 will be converted to null UIs. 587 This will not change the total number of UIs, just null out the first number of UIs. 588 """ 589 590 ui_args = { 591 'fps': args.fps, 592 'animation_path': args.animation_path, 593 'animation_fps': args.animation_fps, 594 'animation_skip_frames': args.animation_skip_frames, 595 'animation_optimize': args.animation_optimize, 596 } 597 598 if (additional_args is not None): 599 ui_args.update(additional_args) 600 601 if (num_uis <= 0): 602 num_uis = args.num_games + args.num_training 603 604 uis = [] 605 for i in range(num_uis): 606 ui_name = args.ui 607 if (i < null_out_uis): 608 ui_name = pacai.util.alias.UI_NULL.long 609 610 uis.append(pacai.util.reflection.new_object(ui_name, **ui_args)) 611 612 setattr(args, '_uis', uis) 613 614 return args
Take in args from a parser that was passed to set_cli_args(),
and initialize the proper components.
Constructed UIs will be placed args._uis.
If num_uis is not provided (or <= 0),
then args.num_games + args.num_training will be used.
If null_out_uis is > 0, then at most that number of UIs (starting at the beginning)
will be converted to null UIs.
This will not change the total number of UIs, just null out the first number of UIs.