diff --git a/src/NoPELib/player_settings.py b/src/NoPELib/player_settings.py index 9e8c462..bec5f21 100644 --- a/src/NoPELib/player_settings.py +++ b/src/NoPELib/player_settings.py @@ -1,7 +1,4 @@ """ -Needs to manage any number of players -Needs to work with mulitple rounds/turns -Needs to handle modifying number of players """ @@ -10,10 +7,23 @@ import copy import json import logging from pathlib import Path +import PDOLib _log = logging.getLogger('NoPE-Lib') +class Hook: + def __init__(self): + self._slot = [] + + def __call__(self, *args, **kwargs): + for fct in self._slot: + fct(*args, **kwargs) + + def connect(self, fct): + self._slot.append(fct) + + class PlayersManager: """ Manager of players for a given game. @@ -22,20 +32,21 @@ class PlayersManager: Attributes: gameID (str): The gameID of the active game + config (dict): The in memory state of the config file Methods: save: """ defaultPlayerConfig = {"flags": [], "expansions": {}, "games": {}} - def __init__(self, gameID: str=None, activePlayers: list[str]=None, playersPath: str='./players.json', loggerID: str='PlayersManager', includedExpansions: tuple[str]=None): + def __init__(self, gameID: str=None, activePlayers: list[str]=None, playersPath: str='../players.json', loggerID: str='PlayersManager', includedExpansions: tuple[str]=None): """ Initialises a list of players. Args: gameID (str): The ID of the game used in the configuration file. activePlayers (list of str): The names of the players that are playing. Defaults to none which includes all players. - playersPath (str, optional): The path of the players.json file. Defaults to './players.json'. + playersPath (str, optional): The path of the players.json file. Defaults to '../players.json'. loggerID (str, optional): The ID used for logging. Defaults to 'Players'. """ @@ -54,45 +65,45 @@ class PlayersManager: for name, player_cfg in self._cfg["players"].items() if name in activePlayers } + # Create the signals + self.onMadePlayerActive = Hook() + self.onMadePlayerInactive = Hook() def __getitem__(self, playerName: str): """ Get a player object. - If the player wasn't active, make them active. + If the player is not active, make them active. If the player didn't exist previously, create a default config and make them active. """ - if playerName not in self._cfg["players"]: - # Create the brand new player - self._cfg["players"][playerName] = copy.deepcopy(self.defaultPlayerConfig) - newPlayer = Player(playerName, self, **self._cfg["players"][playerName]) - # Make the player active - self._player_data[playerName] = newPlayer - self._log.debug(f"Created a new player called {playerName}") - return newPlayer - elif playerName not in self._player_data: + if playerName not in self._player_data: # Fetch the player's data and make them active - playerAdded = Player(playerName, self, **self._cfg["players"][playerName]) - self._player_data[playerName] = playerAdded - self._log.debug(f"Fetched {playerName}'s data and made them active") - return playerAdded - else: - return self._player_data[playerName] + if playerName in self._cfg["players"]: + self[playerName] = self._cfg["players"][playerName] + else: + self[playerName] = copy.deepcopy(self.defaultPlayerConfig) + self._log.debug(f"Made {playerName} active") + self.onMadePlayerActive(playerName) + return self._player_data[playerName] def __setitem__(self, playerName: str, config: dict, makeNewPlayerObject=False): """ - Replace a player's config with another one. It possible to also automatically replace the - player object by specifying the corresponding argument. + Replace a player's config with another one. + + Arguments: + playerName (str): + config (dict): + makeNewPlayerObject (bool): """ - self._log.debug(f"Changed {playerName}'s config") if makeNewPlayerObject: previousInstance = self._player_data[playerName] self._player_data[playerName] = Player(playerName, self, **config) del previousInstance self._cfg["players"][playerName] = config + self._log.debug(f"Changed {playerName}'s config") def __delitem__(self, playerName): - self._log.debug(f"Removing {playerName} from active players") del self._player_data[playerName] + self._log.debug(f"Removed {playerName} from active players") def __len__(self): return len(self._player_data) @@ -102,6 +113,10 @@ class PlayersManager: def __repr__(self): return f"Manager is playing {self.gameID} with {len(self._player_data)} active players" + + @property + def config(self): + return copy.deepcopy(self._cfg) @property def gameID(self): @@ -146,6 +161,7 @@ class Player: Methods: generateConfig: + punish: """ def __init__(self, name: str, manager: PlayersManager, **cfg): self._name = name @@ -195,7 +211,7 @@ class Player: def punish(self, value, preferedExpansion: str=None): do_something = lambda value: value additionalInfos = { - "expansionID": "challengeDB", + "expansionClass": "challengeDB", "balancedValue": 0.2, "showOnScreen": "Do 100 push-up", "error": None, @@ -210,6 +226,7 @@ class Expansion: """ Attributes: + specs: Methods: step: @@ -217,13 +234,19 @@ class Expansion: close: """ - @abc.abstractmethod def __init__(self): """ - Raise an error if not available + Intended behavior of the method: + Initialise the creation of an expansion + Should NEVER use any kind of argument. + Raise an error if not available. """ pass + @property + def specs(self): + return self._specs + @abc.abstractmethod def step(self, action): """ @@ -239,6 +262,9 @@ class Expansion: def close(self): pass + def makeSpecs(self): + pass + class ExpansionsManager: """ @@ -247,15 +273,32 @@ class ExpansionsManager: Attributes: includedExpansions: tuple of str Container of the expansions to try making available - releventExpansions: dict of Expansion + activeExpansions: dict of Expansion Container of the relevent expansions + + TODO Add an interface to allow the modification of the expansion settings """ + defaultExpansionConfig = {"players": (), "types": ()} + keysConvert2Tuple = ("players", "types") tags = ["shock", "spice", "sour", "drink", "challenge"] def __init__(self, playersManager: PlayersManager, includedExpansions: tuple[str]=None): + # Create the attributes self.playersManager = playersManager + self._cfg = playersManager.config["expansions"] self._includedExpansions = tuple(includedExpansions) if includedExpansions is not None else () - self._releventExpansions = {} # TODO Populate the dictionnary + # Convert the required lists into tuples + for expansionID in self._cfg["expansions"]: + for key in self.keysConvert2Tuple: + self._cfg["expansions"][expansionID][key] = tuple(self._cfg["expansions"][expansionID][key]) + # Compute the active expansions + self._activeExpansions = {} + for expansionID in self._listPossiblyValidExpansions(): + self._createExpansion(expansionID) + + # Connect the signals to the slots + playersManager.onMadePlayerActive.connect(self._activePlayerAdded) + playersManager.onMadePlayerInactive.connect(self._activePlayerRemoved) @property def includedExpansions(self): @@ -264,40 +307,104 @@ class ExpansionsManager: @includedExpansions.setter def includedExpansions(self, newIncluded: tuple[str]): self._includedExpansions = tuple(newIncluded) - self._includedChanged() + self._includeChanged() @includedExpansions.deleter def includedExpansions(self): self._includedExpansions = () @property - def releventExpansions(self): - return self._releventExpansions + def activeExpansions(self): + return self._activeExpansions - def _includedChanged(self): + def _getPlayersFillMissing(self, expansionID): + return set(self._cfg["expansions"].get(expansionID, {}).get("players", [])) + + def _listPossiblyValidExpansions(self): """ - Compute the list of only the relevant expansions. - The expansions that do not exists are excluded. - The expansions that have no players assigned to them are excluded. - The expansions that are not responding are excluded. + List expansions that are included, defined in PDOLib and are available to at least one active player. + """ + activePlayers = set(self.playersManager.keys()) + possiblyValidExpansions = [ + expansion for expansion in self._includedExpansions + if hasattr(PDOLib, expansion) and not self._getPlayersFillMissing(expansion).isdisjoint(activePlayers) + ] + return possiblyValidExpansions + + def _createExpansion(self, expansionID): + try: + expansion = getattr(PDOLib, expansionID)() + except: + pass + else: + self._activeExpansions[expansionID] = expansion + + def _removeExpansion(self, expansionID): + expansion = self._activeExpansions.pop(expansionID) + expansion.close() + del expansion + + def _includeChanged(self): + """ + Update the list of active Expansions """ - pass - def _activePlayersChanged(self, activatePlayers): - pass + # Compute the expansions involved in the modification + possiblyValidExpansions = set(self._listPossiblyValidExpansions()) + previousExpansions = set(self._includedExpansions) + # Remove irrelevant expansions + expansionsToRemove = previousExpansions.difference(possiblyValidExpansions) + for expansionID in expansionsToRemove: + self._activeExpansions.pop(expansionID).close() + # Add the new expansions + expansionsToAdd = possiblyValidExpansions.difference(previousExpansions) + for expansionID in expansionsToAdd: + self._createExpansion(expansionID) + + def _activePlayerAdded(self, playerName): + # Find the expansions that are to be created + possibleAddition = set(self._includedExpansions).difference(self.activeExpansions) + filteredAddition = [ + expansionID for expansionID in possibleAddition + if hasattr(PDOLib, expansionID) and playerName in self._getPlayersFillMissing(expansionID) + ] + # Create the expansions + for expansionID in filteredAddition: + self._createExpansion(expansionID) + + def _activePlayerRemoved(self, playerName): + # Find the expansions that are to be created + filteredRemoval = [ + expansionID for expansionID in self.activeExpansions + if playerName in self._cfg["players"] and len(self._cfg["players"]) + ] + # Close the expansions + for expansionID in filteredRemoval: + self._removeExpansion(expansionID) def _errorOccured(self, expansionID): - pass + self._removeExpansion(expansionID) def _expansionPlayersChanged(self, expansionID): - pass + expansionIsActive = expansionID in self._activeExpansions + activePlayers = set(self.playersManager.keys()) + expansionHasNoPlayers = self._getPlayersFillMissing(expansionID).isdisjoint(activePlayers) + expansionDefined = hasattr(PDOLib, expansionID) + if expansionIsActive and expansionHasNoPlayers: + self._removeExpansion(expansionID) + elif (not expansionIsActive) and (not expansionHasNoPlayers) and expansionDefined: + self._createExpansion(expansionID) - def resetExpansion(self, expansion: Expansion, suppressError: bool=False): + def initilizeExpansion(self, expansionID: str, suppressError: bool=False): """ - If the expansion was relevent, close it. - In any case, try creating a new + Initilise the expansion. """ - # If the expansion was relevent, close it + if expansionID in self._activeExpansions: + self._removeExpansion(expansionID) + if not suppressError: + # Make sure that the expansion is relevent + # Verify that creating the expansion was successful + pass # Try creating a new instance of the expansion # Update the list of relevent expansions pass @@ -314,4 +421,5 @@ if __name__ == "__main__": brosef = manager["Brosef"] brosef.gameSave = [1] print(manager["Brosef"].gameSave) - brosef.state = ["alive"] \ No newline at end of file + brosef.state = ["alive"] + # TODO Verify that changing the gameID works as intended \ No newline at end of file