Compare commits

..

14 Commits

3 changed files with 197 additions and 97 deletions

View File

@ -1,10 +1,11 @@
{ {
"expansions": "expansions":
{ {
"ShockColar1": {"players": ["Brosef"], "tags": ["shock"], "config": {"COM_port": "COM3", "shocker_ID": 24770}}, "ShockColar1": {"players": ["Brosef"], "tags": ["shock"], "class": "serialShocker", "config": {"COM_port": "COM3", "shocker_ID": 24770}},
"RobotBarman": {"players": ["Tango", "TRS_MML"], "tags": ["drink"], "config": {}}, "RobotBarman": {"players": ["Tango", "TRS_MML"], "tags": ["drink"], "class": "bar", "config": {}},
"ChallengeDB": {"players": ["TRS_MML"], "tags": ["challenge"], "config": {}}, "ChallengeDB": {"players": ["TRS_MML"], "tags": ["challenge"], "class": "challenge", "config": {}},
"SourCandy": {"players": [], "tags": ["food"], "config": {}} "SourCandy": {"players": [], "tags": ["food"], "class": "candy", "config": {}},
"Simple": {"players": ["Brosef"], "tags": [], "class": "simplest", "config": {}}
}, },
"players": { "players": {
"Brosef": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "gamesSave": {"gameID0": 2}}, "Brosef": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "gamesSave": {"gameID0": 2}},

View File

@ -6,7 +6,7 @@ import copy
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from NoPELib.player_settings import ExpansionsManager from .player_settings import ExpansionsManager
class Hook: class Hook:
@ -36,53 +36,69 @@ class Expansion:
Attributes: Attributes:
players (tuple of str): players (tuple of str):
tags (tuple of flag): tags (tuple of flag):
config: The config of the expansion. Can't be modified during execution.
Methods: Methods:
__call__: Execute a given action with the expansion
Subclass methods:
step: step:
reset: reset:
close: close:
Signals: Signals:
onError: midStepError:
""" """
def __init__(self, ID: str, expansionsManager): def __init__(self, ID: str, expansionsManager: "ExpansionsManager"):
"""
Initialise the creation of an expansion and raise an error if not
available.
"""
self._ID = ID self._ID = ID
self._closed = False
self._manager = expansionsManager self._manager = expansionsManager
self.onError = Hook() self.midStepError = Hook()
self.midStepError.connect(expansionsManager._midStepError)
def __call__(self, action):
try:
assert not self._closed, "Can't use an expansion that has been closed."
return self.step(action)
except Exception as e:
step_info = {
"expansionID": self._ID,
"showOnScreen": None,
"error": e,
"done": False
}
self.midStepError(self._ID)
return step_info
@property @property
def config(self): def config(self):
"""
The config of the expansion.
Can't be modified during execution.
"""
return copy.deepcopy(self._manager.config[self._ID]["config"]) return copy.deepcopy(self._manager.config[self._ID]["config"])
@property @property
def players(self): def players(self):
""" Players that have access to this expansion. """ """ Players that have access to the expansion. """
return self._manager.config[self._ID]["players"] return self._manager.config[self._ID]["players"]
@players.setter @players.setter
def players(self, newPlayers): def players(self, newPlayers):
""" Players that have access to this expansion. """ self._manager.expansionPlayersChange(self._ID, newPlayers)
self._manager.config[self._ID]["players"] = tuple(newPlayers)
self._manager.expansionPlayersChanged(self._ID)
@players.deleter @players.deleter
def players(self): def players(self):
self._manager.config[self._ID]["players"] = () self._manager.expansionPlayersChange(self._ID, ())
self._manager.expansionPlayersChanged(self._ID)
@property @property
def tags(self): def tags(self):
""" Players that have access to this expansion. """ """ Tags of the expansion. """
return self._manager.config[self._ID]["tags"] return self._manager.config[self._ID]["tags"]
@tags.setter @tags.setter
def tags(self, newPlayers): def tags(self, newPlayers):
""" Players that have access to this expansion. """
self._manager.config[self._ID]["tags"] = tuple(newPlayers) self._manager.config[self._ID]["tags"] = tuple(newPlayers)
@tags.deleter @tags.deleter
@ -91,9 +107,6 @@ class Expansion:
@abc.abstractmethod @abc.abstractmethod
def step(self, action): def step(self, action):
"""
Call close if an error is thrown
"""
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
@ -117,7 +130,7 @@ class serialShocker(Expansion):
super().__init__(ID, expansionsManager) super().__init__(ID, expansionsManager)
if self.config["COM_port"] not in serialShocker._api: if self.config["COM_port"] not in serialShocker._api:
self._assignApi() self._assignApi()
self.shocker = serialShocker._api[self.config["COM_port"]].shocker(["shocker_ID"]) self.shocker = serialShocker._api[self.config["COM_port"]].shocker(self.config["shocker_ID"])
def _assignApi(self): def _assignApi(self):
from pishock import SerialAPI from pishock import SerialAPI
@ -131,11 +144,18 @@ class serialShocker(Expansion):
a positive float that defines the duration a positive float that defines the duration
a float within the range [0.0, 1.0] that defines the intensity a float within the range [0.0, 1.0] that defines the intensity
""" """
# Execute the step
vibrateInstead, duration, intensity = action vibrateInstead, duration, intensity = action
if vibrateInstead: callFunc = self.shocker.vibrate if vibrateInstead else self.shocker.shock
self.shocker.vibrate(duration=duration, intensity=intensity) callFunc(duration=duration, intensity=intensity)
else: # Return additionnal info
self.shocker.shock(duration=duration, intensity=intensity) step_info = {
"expansionID": self._ID,
"showOnScreen": None,
"error": None,
"done": True
}
return step_info
def close(self): def close(self):
pass pass
@ -161,12 +181,20 @@ class simplest(Expansion):
a positive float that defines the duration a positive float that defines the duration
a float within the range [0.0, 1.0] that defines the intensity a float within the range [0.0, 1.0] that defines the intensity
""" """
# Execute the step
vibrateInstead, duration, intensity = action vibrateInstead, duration, intensity = action
values = f"duration={duration}, intensity={intensity}" values = f"duration={duration}, intensity={intensity}"
if vibrateInstead: interact_type = "Vibrate" if vibrateInstead else "Shock"
print(f"Vibrate with {values}") message = f"{interact_type} with {values}"
else: print(message)
print(f"Shock with {values}") # Return additionnal info
step_info = {
"expansionID": self._ID,
"showOnScreen": message,
"error": None,
"done": True
}
return step_info
def close(self): def close(self):
print("Closing") print("Closing")

View File

@ -6,7 +6,7 @@ import copy
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
import NoPELib.expansionsLib as expansionsLib from . import expansionsLib
_log = logging.getLogger('NoPE-Lib') _log = logging.getLogger('NoPE-Lib')
@ -227,58 +227,51 @@ class Player:
"games": self._gamesSave "games": self._gamesSave
} }
def punish(self, value, preferedExpansion: str=None):
do_something = lambda value, preferedExpansion: value
additionalInfos = {
"expansionID": "challengeDB",
"balancedValue": 0.2,
"showOnScreen": "Do 100 push-up",
"error": None,
"done": False
}
# NOTE Make a result class instead and an error class
# TODO Implement a way to choose between the available expansions
additionalInfos = do_something(value, preferedExpansion)
return additionalInfos
class ExpansionsManager: class ExpansionsManager:
""" """
Manager of the availability of the expansions. Manager of the availability of the expansions.
Attributes: Attributes:
includedExpansions: tuple of str includeExpansions: tuple of str
Container of the expansions to try making available Container of the expansions to try making available
activeExpansions: dict of Expansion activeExpansions: dict of Expansion
Container of the relevent expansions Container of the relevent expansions
Methods:
expansionPlayersChange: Change the assigned players of an expansion
lookupPlayer: Provide a list of expansion available to a player
""" """
defaultExpansionConfig = {"players": (), "types": ()} defaultExpansionConfig = {"players": (), "types": ()}
keysConvert2Tuple = ("players", "types") keysConvert2Tuple = ("players", "tags")
tags = ["shock", "spice", "sour", "drink", "challenge"] tags = ["shock", "spice", "sour", "drink", "challenge"]
# (attributed players / types)
# TODO Allow to get a list of expansions with a specific tag/player
def __init__(self, playersManager: PlayersManager, includedExpansions: tuple[str]=None): def __init__(self, playersManager: PlayersManager, includeExpansions: tuple[str]=None):
""" """
Arguments: Arguments:
tryInclude: bool=False, tryActivate: bool=False tryInclude: bool=False, tryActivate: bool=False
""" """
# Create the attributes # Create the attributes
self.playersManager = playersManager self.playersManager = playersManager
self._playersLookUp = {}
self._cfg = playersManager.config["expansions"] self._cfg = playersManager.config["expansions"]
if includedExpansions is not None: if includeExpansions is not None:
self._includedExpansions = tuple(includedExpansions) self._includeExpansions = tuple(includeExpansions)
else: else:
self._includedExpansions = () self._includeExpansions = ()
# Convert the required lists into tuples # Convert the required lists into tuples
for expansionID in self._cfg["expansions"]: for expansionID in self._cfg:
for key in self.keysConvert2Tuple: for key in self.keysConvert2Tuple:
converted = tuple(self._cfg["expansions"][expansionID][key]) converted = tuple(self._cfg[expansionID][key])
self._cfg["expansions"][expansionID][key] = converted self._cfg[expansionID][key] = converted
# Compute the active expansions # Compute the active expansions
self._activeExpansions = {} self._activeExpansions = {}
for expansionID in self._listPossiblyValidExpansions(): for expansionID in self._listPossiblyValidExpansions():
self._createExpansion(expansionID) creation_out = self._createExpansion(expansionID)
if isinstance(creation_out, expansionsLib.Expansion):
self._activeExpansions[expansionID] = creation_out
else:
raise creation_out
# Connect the signals to the slots # Connect the signals to the slots
playersManager.onMadePlayerActive.connect(self._activePlayerAdded) playersManager.onMadePlayerActive.connect(self._activePlayerAdded)
@ -297,18 +290,18 @@ class ExpansionsManager:
return f"ExpansionsManager has {len(self._activeExpansions)} active expansions" return f"ExpansionsManager has {len(self._activeExpansions)} active expansions"
@property @property
def includedExpansions(self): def includeExpansions(self):
""" Dictionnary of the included expansions. """ """ Dictionnary of the included expansions. """
return self._includedExpansions return self._includeExpansions
@includedExpansions.setter @includeExpansions.setter
def includedExpansions(self, newIncluded: tuple[str]): def includeExpansions(self, newIncluded: tuple[str]):
self._includedExpansions = tuple(newIncluded) self._includeExpansions = tuple(newIncluded)
self._includeChanged() self._includeChanged()
@includedExpansions.deleter @includeExpansions.deleter
def includedExpansions(self): def includeExpansions(self):
self._includedExpansions = () self._includeExpansions = ()
@property @property
def config(self): def config(self):
@ -316,33 +309,50 @@ class ExpansionsManager:
return self._cfg return self._cfg
def _getPlayersFillMissing(self, expansionID): def _getPlayersFillMissing(self, expansionID):
return set(self._cfg["expansions"].get(expansionID, {}).get("players", [])) return set(self._cfg.get(expansionID, {}).get("players", []))
def _getClassFillMissing(self, expansionID):
return self._cfg.get(expansionID, {}).get("class", "")
def _listPossiblyValidExpansions(self): def _listPossiblyValidExpansions(self):
""" """
List expansions that are included, defined in expansionsLib and are available List expansions that are: included, defined in expansionsLib and available
to at least one active player. to at least one active player.
""" """
activePlayers = set(self.playersManager.keys()) activePlayers = set(self.playersManager.keys())
possiblyValidExpansions = [ possiblyValidExpansions = [
expansionID for expansionID in self._includedExpansions expansionID for expansionID in self._includeExpansions
if hasattr(expansionsLib, expansionID) and if hasattr(expansionsLib, self._getClassFillMissing(expansionID)) and
not self._getPlayersFillMissing(expansionID).isdisjoint(activePlayers) not self._getPlayersFillMissing(expansionID).isdisjoint(activePlayers)
] ]
return possiblyValidExpansions return possiblyValidExpansions
def _createExpansion(self, expansionID): def _createExpansion(self, expansionID):
terminalErrors = SystemExit, KeyboardInterrupt, GeneratorExit
try: try:
expansion = getattr(expansionsLib, expansionID)(expansionID, self) # Create the expansion
except: expansionClass = self._getClassFillMissing(expansionID)
pass class_to_create = getattr(expansionsLib, expansionClass)
expansion = class_to_create(expansionID, self)
# Update the lookup
for player in self._getPlayersFillMissing(expansionID):
if player in self._playersLookUp:
self._playersLookUp[player].append(expansionID)
else: else:
self._activeExpansions[expansionID] = expansion self._playersLookUp[player] = [expansionID]
return expansion
except terminalErrors as e:
raise e
except Exception as e:
return e
def _removeExpansion(self, expansionID): def _removeExpansion(self, expansionID):
# Update the lookup
for player in self._getPlayersFillMissing(expansionID):
self._playersLookUp[player].pop(expansionID)
# Remove the expansion
expansion = self._activeExpansions.pop(expansionID) expansion = self._activeExpansions.pop(expansionID)
expansion.close() expansion.close()
del expansion
def _includeChanged(self): def _includeChanged(self):
""" """
@ -351,7 +361,7 @@ class ExpansionsManager:
# Compute the expansions involved in the modification # Compute the expansions involved in the modification
possiblyValidExpansions = set(self._listPossiblyValidExpansions()) possiblyValidExpansions = set(self._listPossiblyValidExpansions())
previousExpansions = set(self._includedExpansions) previousExpansions = set(self._includeExpansions)
# Remove irrelevant expansions # Remove irrelevant expansions
expansionsToRemove = previousExpansions.difference(possiblyValidExpansions) expansionsToRemove = previousExpansions.difference(possiblyValidExpansions)
for expansionID in expansionsToRemove: for expansionID in expansionsToRemove:
@ -359,19 +369,27 @@ class ExpansionsManager:
# Add the new expansions # Add the new expansions
expansionsToAdd = possiblyValidExpansions.difference(previousExpansions) expansionsToAdd = possiblyValidExpansions.difference(previousExpansions)
for expansionID in expansionsToAdd: for expansionID in expansionsToAdd:
self._createExpansion(expansionID) creation_out = self._createExpansion(expansionID)
if isinstance(creation_out, expansionsLib.Expansion):
self._activeExpansions[expansionID] = creation_out
else:
raise creation_out
def _activePlayerAdded(self, playerName): def _activePlayerAdded(self, playerName):
# Find the expansions that are to be created # Find the expansions that are to be created
possibleAddition = set(self._includedExpansions).difference(self._activeExpansions) possibleAddition = set(self._includeExpansions).difference(self._activeExpansions)
filteredAddition = [ filteredAddition = [
expansionID for expansionID in possibleAddition expansionID for expansionID in possibleAddition
if hasattr(expansionsLib, expansionID) and if hasattr(expansionsLib, self._getClassFillMissing(expansionID)) and
playerName in self._getPlayersFillMissing(expansionID) playerName in self._getPlayersFillMissing(expansionID)
] ]
# Create the expansions # Create the expansions
for expansionID in filteredAddition: for expansionID in filteredAddition:
self._createExpansion(expansionID) creation_out = self._createExpansion(expansionID)
if isinstance(creation_out, expansionsLib.Expansion):
self._activeExpansions[expansionID] = creation_out
else:
raise creation_out
def _activePlayerRemoved(self, playerName): def _activePlayerRemoved(self, playerName):
# Find the expansions that are to be removed # Find the expansions that are to be removed
@ -383,32 +401,85 @@ class ExpansionsManager:
for expansionID in filteredRemoval: for expansionID in filteredRemoval:
self._removeExpansion(expansionID) self._removeExpansion(expansionID)
def _errorOccured(self, expansionID): def _midStepError(self, expansionID):
self._removeExpansion(expansionID) self._removeExpansion(expansionID)
def expansionPlayersChanged(self, expansionID): def expansionPlayersChange(self, expansionID, newPlayers):
"""
Method that allows changing the assigned players of an expansion.
Arguments:
expansionID (str):
newPlayers (tuple of str):
"""
# Save the change
previousPlayers = self._cfg[expansionID]["players"]
self._cfg[expansionID]["players"] = tuple(newPlayers)
# Check the state of the expansion
expansionIsActive = expansionID in self._activeExpansions expansionIsActive = expansionID in self._activeExpansions
activePlayers = set(self.playersManager.keys()) activePlayers = set(self.playersManager.keys())
expansionHasNoPlayers = self._getPlayersFillMissing(expansionID).isdisjoint(activePlayers) expansionHasNoPlayers = self._getPlayersFillMissing(expansionID).isdisjoint(activePlayers)
expansionDefined = hasattr(expansionsLib, expansionID) expansionDefined = hasattr(expansionsLib, self._getClassFillMissing(expansionID))
# Update the activation status
activeChanged = False
if expansionIsActive and expansionHasNoPlayers: if expansionIsActive and expansionHasNoPlayers:
self._removeExpansion(expansionID) self._removeExpansion(expansionID)
activeChanged = True
elif (not expansionIsActive) and (not expansionHasNoPlayers) and expansionDefined: elif (not expansionIsActive) and (not expansionHasNoPlayers) and expansionDefined:
self._createExpansion(expansionID) creation_out = self._createExpansion(expansionID)
if isinstance(creation_out, expansionsLib.Expansion):
self._activeExpansions[expansionID] = creation_out
activeChanged = True
else:
raise creation_out
# Update the inverse lookup
if not activeChanged:
removedPlayers = set(previousPlayers).difference(newPlayers)
addedPlayers = set(newPlayers).difference(previousPlayers)
for playerName in removedPlayers:
self._playersLookUp[playerName].pop(expansionID)
for playerName in addedPlayers:
self._playersLookUp[playerName].append(expansionID)
def lookupPlayer(self, playerName):
"""
Get a tuple of all expansions available to a player.
Arguments:
playerName (str): Name of the player
Returns:
expansionsID (tuple of str): ID of the expansions available to the
player
"""
return tuple(self._playersLookUp.get(playerName, []))
if __name__ == "__main__": if __name__ == "__main__":
# Test run to make sure nothing is flagrantly flawed # Test run to make sure nothing is flagrantly flawed
configPath = Path(__file__).parent / "players.json" configPath = Path(__file__).parent.parent.parent / "players.json"
pm = PlayersManager(playersPath=configPath, gameID="gameID0") pm = PlayersManager(gameID="gameID0", activePlayers=["Brosef"], playersPath=configPath)
# Iteration # Iteration
for name in pm: for name in pm:
print(name) print(name)
# Modification of a players data # Modification of a players data
brosef = pm["Brosef"] brosef = pm["Brosef"]
brosef.gameSave = [1] brosef.gameSave = [1]
print(pm["Brosef"].gameSave)
brosef.state = ["alive"] brosef.state = ["alive"]
# TODO Verify that changing the gameID works as intended
# TODO Test out the expansion manager # Test run for the expansionsManager
# Implement a way to search within expansions # Create the managers
configPath = Path(__file__).parent.parent.parent / "players.json"
pm = PlayersManager(gameID="gameID0", activePlayers=["Brosef"], playersPath=configPath)
em = ExpansionsManager(pm, includeExpansions=["Simple", "SourCandy"])
# Use the expansion manager to interact with a player using the expansion "Simple"
brosefAvailableExpansionsID = em.lookupPlayer("Brosef")
if "Simple" in brosefAvailableExpansionsID:
# Define some random action
vibrateInstead, duration, intensity = False, 1.0, 100
action = vibrateInstead, duration, intensity
# Perform the action
expansionObject = em["Simple"]
return_infos = expansionObject(action)
assert return_infos["done"], "The step was not successfully completed"