import random
from typing import Self, Callable
from game.common.avatar import Avatar
from game.common.enums import *
from game.common.game_object import GameObject
from game.common.map.occupiable import Occupiable
from game.common.map.tile import Tile
from game.common.map.wall import Wall
from game.common.stations.occupiable_station import OccupiableStation
from game.common.stations.station import Station
from game.utils.vector import Vector
[docs]class GameBoard(GameObject):
"""
`GameBoard Class Notes:`
Map Size:
---------
map_size is a Vector object, allowing you to specify the size of the (x, y) plane of the game board.
For example, a Vector object with an 'x' of 5 and a 'y' of 7 will create a board 5 tiles wide and
7 tiles long.
Example:
::
_ _ _ _ _ y = 0
| |
| |
| |
| |
| |
| |
_ _ _ _ _ y = 6
-----
Locations:
----------
This is the bulkiest part of the generation.
The locations field is a dictionary with a key of a tuple of Vectors, and the value being a list of
GameObjects (the key **must** be a tuple instead of a list because Python requires dictionary keys to be
immutable).
This is used to assign the given GameObjects the given coordinates via the Vectors. This is done in two ways:
Statically:
If you want a GameObject to be at a specific coordinate, ensure that the key-value pair is
*ONE* Vector and *ONE* GameObject.
An example of this would be the following:
::
locations = { (vector_2_4) : [station_0] }
In this example, vector_2_4 contains the coordinates (2, 4). (Note that this naming convention
isn't necessary, but was used to help with the concept). Furthermore, station_0 is the
GameObject that will be at coordinates (2, 4).
Dynamically:
If you want to assign multiple GameObjects to different coordinates, use a key-value
pair of any length.
**NOTE**: The length of the tuple and list *MUST* be equal, otherwise it will not
work. In this case, the assignments will be random. An example of this would be the following:
::
locations =
{
(vector_0_0, vector_1_1, vector_2_2) : [station_0, station_1, station_2]
}
(Note that the tuple and list both have a length of 3).
When this is passed in, the three different vectors containing coordinates (0, 0), (1, 1), or
(2, 2) will be randomly assigned station_0, station_1, or station_2.
If station_0 is randomly assigned at (1, 1), station_1 could be at (2, 2), then station_2 will be at (0, 0).
This is just one case of what could happen.
Lastly, another example will be shown to explain that you can combine both static and
dynamic assignments in the same dictionary:
::
locations =
{
(vector_0_0) : [station_0],
(vector_0_1) : [station_1],
(vector_1_1, vector_1_2, vector_1_3) : [station_2, station_3, station_4]
}
In this example, station_0 will be at vector_0_0 without interference. The same applies to
station_1 and vector_0_1. However, for vector_1_1, vector_1_2, and vector_1_3, they will randomly
be assigned station_2, station_3, and station_4.
-----
Walled:
-------
This is simply a bool value that will create a wall barrier on the boundary of the game_board. If
walled is True, the wall will be created for you.
For example, let the dimensions of the map be (5, 7). There will be wall Objects horizontally across
x = 0 and x = 4. There will also be wall Objects vertically at y = 0 and y = 6
Below is a visual example of this, with 'x' being where the wall Objects are.
Example:
::
x x x x x y = 0
x x
x x
x x
x x
x x
x x x x x y = 6
"""
def __init__(self, seed: int | None = None, map_size: Vector = Vector(),
locations: dict[tuple[Vector]:list[GameObject]] | None = None, walled: bool = False):
super().__init__()
# game_map is initially going to be None. Since generation is slow, call generate_map() as needed
self.game_map: list[list[Tile]] | None = None
self.seed: int | None = seed
random.seed(seed)
self.object_type: ObjectType = ObjectType.GAMEBOARD
self.event_active: int | None = None
self.map_size: Vector = map_size
# 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
@property
def seed(self) -> int:
return self.__seed
@seed.setter
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 int. '
f'It is a(n) {seed.__class__.__name__} with the value of {seed}.')
self.__seed = seed
@property
def game_map(self) -> list[list[Tile]] | None:
return self.__game_map
@game_map.setter
def game_map(self, game_map: list[list[Tile]]) -> 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, Tile), tile_list))
for tile_list in game_map])):
raise ValueError(
f'{self.__class__.__name__}.game_map must be a list[list[Tile]]. '
f'It is a(n) {game_map.__class__.__name__} with the value of {game_map}.')
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) -> 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(
f'{self.__class__.__name__}.map_size must be a Vector. '
f'It is a(n) {map_size.__class__.__name__} with the value of {map_size}.')
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) -> 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(
f'Locations must be a dict. The key must be a tuple of Vector Objects, '
f'and the value a list of GameObject. '
f'It is a(n) {locations.__class__.__name__} with the value of {locations}.')
self.__locations = locations
@property
def walled(self) -> bool:
return self.__walled
@walled.setter
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(
f'{self.__class__.__name__}.walled must be a bool. '
f'It is a(n) {walled.__class__.__name__} with the value of {walled}.')
self.__walled = walled
[docs] 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)]
if self.walled:
for x in range(self.map_size.x):
if x == 0 or x == self.map_size.x - 1:
for y in range(self.map_size.y):
self.game_map[y][x].occupied_by = Wall()
self.game_map[0][x].occupied_by = Wall()
self.game_map[self.map_size.y - 1][x].occupied_by = Wall()
self.__populate_map()
def __populate_map(self) -> None:
for k, v in self.locations.items():
if len(k) == 0 or len(v) == 0: # Key-Value lengths must be > 0 and equal
raise ValueError(
f'A key-value pair from game_board.locations has a length of 0. '
f'The length of the keys is {len(k)} and the length of the values is {len(v)}.')
# 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 __occupied_filter(self, game_object_list: list[GameObject]) -> list[GameObject]:
"""
A helper method that returns a list of game objects that have the 'occupied_by' attribute.
:param game_object_list:
:return: a list of game object
"""
return [game_object for game_object in game_object_list if isinstance(game_object, Occupiable)]
def __help_populate(self, vector_list: list[Vector], game_object_list: list[GameObject]) -> None:
"""
A helper method that helps populate the game map.
:param vector_list:
:param game_object_list:
:return: None
"""
zipped_list: [tuple[list[Vector], list[GameObject]]] = list(zip(vector_list, game_object_list))
last_vec: Vector = zipped_list[-1][0]
remaining_objects: list[GameObject] | None = self.__occupied_filter(game_object_list[len(zipped_list):]) \
if len(self.__occupied_filter(game_object_list)) > len(zipped_list) \
else None
# Will cap at smallest list when zipping two together
for vector, game_object in zipped_list:
if isinstance(game_object, Avatar): # If the GameObject is an Avatar, assign it the coordinate position
game_object.position = vector
temp_tile: GameObject = self.game_map[vector.y][vector.x]
while temp_tile.occupied_by is not None and isinstance(temp_tile.occupied_by, Occupiable):
temp_tile = temp_tile.occupied_by
if temp_tile.occupied_by is not None:
raise ValueError(
f'Last item on the given tile doesn\'t have the \'occupied_by\' attribute. '
f'It is a(n) {temp_tile.occupied_by.__class__.__name__} with the value of {temp_tile.occupied_by}.')
temp_tile.occupied_by = game_object
if remaining_objects is None:
return
# stack remaining game_objects on last vector
temp_tile: GameObject = self.game_map[last_vec.y][last_vec.x]
while temp_tile.occupied_by is not None and isinstance(temp_tile.occupied_by, Occupiable):
temp_tile = temp_tile.occupied_by
for game_object in remaining_objects:
if not isinstance(temp_tile, Occupiable) or temp_tile.occupied_by is not None:
raise ValueError(
f'Last item on the given tile doesn\'t have the \'occupied_by\' attribute.'
f' It is a(n) {temp_tile.occupied_by.__class__.__name__} with the value of {temp_tile.occupied_by}.')
temp_tile.occupied_by = game_object
temp_tile = temp_tile.occupied_by
# Returns the Vector and a list of GameObject for whatever objects you are trying to get
[docs] def get_objects(self, look_for: ObjectType) -> list[tuple[Vector, list[GameObject]]]:
to_return: list[tuple[Vector, list[GameObject]]] = list()
for y, row in enumerate(self.game_map):
for x, object_in_row in enumerate(row):
go_list: list[GameObject] = []
temp: GameObject = object_in_row
self.__get_objects_help(look_for, temp, go_list)
if len(go_list) > 0:
to_return.append((Vector(x=x, y=y), [*go_list, ]))
return to_return
# Add the objects to the end of to_return (a list of GameObject)
def __get_objects_help(self, look_for: ObjectType, temp: GameObject | Tile, to_return: list[GameObject]):
while isinstance(temp, Occupiable):
if temp.object_type is look_for:
to_return.append(temp)
# The final temp is the last occupied by option which is either an Avatar, Station, or None
temp = temp.occupied_by
if temp is not None and temp.object_type is look_for:
to_return.append(temp)
[docs] def to_json(self) -> dict:
data: dict[str, object] = super().to_json()
temp: list[list[Tile]] = 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.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
[docs] def generate_event(self, start: int, end: int) -> None:
self.event_active = random.randint(start, end)
def __from_json_helper(self, data: dict) -> GameObject:
temp: ObjectType = ObjectType(data['object_type'])
match temp:
case ObjectType.WALL:
return Wall().from_json(data)
case ObjectType.OCCUPIABLE_STATION:
return OccupiableStation().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 object type of the object is not handled properly. The object type passed in is {temp}.')
[docs] def from_json(self, data: dict) -> Self:
super().from_json(data)
temp = data["game_map"]
self.seed: int | None = data["seed"]
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: int = data['event_active']
self.game_map: list[list[Tile]] = [
[Tile().from_json(tile) for tile in y] for y in temp] if temp is not None else None
return self