Source code for game.common.avatar

from typing import Self

from game.common.enums import ObjectType
from game.common.game_object import GameObject
from game.common.items.item import Item
from game.utils.vector import Vector


[docs]class Avatar(GameObject): """ `Avatar Inventory Notes:` The avatar's inventory is a list of items. Each item has a quantity and a stack_size (the max amount of an item that can be held in a stack. Think of the Minecraft inventory). This upcoming example is just to facilitate understanding the concept. The Dispensing Station concept that will be mentioned is completely optional to implement if you desire. The Dispensing Station is used to help with the explanation. ---- **Items:** Every Item has a quantity and a stack_size. The quantity is how much of the Item the player *currently* has. The stack_size is the max of that Item that can be in a stack. For example, if the quantity is 5, and the stack_size is 5 (5/5), the item cannot be added to that stack ----- **Picking up items:** Example 1: :: When you pick up an item (which will now be referred to as picked_up_item), picked_up_item has a given quantity. In this case, let's say the quantity of picked_up_item is 2. Imagine you already have this item in your inventory (which will now be referred to as inventory_item), and inventory_item has a quantity of 1 and a stack_size of 10 (think of this as a fraction: 1/10). When you pick up picked_up_item, inventory_item will be checked. If picked_up_item's quantity + inventory_item < stack_size, it'll be added without issue. Remember, for this example: picked_up_item quantity is 2, and inventory_item quantity is 1, and stack_size is 10. Inventory_item quantity before picking up: 1/10 :: 2 + 1 < 10 --> True Inventory_item quantity after picking up: 3/10 ---- Example 2: For the next two examples, the total inventory size will be considered. Let's say inventory_item has quantity 4 and a stack_size of 5. Now say that picked_up_item has quantity 3. Recall: if picked_up_item's quantity + inventory_item < stack_size, it will be added without issue Inventory_item quantity before picking up: 4/5 :: 3 + 4 < 5 --> False What do we do in this situation? If you want to add picked_up_item to inventory_item and there is an overflow of quantity, that is handled for you. Let's say that your inventory size (which will now be referred to as max_inventory_size) is 5. You already have inventory_item in there that has a quantity of 4 and a stack_size of 5. An image of the inventory is below. 'None' is used to help show the max_inventory_size. Inventory_item quantity and stack_size will be listed in parentheses as a fraction. :: Inventory: [ inventory_item (4/5), None, None, None, None ] Now we will add picked_up_item and its quantity of 3: :: Inventory before: [ inventory_item (4/5), None, None, None, None ] 3 + 4 < 5 --> False inventory_item (4/5) will now be inventory_item (5/5) picked_up_item now has a quantity of 2 instead of 3 Since we have a surplus, we will append the same item with a quantity of 2 in the inventory. :: The result is: [ inventory_item (5/5), inventory_item (2/5), None, None, None ] ---- Example 3: For this last example, assume your inventory looks like this: :: [ inventory_item (5/5), inventory_item (5/5), inventory_item (5/5), inventory_item (5/5), inventory_item (4/5) ] You can only fit one more inventory_item into the last stack before the inventory is full. Let's say that picked_up_item has quantity of 3 again. :: Inventory before: [ inventory_item (5/5), inventory_item (5/5), inventory_item (5/5), inventory_item (5/5), inventory_item (4/5) ] 3 + 4 < 5 --> False inventory_item (4/5) will now be inventory_item (5/5) picked_up_item now has a quantity of 2 However, despite the surplus, we cannot add it into our inventory, so the remaining quantity of picked_up_item is left where it was first found. :: Inventory after: [ inventory_item (5/5), inventory_item (5/5), inventory_item (5/5), inventory_item (5/5), inventory_item (5/5) ] """ def __init__(self, position: Vector | None = None, max_inventory_size: int = 10): super().__init__() self.object_type: ObjectType = ObjectType.AVATAR self.score: int = 0 self.position: Vector | None = position self.max_inventory_size: int = max_inventory_size self.inventory: list[Item | None] = [None] * max_inventory_size self.held_item: Item | None = self.inventory[0] self.__held_index: int = 0 @property def held_item(self) -> Item | None: self.__clean_inventory() return self.inventory[self.__held_index] @property def score(self) -> int: return self.__score @property def position(self) -> Vector | None: return self.__position @property def inventory(self) -> list[Item | None]: return self.__inventory @property def max_inventory_size(self) -> int: return self.__max_inventory_size @held_item.setter def held_item(self, item: Item | None) -> None: self.__clean_inventory() # If it's not an item, and it's not None, raise the error if item is not None and not isinstance(item, Item): raise ValueError( f'{self.__class__.__name__}.held_item must be an Item or None. It is a(n) ' f'{item.__class__.__name__} and has the value of {item}') # If the item is not contained in the inventory, the error will be raised. if not self.inventory.__contains__(item): raise ValueError(f'{self.__class__.__name__}.held_item must be set to an item that already exists' f' in the inventory. It has the value of {item}') # If the item is contained in the inventory, set the held_index to that item's index self.__held_index = self.inventory.index(item) @score.setter def score(self, score: int) -> None: if score is None or not isinstance(score, int): raise ValueError( f'{self.__class__.__name__}.score must be an int. It is a(n) {score.__class__.__name__} and has the value of ' f'{score}') self.__score: int = score @position.setter def position(self, position: Vector | None) -> None: if position is not None and not isinstance(position, Vector): raise ValueError( f'{self.__class__.__name__}.position must be a Vector or None. It is a(n) ' f'{position.__class__.__name__} and has the value of {position}') self.__position: Vector | None = position @inventory.setter def inventory(self, inventory: list[Item | None]) -> None: # If every item in the inventory is not of type None or Item, throw an error if inventory is None or not isinstance(inventory, list) \ or (len(inventory) > 0 and any(map(lambda item: item is not None and not isinstance(item, Item), inventory))): raise ValueError( f'{self.__class__.__name__}.inventory must be a list of Items. It is a(n) {inventory.__class__.__name__} ' f'and has the value of {inventory}') if len(inventory) > self.max_inventory_size: raise ValueError(f'{self.__class__.__name__}.inventory size must be less than or equal to ' f'max_inventory_size. It has the value of {len(inventory)}') self.__inventory: list[Item] = inventory @max_inventory_size.setter def max_inventory_size(self, size: int) -> None: if size is None or not isinstance(size, int): raise ValueError(f'{self.__class__.__name__}.max_inventory_size must be an int. It is a(n) {size.__class__.__name__} ' f'and has the value of {size}') self.__max_inventory_size: int = size # Private helper method that cleans the inventory of items that have a quantity of 0. This is a safety check def __clean_inventory(self) -> None: """ This method is used to manage the inventory. Whenever an item has a quantity of 0, it will set that item object to None since it doesn't exist in the inventory anymore. Otherwise, if there are multiple instances of an item in the inventory, and they can be consolidated, this method does that. Example: In inventory[0], there is a stack of gold with a quantity and stack_size of 5. In inventory[1], there is another stack of gold with quantity of 3, stack size of 5. If you want to take away 4 gold, inventory[0] will have quantity 1, and inventory[1] will have quantity 3. Then, when clean_inventory is called, it will consolidate the two. This mean that inventory[0] will have quantity 4 and inventory[1] will be set to None. :return: None """ # This condenses the inventory if there are duplicate items and combines them together for i, item in enumerate(self.inventory): [j.pick_up(item) for j in self.inventory[:i] if j is not None] # This removes any items in the inventory that have a quantity of 0 and replaces them with None remove: [int] = [x[0] for x in enumerate(self.inventory) if x[1] is not None and x[1].quantity == 0] for i in remove: self.inventory[i] = None
[docs] def drop_held_item(self) -> Item | None: """ Call this method when a station is taking the held item from the avatar. This method can be modified more for different scenarios where the held item would be dropped (e.g., you make a game where being attacked forces you to drop your held item). If you want the held item to go somewhere specifically and not become None, that can be changed too. Make sure to keep clean_inventory() in this method. """ # The held item will be taken from the avatar will be replaced with None in the inventory held_item = self.held_item self.inventory[self.__held_index] = None self.__clean_inventory() return held_item
[docs] def take(self, item: Item | None) -> Item | None: """ To use this method, pass in an item object. Whatever this item's quantity is will be the amount subtracted from the avatar's inventory. For example, if the item in the inventory is has a quantity of 5 and this method is called with the parameter having a quantity of 2, the item in the inventory will have a quantity of 3. Furthermore, when this method is called and the potential item is taken away, the clean_inventory method is called. It will consolidate all similar items together to ensure that the inventory is clean. Reference test_avatar_inventory.py and the clean_inventory method for further documentation on this method and how the inventory is managed. :param item: :return: Item or None """ # Calls the take method on every index on the inventory. If i isn't None, call the method # NOTE: If the list is full of None (i.e., no Item objects are in it), nothing will happen [item := i.take(item) for i in self.inventory if i is not None] self.__clean_inventory() return item
[docs] def pick_up(self, item: Item | None) -> Item | None: self.__clean_inventory() # Calls the pick_up method on every index on the inventory. If i isn't None, call the method [item := i.pick_up(item) for i in self.inventory if i is not None] # If the inventory has a slot with None, it will replace that None value with the item if self.inventory.__contains__(None): index = self.inventory.index(None) self.inventory.pop(index) self.inventory.insert(index, item) # Nothing is then returned return None # If the item can't be in the inventory, return it return item
[docs] def to_json(self) -> dict: data: dict = super().to_json() data['held_index'] = self.__held_index data['held_item'] = self.held_item.to_json() if self.held_item is not None else None data['score'] = self.score data['position'] = self.position.to_json() if self.position is not None else None data['inventory'] = self.inventory data['max_inventory_size'] = self.max_inventory_size return data
[docs] def from_json(self, data: dict) -> Self: super().from_json(data) self.score: int = data['score'] self.position: Vector | None = None if data['position'] is None else Vector().from_json(data['position']) self.inventory: list[Item] = data['inventory'] self.max_inventory_size: int = data['max_inventory_size'] self.held_item: Item | None = self.inventory[data['held_index']] return self