import logging from random import randint,uniform from time import time from typing import cast from geniusweb.actions.Accept import Accept from geniusweb.actions.Action import Action from geniusweb.actions.LearningDone import LearningDone from geniusweb.actions.Offer import Offer from geniusweb.actions.PartyId import PartyId from geniusweb.actions.Vote import Vote from geniusweb.actions.Votes import Votes from geniusweb.bidspace.AllBidsList import AllBidsList from geniusweb.inform.ActionDone import ActionDone from geniusweb.inform.Finished import Finished from geniusweb.inform.Inform import Inform from geniusweb.inform.OptIn import OptIn from geniusweb.inform.Settings import Settings from geniusweb.inform.Voting import Voting from geniusweb.inform.YourTurn import YourTurn from geniusweb.issuevalue.Bid import Bid from geniusweb.issuevalue.Domain import Domain from geniusweb.issuevalue.Value import Value from geniusweb.issuevalue.ValueSet import ValueSet from geniusweb.party.Capabilities import Capabilities from geniusweb.party.DefaultParty import DefaultParty from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import ( LinearAdditiveUtilitySpace, ) from geniusweb.profileconnection.ProfileConnectionFactory import ( ProfileConnectionFactory, ) from geniusweb.progress.ProgressTime import ProgressTime from geniusweb.references.Parameters import Parameters from tudelft_utilities_logging.ReportToLogger import ReportToLogger from .utils.opponent_model import OpponentModel from geniusweb.progress.ProgressRounds import ProgressRounds from geniusweb.utils import val from geniusweb.profileconnection.ProfileInterface import ProfileInterface from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive from geniusweb.progress.Progress import Progress from tudelft.utilities.immutablelist.ImmutableList import ImmutableList from decimal import Decimal import sys from .extended_util_space import ExtendedUtilSpace from tudelft_utilities_logging.Reporter import Reporter class AgentFish(DefaultParty): """ Template of a Python geniusweb agent. """ def __init__(self): super().__init__() self.logger: ReportToLogger = self.getReporter() self.domain: Domain = None self.parameters: Parameters = None self.profile: LinearAdditiveUtilitySpace = None self.progress: ProgressTime = None self.me: PartyId = None self.other: str = None self.settings: Settings = None self.storage_dir: str = None self.last_received_bid: Bid = None self.opponent_model: OpponentModel = None self.logger.log(logging.INFO, "party is initialized") self.profileint: ProfileInterface = None # type:ignore self.utilspace: LinearAdditive = None # type:ignore self.extendedspace: ExtendedUtilSpace = None # type:ignore self.e: float = 1.2 self.lastvotes: Votes = None # type:ignore self.opponent_max = 0.0 def notifyChange(self, data: Inform): """MUST BE IMPLEMENTED This is the entry point of all interaction with your agent after is has been initialised. How to handle the received data is based on its class type. Args: info (Inform): Contains either a request for action or information. """ # a Settings message is the first message that will be send to your # agent containing all the information about the negotiation session. if isinstance(data, Settings): self.settings = cast(Settings, data) self.me = self.settings.getID() # progress towards the deadline has to be tracked manually through the use of the Progress object self.progress = self.settings.getProgress() self.parameters = self.settings.getParameters() self.storage_dir = self.parameters.get("storage_dir") # the profile contains the preferences of the agent over the domain profile_connection = ProfileConnectionFactory.create( data.getProfile().getURI(), self.getReporter() ) self.profile = profile_connection.getProfile() self.domain = self.profile.getDomain() profile_connection.close() # ActionDone informs you of an action (an offer or an accept) # that is performed by one of the agents (including yourself). elif isinstance(data, ActionDone): action = cast(ActionDone, data).getAction() actor = action.getActor() # ignore action if it is our action if actor != self.me: # obtain the name of the opponent, cutting of the position ID. self.other = str(actor).rsplit("_", 1)[0] # process action done by opponent self.opponent_action(action) # YourTurn notifies you that it is your turn to act elif isinstance(data, YourTurn): # execute a turn self.my_turn() # Finished will be send if the negotiation has ended (through agreement or deadline) elif isinstance(data, Finished): self.save_data() # terminate the agent MUST BE CALLED self.logger.log(logging.INFO, "party is terminating:") super().terminate() else: self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data)) def getE(self) -> float: return self.e def getCapabilities(self) -> Capabilities: """MUST BE IMPLEMENTED Method to indicate to the protocol what the capabilities of this agent are. Leave it as is for the ANL 2022 competition Returns: Capabilities: Capabilities representation class """ return Capabilities( set(["SAOP"]), set(["geniusweb.profile.utilityspace.LinearAdditive"]), ) def send_action(self, action: Action): """Sends an action to the opponent(s) Args: action (Action): action of this agent """ self.getConnection().send(action) # give a description of your agent def getDescription(self) -> str: """MUST BE IMPLEMENTED Returns a description of your agent. 1 or 2 sentences. Returns: str: Agent description """ return "Foot in the Door agent for the ANL 2022 competition" def opponent_action(self, action): """Process an action that was received from the opponent. Args: action (Action): action of opponent """ # if it is an offer, set the last received bid if isinstance(action, Offer): # create opponent model if it was not yet initialised if self.opponent_model is None: self.opponent_model = OpponentModel(self.domain) bid = cast(Offer, action).getBid() # update opponent model with bid self.opponent_model.update(bid) # set bid as last received self.last_received_bid = bid if self.opponent_max < self.profile.getUtility(bid): self.opponent_max = self.profile.getUtility(bid) def my_turn(self): """This method is called when it is our turn. It should decide upon an action to perform and send this action to the opponent. """ # check if the last received offer is good enough self.updateUtilSpace() if self.accept_condition(self.last_received_bid): # if so, accept the offer action = Accept(self.me, self.last_received_bid) else: # if not, find a bid to propose as counter offer bid = self.makeBid() action = Offer(self.me, bid) # send the action self.send_action(action) def updateUtilSpace(self) -> LinearAdditive: # throws IOException newutilspace = self.profile if not newutilspace == self.utilspace: self.utilspace = cast(LinearAdditive, newutilspace) self.extendedspace = ExtendedUtilSpace(self.utilspace) return self.utilspace def makeBid(self) -> Bid: """ @return next possible bid with current target utility, or null if no such bid. """ time_to_deadline = self.progress.get(time() * 1000) utilityGoal = self.getUtilityGoal( time_to_deadline, self.getE(), self.extendedspace.getMin(), self.extendedspace.getMax(), ) options: ImmutableList[Bid] = self.extendedspace.getBids(utilityGoal) if options.size() == 0: # if we can't find good bid, get max util bid.... options = self.extendedspace.getBids(self.extendedspace.getMax()) # pick a random one. return options.get(randint(0, options.size() - 1)) def getUtilityGoal( self, t: float, e: float, minUtil: Decimal, maxUtil: Decimal ) -> Decimal: """ @param t the time in [0,1] where 0 means start of nego and 1 the end of nego (absolute time/round limit) @param e the e value that determinses how fast the party makes concessions with time. Typically around 1. 0 means no concession, 1 linear concession, >1 faster than linear concession. @param minUtil the minimum utility possible in our profile @param maxUtil the maximum utility possible in our profile @return the utility goal for this time and e value """ ft1 = Decimal(1) if e != 0: ft1 = round(Decimal(1 - pow(t, 1 / e)), 6) # defaults ROUND_HALF_UP myoffer = max(min((maxUtil - (maxUtil - minUtil) * ft1), maxUtil), minUtil) return max(myoffer, self.opponent_max) def save_data(self): """This method is called after the negotiation is finished. It can be used to store data for learning capabilities. Note that no extensive calculations can be done within this method. Taking too much time might result in your agent being killed, so use it for storage only. """ data = "Data for learning (see README.md)" with open(f"{self.storage_dir}/data.md", "w") as f: f.write(data) ########################################################################################### ################################## Example methods below ################################## ########################################################################################### def accept_condition(self, bid: Bid) -> bool: if bid is None: return False # progress of the negotiation session between 0 and 1 (1 is deadline) progress = self.progress.get(time() * 1000) # very basic approach that accepts if the offer is valued above 0.7 and # 95% of the time towards the deadline has passed maxAccept = 0.8 minAccept = 0.6 ft = round(Decimal(1 - progress), 6) # defaults ROUND_HALF_UP acceptline = max(min((minAccept + (maxAccept - minAccept) * float(ft)), maxAccept), minAccept) conditions = [ float(self.profile.getUtility(bid)) > acceptline, progress > uniform(0.8, 0.95), progress > 0.95, ] return all([any(conditions), progress > 0.1]) def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float: """Calculate heuristic score for a bid Args: bid (Bid): Bid to score alpha (float, optional): Trade-off factor between self interested and altruistic behaviour. Defaults to 0.95. eps (float, optional): Time pressure factor, balances between conceding and Boulware behaviour over time. Defaults to 0.1. Returns: float: score """ progress = self.progress.get(time() * 1000) our_utility = float(self.profile.getUtility(bid)) time_pressure = 1.0 - progress ** (1 / eps) score = alpha * time_pressure * our_utility if self.opponent_model is not None: opponent_utility = self.opponent_model.get_predicted_utility(bid) opponent_score = (1.0 - alpha * time_pressure) * opponent_utility score += opponent_score return score