diff --git a/game/common/map/game_board.py b/game/common/map/game_board.py index 135b2f0..60f1701 100644 --- a/game/common/map/game_board.py +++ b/game/common/map/game_board.py @@ -98,7 +98,7 @@ def __init__(self, seed: int | None = None, map_size: Vector = Vector(), 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 + self.game_map: list[list[Tile]] | None = None @property def seed(self) -> int: diff --git a/game/common/player.py b/game/common/player.py index 11b668e..1b4daf7 100644 --- a/game/common/player.py +++ b/game/common/player.py @@ -5,7 +5,7 @@ class Player(GameObject): - def __init__(self, code: object | None = None, team_name: str | None = None, action: ActionType | None = None, + def __init__(self, code: object | None = None, team_name: str | None = None, actions: list[ActionType] = [], avatar: Avatar | None = None): super().__init__() self.object_type: ObjectType = ObjectType.PLAYER @@ -14,21 +14,23 @@ def __init__(self, code: object | None = None, team_name: str | None = None, act self.team_name: str | None = team_name self.code: object = code # self.action: Action = action - self.action: ActionType | None = action + self.actions: list[ActionType] | list = actions self.avatar: Avatar | None = avatar @property - def action(self) -> ActionType | None: # change to Action if you want to use the action object - return self.__action + def actions(self) -> list[ActionType] | list: # change to Action if you want to use the action object + return self.__actions - @action.setter - def action(self, action: ActionType | None) -> None: # showing it returns nothing(like void in java) + @actions.setter + def actions(self, actions: list[ActionType] | list) -> None: # showing it returns nothing(like void in java) # if it's (not none = and) if its (none = or) - if action is not None and not isinstance(action, ActionType): # since it can be either none or action type we need it to be and not or - # ^change to Action if you want to use the action object - raise ValueError(f'{self.__class__.__name__}.action must be ActionType or None') + # going across all action types and making it a boolean, if any are true this will be true\/ + if actions is None or not isinstance(actions, list) \ + or (len(actions) > 0 + and any(map(lambda action_type: not isinstance(action_type, ActionType), actions))): + raise ValueError(f'{self.__class__.__name__}.action must be an empty list or a list of action types') # ^if it's not either throw an error - self.__action = action + self.__actions = actions @property def functional(self) -> bool: @@ -76,22 +78,21 @@ def to_json(self): data['functional'] = self.functional # data['error'] = self.error # .to_json() if self.error is not None else None data['team_name'] = self.team_name - data['action'] = self.actiontype.to_json() if self.actiontype is not None else None + data['actions'] = self.actions data['avatar'] = self.avatar.to_json() if self.avatar is not None else None return data def from_json(self, data): super().from_json(data) - + self.functional = data['functional'] # self.error = data['error'] # .from_json(data['action']) if data['action'] is not None else None self.team_name = data['team_name'] - action: ActionType | None = data['action'] + self.actions: list[ActionType] | list = data['actions'] avatar: Avatar | None = data['avatar'] - if action is None and avatar is None: - self.action = None + if avatar is None: self.avatar = None return self # match case for action diff --git a/game/config.py b/game/config.py index 47e8d41..aa3a065 100644 --- a/game/config.py +++ b/game/config.py @@ -10,6 +10,8 @@ MAX_SECONDS_PER_TURN = 0.1 # max number of basic operations clients have for their turns +MAX_NUMBER_OF_ACTIONS_PER_TURN = 2 # max number of actions per turn is currently set to 2 + MIN_CLIENTS_START = None # minimum number of clients required to start running the game; should be None when SET_NUMBER_OF_CLIENTS is used MAX_CLIENTS_START = None # maximum number of clients required to start running the game; should be None when SET_NUMBER_OF_CLIENTS is used SET_NUMBER_OF_CLIENTS_START = 2 # required number of clients to start running the game; should be None when MIN_CLIENTS or MAX_CLIENTS are used diff --git a/game/controllers/controller.py b/game/controllers/controller.py index 233adc8..7f7dd2b 100644 --- a/game/controllers/controller.py +++ b/game/controllers/controller.py @@ -1,5 +1,7 @@ -from game.common.enums import DebugLevel +from game.common.enums import DebugLevel, ActionType from game.config import Debug +from game.common.player import Player +from game.common.map.game_board import GameBoard class Controller: @@ -8,7 +10,7 @@ def __init__(self): self.debug_level = DebugLevel.controller self.debug = False - def handle_actions(self, client, world): + def handle_actions(self, action: ActionType, client: Player, world: GameBoard): return def print(self, *args): diff --git a/game/controllers/interact_controller.py b/game/controllers/interact_controller.py index 40ed713..fefa015 100644 --- a/game/controllers/interact_controller.py +++ b/game/controllers/interact_controller.py @@ -12,10 +12,10 @@ class InteractController(Controller): def __init__(self): super().__init__() - def handle_actions(self, client: Player, world: GameBoard): + def handle_actions(self, action: ActionType, client: Player, world: GameBoard): # match interaction type with x and y vector: Vector - match client.action: + match action: case ActionType.INTERACT_UP: vector = Vector(x=0, y=-1) case ActionType.INTERACT_DOWN: diff --git a/game/controllers/master_controller.py b/game/controllers/master_controller.py index b97a03b..01f7dab 100644 --- a/game/controllers/master_controller.py +++ b/game/controllers/master_controller.py @@ -1,25 +1,35 @@ from copy import deepcopy +import random from game.common.action import Action +from game.common.avatar import Avatar from game.common.enums import * from game.common.player import Player -import game.config as config +import game.config as config # this is for turns from game.utils.thread import CommunicationThread - +from game.controllers.movement_controller import MovementController from game.controllers.controller import Controller +from game.controllers.interact_controller import InteractController +from game.common.map.game_board import GameBoard +from game.config import MAX_NUMBER_OF_ACTIONS_PER_TURN class MasterController(Controller): def __init__(self): super().__init__() - self.game_over = False - - self.turn = None - self.current_world_data = None + self.game_over: bool = False + # self.event_timer = GameStats.event_timer # anything related to events are commented it out until made + # self.event_times: tuple[int, int] | None = None + self.turn: int = 1 + self.current_world_data: dict = None + self.movement_controller: MovementController = MovementController() + self.interact_controller: InteractController = InteractController() # Receives all clients for the purpose of giving them the objects they will control - def give_clients_objects(self, clients): - pass + def give_clients_objects(self, clients: list[Player], world: dict): + # starting_positions = [[3, 3], [3, 9]] # would be done in generate game + for index, client in enumerate(clients): + client.avatar: Avatar = Avatar(position=world[index]) # Generator function. Given a key:value pair where the key is the identifier for the current world and the value is # the state of the world, returns the key that will give the appropriate world information @@ -34,35 +44,57 @@ def game_loop_logic(self, start=1): self.turn += 1 # Receives world data from the generated game log and is responsible for interpreting it - def interpret_current_turn_data(self, clients, world, turn): + def interpret_current_turn_data(self, clients: list[Player], world: dict, turn): self.current_world_data = world + if turn == 1: + random.seed(world["game_board"].seed) + # self.event_times = random.randrange(162, 172), random.randrange(329, 339) # Receive a specific client and send them what they get per turn. Also obfuscates necessary objects. - def client_turn_arguments(self, client, turn): - actions = Action() - client.action = actions + def client_turn_arguments(self, client: Player, turn): + # turn_action: Action = Action() + # client.action: Action = turn_action + # ^if you want to use action as an object instead of an enum + + turn_actions: list[ActionType] | list = [] + client.actions = turn_actions # Create deep copies of all objects sent to the player + current_world = deepcopy(self.current_world_data["game_board"]) # what is current world and copy avatar + copy_avatar = deepcopy(client.avatar) # Obfuscate data in objects that that player should not be able to see - - args = (self.turn, actions, self.current_world_data) + # Currently world data isn't obfuscated at all + args = (self.turn, turn_actions, current_world, copy_avatar) return args # Perform the main logic that happens per turn - def turn_logic(self, clients, turn): - pass + def turn_logic(self, clients: list[Player], turn): + for client in clients: + for i in range(MAX_NUMBER_OF_ACTIONS_PER_TURN): + self.movement_controller.handle_actions(client.actions[i], client, self.current_world_data["game_board"]) + self.interact_controller.handle_actions(client.actions[i], client, self.current_world_data["game_board"]) + # checks event logic at the end of round + # self.handle_events(clients) + + # comment out for now, nothing is in place for event types yet + # def handle_events(self, clients): + # If it is time to run an event, master controller picks an event to run + # if self.turn == self.event_times[0] or self.turn == self.event_times[1]: + # self.current_world_data["game_map"].generate_event(EventType.example, EventType.example) + # event type.example is just a placeholder for now # Return serialized version of game - def create_turn_log(self, clients, turn): + def create_turn_log(self, clients: list[Player], turn): data = dict() - + data['tick'] = turn + data['clients'] = [client.to_json() for client in clients] # Add things that should be thrown into the turn logs here - data['temp'] = None + data['game_board'] = self.current_world_data["game_board"].to_json() return data # Gather necessary data together in results file - def return_final_results(self, clients, turn): + def return_final_results(self, clients: list[Player], turn): data = dict() data['players'] = list() diff --git a/game/controllers/movement_controller.py b/game/controllers/movement_controller.py index 0838fd0..4f2af7a 100644 --- a/game/controllers/movement_controller.py +++ b/game/controllers/movement_controller.py @@ -11,11 +11,11 @@ class MovementController(Controller): def __init__(self): super().__init__() - def handle_actions(self, client: Player, world: GameBoard): + def handle_actions(self, action: ActionType, client: Player, world: GameBoard): avatar_pos: Vector = Vector(client.avatar.position.x, client.avatar.position.y) pos_mod: Vector - match client.action: + match action: case ActionType.MOVE_UP: pos_mod = Vector(x=0, y=-1) case ActionType.MOVE_DOWN: diff --git a/game/engine.py b/game/engine.py index 5a51325..178cc14 100644 --- a/game/engine.py +++ b/game/engine.py @@ -5,6 +5,7 @@ import sys import traceback +from game.common.map.game_board import GameBoard from game.common.player import Player from game.config import * from game.controllers.master_controller import MasterController @@ -36,8 +37,8 @@ def loop(self): if self.quiet_mode: f = open(os.devnull, 'w') sys.stdout = f - self.boot() self.load() + self.boot() for self.current_world_key in tqdm( self.master_controller.game_loop_logic(), bar_format=TQDM_BAR_FORMAT, @@ -146,9 +147,9 @@ def boot(self): self.clients.sort(key=lambda clnt: clnt.team_name, reverse=True) # Finally, request master controller to establish clients with basic objects if SET_NUMBER_OF_CLIENTS_START == 1: - self.master_controller.give_clients_objects(self.clients[0]) + self.master_controller.give_clients_objects(self.clients[0], self.world) else: - self.master_controller.give_clients_objects(self.clients) + self.master_controller.give_clients_objects(self.clients, self.world) # Loads in the world def load(self): @@ -167,8 +168,10 @@ def load(self): world = None with open(GAME_MAP_FILE) as json_file: world = json.load(json_file) + world['game_board'] = GameBoard().from_json(world['game_board']) self.world = world + # Sits on top of all actions that need to happen before the player takes their turn def pre_tick(self): # Increment the tick @@ -260,7 +263,8 @@ def post_tick(self): else: data = self.master_controller.create_turn_log(self.clients, self.tick_number) - self.game_logs[self.tick_number] = data + with open(os.path.join(LOGS_DIR, f"turn_{self.tick_number:04d}.json"), 'w+') as f: + json.dump(data, f) # Perform a game over check if self.master_controller.game_over: