Skip to content

Commit

Permalink
Test item/avatar (#23)
Browse files Browse the repository at this point in the history
* tests for game_board + changes

* Update avatar.py

* Item/Avatar unit tests

Made unit tests for Item and Avatar.

---------

Co-authored-by: Gunnar Moody <[email protected]>
  • Loading branch information
KingPhilip14 and MoodyMan04 authored Apr 19, 2023
1 parent 78eab66 commit fbe4e4b
Show file tree
Hide file tree
Showing 11 changed files with 452 additions and 74 deletions.
22 changes: 11 additions & 11 deletions game/common/avatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class Avatar(GameObject):
"""
'''
Notes for the inventory:
The avatar's inventory is a list of items. Each item has a quantity and a stack_size (the max amount of an
Expand Down Expand Up @@ -89,7 +89,7 @@ class Avatar(GameObject):
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, item: Item | None = None, position: Vector | None = None, inventory: list[Item] = [],
max_inventory_size: int = 10):
Expand Down Expand Up @@ -125,34 +125,34 @@ def max_inventory_size(self) -> int:
def held_item(self, item: Item | None) -> None:
# 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.")
raise ValueError(f'{self.__class__.__name__}.held_item must be an Item or None.')
self.__held_item: Item = 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.")
raise ValueError(f'{self.__class__.__name__}.score must be an int.')
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.")
raise ValueError(f'{self.__class__.__name__}.position must be a Vector or None.')
self.__position: Vector | None = position

@inventory.setter
def inventory(self, inventory: list[Item]) -> None:
if inventory is None or not isinstance(inventory, list) \
or (len(inventory) > 0 and any(map(lambda item: not isinstance(item, Item), inventory))):
raise ValueError(f"{self.__class__.__name__}.inventory must be a list of Items.")
raise ValueError(f'{self.__class__.__name__}.inventory must be a list of Items.')
if len(inventory) > self.max_inventory_size:
raise ValueError(f"{self.__class__.__name__}.inventory size must be less than max_inventory_size")
raise ValueError(f'{self.__class__.__name__}.inventory size must be less than max_inventory_size')
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.")
raise ValueError(f'{self.__class__.__name__}.max_inventory_size must be an int.')
self.__max_inventory_size: int = size

def pick_up(self, item: Item) -> Item | None:
Expand All @@ -169,15 +169,15 @@ def to_json(self) -> dict:
data: dict = super().to_json()
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
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

def from_json(self, data: dict) -> Self:
super().from_json(data)
self.score: int = data['score']
self.position: Vector | None = data['position']
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']
held_item: Item | None = data['held_item']
Expand All @@ -189,6 +189,6 @@ def from_json(self, data: dict) -> Self:
case ObjectType.ITEM:
self.held_item = Item().from_json(data['held_item'])
case _:
raise ValueError(f"{self.__class__.__name__}.held_item needs to be an item.")
raise ValueError(f'{self.__class__.__name__}.held_item needs to be an item.')

return self
120 changes: 78 additions & 42 deletions game/common/game_board.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import random
from typing import Self
from game.utils.vector import Vector
from game.common.stations.occupiable_station import Occupiable_Station
from game.common.stations.station import Station
from game.common.avatar import Avatar
from game.common.game_object import GameObject
from game.common.map.tile import Tile
Expand Down Expand Up @@ -102,50 +104,69 @@ def __init__(self, seed: int | None = None, map_size: Vector = Vector(),
locations: dict[tuple[Vector]:list[GameObject]] | None = None, walled: bool = False):

super().__init__()
self.seed = seed
# game_map is initially going to be None. Since generation is slow, call generate_map() as needed
self.game_map: list[list[GameObject]] | None = None
self.seed: int | None = seed
random.seed(seed)
self.object_type: ObjectType = ObjectType.GAMEBOARD
self.event_active = None
self.event_active: int | None = None
self.map_size: Vector = map_size
self.locations: dict = locations
# when passing Vectors as a tuple, end the tuple of Vectors with a comma so it is recognized as a tuple
self.locations: dict | None = locations
self.walled: bool = walled

# game_map is initially going to be None. Since generation is slow, call generate_map() as needed
self.game_map: list[list[GameObject]] | None = None


@property
def seed(self) -> int:
return self.__seed

@seed.setter
def seed(self, seed: int | None):
if seed is not None or not isinstance(seed, int):
raise ValueError("Seed must be an integer.")
def seed(self, seed: int | None) -> None:
if self.game_map is not None:
raise RuntimeError(f'{self.__class__.__name__} variables cannot be changed once generate_map is run.')
if seed is not None and not isinstance(seed, int):
raise ValueError(f'{self.__class__.__name__}.seed must be an integer.')
self.__seed = seed

@property
def game_map(self) -> list[list[GameObject]] | None:
return self.__game_map

@game_map.setter
def game_map(self, game_map: list[list[GameObject]]) -> None:
if game_map is not None and (not isinstance(game_map, list) or \
any(map(lambda l: not isinstance(l, list), game_map)) or \
any([any(map(lambda g: not isinstance(g, GameObject), l)) for l in game_map])):
raise ValueError(f'{self.__class__.__name__}.game_map must be a list[list[GameObject]].')
self.__game_map = game_map

@property
def map_size(self) -> Vector:
return self.__map_size

@map_size.setter
def map_size(self, map_size: Vector):
def map_size(self, map_size: Vector) -> None:
if self.game_map is not None:
raise RuntimeError(f'{self.__class__.__name__} variables cannot be changed once generate_map is run.')
if map_size is None or not isinstance(map_size, Vector):
raise ValueError("Map_size must be a Vector.")
raise ValueError(f'{self.__class__.__name__}.map_size must be a Vector.')
self.__map_size = map_size

@property
def locations(self) -> dict:
return self.__locations

@locations.setter
def locations(self, locations: dict[tuple[Vector]:list[GameObject]] | None):
if locations is not None or not isinstance(locations, dict):
def locations(self, locations: dict[tuple[Vector]:list[GameObject]] | None) -> None:
if self.game_map is not None:
raise RuntimeError(f'{self.__class__.__name__} variables cannot be changed once generate_map is run.')
if locations is not None and not isinstance(locations, dict):
raise ValueError("Locations must be a dict. The key must be a tuple of Vector Objects, and the "
"value a list of GameObject.")
for k,v in locations:
if len(k) != len(v):
raise ValueError("Cannot set the locations for the game_board. A key has a different "
"length than its key.")
if locations is not None:
for k,v in locations.items():
if len(k) != len(v):
raise ValueError("Cannot set the locations for the game_board. A key has a different "
"length than its key.")

self.__locations = locations

Expand All @@ -154,13 +175,15 @@ def walled(self) -> bool:
return self.__walled

@walled.setter
def walled(self, walled: bool):
def walled(self, walled: bool) -> None:
if self.game_map is not None:
raise RuntimeError(f'{self.__class__.__name__} variables cannot be changed once generate_map is run.')
if walled is None or not isinstance(walled, bool):
raise ValueError("Walled must be a bool.")
raise ValueError(f'{self.__class__.__name__}.walled must be a bool.')

self.__walled = walled

def generate_map(self):
def generate_map(self) -> None:
# generate map
self.game_map = [[Tile() for _ in range(self.map_size.x)] for _ in range(self.map_size.y)]

Expand All @@ -174,35 +197,33 @@ def generate_map(self):

self.__populate_map()

def __populate_map(self):
def __populate_map(self) -> None:
for k, v in self.locations.items():
if len(k) != len(v) or (len(k) == 0 or len(v) == 0): # Key-Value lengths must be > 0 and equal
raise ValueError("A key-value pair from game_board.locations has mismatching lengths. "
"They must be the same length, regardless of size.")

# random.choices returns a randomized list which is used in __help_populate()
j = random.choices(k, k=len(k))
# random.sample returns a randomized list which is used in __help_populate()
j = random.sample(k, k=len(k))
self.__help_populate(j, v)

def __help_populate(self, vector_list: list[Vector], v: list[GameObject]):
for i in v:
temp_vector: Vector = vector_list.pop()

def __help_populate(self, vector_list: list[Vector], v: list[GameObject]) -> None:
for j, i in zip(vector_list,v):
if isinstance(i, Avatar): # If the GameObject is an Avatar, assign it the coordinate position
i.position = temp_vector
i.position = j

temp_tile: GameObject = self.game_map[temp_vector.y][temp_vector.x]
temp_tile: GameObject = self.game_map[j.y][j.x]

while hasattr(temp_tile.occupied_by, 'occupied_by'):
temp_tile = temp_tile.occupied_by

if temp_tile is not None:
if temp_tile is None:
raise ValueError("Last item on the given tile doesn't have the 'occupied_by' attribute.")

temp_tile.occupied_by = i

def get_objects(self, look_for: ObjectType) -> list[GameObject]:
to_return = list()
to_return: list[GameObject] = list()

for row in self.game_map:
for object_in_row in row:
Expand All @@ -211,7 +232,7 @@ def get_objects(self, look_for: ObjectType) -> list[GameObject]:

return to_return

def __get_objects_help(look_for: ObjectType, temp: GameObject | Tile, to_return: list[GameObject]):
def __get_objects_help(self, look_for: ObjectType, temp: GameObject | Tile, to_return: list[GameObject]):
while hasattr(temp, 'occupied_by'):
if temp.object_type is look_for:
to_return.append(temp)
Expand All @@ -224,25 +245,40 @@ def __get_objects_help(look_for: ObjectType, temp: GameObject | Tile, to_return:

def to_json(self) -> dict:
data: dict[str, str] = super().to_json()
temp: list[list[GameObject]] = list((map(lambda tile: tile.to_json(), y)) for y in self.game_map)
temp: list[list[GameObject]] = list(list(map(lambda tile: tile.to_json(), y)) for y in self.game_map) if self.game_map is not None else None
data["game_map"] = temp
data["seed"] = self.seed
data["map_size"] = self.map_size
data["locations"] = self.locations
data["map_size"] = self.map_size.to_json()
data["location_vectors"] = [[vec.to_json() for vec in k] for k in self.locations.keys()] if self.locations is not None else None
data["location_objects"] = [[obj.to_json() for obj in v] for v in self.locations.values()] if self.locations is not None else None
data["walled"] = self.walled
data['event_active'] = self.event_active
return data

def generate_event(self, start, end):
def generate_event(self, start: int, end: int) -> None:
self.event_active = random.randint(start, end)

def from_json(self, data) -> Self:
def __from_json_helper(self, data: dict) -> GameObject:
match data['object_type']:
case ObjectType.WALL:
return Wall().from_json(data)
case ObjectType.OCCUPIABLE_STATION:
return Occupiable_Station().from_json(data)
case ObjectType.STATION:
return Station().from_json(data)
case ObjectType.AVATAR:
return Avatar().from_json(data)
# If adding more ObjectTypes that can be placed on the game_board, specify here
case _:
raise ValueError(f'The location (dict) must have a valid key (tuple of vectors) and a valid value (list of GameObjects).')

def from_json(self, data: dict) -> Self:
super().from_json(data)
temp = data["game_map"]
self.game_map: list[list[GameObject]] = list((map(lambda tile: Tile().from_json(tile), y)) for y in temp)
self.seed: int | None = data["seed"]
self.map_size: Vector = data["map_size"]
self.locations: dict[tuple[Vector]:list[GameObject]] = data["locations"]
self.map_size: Vector = Vector().from_json(data["map_size"])
self.locations: dict[tuple[Vector]:list[GameObject]] = {tuple(map(lambda vec: Vector().from_json(vec), k)) : [self.__from_json_helper(obj) for obj in v] for k, v in zip(data["location_vectors"], data["location_objects"])} if data["location_vectors"] is not None else None
self.walled: bool = data["walled"]
self.event_active = data['event_active']
self.event_active: int = data['event_active']
self.game_map: list[list[GameObject]] = list(list(map(lambda tile: Tile().from_json(tile), y)) for y in temp) if temp is not None else None
return self
6 changes: 4 additions & 2 deletions game/common/game_object.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import uuid

from game.common.enums import ObjectType
from typing import Self


class GameObject:
def __init__(self, **kwargs):
self.id = str(uuid.uuid4())
self.object_type = ObjectType.NONE

def to_json(self):
def to_json(self) -> dict:
# It is recommended call this using super() in child implementations
data = dict()

Expand All @@ -17,10 +18,11 @@ def to_json(self):

return data

def from_json(self, data):
def from_json(self, data: dict) -> Self:
# It is recommended call this using super() in child implementations
self.id = data['id']
self.object_type = data['object_type']
return self

def obfuscate(self):
pass
15 changes: 12 additions & 3 deletions game/common/items/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Item(GameObject):
def __init__(self, value: int = 1, durability: int | None = 100, quantity: int = 1, stack_size: int = 1):
super().__init__()
self.__quantity = None # This is here to prevent an error
self.__durability = None
self.object_type: ObjectType = ObjectType.ITEM
self.value: int = value # Value can more specified based on purpose (e.g., the sell price)
self.stack_size: int = stack_size # the max quantity this item can contain
Expand All @@ -31,8 +32,10 @@ def stack_size(self) -> int:

@durability.setter
def durability(self, durability: int | None):
if durability is not None and not isinstance(durability, int) or self.stack_size > 1:
raise ValueError(f'{self.__class__.__name__}.durability must be an int or None, and stack_size must be 1.')
if durability is not None and not isinstance(durability, int):
raise ValueError(f'{self.__class__.__name__}.durability must be an int or None.')
if durability is not None and self.stack_size != 1:
raise ValueError(f'{self.__class__.__name__}.durability must be set to None if stack_size is not equal to 1.')
self.__durability = durability

@value.setter
Expand All @@ -45,7 +48,7 @@ def value(self, value: int) -> None:
def quantity(self, quantity: int) -> None:
if quantity is None or not isinstance(quantity, int):
raise ValueError(f'{self.__class__.__name__}.quantity must be an int.')
if quantity < 0:
if quantity <= 0:
raise ValueError(f'{self.__class__.__name__}.quantity must be greater than 0.')

# The self.quantity is set to the lower value between stack_size and the given quantity
Expand All @@ -59,11 +62,17 @@ def quantity(self, quantity: int) -> None:
def stack_size(self, stack_size: int) -> None:
if stack_size is None or not isinstance(stack_size, int):
raise ValueError(f'{self.__class__.__name__}.stack_size must be an int.')
if self.durability is not None and stack_size != 1:
raise ValueError(f'{self.__class__.__name__}.stack_size must be 1 if {self.__class__.__name__}.durability '
f'is not None.')
if self.__quantity is not None and stack_size < self.__quantity:
raise ValueError(f'{self.__class__.__name__}.stack_size must be greater than or equal to the quantity.')
self.__stack_size: int = stack_size

def pick_up(self, item: Self) -> Self | None:
if item is None or not isinstance(item, Item):
raise ValueError(f'{item.__class__.__name__} is not of type Item.')

# If the items don't match, return the given item without modifications
if self.object_type != item.object_type:
return item
Expand Down
2 changes: 1 addition & 1 deletion game/common/map/wall.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ class Wall(GameObject):
def __init__(self):
super().__init__()
self.object_type = ObjectType.WALL

2 changes: 1 addition & 1 deletion game/common/stations/station.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def item(self) -> Item|None:
@item.setter
def item(self, item: Item) -> None:
if item is not None and not isinstance(item, Item):
raise ValueError(f"{self.__class__.__name__}.item must be an Item or None, not {item}.")
raise ValueError(f'{self.__class__.__name__}.item must be an Item or None, not {item}.')
self.__item = item

# take action method
Expand Down
Loading

0 comments on commit fbe4e4b

Please sign in to comment.