Compare commits
27 Commits
dd72d14ef0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
3d8bcc0efb
|
|||
| 23aea1777f | |||
| d16567ea18 | |||
| 63f44aacec | |||
| bb2b953b04 | |||
| 919793df09 | |||
| 81f38a8044 | |||
| ed7cdd21c1 | |||
| 43ee91c853 | |||
| bab32622ff | |||
| 587d93f7ea | |||
| 20a9aa5505 | |||
| 31e8d9fcb2 | |||
| b46d221173 | |||
| 8abb0acf59 | |||
| 6117d8c2ed | |||
| 53c86cb2dc | |||
| 239f9573be | |||
| d83c29e1ec | |||
| fca81916d5 | |||
| 2f51162eef | |||
| 075e9899ba | |||
| 38f6142ed6 | |||
| 93c70ba576 | |||
| 1abfec05da | |||
| c39fd28d96 | |||
| fdf89ed0a0 |
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"exampleExpansion": {
|
|
||||||
"optionA": 1,
|
|
||||||
"optionB": 2
|
|
||||||
},
|
|
||||||
|
|
||||||
"availableExpansion":
|
|
||||||
{
|
|
||||||
"bracelet1": "Brosef",
|
|
||||||
"robotic_barman": "Tango",
|
|
||||||
"challenge": "TRS"
|
|
||||||
},
|
|
||||||
|
|
||||||
"players": {
|
|
||||||
"Brosef": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "games": {"gameID0": []}},
|
|
||||||
"TRS_MML": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "games": {"gameID0": []}},
|
|
||||||
"Tango": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "games": {"gameID0": []}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
players.json
Normal file
15
players.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"expansions":
|
||||||
|
{
|
||||||
|
"ShockColar1": {"players": ["Brosef"], "tags": ["shock"], "class": "serialShocker", "config": {"COM_port": "COM3", "shocker_ID": 24770}},
|
||||||
|
"RobotBarman": {"players": ["Tango", "TRS_MML"], "tags": ["drink"], "class": "bar", "config": {}},
|
||||||
|
"ChallengeDB": {"players": ["TRS_MML"], "tags": ["challenge"], "class": "challenge", "config": {}},
|
||||||
|
"SourCandy": {"players": [], "tags": ["food"], "class": "candy", "config": {}},
|
||||||
|
"Simple": {"players": ["Brosef"], "tags": [], "class": "simplest", "config": {}}
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"Brosef": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "gamesSave": {"gameID0": 2}},
|
||||||
|
"TRS_MML": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "gamesSave": {"gameID0": 1}},
|
||||||
|
"Tango": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "gamesSave": {"gameID0": 0}}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/NoPELib/__init__.py
Normal file
1
src/NoPELib/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .player_settings import PlayersManager, ExpansionsManager
|
||||||
203
src/NoPELib/expansionsLib.py
Normal file
203
src/NoPELib/expansionsLib.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
import abc
|
||||||
|
import copy
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .player_settings import ExpansionsManager
|
||||||
|
|
||||||
|
|
||||||
|
class Hook:
|
||||||
|
"""
|
||||||
|
A class that allows the creation of hooks.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
connect: Connect a slot when the signal is launched.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._slot = []
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
for fct in self._slot:
|
||||||
|
fct(*args, **kwargs)
|
||||||
|
|
||||||
|
def connect(self, fct):
|
||||||
|
""" Connect a slot when the signal is launched. """
|
||||||
|
self._slot.append(fct)
|
||||||
|
|
||||||
|
|
||||||
|
class Expansion:
|
||||||
|
"""
|
||||||
|
A meta class implementation meant to describe how to interact with any kind
|
||||||
|
of expansions.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
players (tuple of str):
|
||||||
|
tags (tuple of flag):
|
||||||
|
config: The config of the expansion. Can't be modified during execution.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
__call__: Execute a given action with the expansion
|
||||||
|
|
||||||
|
Subclass methods:
|
||||||
|
step:
|
||||||
|
reset:
|
||||||
|
close:
|
||||||
|
|
||||||
|
Signals:
|
||||||
|
midStepError:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ID: str, expansionsManager: "ExpansionsManager"):
|
||||||
|
self._ID = ID
|
||||||
|
self._closed = False
|
||||||
|
self._manager = expansionsManager
|
||||||
|
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
|
||||||
|
def config(self):
|
||||||
|
"""
|
||||||
|
The config of the expansion.
|
||||||
|
Can't be modified during execution.
|
||||||
|
"""
|
||||||
|
return copy.deepcopy(self._manager.config[self._ID]["config"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def players(self):
|
||||||
|
""" Players that have access to the expansion. """
|
||||||
|
return self._manager.config[self._ID]["players"]
|
||||||
|
|
||||||
|
@players.setter
|
||||||
|
def players(self, newPlayers):
|
||||||
|
self._manager.expansionPlayersChange(self._ID, newPlayers)
|
||||||
|
|
||||||
|
@players.deleter
|
||||||
|
def players(self):
|
||||||
|
self._manager.expansionPlayersChange(self._ID, ())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self):
|
||||||
|
""" Tags of the expansion. """
|
||||||
|
return self._manager.config[self._ID]["tags"]
|
||||||
|
|
||||||
|
@tags.setter
|
||||||
|
def tags(self, newPlayers):
|
||||||
|
self._manager.config[self._ID]["tags"] = tuple(newPlayers)
|
||||||
|
|
||||||
|
@tags.deleter
|
||||||
|
def tags(self):
|
||||||
|
self._manager.config[self._ID]["tags"] = ()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def step(self, action):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def reset(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def close(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class serialShocker(Expansion):
|
||||||
|
"""
|
||||||
|
Shockers
|
||||||
|
|
||||||
|
TODO Describe the config file
|
||||||
|
"""
|
||||||
|
_api = {}
|
||||||
|
|
||||||
|
def __init__(self, ID, expansionsManager):
|
||||||
|
super().__init__(ID, expansionsManager)
|
||||||
|
if self.config["COM_port"] not in serialShocker._api:
|
||||||
|
self._assignApi()
|
||||||
|
self.shocker = serialShocker._api[self.config["COM_port"]].shocker(self.config["shocker_ID"])
|
||||||
|
|
||||||
|
def _assignApi(self):
|
||||||
|
from pishock import SerialAPI
|
||||||
|
serialShocker._api[self.config["COM_port"]] = SerialAPI(self.config["COM_port"])
|
||||||
|
|
||||||
|
def step(self, action):
|
||||||
|
"""
|
||||||
|
Arguments:
|
||||||
|
action (tuple): Tuple containing:
|
||||||
|
a bool deciding if vibrate should be used instead of shock
|
||||||
|
a positive float that defines the duration
|
||||||
|
a float within the range [0.0, 1.0] that defines the intensity
|
||||||
|
"""
|
||||||
|
# Execute the step
|
||||||
|
vibrateInstead, duration, intensity = action
|
||||||
|
callFunc = self.shocker.vibrate if vibrateInstead else self.shocker.shock
|
||||||
|
callFunc(duration=duration, intensity=intensity)
|
||||||
|
# Return additionnal info
|
||||||
|
step_info = {
|
||||||
|
"expansionID": self._ID,
|
||||||
|
"showOnScreen": None,
|
||||||
|
"error": None,
|
||||||
|
"done": True
|
||||||
|
}
|
||||||
|
return step_info
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
if self.config["COM_port"] not in serialShocker._api:
|
||||||
|
serialShocker._api[self.config["COM_port"]].restart()
|
||||||
|
else:
|
||||||
|
self._assignApi()
|
||||||
|
|
||||||
|
|
||||||
|
class simplest(Expansion):
|
||||||
|
""" A very simple expansion that only prints on screen. """
|
||||||
|
def __init__(self, ID, expansionsManager):
|
||||||
|
super().__init__(ID, expansionsManager)
|
||||||
|
print(f"Initialising with config {self.config}")
|
||||||
|
|
||||||
|
def step(self, action):
|
||||||
|
"""
|
||||||
|
Arguments:
|
||||||
|
action (tuple): Tuple containing:
|
||||||
|
a bool deciding if vibrate should be used instead of shock
|
||||||
|
a positive float that defines the duration
|
||||||
|
a float within the range [0.0, 1.0] that defines the intensity
|
||||||
|
"""
|
||||||
|
# Execute the step
|
||||||
|
vibrateInstead, duration, intensity = action
|
||||||
|
values = f"duration={duration}, intensity={intensity}"
|
||||||
|
interact_type = "Vibrate" if vibrateInstead else "Shock"
|
||||||
|
message = f"{interact_type} with {values}"
|
||||||
|
print(message)
|
||||||
|
# Return additionnal info
|
||||||
|
step_info = {
|
||||||
|
"expansionID": self._ID,
|
||||||
|
"showOnScreen": message,
|
||||||
|
"error": None,
|
||||||
|
"done": True
|
||||||
|
}
|
||||||
|
return step_info
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
print("Closing")
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
print("Reseting")
|
||||||
485
src/NoPELib/player_settings.py
Normal file
485
src/NoPELib/player_settings.py
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from . import expansionsLib
|
||||||
|
|
||||||
|
_log = logging.getLogger('NoPE-Lib')
|
||||||
|
|
||||||
|
|
||||||
|
class PlayersManager:
|
||||||
|
"""
|
||||||
|
Manager of players for a given game.
|
||||||
|
Since this class implements most methods available to classic dict,
|
||||||
|
you can think of this class as a python dict.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
gameID (str): The gameID of the active game
|
||||||
|
config (dict): The in memory state of the config file
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
save:
|
||||||
|
|
||||||
|
Signals:
|
||||||
|
onMadePlayerActive:
|
||||||
|
onMadePlayerInactive:
|
||||||
|
"""
|
||||||
|
defaultPlayerConfig = {"flags": [], "expansions": {}, "games": {}}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, gameID: str=None, activePlayers: list[str]=None,
|
||||||
|
playersPath: str='../players.json', loggerID: str='PlayersManager'
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
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'.
|
||||||
|
loggerID (str, optional): The ID used for logging. Defaults to
|
||||||
|
'Players'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Store the arguments
|
||||||
|
self._log = _log.getChild(loggerID)
|
||||||
|
self._currentGameID = gameID
|
||||||
|
# Deal with the path
|
||||||
|
self._playersPath = Path(playersPath)
|
||||||
|
# Get the config file
|
||||||
|
with open(self._playersPath, "r", encoding="utf-8") as f:
|
||||||
|
self._cfg = json.load(f)
|
||||||
|
# Create the players
|
||||||
|
activePlayers = activePlayers if activePlayers is not None else self._cfg["players"].keys()
|
||||||
|
self._player_data = {
|
||||||
|
name: Player(name, self, **player_cfg)
|
||||||
|
for name, player_cfg in self._cfg["players"].items()
|
||||||
|
if name in activePlayers
|
||||||
|
}
|
||||||
|
# Create the signals
|
||||||
|
self.onMadePlayerActive = expansionsLib.Hook()
|
||||||
|
self.onMadePlayerInactive = expansionsLib.Hook()
|
||||||
|
|
||||||
|
def __getitem__(self, playerName: str):
|
||||||
|
"""
|
||||||
|
Get a player object.
|
||||||
|
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._player_data:
|
||||||
|
# Fetch the player's data and make them active
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Replace a player's config with another one.
|
||||||
|
"""
|
||||||
|
makeNewPlayerObject = False
|
||||||
|
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):
|
||||||
|
del self._player_data[playerName]
|
||||||
|
self.onMadePlayerInactive(playerName)
|
||||||
|
self._log.debug(f"Removed {playerName} from active players")
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._player_data)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._player_data.__iter__()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Manager is playing {self.gameID} with {len(self._player_data)} active players"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
""" Deepcopy of the dictionnary of the config. """
|
||||||
|
return copy.deepcopy(self._cfg)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gameID(self):
|
||||||
|
""" String of the current game ID. """
|
||||||
|
return self._currentGameID
|
||||||
|
|
||||||
|
@gameID.setter
|
||||||
|
def gameID(self, gameID: str):
|
||||||
|
self._currentGameID = gameID
|
||||||
|
self._player_data = {
|
||||||
|
name: Player(name, self, **cfg)
|
||||||
|
for name, cfg in self._cfg["players"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
@gameID.deleter
|
||||||
|
def gameID(self):
|
||||||
|
self._currentGameID = None
|
||||||
|
self._player_data = {
|
||||||
|
name: Player(name, self, **cfg)
|
||||||
|
for name, cfg in self._cfg["players"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
""" Iterator of the active players' names """
|
||||||
|
return self._player_data.keys()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
""" Iterator of the active players' objects """
|
||||||
|
return self._player_data.keys()
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
""" Two iterators of the activate players' names and object """
|
||||||
|
return self._player_data.items()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
""" Save the config to the disk. """
|
||||||
|
with open(self._playersPath, "w", encoding="utf-8") as f:
|
||||||
|
self._cfg = f.write(json.dump(f, self._cfg))
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
"""
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
flags:
|
||||||
|
availableExpansions:
|
||||||
|
expansionsConfig:
|
||||||
|
gameSave: The game settings are not guaranteed to have data in it.
|
||||||
|
gameState:
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
exportConfig:
|
||||||
|
punish:
|
||||||
|
"""
|
||||||
|
def __init__(self, playerName: str, manager: PlayersManager, **cfg):
|
||||||
|
self._name = playerName
|
||||||
|
self._manager = manager
|
||||||
|
self._gamesSave = {}
|
||||||
|
for key, val in cfg.items():
|
||||||
|
setattr(self, "_" + key, val)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
txt = f"Player {self._name} has "
|
||||||
|
txt += f"{len(self.flags)} flags, "
|
||||||
|
txt += f"{len(self._expansions)} expansions and "
|
||||||
|
txt += f"{len(self._gamesSave)} saved game"
|
||||||
|
return txt
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" String representing the name of the player. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def flags(self):
|
||||||
|
""" List of the flags associated with the player. """
|
||||||
|
return self._flags
|
||||||
|
|
||||||
|
@flags.setter
|
||||||
|
def flags(self, newFlags):
|
||||||
|
self._flags = newFlags
|
||||||
|
# The container was changed and must be transmited to the manager
|
||||||
|
self._manager[self._name] = self.exportConfig()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expansions(self):
|
||||||
|
""" Dictionnary of the player's expansion settings. """
|
||||||
|
return self._expansions
|
||||||
|
|
||||||
|
@expansions.setter
|
||||||
|
def expansions(self, newExpansions):
|
||||||
|
self._expansions = newExpansions
|
||||||
|
# The container was changed and must be transmited to the manager
|
||||||
|
self._manager[self._name] = self.exportConfig()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gameSave(self):
|
||||||
|
"""
|
||||||
|
The save for the current game. If no configuration was found None
|
||||||
|
is returned.
|
||||||
|
"""
|
||||||
|
return self._gamesSave.get(self._manager.gameID)
|
||||||
|
|
||||||
|
@gameSave.setter
|
||||||
|
def gameSave(self, newGameSave):
|
||||||
|
self._gamesSave[self._manager.gameID] = newGameSave
|
||||||
|
|
||||||
|
def exportConfig(self):
|
||||||
|
""" Export the exact player's configuration. """
|
||||||
|
return {
|
||||||
|
"flags": self._flags,
|
||||||
|
"expansions": self._expansions,
|
||||||
|
"games": self._gamesSave
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ExpansionsManager:
|
||||||
|
"""
|
||||||
|
Manager of the availability of the expansions.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
includeExpansions: tuple of str
|
||||||
|
Container of the expansions to try making available
|
||||||
|
activeExpansions: dict of Expansion
|
||||||
|
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": ()}
|
||||||
|
keysConvert2Tuple = ("players", "tags")
|
||||||
|
tags = ["shock", "spice", "sour", "drink", "challenge"]
|
||||||
|
|
||||||
|
def __init__(self, playersManager: PlayersManager, includeExpansions: tuple[str]=None):
|
||||||
|
"""
|
||||||
|
Arguments:
|
||||||
|
tryInclude: bool=False, tryActivate: bool=False
|
||||||
|
"""
|
||||||
|
# Create the attributes
|
||||||
|
self.playersManager = playersManager
|
||||||
|
self._playersLookUp = {}
|
||||||
|
self._cfg = playersManager.config["expansions"]
|
||||||
|
if includeExpansions is not None:
|
||||||
|
self._includeExpansions = tuple(includeExpansions)
|
||||||
|
else:
|
||||||
|
self._includeExpansions = ()
|
||||||
|
# Convert the required lists into tuples
|
||||||
|
for expansionID in self._cfg:
|
||||||
|
for key in self.keysConvert2Tuple:
|
||||||
|
converted = tuple(self._cfg[expansionID][key])
|
||||||
|
self._cfg[expansionID][key] = converted
|
||||||
|
# Compute the active expansions
|
||||||
|
self._activeExpansions = {}
|
||||||
|
for expansionID in self._listPossiblyValidExpansions():
|
||||||
|
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
|
||||||
|
playersManager.onMadePlayerActive.connect(self._activePlayerAdded)
|
||||||
|
playersManager.onMadePlayerInactive.connect(self._activePlayerRemoved)
|
||||||
|
|
||||||
|
def __getitem__(self, expansionID: str):
|
||||||
|
return self._activeExpansions[expansionID]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._activeExpansions)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._activeExpansions.__iter__()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"ExpansionsManager has {len(self._activeExpansions)} active expansions"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def includeExpansions(self):
|
||||||
|
""" Dictionnary of the included expansions. """
|
||||||
|
return self._includeExpansions
|
||||||
|
|
||||||
|
@includeExpansions.setter
|
||||||
|
def includeExpansions(self, newIncluded: tuple[str]):
|
||||||
|
self._includeExpansions = tuple(newIncluded)
|
||||||
|
self._includeChanged()
|
||||||
|
|
||||||
|
@includeExpansions.deleter
|
||||||
|
def includeExpansions(self):
|
||||||
|
self._includeExpansions = ()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
""" Dictionnary of the expansions' settings. """
|
||||||
|
return self._cfg
|
||||||
|
|
||||||
|
def _getPlayersFillMissing(self, expansionID):
|
||||||
|
return set(self._cfg.get(expansionID, {}).get("players", []))
|
||||||
|
|
||||||
|
def _getClassFillMissing(self, expansionID):
|
||||||
|
return self._cfg.get(expansionID, {}).get("class", "")
|
||||||
|
|
||||||
|
def _listPossiblyValidExpansions(self):
|
||||||
|
"""
|
||||||
|
List expansions that are: included, defined in expansionsLib and available
|
||||||
|
to at least one active player.
|
||||||
|
"""
|
||||||
|
activePlayers = set(self.playersManager.keys())
|
||||||
|
possiblyValidExpansions = [
|
||||||
|
expansionID for expansionID in self._includeExpansions
|
||||||
|
if hasattr(expansionsLib, self._getClassFillMissing(expansionID)) and
|
||||||
|
not self._getPlayersFillMissing(expansionID).isdisjoint(activePlayers)
|
||||||
|
]
|
||||||
|
return possiblyValidExpansions
|
||||||
|
|
||||||
|
def _createExpansion(self, expansionID):
|
||||||
|
terminalErrors = SystemExit, KeyboardInterrupt, GeneratorExit
|
||||||
|
try:
|
||||||
|
# Create the expansion
|
||||||
|
expansionClass = self._getClassFillMissing(expansionID)
|
||||||
|
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:
|
||||||
|
self._playersLookUp[player] = [expansionID]
|
||||||
|
return expansion
|
||||||
|
except terminalErrors as e:
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
return e
|
||||||
|
|
||||||
|
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.close()
|
||||||
|
|
||||||
|
def _includeChanged(self):
|
||||||
|
"""
|
||||||
|
Update the list of active Expansions
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Compute the expansions involved in the modification
|
||||||
|
possiblyValidExpansions = set(self._listPossiblyValidExpansions())
|
||||||
|
previousExpansions = set(self._includeExpansions)
|
||||||
|
# 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:
|
||||||
|
creation_out = self._createExpansion(expansionID)
|
||||||
|
if isinstance(creation_out, expansionsLib.Expansion):
|
||||||
|
self._activeExpansions[expansionID] = creation_out
|
||||||
|
else:
|
||||||
|
raise creation_out
|
||||||
|
|
||||||
|
def _activePlayerAdded(self, playerName):
|
||||||
|
# Find the expansions that are to be created
|
||||||
|
possibleAddition = set(self._includeExpansions).difference(self._activeExpansions)
|
||||||
|
filteredAddition = [
|
||||||
|
expansionID for expansionID in possibleAddition
|
||||||
|
if hasattr(expansionsLib, self._getClassFillMissing(expansionID)) and
|
||||||
|
playerName in self._getPlayersFillMissing(expansionID)
|
||||||
|
]
|
||||||
|
# Create the expansions
|
||||||
|
for expansionID in filteredAddition:
|
||||||
|
creation_out = self._createExpansion(expansionID)
|
||||||
|
if isinstance(creation_out, expansionsLib.Expansion):
|
||||||
|
self._activeExpansions[expansionID] = creation_out
|
||||||
|
else:
|
||||||
|
raise creation_out
|
||||||
|
|
||||||
|
def _activePlayerRemoved(self, playerName):
|
||||||
|
# Find the expansions that are to be removed
|
||||||
|
filteredRemoval = [
|
||||||
|
expansionID for expansionID in self._activeExpansions
|
||||||
|
if playerName in self._cfg["players"] and len(self._cfg["players"]) == 1
|
||||||
|
]
|
||||||
|
# Close the expansions
|
||||||
|
for expansionID in filteredRemoval:
|
||||||
|
self._removeExpansion(expansionID)
|
||||||
|
|
||||||
|
def _midStepError(self, expansionID):
|
||||||
|
self._removeExpansion(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
|
||||||
|
activePlayers = set(self.playersManager.keys())
|
||||||
|
expansionHasNoPlayers = self._getPlayersFillMissing(expansionID).isdisjoint(activePlayers)
|
||||||
|
expansionDefined = hasattr(expansionsLib, self._getClassFillMissing(expansionID))
|
||||||
|
# Update the activation status
|
||||||
|
activeChanged = False
|
||||||
|
if expansionIsActive and expansionHasNoPlayers:
|
||||||
|
self._removeExpansion(expansionID)
|
||||||
|
activeChanged = True
|
||||||
|
elif (not expansionIsActive) and (not expansionHasNoPlayers) and expansionDefined:
|
||||||
|
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__":
|
||||||
|
# Test run to make sure nothing is flagrantly flawed
|
||||||
|
configPath = Path(__file__).parent.parent.parent / "players.json"
|
||||||
|
pm = PlayersManager(gameID="gameID0", activePlayers=["Brosef"], playersPath=configPath)
|
||||||
|
# Iteration
|
||||||
|
for name in pm:
|
||||||
|
print(name)
|
||||||
|
# Modification of a players data
|
||||||
|
brosef = pm["Brosef"]
|
||||||
|
brosef.gameSave = [1]
|
||||||
|
brosef.state = ["alive"]
|
||||||
|
|
||||||
|
# Test run for the expansionsManager
|
||||||
|
# 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"
|
||||||
@ -1 +0,0 @@
|
|||||||
pass
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
class Expansion:
|
|
||||||
def __init__(self, parent, globalConfig):
|
|
||||||
setattr(parent, self.__class__.ID, self)
|
|
||||||
|
|
||||||
class PlayerExpansion:
|
|
||||||
def __init__(self, player, localConfig):
|
|
||||||
pass
|
|
||||||
@ -1,311 +0,0 @@
|
|||||||
"""
|
|
||||||
Needs to manage any number of players
|
|
||||||
Needs to work with mulitple rounds/turns
|
|
||||||
Needs to handle modifying number of players
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import copy
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
_log = logging.getLogger('NoPE-Lib')
|
|
||||||
|
|
||||||
|
|
||||||
class PlayersManager:
|
|
||||||
"""
|
|
||||||
Manager of players for a given game.
|
|
||||||
Since this class implements most methods available to classic dict,
|
|
||||||
you can think of this class as a python dict.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
gameID (str): The gameID of the active game
|
|
||||||
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
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'.
|
|
||||||
loggerID (str, optional): The ID used for logging. Defaults to 'Players'.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Store the arguments
|
|
||||||
self._log = _log.getChild(loggerID)
|
|
||||||
self._currentGameID = gameID
|
|
||||||
# Deal with the path
|
|
||||||
self._playersPath = Path(playersPath)
|
|
||||||
# Get the config file
|
|
||||||
with open(self._playersPath, 'r') as f:
|
|
||||||
self._cfg = json.load(f)
|
|
||||||
# Create the players
|
|
||||||
activePlayers = activePlayers if activePlayers is not None else self._cfg["players"].keys()
|
|
||||||
self._player_data = {
|
|
||||||
name: Player(name, self, **player_cfg)
|
|
||||||
for name, player_cfg in self._cfg["players"].items()
|
|
||||||
if name in activePlayers
|
|
||||||
}
|
|
||||||
|
|
||||||
def __getitem__(self, playerName: str):
|
|
||||||
"""
|
|
||||||
Get a player object.
|
|
||||||
If the player wasn't 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:
|
|
||||||
# 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]
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
def __delitem__(self, playerName):
|
|
||||||
self._log.debug(f"Removing {playerName} from active players")
|
|
||||||
del self._player_data[playerName]
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._player_data)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self._player_data.__iter__()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"Manager is playing {self.gameID} with {len(self._player_data)} active players"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gameID(self):
|
|
||||||
return self._currentGameID
|
|
||||||
|
|
||||||
@gameID.setter
|
|
||||||
def gameID(self, gameID: str):
|
|
||||||
self._currentGameID = gameID
|
|
||||||
self._player_data = {name: Player(name, self, **cfg) for name, cfg in self._cfg["players"].items()}
|
|
||||||
|
|
||||||
@gameID.deleter
|
|
||||||
def gameID(self):
|
|
||||||
self._currentGameID = None
|
|
||||||
self._player_data = {name: Player(name, self, **cfg) for name, cfg in self._cfg["players"].items()}
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
""" Iterator of the active players' names """
|
|
||||||
return self._player_data.keys()
|
|
||||||
|
|
||||||
def values(self):
|
|
||||||
""" Iterator of the active players' objects """
|
|
||||||
return self._player_data.keys()
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
""" Two iterators of the activate players' names and object """
|
|
||||||
return self._player_data.items()
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
with open(self._playersPath, 'w') as f:
|
|
||||||
self._cfg = json.dump(f, self._cfg)
|
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
|
||||||
"""
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
flags:
|
|
||||||
availableExpansions:
|
|
||||||
expansionsConfig:
|
|
||||||
gameSave: The game settings are not guaranteed to have data in it.
|
|
||||||
gameState:
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
generateConfig:
|
|
||||||
"""
|
|
||||||
def __init__(self, name: str, manager: PlayersManager, **cfg):
|
|
||||||
self._name = name
|
|
||||||
self._manager = manager
|
|
||||||
for key, val in cfg.items():
|
|
||||||
setattr(self, "_" + key, val)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"Player {self._name} has {len(self._flags)} flag(s), {len(self._expansions)} expansions and {len(self._games)} saved game"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def flags(self):
|
|
||||||
return self._flags
|
|
||||||
|
|
||||||
@flags.setter
|
|
||||||
def flags(self, newFlags):
|
|
||||||
self._flags = newFlags
|
|
||||||
# The container was changed and must be transmited to the manager
|
|
||||||
self._manager[self._name] = self.generateConfig()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def expansionsConfig(self):
|
|
||||||
return self._expansionsConfig
|
|
||||||
|
|
||||||
@expansionsConfig.setter
|
|
||||||
def expansions(self, newExpansions):
|
|
||||||
self._expansionsConfig = newExpansions
|
|
||||||
# The container was changed and must be transmited to the manager
|
|
||||||
self._manager[self._name] = self.generateConfig()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gameSave(self):
|
|
||||||
"""The save for the current game. If no configuration was found None is returned."""
|
|
||||||
return self._games.get(self._manager.gameID)
|
|
||||||
|
|
||||||
@gameSave.setter
|
|
||||||
def gameSave(self, newGameSave):
|
|
||||||
self._games[self._manager.gameID] = newGameSave
|
|
||||||
|
|
||||||
def generateConfig(self):
|
|
||||||
return {"flags": self._flags, "expansions": self._expansions, "games": self._games}
|
|
||||||
|
|
||||||
def punish(self, value, preferedExpansion: str=None):
|
|
||||||
do_something = lambda value: 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
|
|
||||||
additionalInfos = do_something(value)
|
|
||||||
return additionalInfos
|
|
||||||
|
|
||||||
|
|
||||||
class Expansion:
|
|
||||||
"""
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
step:
|
|
||||||
reset:
|
|
||||||
close:
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
Raise an error if not available
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def step(self, action):
|
|
||||||
"""
|
|
||||||
Call close if an error is thrown
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def reset(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def close(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ExpansionManager:
|
|
||||||
"""
|
|
||||||
Manager of the availability of the expansions.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
includedExpansions: tuple of str
|
|
||||||
Container of the expansions to try making available
|
|
||||||
releventExpansions: dict of Expansion
|
|
||||||
Container of the relevent expansions
|
|
||||||
"""
|
|
||||||
tags = ["shock", "spice", "sour", "drink", "challenge"]
|
|
||||||
|
|
||||||
def __init__(self, playersManager: PlayersManager, includedExpansions: tuple[str]=None):
|
|
||||||
self.playersManager = playersManager
|
|
||||||
self._includedExpansions = tuple(includedExpansions) if includedExpansions is not None else ()
|
|
||||||
self._releventExpansions = {} # TODO Populate the dictionnary
|
|
||||||
|
|
||||||
@property
|
|
||||||
def includedExpansions(self):
|
|
||||||
return self._includedExpansions
|
|
||||||
|
|
||||||
@includedExpansions.setter
|
|
||||||
def includedExpansions(self, newIncluded: tuple[str]):
|
|
||||||
self._includedExpansions = tuple(newIncluded)
|
|
||||||
self._includedChanged()
|
|
||||||
|
|
||||||
@includedExpansions.deleter
|
|
||||||
def includedExpansions(self):
|
|
||||||
self._includedExpansions = ()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def releventExpansions(self):
|
|
||||||
return self._releventExpansions
|
|
||||||
|
|
||||||
def _includedChanged(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.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _activePlayersChanged(self, activatePlayers):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _errorOccured(self, expansionID):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _expansionPlayersChanged(self, expansionID):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def resetExpansion(self, expansion: Expansion, suppressError: bool=False):
|
|
||||||
"""
|
|
||||||
If the expansion was relevent, close it.
|
|
||||||
In any case, try creating a new
|
|
||||||
"""
|
|
||||||
# If the expansion was relevent, close it
|
|
||||||
# Try creating a new instance of the expansion
|
|
||||||
# Update the list of relevent expansions
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Test run to make sure nothing is flagrantly flawed
|
|
||||||
configPath = Path(__file__).parent / "players.json"
|
|
||||||
manager = PlayersManager(playersPath=configPath, gameID="gameID0")
|
|
||||||
# Iteration
|
|
||||||
for playerName in manager:
|
|
||||||
print(playerName)
|
|
||||||
# Modification of a players data
|
|
||||||
brosef = manager["Brosef"]
|
|
||||||
brosef.gameSave = [1]
|
|
||||||
print(manager["Brosef"].gameSave)
|
|
||||||
brosef.state = ["alive"]
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"expansions":
|
|
||||||
{
|
|
||||||
"shockColar1": {"players": ["Brosef"], "class": "pishock", "type": ["shock"]},
|
|
||||||
"robotBarman": {"players": ["Tango", "TRS_MML"], "class": "roboticBarman", "type": ["drink"]},
|
|
||||||
"challengeDB": {"players": ["TRS_MML"], "class": "challengeDataBase", "type": ["challenge"]},
|
|
||||||
"sourCandy": {"players": [], "class": "sourCandy", "type": ["food"]}
|
|
||||||
},
|
|
||||||
"players": {
|
|
||||||
"Brosef": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "games": {"gameID0": 2}},
|
|
||||||
"TRS_MML": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "games": {"gameID0": 1}},
|
|
||||||
"Tango": {"flags": [], "expansions": {"exampleExpansion": {"playerOption": 5}}, "games": {"gameID0": 0}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user