[74] | 1 | import decimal
|
---|
| 2 | import time
|
---|
| 3 | from random import randint
|
---|
| 4 | from typing import cast
|
---|
| 5 |
|
---|
| 6 | from geniusweb.actions.Accept import Accept
|
---|
| 7 | from geniusweb.actions.Action import Action
|
---|
| 8 | from geniusweb.actions.Offer import Offer
|
---|
| 9 | from geniusweb.bidspace.AllBidsList import AllBidsList
|
---|
| 10 | from geniusweb.inform.ActionDone import ActionDone
|
---|
| 11 | from geniusweb.inform.Finished import Finished
|
---|
| 12 | from geniusweb.inform.Inform import Inform
|
---|
| 13 | from geniusweb.inform.Settings import Settings
|
---|
| 14 | from geniusweb.inform.YourTurn import YourTurn
|
---|
| 15 | from geniusweb.issuevalue.Bid import Bid
|
---|
| 16 | from geniusweb.opponentmodel.FrequencyOpponentModel import FrequencyOpponentModel
|
---|
| 17 | from geniusweb.party.Capabilities import Capabilities
|
---|
| 18 | from geniusweb.party.DefaultParty import DefaultParty
|
---|
| 19 | from geniusweb.profileconnection.ProfileConnectionFactory import (
|
---|
| 20 | ProfileConnectionFactory,
|
---|
| 21 | )
|
---|
| 22 | from geniusweb.progress.ProgressRounds import ProgressRounds
|
---|
| 23 | from tudelft_utilities_logging.Reporter import Reporter
|
---|
| 24 |
|
---|
| 25 | reservation_progress = 0.995
|
---|
| 26 | search_numb = 2500
|
---|
| 27 |
|
---|
| 28 |
|
---|
| 29 | class Agent41(DefaultParty):
|
---|
| 30 | """
|
---|
| 31 | Template agent that offers random bids until a bid with sufficient utility is offered.
|
---|
| 32 | """
|
---|
| 33 |
|
---|
| 34 | def __init__(self, reporter: Reporter = None):
|
---|
| 35 | super().__init__(reporter)
|
---|
| 36 | # self.getReporter().log(logging.INFO, "party is initialized")
|
---|
| 37 | self._profile = None
|
---|
| 38 | self._last_received_bid: Bid = None
|
---|
| 39 | self._best_past_bid: Bid = None
|
---|
| 40 | self.opponentModel = FrequencyOpponentModel.create()
|
---|
| 41 |
|
---|
| 42 |
|
---|
| 43 | # maximal advantage that our opponent might have over us to accept the bid in the later phase
|
---|
| 44 | self.util_adv_from_accept = decimal.Decimal(0.2)
|
---|
| 45 | # maximal advantage that our opponent might have over us
|
---|
| 46 | # to propose the bid in the later phase
|
---|
| 47 | self.util_adv_from_offer = decimal.Decimal(0.1)
|
---|
| 48 | # maximal advantage that our agent might have over the opponent
|
---|
| 49 | # to propose the bid in the later phase (potential to be removed)
|
---|
| 50 | self.util_adv_to_offer = decimal.Decimal(0.8)
|
---|
| 51 | # progress point at which the intermediate strategy starts being applied
|
---|
| 52 | self.progress_mid = 0.6
|
---|
| 53 | # progress point at which the late strategy starts being applied
|
---|
| 54 | self.progress_fast = 0.95
|
---|
| 55 | # starting utility range for which the offers are considered
|
---|
| 56 | self.utility_range = [decimal.Decimal(0.9),
|
---|
| 57 | decimal.Decimal(1.1)]
|
---|
| 58 | # linear decrease factors in different stages of the negotiations
|
---|
| 59 | self.slow_decrease = decimal.Decimal(0.0005)
|
---|
| 60 | self.mid_decrease = decimal.Decimal(0.001)
|
---|
| 61 | self.fast_decrease = decimal.Decimal(0.002)
|
---|
| 62 | # initial reservation value under which bids are denied
|
---|
| 63 | self.minimal_reservation_val = decimal.Decimal(0.6)
|
---|
| 64 |
|
---|
| 65 | def notifyChange(self, info: Inform):
|
---|
| 66 | """This is the entry point of all interaction with your agent after is has been initialised.
|
---|
| 67 |
|
---|
| 68 | Args:
|
---|
| 69 | info (Inform): Contains either a request for action or information.
|
---|
| 70 | """
|
---|
| 71 |
|
---|
| 72 | # a Settings message is the first message that will be send to your
|
---|
| 73 | # agent containing all the information about the negotiation session.
|
---|
| 74 | if isinstance(info, Settings):
|
---|
| 75 | self._settings: Settings = cast(Settings, info)
|
---|
| 76 | self._me = self._settings.getID()
|
---|
| 77 |
|
---|
| 78 | # progress towards the deadline has to be tracked manually through the use of the Progress object
|
---|
| 79 | self._progress: ProgressRounds = self._settings.getProgress()
|
---|
| 80 |
|
---|
| 81 | # the profile contains the preferences of the agent over the domain
|
---|
| 82 | self._profile = ProfileConnectionFactory.create(
|
---|
| 83 | info.getProfile().getURI(), self.getReporter()
|
---|
| 84 | )
|
---|
| 85 |
|
---|
| 86 | domain = self._profile.getProfile().getDomain()
|
---|
| 87 | self.opponentModel = self.opponentModel.With(newDomain=domain, newResBid=0)
|
---|
| 88 |
|
---|
| 89 | # ActionDone is an action send by an opponent (an offer or an accept)
|
---|
| 90 | elif isinstance(info, ActionDone):
|
---|
| 91 | action: Action = cast(ActionDone, info).getAction()
|
---|
| 92 |
|
---|
| 93 | self.opponentModel = self.opponentModel.WithAction(action=action,
|
---|
| 94 | progress=self._progress)
|
---|
| 95 |
|
---|
| 96 | # if it is an offer, set the last received bid
|
---|
| 97 | if isinstance(action, Offer):
|
---|
| 98 | self._last_received_bid = cast(Offer, action).getBid()
|
---|
| 99 |
|
---|
| 100 |
|
---|
| 101 | # YourTurn notifies you that it is your turn to act
|
---|
| 102 | elif isinstance(info, YourTurn):
|
---|
| 103 | action = self._my_turn()
|
---|
| 104 | if isinstance(self._progress, ProgressRounds):
|
---|
| 105 | self._progress = self._progress.advance()
|
---|
| 106 | self.getConnection().send(action)
|
---|
| 107 |
|
---|
| 108 | # Finished will be send if the negotiation has ended (through agreement or deadline)
|
---|
| 109 | elif isinstance(info, Finished):
|
---|
| 110 | # terminate the agent MUST BE CALLED
|
---|
| 111 | self.terminate()
|
---|
| 112 | # else:
|
---|
| 113 | # self.getReporter().log(
|
---|
| 114 | # logging.WARNING, "Ignoring unknown info " + str(info)
|
---|
| 115 | # )
|
---|
| 116 |
|
---|
| 117 | # lets the geniusweb system know what settings this agent can handle
|
---|
| 118 | # leave it as it is for this competition
|
---|
| 119 | def getCapabilities(self) -> Capabilities:
|
---|
| 120 | return Capabilities(
|
---|
| 121 | set(["SAOP"]),
|
---|
| 122 | set(["geniusweb.profile.utilityspace.LinearAdditive"]),
|
---|
| 123 | )
|
---|
| 124 |
|
---|
| 125 | # terminates the agent and its connections
|
---|
| 126 | # leave it as it is for this competition
|
---|
| 127 | def terminate(self):
|
---|
| 128 | # self.getReporter().log(logging.INFO, "party is terminating:")
|
---|
| 129 | super().terminate()
|
---|
| 130 | if self._profile is not None:
|
---|
| 131 | self._profile.close()
|
---|
| 132 | self._profile = None
|
---|
| 133 |
|
---|
| 134 |
|
---|
| 135 |
|
---|
| 136 | # give a description of your agent
|
---|
| 137 | def getDescription(self) -> str:
|
---|
| 138 | return "Agent orange for Collaborative AI course"
|
---|
| 139 |
|
---|
| 140 | def _my_turn(self):
|
---|
| 141 | """
|
---|
| 142 | Executes a turn.
|
---|
| 143 | """
|
---|
| 144 | # update the utility range
|
---|
| 145 | self._update_range()
|
---|
| 146 |
|
---|
| 147 | # check if the last received offer if the opponent is good enough
|
---|
| 148 | if self._is_good(self._last_received_bid):
|
---|
| 149 | # if so, accept the offer
|
---|
| 150 | action = Accept(self._me, self._last_received_bid)
|
---|
| 151 | else:
|
---|
| 152 | # if not, find a bid to propose as counter offer
|
---|
| 153 | bid = self._find_bid()
|
---|
| 154 | action = Offer(self._me, bid)
|
---|
| 155 |
|
---|
| 156 | # send the action
|
---|
| 157 | return action
|
---|
| 158 |
|
---|
| 159 | # method that checks if we would agree with an offer
|
---|
| 160 | def _is_good(self, bid: Bid) -> bool:
|
---|
| 161 | """
|
---|
| 162 | Checks if the received offer is good enough to be accepted.
|
---|
| 163 | """
|
---|
| 164 | if bid is None:
|
---|
| 165 | return False
|
---|
| 166 |
|
---|
| 167 | profile = self._profile.getProfile()
|
---|
| 168 |
|
---|
| 169 | # Update the best past bid effectively making it a reservation bid
|
---|
| 170 | if self._best_past_bid is None:
|
---|
| 171 | self._best_past_bid = bid
|
---|
| 172 | elif profile.getUtility(bid) > profile.getUtility(self._best_past_bid):
|
---|
| 173 | self._best_past_bid = bid
|
---|
| 174 |
|
---|
| 175 | # get the utility of us and our opponent (frequency model)
|
---|
| 176 | our_util = profile.getUtility(bid)
|
---|
| 177 | opp_util = self.opponentModel.getUtility(bid)
|
---|
| 178 | progress = self._progress.get(time.time() * 1000)
|
---|
| 179 |
|
---|
| 180 | # immediately return the offer it is below our lower utility threshold
|
---|
| 181 | if our_util < self.utility_range[0]:
|
---|
| 182 | return False
|
---|
| 183 | # if the progress reached a certain point,
|
---|
| 184 | # accept the ofer only if it is not overwhelmingly better than the opponents
|
---|
| 185 | if progress > self.progress_mid:
|
---|
| 186 | return our_util + self.util_adv_from_accept >= opp_util
|
---|
| 187 | # if this progress has not been reached yet,
|
---|
| 188 | # simply accept the offer if it is better than our utility
|
---|
| 189 | return True
|
---|
| 190 |
|
---|
| 191 | def _find_bid(self) -> Bid:
|
---|
| 192 | """
|
---|
| 193 | Find the most suitable bid to offer given the current state of negotiations.
|
---|
| 194 | """
|
---|
| 195 | domain = self._profile.getProfile().getDomain()
|
---|
| 196 | profile = self._profile.getProfile()
|
---|
| 197 |
|
---|
| 198 | # verify if the reservation bid should be used
|
---|
| 199 | if self._verify_reservation_val():
|
---|
| 200 | return self._best_past_bid
|
---|
| 201 |
|
---|
| 202 | # store the best bid found so far
|
---|
| 203 | best_bid: Bid = self._best_past_bid
|
---|
| 204 | # store the difference between the utility of the best bid
|
---|
| 205 | # and the upper threshold of the range
|
---|
| 206 | best_dist = decimal.Decimal(1.0)
|
---|
| 207 |
|
---|
| 208 | all_bids = AllBidsList(domain)
|
---|
| 209 | # search through a number of random bids to find a suitable bid
|
---|
| 210 | for _ in range(search_numb):
|
---|
| 211 | # get the random bid and the utility of it
|
---|
| 212 | bid = all_bids.get(randint(0, all_bids.size() - 1))
|
---|
| 213 | our_util = profile.getUtility(bid)
|
---|
| 214 | # verify if the bid is suitable to be offered to the opponent
|
---|
| 215 | if self._verify_bid(bid):
|
---|
| 216 | best_bid = bid
|
---|
| 217 | break
|
---|
| 218 | # If the best bid was not found, check if the current bid is closer to idea value
|
---|
| 219 | curr_dist = abs(our_util - self.utility_range[1])
|
---|
| 220 | if curr_dist < best_dist:
|
---|
| 221 | best_bid = bid
|
---|
| 222 | best_dist = curr_dist
|
---|
| 223 |
|
---|
| 224 | return best_bid
|
---|
| 225 |
|
---|
| 226 | def _update_range(self):
|
---|
| 227 | """
|
---|
| 228 | Update the acceptance range depending on the current progress of the negotiation.
|
---|
| 229 | The further we are into the negotiation, the faster the utility range drops.
|
---|
| 230 | """
|
---|
| 231 | progress = self._progress.get(time.time() * 1000)
|
---|
| 232 | if progress < self.progress_mid:
|
---|
| 233 | self.utility_range[1] = self.utility_range[1] - self.slow_decrease
|
---|
| 234 | self.utility_range[0] = self.utility_range[0] - self.slow_decrease
|
---|
| 235 | elif progress > self.progress_fast:
|
---|
| 236 | self.utility_range[1] = self.utility_range[1] - self.fast_decrease
|
---|
| 237 | self.utility_range[0] = self.utility_range[0] - self.fast_decrease
|
---|
| 238 | else:
|
---|
| 239 | self.utility_range[1] = self.utility_range[1] - self.mid_decrease
|
---|
| 240 | self.utility_range[0] = self.utility_range[0] - self.mid_decrease
|
---|
| 241 |
|
---|
| 242 | def _verify_reservation_val(self) -> bool:
|
---|
| 243 | """
|
---|
| 244 | Return the best bid received so far, if the time is almost up and the bid is good enough.
|
---|
| 245 | The reservation value is equal to the best offer received from the opponent.
|
---|
| 246 | It is sent if our utility coming from this offer exceeds 0.7
|
---|
| 247 | or if the utility of the reservation bid exceeds our upper utility threshold.
|
---|
| 248 | """
|
---|
| 249 | if self._best_past_bid is not None:
|
---|
| 250 | reservation_val = self._profile.getProfile().getUtility(
|
---|
| 251 | self._best_past_bid)
|
---|
| 252 | if self._progress.get(
|
---|
| 253 | 0) >= reservation_progress and reservation_val >= self.minimal_reservation_val:
|
---|
| 254 | return True
|
---|
| 255 | if self._profile.getProfile().getUtility(self._best_past_bid) >= self.utility_range[1]:
|
---|
| 256 | return True
|
---|
| 257 | return False
|
---|
| 258 |
|
---|
| 259 | def _verify_bid(self, bid: Bid) -> bool:
|
---|
| 260 | """
|
---|
| 261 | Verifies if the bid that our agent came up with is good enough to be offered to another agent.
|
---|
| 262 | """
|
---|
| 263 | progress = self._progress.get(time.time() * 1000)
|
---|
| 264 | profile = self._profile.getProfile()
|
---|
| 265 | our_util = profile.getUtility(bid)
|
---|
| 266 |
|
---|
| 267 | # do not consider the bid if it is worse than our reservation bid
|
---|
| 268 | if self._best_past_bid is not None and our_util < profile.getUtility(self._best_past_bid):
|
---|
| 269 | return False
|
---|
| 270 | # if a certain point in the negotiation has not been reached,
|
---|
| 271 | # propose the bid if it falls into the accepted utility range
|
---|
| 272 | if progress < self.progress_mid:
|
---|
| 273 | return self.utility_range[0] < our_util < self.utility_range[1]
|
---|
| 274 |
|
---|
| 275 | # otherwise, in addition to considering the utility range, also consider the utility of our opponent, make sure
|
---|
| 276 | # that the utility difference between us and the opponent is not too high
|
---|
| 277 | opp_util = self.opponentModel.getUtility(bid)
|
---|
| 278 | return our_util + self.util_adv_from_offer > opp_util > our_util - self.util_adv_to_offer \
|
---|
| 279 | and self.utility_range[0] < our_util < self.utility_range[1]
|
---|