diff --git a/game/common/map/game_board.py b/game/common/map/game_board.py index d47d8fb..d7a1ebc 100644 --- a/game/common/map/game_board.py +++ b/game/common/map/game_board.py @@ -249,10 +249,10 @@ def __help_populate(self, vector_list: list[Vector], game_object_list: list[Game temp_tile: GameObject = self.game_map[vector.y][vector.x] - while hasattr(temp_tile.occupied_by, 'occupied_by'): + while temp_tile.occupied_by is not None and hasattr(temp_tile.occupied_by, 'occupied_by'): temp_tile = temp_tile.occupied_by - if temp_tile is None: + if temp_tile.occupied_by is not None: raise ValueError("Last item on the given tile doesn't have the 'occupied_by' attribute.") temp_tile.occupied_by = game_object @@ -263,13 +263,12 @@ def __help_populate(self, vector_list: list[Vector], game_object_list: list[Game # stack remaining game_objects on last vector temp_tile: GameObject = self.game_map[last_vec.y][last_vec.x] - while hasattr(temp_tile.occupied_by, 'occupied_by'): + while temp_tile.occupied_by is not None and hasattr(temp_tile.occupied_by, 'occupied_by'): temp_tile = temp_tile.occupied_by for game_object in remaining_objects: - if temp_tile is None: + if not hasattr(temp_tile, 'occupied_by') or temp_tile.occupied_by is not None: raise ValueError("Last item on the given tile doesn't have the 'occupied_by' attribute.") - temp_tile.occupied_by = game_object temp_tile = temp_tile.occupied_by diff --git a/game/common/map/tile.py b/game/common/map/tile.py index 3079a25..58060f8 100644 --- a/game/common/map/tile.py +++ b/game/common/map/tile.py @@ -21,7 +21,7 @@ class Tile(Occupiable): inherit from this class. """ def __init__(self, occupied_by: GameObject = None): - super().__init__() + super().__init__(occupied_by) self.object_type: ObjectType = ObjectType.TILE def from_json(self, data: dict) -> Self: diff --git a/game/test_suite/tests/test_movement_controller_if_stations.py b/game/test_suite/tests/test_movement_controller_if_stations.py index 4aba84b..5b3fb1f 100644 --- a/game/test_suite/tests/test_movement_controller_if_stations.py +++ b/game/test_suite/tests/test_movement_controller_if_stations.py @@ -27,10 +27,7 @@ def setUp(self) -> None: Vector(3, 1), Vector(3, 2)): [Station(None), Station(None), Station(None), Station(None), Station(None), Station(None), Station(None), Station(None)]} - - self.occ_station = OccupiableStation() - self.game_board = GameBoard(0, Vector(4, 4), self.locations, True) - self.wall = Wall() + self.game_board = GameBoard(0, Vector(4, 4), self.locations, False) # test movements up, down, left and right by starting with default 3,3 then know if it changes from there \/ self.avatar = Avatar(Vector(2, 2), 1) self.client = Player(None, None, [], self.avatar) diff --git a/requirements.txt b/requirements.txt index c5135ff..a18074a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,11 @@ tqdm~=4.65.0 pygame~=2.3.0 numpy~=1.24.2 -Pillow~=9.5.0 opencv-python~=4.8.0.76 Sphinx~=7.0.1 Myst-Parser~=2.0.0 furo~=2023.7.26 parsec~=3.15 -opencv-python~=4.8.0.76 -Sphinx~=7.0.1 -Myst-Parser~=2.0.0 -furo~=2023.7.26 SQLAlchemy~=2.0.2 pydantic~=2.3.0 fastapi[all]~=0.103.1 diff --git a/visualizer/adapter.py b/visualizer/adapter.py index 9da206b..dc749fd 100644 --- a/visualizer/adapter.py +++ b/visualizer/adapter.py @@ -9,7 +9,6 @@ from game.utils.vector import Vector from visualizer.utils.text import Text from visualizer.utils.button import Button, ButtonColors -from visualizer.utils.sidebars import Sidebars from visualizer.bytesprites.bytesprite import ByteSprite from visualizer.templates.menu_templates import Basic, MenuTemplate from visualizer.templates.playback_template import PlaybackTemplate, PlaybackButtons @@ -90,7 +89,6 @@ def populate_bytesprite_factories(self) -> dict[int: Callable[[pygame.Surface], def render(self) -> None: # self.button.render() # any logic for rendering text, buttons, and other visuals - # to access sidebars do sidebars.[whichever sidebar you are doing] text = Text(self.screen, f'{self.turn_number} / {self.turn_max}', 48) text.rect.center = Vector.add_vectors(Vector(*self.screen.get_rect().midtop), Vector(0, 50)).as_tuple() text.render() diff --git a/visualizer/bytesprites/bytesprite.py b/visualizer/bytesprites/bytesprite.py index 8d5bd33..35974b8 100644 --- a/visualizer/bytesprites/bytesprite.py +++ b/visualizer/bytesprites/bytesprite.py @@ -9,10 +9,97 @@ class ByteSprite(pyg.sprite.Sprite): + """ + `ByteSprite Class Notes:` + + PyGame Notes + ------------ + Here are listed definitions of the PyGame objects that are used in this file: + PyGame.Rect: + "An object for storing rectangular coordinates." This is used to help position things on the screen. + + PyGame.Surface: + "An object for representing images." This is used mostly for getting the screen and individual images in + a spritesheet. + + + Class Variables + --------------- + Active Sheet: + The active_sheet is the list of images (sprites) that is currently being used. In other words, it's a strip + of sprites that will be used. + + Spritesheets: + This is a 2D array of sprites. For example, refer to the ``ExampleSpritesheet.png`` file. The entirety of the + 4x4 would be a spritesheet. One row of it would be used as an active sheet. + + Object Type: + This is an int that represents the enum value of the Object the sprite represents. For example, the + ``ExampleSpritesheet.png`` shows the Avatar. The Avatar object_type's enum value is found in the JSON logs and + is the number 4. This would change if the order of the ObjectType enum changes, so be mindful of that and + refer to the JSON logs for the exact values. + + Rect: + The rect is an object used for rectangular objects. You can offset the top left corner of the Rect by + passing parameters. + + Example: + + On the left, the Rect's offset is depicted as being at (0, 0), meaning there is no offset. A Rect object + has parameters that will determine the offset by passing in (x, y). The 'x' is the offset from the left + side, and the 'y' is the offset from the top. Therefore, passing in an (x, y) of (3, 2) in a Rect object + will move the object 3 units to the right, and 2 units down. + + In the visual below, the left side shows a Rect at (0, 0) (i.e., no offset). The image on the right depicts + the Rect object further to the right, showing its offset from the left corner of the screen. + + Rect Example: + :: + ----------------------- ----------------------- + |------ | | ------ | + || | | | | | | + |______ | --------> | ______ | + | | | | + | | | | + | | | | + ----------------------- ----------------------- + + Screen: + The screen is also a PyGame.Screen object, so it simply represents an image of the screen itself. + + Image: + The image is an individual sprite in a spritesheet. + + Frame Index: + The frame index is an int that is used to determine which sprite to use from the active_sheet. For example, + say the active_sheet is the first row in the ``ExampleSpritesheet.png``. If the frame_index is 1, the first + image will be used where the head is centered. If the frame_index is 3, the sprite will now have the head of + the Avatar in a different position than in frame_index 1. + + Config: + This is an object reference to the ``config.py`` file. It's used to access the fixed values that are only + accessed in the configurations of the file. + + Update Function: + The update function is a method that is assigned during instantiation of the ByteSprite. That function is + used to update what the active_sheet is depending on what is implemented in ByteSprite classes. + + Examine the ``exampleBS.py`` file. In that implementation of the update method, it selects the active_sheet + based on a chain of if statements. Next, in the ``create_bytesprite`` method, the implemented ``update`` + method is passed into the returned ByteSprite object. + + Now, in the ByteSprite object's update method, it will set the active_sheet to be based on what is returned + from the BytespriteFactory's method. + + To recap, first, a ByteSprite's update function depends on the BytespriteFactory's implementation. Then, the + BytespriteFactory's implementation will return which sprite_sheet is supposed to be used. Finally, the + ByteSprite's update function will take what is returned from the BytespriteFactory's method and assign + the active_sheet to be what is returned. The two work in tandem. + """ + active_sheet: list[pyg.Surface] # The current spritesheet being used. spritesheets: list[list[pyg.Surface]] object_type: int - layer: int rect: pyg.Rect screen: pyg.Surface image: pyg.Surface @@ -23,7 +110,7 @@ class ByteSprite(pyg.sprite.Sprite): # make sure that all inherited classes constructors only take screen as a parameter def __init__(self, screen: pyg.Surface, filename: str, num_of_states: int, object_type: int, update_function: Callable[[dict, int, Vector, list[list[pyg.Surface]]], list[pyg.Surface]], - colorkey: pyg.Color | None = None, layer: int = 0, top_left: Vector = Vector(0, 0)): + colorkey: pyg.Color | None = None, top_left: Vector = Vector(0, 0)): # Add implementation here for selecting the sprite sheet to use super().__init__() self.spritesheet_parser: SpriteSheet = SpriteSheet(filename) @@ -43,7 +130,6 @@ def __init__(self, screen: pyg.Surface, filename: str, num_of_states: int, objec self.active_sheet: list[pyg.Surface] = self.spritesheets[0] self.object_type: int = object_type self.screen: pyg.Surface = screen - self.layer: int = layer @property def active_sheet(self) -> list[pyg.Surface]: @@ -56,11 +142,6 @@ def spritesheets(self) -> list[list[pyg.Surface]]: @property def object_type(self) -> int: return self.__object_type - - @property - def layer(self) -> int: - return self.__layer - @property def rect(self) -> pyg.Rect: return self.__rect @@ -99,15 +180,6 @@ def object_type(self, object_type: int) -> None: raise ValueError(f'{self.__class__.__name__}.object_type can\'t be negative.') self.__object_type = object_type - @layer.setter - def layer(self, layer: int) -> None: - if layer is None or not isinstance(layer, int): - raise ValueError(f'{self.__class__.__name__}.layer must be an int.') - - if layer < 0: - raise ValueError(f'{self.__class__.__name__}.layer can\'t be negative.') - self.__layer = layer - @rect.setter def rect(self, rect: pyg.Rect) -> None: if rect is None or not isinstance(rect, pyg.Rect): @@ -128,19 +200,30 @@ def update_function(self, update_function: Callable[[dict, int, Vector, list[lis # Inherit this method to implement sprite logic def update(self, data: dict, layer: int, pos: Vector) -> None: + """ + This method will start an animation based on the currently set active_sheet. Then, it will reassign the + active_sheet based on what the BytespriteFactory's update method will return. Lastly, the + ``set_image_and_render`` method is then called to then display the new sprites in the active_sheet. + :param data: + :param layer: + :param pos: + :return: None + """ self.__frame_index = 0 # Starts the new spritesheet at the beginning self.rect.topleft = ( pos.x * self.__config.TILE_SIZE * self.__config.SCALE + self.__config.GAME_BOARD_MARGIN_LEFT, pos.y * self.__config.TILE_SIZE * self.__config.SCALE + self.__config.GAME_BOARD_MARGIN_TOP) - self.update_function(data, layer, pos, self.spritesheets) + self.active_sheet = self.update_function(data, layer, pos, self.spritesheets) self.set_image_and_render() # Call this method at the end of the implemented logic and for each frame def set_image_and_render(self): + """ + This method will take a single image from the current active_sheet and then display it on the screen. + :return: + """ self.image = self.active_sheet[self.__frame_index] self.__frame_index = (self.__frame_index + 1) % self.__config.NUMBER_OF_FRAMES_PER_TURN self.screen.blit(self.image, self.rect) - - diff --git a/visualizer/bytesprites/bytesprite_factory.py b/visualizer/bytesprites/bytesprite_factory.py index 7ddf5c1..627d64b 100644 --- a/visualizer/bytesprites/bytesprite_factory.py +++ b/visualizer/bytesprites/bytesprite_factory.py @@ -7,8 +7,23 @@ class ByteSpriteFactory: @staticmethod def update(data: dict, layer: int, pos: Vector, spritesheets: list[list[pyg.Surface]]) -> list[pyg.Surface]: + """ + This is a method that **must** be implemented in every ByteSpriteFactory class. Look at the example files + to see how this *could* be implemented. Implementation may vary. + :param data: + :param layer: + :param pos: + :param spritesheets: + :return: list[pyg.Surface] + """ ... @staticmethod def create_bytesprite(screen: pyg.Surface) -> ByteSprite: + """ + This is a method that **must** be implemented in every ByteSpriteFactory class. Look at the example files + to see how this can be implemented. + :param screen: + :return: + """ ... diff --git a/visualizer/bytesprites/exampleBS.py b/visualizer/bytesprites/exampleBS.py index 50fe93e..d5efeae 100644 --- a/visualizer/bytesprites/exampleBS.py +++ b/visualizer/bytesprites/exampleBS.py @@ -9,8 +9,23 @@ class AvatarBytespriteFactoryExample(ByteSpriteFactory): + """ + `Avatar Bytesprite Factory Example Notes`: + + This is a factory class that will produce Bytesprite objects of the Avatar. + """ @staticmethod def update(data: dict, layer: int, pos: Vector, spritesheets: list[list[pyg.Surface]]) -> list[pyg.Surface]: + """ + This method will select which spritesheet to select from the ``ExampleSpritesheet.png`` file. For example, + the first if statement will return the second row of sprites in the image if conditions are met. + :param data: + :param layer: + :param pos: + :param spritesheets: + :return: list[pyg.Surface] + """ + # Logic for selecting active animation if data['inventory'][data['held_index']] is not None: return spritesheets[1] @@ -23,5 +38,10 @@ def update(data: dict, layer: int, pos: Vector, spritesheets: list[list[pyg.Surf @staticmethod def create_bytesprite(screen: pyg.Surface) -> ByteSprite: + """ + This file will return a new ByteSprite object that is to be displayed on the screen. + :param screen: ByteSprite + :return: + """ return ByteSprite(screen, os.path.join(os.getcwd(), 'visualizer/spritesheets/ExampleSpritesheet.png'), 4, 4, AvatarBytespriteFactoryExample.update, pyg.Color("#FBBBAD")) diff --git a/visualizer/bytesprites/exampleTileBS.py b/visualizer/bytesprites/exampleTileBS.py index d823449..5f4aef8 100644 --- a/visualizer/bytesprites/exampleTileBS.py +++ b/visualizer/bytesprites/exampleTileBS.py @@ -8,8 +8,29 @@ class TileBytespriteFactoryExample(ByteSpriteFactory): + """ + This class is used to demonstrate an example of the Tile Bytesprite. It demonstrates how any class inheriting + from ByteSpriteFactory must implement the `update()` and `create_bytesprite()` static methods. These methods may + have unique implementations based on how the sprites are meant to look and interact with other objects in the game. + """ @staticmethod def update(data: dict, layer: int, pos: Vector, spritesheets: list[list[pyg.Surface]]) -> list[pyg.Surface]: + """ + This implementation of the update method is different from the exampleWallBS.py file. In this method, the + data dictionary is used. The `data` is a dict representing a Tile object in JSON notation. + + For this unique implementation, an if statement is used to check if something is occupying the Tile object. + If true, the second spritesheet is used. If false, the first spritesheet is used. + + Examining the ExampleTileSS.png, it is apparent that the first spritesheet shows a Tile with an animation with + only the pink color. However, the second spritesheet (the one used if something occupies that tile) has a unique + animation that is blue instead. + :param data: + :param layer: + :param pos: + :param spritesheets: + :return: + """ if data['occupied_by'] is not None: return spritesheets[1] else: @@ -17,5 +38,11 @@ def update(data: dict, layer: int, pos: Vector, spritesheets: list[list[pyg.Surf @staticmethod def create_bytesprite(screen: pyg.Surface) -> ByteSprite: + """ + This method takes a screen from Pygame.Surface. That screen is then passed in as a parameter into the + returned Bytesprite object. + :param screen: + :return: + """ return ByteSprite(screen, os.path.join(os.getcwd(), 'visualizer/spritesheets/ExampleTileSS.png'), 2, 7, TileBytespriteFactoryExample.update) diff --git a/visualizer/bytesprites/exampleWallBS.py b/visualizer/bytesprites/exampleWallBS.py index 8989841..659cd1a 100644 --- a/visualizer/bytesprites/exampleWallBS.py +++ b/visualizer/bytesprites/exampleWallBS.py @@ -8,11 +8,32 @@ class WallBytespriteFactoryExample(ByteSpriteFactory): + """ + This class is used to demonstrate an example of the Wall Bytesprite. It demonstrates how any class inheriting + from ByteSpriteFactory must implement the `update()` and `create_bytesprite()` static methods. These methods may + have unique implementations based on how the sprites are meant to look and interact with other objects in the game. + """ @staticmethod def update(data: dict, layer: int, pos: Vector, spritesheets: list[list[pyg.Surface]]) -> list[pyg.Surface]: + """ + This method implementation simply returns the first spritesheet in the list of given spritesheets. Examining the + `ExampleWallSS.png` file, it is clear that there is only one spritesheet, so that is all this method needs to + do. + :param data: + :param layer: + :param pos: + :param spritesheets: + :return: + """ return spritesheets[0] @staticmethod def create_bytesprite(screen: pyg.Surface) -> ByteSprite: + """ + This method takes a screen from Pygame.Surface. That screen is then passed in as a parameter into the + returned Bytesprite object. + :param screen: + :return: a ByteSprite object + """ return ByteSprite(screen, os.path.join(os.getcwd(), 'visualizer/spritesheets/ExampleWallSS.png'), 1, 8, WallBytespriteFactoryExample.update) diff --git a/visualizer/main.py b/visualizer/main.py index b9e24db..60089df 100644 --- a/visualizer/main.py +++ b/visualizer/main.py @@ -5,7 +5,6 @@ import numpy import pygame import cv2 -from PIL import Image import game.config from typing import Callable @@ -42,10 +41,9 @@ def __init__(self): self.paused: bool = False self.recording: bool = False - size: tuple[int, int] = (self.config.SCREEN_SIZE.x, self.config.SCREEN_SIZE.y) # Scale for video saving (division can be adjusted, higher division = lower quality) - self.scaled: tuple[int, int] = (size[0] // 2, size[1] // 2) - self.writer: cv2.VideoWriter = cv2.VideoWriter("out.mp4", cv2.VideoWriter_fourcc(*'H264'), + self.scaled: tuple[int, int] = (self.size.x // 2, self.size.y // 2) + self.writer: cv2.VideoWriter = cv2.VideoWriter("out.mp4", cv2.VideoWriter_fourcc(*'mp4v'), self.default_frame_rate, self.scaled) def load(self) -> None: @@ -122,13 +120,13 @@ def __playback_controls(self, button_pressed: PlaybackButtons) -> None: # Method to deal with saving game to mp4 (called in render if save button pressed) def save_video(self) -> None: # Convert to PIL Image - new_image = pygame.image.tostring(self.screen.copy(), "RGBA", False) - new_image = Image.frombytes("RGBA", self.screen.get_rect().size, new_image) - # Scale image (using Bicubic, can be adjusted) - new_image.thumbnail(self.scaled, Image.BICUBIC) + new_image = pygame.surfarray.pixels3d(self.screen.copy()) + # Rotate ndarray + new_image = new_image.swapaxes(1, 0) + # shrink size for recording + new_image = cv2.resize(new_image, self.scaled) # Convert to OpenCV Image with numpy - new_image = numpy.array(new_image) - new_image = cv2.cvtColor(new_image, cv2.COLOR_RGBA2BGRA) + new_image = cv2.cvtColor(new_image, cv2.COLOR_RGBA2BGR) # Write image and go to next turn self.writer.write(new_image) @@ -302,7 +300,7 @@ def __results_loop(self, in_phase: bool) -> None: if not in_phase: break self.clock.tick(math.floor(self.default_frame_rate * self.playback_speed)) - + self.writer.release() if __name__ == '__main__': byte_visualiser: ByteVisualiser = ByteVisualiser() diff --git a/visualizer/utils/sidebars.py b/visualizer/utils/sidebars.py deleted file mode 100644 index 5053beb..0000000 --- a/visualizer/utils/sidebars.py +++ /dev/null @@ -1,29 +0,0 @@ -import pygame as pyg -from visualizer.config import Config -from game.utils.vector import Vector - - -class Sidebars: - - def __init__(self): - self.__config = Config() - self.top: pyg.Surface = pyg.Surface(self.__config.SIDEBAR_TOP_DIMENSIONS.as_tuple()) - self.top_rect: pyg.Rect = self.top.get_rect() - self.top_rect.midtop = Vector(x=self.__config.SCREEN_SIZE.x // 2, - y=self.__config.SIDEBAR_TOP_PADDING).as_tuple() # Returns (x: int, y: int) - - self.bottom: pyg.Surface = pyg.Surface(self.__config.SIDEBAR_BOTTOM_DIMENSIONS.as_tuple()) - self.bottom_rect: pyg.Rect = self.bottom.get_rect() - self.bottom_rect.midbottom = Vector(x=self.__config.SCREEN_SIZE.x // 2, - y=self.__config.SCREEN_SIZE.y - self.__config.SIDEBAR_BOTTOM_PADDING).as_tuple() # handling the correct padding since it will be on the bottom # Returns (x: int, y: int) - - self.left: pyg.Surface = pyg.Surface(self.__config.SIDEBAR_LEFT_DIMENSIONS.as_tuple()) - self.left_rect: pyg.Rect = self.left.get_rect() - self.left_rect.midleft = Vector(x=self.__config.SIDEBAR_LEFT_PADDING, - y=self.__config.SCREEN_SIZE.y // 2).as_tuple() # Returns (x: int, y: int) - - self.right: pyg.Surface = pyg.Surface(self.__config.SIDEBAR_RIGHT_DIMENSIONS.as_tuple()) - self.right_rect: pyg.Rect = self.right.get_rect() - self.right_rect.midright = Vector(x=self.__config.SCREEN_SIZE.x - self.__config.SIDEBAR_RIGHT_PADDING, - y=self.__config.SCREEN_SIZE.y // 2).as_tuple() # Returns (x: int, y: int) -