def init(self, game_data: GameData, agent_pbk: Address) -> None: """ Populate data structures with the game data. :param game_data: the game instance data :param agent_pbk: the public key of the agent :return: None """ # TODO: extend TAC messages to include reference to version id; then replace below with assert game_data.version_id = self.expected_version_id self._game_configuration = GameConfiguration( game_data.version_id, game_data.nb_agents, game_data.nb_goods, game_data.tx_fee, game_data.agent_pbk_to_name, game_data.good_pbk_to_name, ) self._initial_agent_state = AgentState(game_data.money, game_data.endowment, game_data.utility_params) self._agent_state = AgentState(game_data.money, game_data.endowment, game_data.utility_params) if self.strategy.is_world_modeling: opponent_pbks = self.game_configuration.agent_pbks opponent_pbks.remove(agent_pbk) self._world_state = WorldState( opponent_pbks, self.game_configuration.good_pbks, self.initial_agent_state, )
def __init__(self, configuration: GameConfiguration, initialization: GameInitialization): """ Initialize a game. :param configuration: the game configuration. :param initialization: the game initialization. """ self._configuration = configuration # type GameConfiguration self._initialization = initialization # type: GameInitialization self.transactions = [] # type: List[Transaction] self._initial_agent_states = dict( (agent_pbk, AgentState(initialization.initial_money_amounts[i], initialization.endowments[i], initialization.utility_params[i])) for agent_pbk, i in zip( configuration.agent_pbks, range( configuration.nb_agents))) # type: Dict[str, AgentState] self.agent_states = dict( (agent_pbk, AgentState(initialization.initial_money_amounts[i], initialization.endowments[i], initialization.utility_params[i])) for agent_pbk, i in zip( configuration.agent_pbks, range( configuration.nb_agents))) # type: Dict[str, AgentState] self.good_states = dict( (good_pbk, GoodState(DEFAULT_PRICE)) for good_pbk in configuration.good_pbks) # type: Dict[str, GoodState]
def test_get_game_data_from_agent_label(self): """Test that the getter of game states by agent label works as expected.""" version_id = "1" nb_agents = 3 nb_goods = 3 tx_fee = 1.0 agent_pbk_to_name = { "tac_agent_0_pbk": "tac_agent_0", "tac_agent_1_pbk": "tac_agent_1", "tac_agent_2_pbk": "tac_agent_2", } good_pbk_to_name = { "tac_good_0_pbk": "tac_good_0", "tac_good_1_pbk": "tac_good_1", "tac_good_2_pbk": "tac_good_2", } money_amounts = [20, 20, 20] endowments = [[1, 1, 1], [2, 1, 1], [1, 1, 2]] utility_params = [[20.0, 40.0, 40.0], [10.0, 50.0, 40.0], [40.0, 30.0, 30.0]] eq_prices = [1.0, 1.0, 4.0] eq_good_holdings = [[1.0, 1.0, 4.0], [1.0, 5.0, 1.0], [6.0, 1.0, 2.0]] eq_money_holdings = [20.0, 20.0, 20.0] game_configuration = GameConfiguration(version_id, nb_agents, nb_goods, tx_fee, agent_pbk_to_name, good_pbk_to_name) game_initialization = GameInitialization( money_amounts, endowments, utility_params, eq_prices, eq_good_holdings, eq_money_holdings, ) game = Game(game_configuration, game_initialization) actual_agent_state_0 = game.get_agent_state_from_agent_pbk( "tac_agent_0_pbk") actual_agent_state_1 = game.get_agent_state_from_agent_pbk( "tac_agent_1_pbk") expected_agent_state_0 = AgentState(money_amounts[0], endowments[0], utility_params[0]) expected_agent_state_1 = AgentState(money_amounts[1], endowments[1], utility_params[1]) assert actual_agent_state_0 == expected_agent_state_0 assert actual_agent_state_1 == expected_agent_state_1
def adjusted_score(self) -> Tuple[List[str], np.ndarray]: """ Compute the adjusted score of each agent. :return: a matrix of shape (1, nb_agents), where every column i contains the score of the agent. """ nb_agents = self.game.configuration.nb_agents current_scores = np.zeros((1, nb_agents), dtype=np.float32) eq_agent_states = dict(( agent_pbk, AgentState( self.game.initialization.eq_money_holdings[i], [int(h) for h in self.game.initialization.eq_good_holdings[i]], self.game.initialization.utility_params[i], ), ) for agent_pbk, i in zip( self.game.configuration.agent_pbks, range(self.game.configuration.nb_agents), )) # type: Dict[str, AgentState] result = np.zeros((1, nb_agents), dtype=np.float32) eq_scores = np.zeros((1, nb_agents), dtype=np.float32) eq_scores[0, :] = [ eq_agent_state.get_score() for eq_agent_state in eq_agent_states.values() ] temp_game = Game(self.game.configuration, self.game.initialization) # initial scores initial_scores = np.zeros((1, nb_agents), dtype=np.float32) scores_dict = temp_game.get_scores() initial_scores[0, :] = list(scores_dict.values()) keys = list(scores_dict.keys()) current_scores = np.zeros((1, nb_agents), dtype=np.float32) current_scores[0, :] = initial_scores[0, :] # compute the partial scores for every agent after every transaction # (remember that indexes of the transaction start from one, because index 0 is reserved for the initial scores) for idx, tx in enumerate(self.game.transactions): temp_game.settle_transaction(tx) scores_dict = temp_game.get_scores() current_scores[0, :] = list(scores_dict.values()) result[0, :] = np.divide( np.subtract(current_scores, initial_scores), np.subtract(eq_scores, initial_scores), ) result = np.transpose(result) return keys, result
def _update_score(self, agent_state: AgentState, append: bool = True) -> None: window_name = "{}_score_history".format(self.env_name) self.viz.line(X=[self._update_nb_agent_state], Y=[agent_state.get_score()], update="append" if append else "replace", env=self.env_name, win=window_name, opts=dict(title="{}'s Score".format(repr( self.agent_name)), xlabel="Transactions", ylabel="Score"))
def get_eq_scores(self) -> Dict[str, float]: """ Get the equilibrium score for all agents. :return: dictionary mapping agent name to equilibrium score. """ eq_agent_states = dict(( agent_pbk, AgentState( self.game.initialization.eq_money_holdings[i], [int(h) for h in self.game.initialization.eq_good_holdings[i]], self.game.initialization.utility_params[i], ), ) for agent_pbk, i in zip( self.game.configuration.agent_pbks, range(self.game.configuration.nb_agents), )) # type: Dict[str, AgentState] result = { self.game.configuration.agent_pbk_to_name[agent_pbk]: eq_agent_state.get_score() for agent_pbk, eq_agent_state in eq_agent_states.items() } return result
class GameInstance: """The GameInstance maintains state of the game from the agent's perspective.""" def __init__( self, agent_name: str, strategy: Strategy, mail_stats: MailStats, expected_version_id: str, services_interval: int = 10, pending_transaction_timeout: int = 10, dashboard: Optional[AgentDashboard] = None, ) -> None: """ Instantiate a game instance. :param agent_name: the name of the agent. :param strategy: the strategy of the agent. :param mail_stats: the mail stats of the mailbox. :param expected_version_id: the expected version of the TAC. :param services_interval: the interval at which services are updated. :param pending_transaction_timeout: the timeout after which transactions are removed from the lock manager. :param dashboard: the agent dashboard. :return: None """ self.agent_name = agent_name self.controller_pbk = None # type: Optional[str] self._strategy = strategy self._search = Search() self._dialogues = Dialogues() self._game_phase = GamePhase.PRE_GAME self._expected_version_id = expected_version_id self._game_configuration = None # type: Optional[GameConfiguration] self._initial_agent_state = None # type: Optional[AgentState] self._agent_state = None # type: Optional[AgentState] self._world_state = None # type: Optional[WorldState] self._services_interval = datetime.timedelta(0, services_interval) self._last_update_time = datetime.datetime.now( ) - self._services_interval self._last_search_time = datetime.datetime.now() - datetime.timedelta( 0, round(services_interval / 2.0)) self.goods_supplied_description = None self.goods_demanded_description = None self.transaction_manager = TransactionManager( agent_name, pending_transaction_timeout=pending_transaction_timeout) self.stats_manager = StatsManager(mail_stats, dashboard) self.dashboard = dashboard if self.dashboard is not None: self.dashboard.start() self.stats_manager.start() def init(self, game_data: GameData, agent_pbk: Address) -> None: """ Populate data structures with the game data. :param game_data: the game instance data :param agent_pbk: the public key of the agent :return: None """ # TODO: extend TAC messages to include reference to version id; then replace below with assert game_data.version_id = self.expected_version_id self._game_configuration = GameConfiguration( game_data.version_id, game_data.nb_agents, game_data.nb_goods, game_data.tx_fee, game_data.agent_pbk_to_name, game_data.good_pbk_to_name, ) self._initial_agent_state = AgentState(game_data.money, game_data.endowment, game_data.utility_params) self._agent_state = AgentState(game_data.money, game_data.endowment, game_data.utility_params) if self.strategy.is_world_modeling: opponent_pbks = self.game_configuration.agent_pbks opponent_pbks.remove(agent_pbk) self._world_state = WorldState( opponent_pbks, self.game_configuration.good_pbks, self.initial_agent_state, ) def on_state_update(self, message: TACMessage, agent_pbk: Address) -> None: """ Update the game instance with a State Update from the controller. :param state_update: the state update :param agent_pbk: the public key of the agent :return: None """ self.init(message.get("initial_state"), agent_pbk) self._game_phase = GamePhase.GAME for tx in message.get("transactions"): self.agent_state.update(tx, message.get("initial_state").get("tx_fee")) @property def expected_version_id(self) -> str: """Get the expected version id of the TAC.""" return self._expected_version_id @property def strategy(self) -> Strategy: """Get the strategy.""" return self._strategy @property def search(self) -> Search: """Get the search.""" return self._search @property def dialogues(self) -> Dialogues: """Get the dialogues.""" return self._dialogues @property def game_phase(self) -> GamePhase: """Get the game phase.""" return self._game_phase @property def game_configuration(self) -> GameConfiguration: """Get the game configuration.""" assert self._game_configuration is not None, "Game configuration not assigned!" return self._game_configuration @property def initial_agent_state(self) -> AgentState: """Get the initial agent state.""" assert (self._initial_agent_state is not None), "Initial agent state not assigned!" return self._initial_agent_state @property def agent_state(self) -> AgentState: """Get the agent state.""" assert self._agent_state is not None, "Agent state not assigned!" return self._agent_state @property def world_state(self) -> WorldState: """Get the world state.""" assert self._world_state is not None, "World state not assigned!" return self._world_state @property def services_interval(self) -> datetime.timedelta: """Get the services interval.""" return self._services_interval @property def last_update_time(self) -> datetime.datetime: """Get the last services update time.""" return self._last_update_time @property def last_search_time(self) -> datetime.datetime: """Get the last services search time.""" return self._last_search_time def is_time_to_update_services(self) -> bool: """ Check if the agent should update the service directory. :return: bool indicating the action """ now = datetime.datetime.now() result = now - self.last_update_time > self.services_interval if result: self._last_update_time = now return result def is_time_to_search_services(self) -> bool: """ Check if the agent should search the service directory. :return: bool indicating the action """ now = datetime.datetime.now() result = now - self.last_search_time > self.services_interval if result: self._last_search_time = now return result def is_profitable_transaction(self, transaction: Transaction, dialogue: Dialogue) -> Tuple[bool, str]: """ Check if a transaction is profitable. Is it a profitable transaction? - apply all the locks for role. - check if the transaction is consistent with the locks (enough money/holdings) - check that we gain score. :param transaction: the transaction :param dialogue: the dialogue :return: True if the transaction is good (as stated above), False otherwise. """ state_after_locks = self.state_after_locks(dialogue.is_seller) if not state_after_locks.check_transaction_is_consistent( transaction, self.game_configuration.tx_fee): message = "[{}]: the proposed transaction is not consistent with the state after locks.".format( self.agent_name) return False, message proposal_delta_score = state_after_locks.get_score_diff_from_transaction( transaction, self.game_configuration.tx_fee) result = self.strategy.is_acceptable_proposal(proposal_delta_score) message = "[{}]: is good proposal for {}? {}: tx_id={}, delta_score={}, amount={}".format( self.agent_name, dialogue.role, result, transaction.transaction_id, proposal_delta_score, transaction.amount, ) return result, message def get_service_description(self, is_supply: bool) -> Description: """ Get the description of the supplied goods (as a seller), or the demanded goods (as a buyer). :param is_supply: Boolean indicating whether it is supply or demand. :return: the description (to advertise on the Service Directory). """ desc = get_goods_quantities_description( self.game_configuration.good_pbks, self.get_goods_quantities(is_supply), is_supply=is_supply, ) return desc def build_services_query( self, is_searching_for_sellers: bool) -> Optional[Query]: """ Build a query to search for services. In particular, build the query to look for agents - which supply the agent's demanded goods (i.e. sellers), or - which demand the agent's supplied goods (i.e. buyers). :param is_searching_for_sellers: Boolean indicating whether the search is for sellers or buyers. :return: the Query, or None. """ good_pbks = self.get_goods_pbks(is_supply=not is_searching_for_sellers) res = (None if len(good_pbks) == 0 else build_query( good_pbks, is_searching_for_sellers)) return res def build_services_dict( self, is_supply: bool) -> Optional[Dict[str, Sequence[str]]]: """ Build a dictionary containing the services demanded/supplied. :param is_supply: Boolean indicating whether the services are demanded or supplied. :return: a Dict. """ good_pbks = self.get_goods_pbks(is_supply=is_supply) res = None if len(good_pbks) == 0 else build_dict(good_pbks, is_supply) return res def is_matching( self, cfp_services: Dict[str, Union[bool, List[Any]]], goods_description: Description, ) -> bool: """ Check for a match between the CFP services and the goods description. :param cfp_services: the services associated with the cfp. :param goods_description: a description of the goods. :return: Bool """ services = cfp_services["services"] services = cast(List[Any], services) if cfp_services["description"] is goods_description.data_model.name: # The call for proposal description and the goods model name cannot be the same for trading agent pairs. return False for good_pbk in goods_description.data_model.attributes_by_name.keys(): if good_pbk not in services: continue return True return False def get_goods_pbks(self, is_supply: bool) -> Set[str]: """ Wrap the function which determines supplied and demanded good public keys. :param is_supply: Boolean indicating whether it is referencing the supplied or demanded public keys. :return: a list of good public keys """ state_after_locks = self.state_after_locks(is_seller=is_supply) good_pbks = (self.strategy.supplied_good_pbks( self.game_configuration.good_pbks, state_after_locks.current_holdings) if is_supply else self.strategy.demanded_good_pbks( self.game_configuration.good_pbks, state_after_locks.current_holdings)) return good_pbks def get_goods_quantities(self, is_supply: bool) -> List[int]: """ Wrap the function which determines supplied and demanded good quantities. :param is_supply: Boolean indicating whether it is referencing the supplied or demanded quantities. :return: the vector of good quantities offered/requested. """ state_after_locks = self.state_after_locks(is_seller=is_supply) quantities = (self.strategy.supplied_good_quantities( state_after_locks.current_holdings) if is_supply else self.strategy.demanded_good_quantities( state_after_locks.current_holdings)) return quantities def state_after_locks(self, is_seller: bool) -> AgentState: """ Apply all the locks to the current state of the agent. This assumes, that all the locked transactions will be successful. :param is_seller: Boolean indicating the role of the agent. :return: the agent state with the locks applied to current state """ assert self._agent_state is not None, "Agent state not assigned!" transactions = ( list(self.transaction_manager.locked_txs_as_seller.values()) if is_seller else list( self.transaction_manager.locked_txs_as_buyer.values())) state_after_locks = self._agent_state.apply( transactions, self.game_configuration.tx_fee) return state_after_locks def generate_proposal(self, cfp_services: Dict[str, Union[bool, List[Any]]], is_seller: bool) -> Optional[Description]: """ Wrap the function which generates proposals from a seller or buyer. If there are locks as seller, it applies them. :param cfp_services: the query associated with the cfp. :param is_seller: Boolean indicating the role of the agent. :return: a list of descriptions """ state_after_locks = self.state_after_locks(is_seller=is_seller) candidate_proposals = self.strategy.get_proposals( self.game_configuration.good_pbks, state_after_locks.current_holdings, state_after_locks.utility_params, self.game_configuration.tx_fee, is_seller, self._world_state, ) proposals = [] for proposal in candidate_proposals: if not self.is_matching(cfp_services, proposal): continue if not proposal.values["price"] > 0: continue proposals.append(proposal) if not proposals: return None else: return random.choice(proposals) def stop(self): """Stop the services attached to the game instance.""" self.stats_manager.stop()