Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Master controller #14

Merged
merged 12 commits into from
Apr 15, 2023
2 changes: 1 addition & 1 deletion game/common/map/game_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
31 changes: 16 additions & 15 deletions game/common/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] | list = [],
JuliaCaesar marked this conversation as resolved.
Show resolved Hide resolved
avatar: Avatar | None = None):
super().__init__()
self.object_type: ObjectType = ObjectType.PLAYER
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions game/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions game/controllers/controller.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions game/controllers/interact_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
72 changes: 52 additions & 20 deletions game/controllers/master_controller.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
JuliaCaesar marked this conversation as resolved.
Show resolved Hide resolved
data = dict()

data['players'] = list()
Expand Down
4 changes: 2 additions & 2 deletions game/controllers/movement_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 8 additions & 4 deletions game/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down