[74] | 1 | import logging
|
---|
| 2 | import random
|
---|
| 3 | import time
|
---|
| 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.party.Capabilities import Capabilities
|
---|
| 17 | from geniusweb.party.DefaultParty import DefaultParty
|
---|
| 18 | from geniusweb.profileconnection.ProfileConnectionFactory import (
|
---|
| 19 | ProfileConnectionFactory,
|
---|
| 20 | )
|
---|
| 21 |
|
---|
| 22 | from .MyOpponentModel import MyOpponentModel
|
---|
| 23 | from geniusweb.progress.ProgressRounds import ProgressRounds
|
---|
| 24 | from tudelft_utilities_logging.Reporter import Reporter
|
---|
| 25 |
|
---|
| 26 |
|
---|
| 27 | class Agent11(DefaultParty):
|
---|
| 28 | """
|
---|
| 29 | Template agent that offers random bids until a bid with sufficient utility is offered.
|
---|
| 30 | """
|
---|
| 31 |
|
---|
| 32 | def __init__(self, reporter: Reporter = None):
|
---|
| 33 | super().__init__(reporter)
|
---|
| 34 | self.getReporter().log(logging.INFO, "party is initialized")
|
---|
| 35 | self._profile = None
|
---|
| 36 | self._last_received_bid = None
|
---|
| 37 |
|
---|
| 38 | self.sorted_bids = None
|
---|
| 39 | self.opponent_model = None
|
---|
| 40 | self.concede_range = 0.7
|
---|
| 41 | self._second_to_last_received_bid = None
|
---|
| 42 | self._last_offered_bid = None
|
---|
| 43 | self._window_size = 20
|
---|
| 44 | self._bias_correction = 0.015
|
---|
| 45 | self.concede_count = 0
|
---|
| 46 | self.non_concede_count = 0
|
---|
| 47 | self.concede_strategy = 10
|
---|
| 48 | self.received_bids = []
|
---|
| 49 | # list that tries to keep track of the movement of the opponent bids
|
---|
| 50 | self.opponent_delta = []
|
---|
| 51 |
|
---|
| 52 | def notifyChange(self, info: Inform):
|
---|
| 53 | """This is the entry point of all interaction with your agent after is has been initialised.
|
---|
| 54 |
|
---|
| 55 | Args:
|
---|
| 56 | info (Inform): Contains either a request for action or information.
|
---|
| 57 | """
|
---|
| 58 |
|
---|
| 59 | # a Settings message is the first message that will be send to your
|
---|
| 60 | # agent containing all the information about the negotiation session.
|
---|
| 61 | if isinstance(info, Settings):
|
---|
| 62 | self._settings: Settings = cast(Settings, info)
|
---|
| 63 | self._me = self._settings.getID()
|
---|
| 64 |
|
---|
| 65 | # progress towards the deadline has to be tracked manually through the use of the Progress object
|
---|
| 66 | self._progress = self._settings.getProgress()
|
---|
| 67 |
|
---|
| 68 | # the profile contains the preferences of the agent over the domain
|
---|
| 69 | self._profile = ProfileConnectionFactory.create(
|
---|
| 70 | info.getProfile().getURI(), self.getReporter()
|
---|
| 71 | )
|
---|
| 72 | # initialize opponent model
|
---|
| 73 | self.opponent_model = MyOpponentModel.create().With(self._profile.getProfile().getDomain(), None)
|
---|
| 74 | # self.opponent_model = FrequencyOpponentModel.create().With(self._profile.getProfile().getDomain(), None)
|
---|
| 75 |
|
---|
| 76 | # ActionDone is an action send by an opponent (an offer or an accept)
|
---|
| 77 | elif isinstance(info, ActionDone):
|
---|
| 78 | action: Action = cast(ActionDone, info).getAction()
|
---|
| 79 |
|
---|
| 80 | # if it is an offer, set the last received bid
|
---|
| 81 | if isinstance(action, Offer) and action.getActor() != self._me:
|
---|
| 82 | self._last_received_bid = cast(Offer, action).getBid()
|
---|
| 83 | self.received_bids.append(self._last_received_bid)
|
---|
| 84 |
|
---|
| 85 | # YourTurn notifies you that it is your turn to act
|
---|
| 86 | elif isinstance(info, YourTurn):
|
---|
| 87 | action = self._myTurn()
|
---|
| 88 | if isinstance(self._progress, ProgressRounds):
|
---|
| 89 | self._progress = self._progress.advance()
|
---|
| 90 | self.getConnection().send(action)
|
---|
| 91 |
|
---|
| 92 | # Finished will be send if the negotiation has ended (through agreement or deadline)
|
---|
| 93 | elif isinstance(info, Finished):
|
---|
| 94 | # terminate the agent MUST BE CALLED
|
---|
| 95 | self.terminate()
|
---|
| 96 | else:
|
---|
| 97 | self.getReporter().log(
|
---|
| 98 | logging.WARNING, "Ignoring unknown info " + str(info)
|
---|
| 99 | )
|
---|
| 100 |
|
---|
| 101 | # lets the geniusweb system know what settings this agent can handle
|
---|
| 102 | # leave it as it is for this competition
|
---|
| 103 | def getCapabilities(self) -> Capabilities:
|
---|
| 104 | return Capabilities(
|
---|
| 105 | set(["SAOP"]),
|
---|
| 106 | set(["geniusweb.profile.utilityspace.LinearAdditive"]),
|
---|
| 107 | )
|
---|
| 108 |
|
---|
| 109 | # terminates the agent and its connections
|
---|
| 110 | # leave it as it is for this competition
|
---|
| 111 | def terminate(self):
|
---|
| 112 | self.getReporter().log(logging.INFO, "party is terminating:")
|
---|
| 113 | super().terminate()
|
---|
| 114 | if self._profile is not None:
|
---|
| 115 | self._profile.close()
|
---|
| 116 | self._profile = None
|
---|
| 117 |
|
---|
| 118 | # give a description of your agent
|
---|
| 119 | def getDescription(self) -> str:
|
---|
| 120 | return "Agent11"
|
---|
| 121 |
|
---|
| 122 | # execute a turn
|
---|
| 123 | def _myTurn(self):
|
---|
| 124 | progress = self._progress.get(time.time() * 1000)
|
---|
| 125 | # register bet with the opponent model
|
---|
| 126 | if self._last_received_bid:
|
---|
| 127 | self.opponent_model = self.opponent_model.WithAction(Offer(None, self._last_received_bid), progress)
|
---|
| 128 |
|
---|
| 129 | self._recognize_move()
|
---|
| 130 | # Check if last received offer is good enough and more than 80% passed
|
---|
| 131 | if self._isGood(self._last_received_bid):
|
---|
| 132 | action = Accept(self._me, self._last_received_bid)
|
---|
| 133 | else:
|
---|
| 134 | # Opponents bid was not good enough, we make a bid.
|
---|
| 135 | bid = self._findBid()
|
---|
| 136 |
|
---|
| 137 | self._secondToLast_offered_bid = self._last_offered_bid
|
---|
| 138 | self._last_offered_bid = bid
|
---|
| 139 |
|
---|
| 140 | action = Offer(self._me, bid)
|
---|
| 141 |
|
---|
| 142 | return action
|
---|
| 143 |
|
---|
| 144 | def _isGood(self, bid: Bid) -> bool:
|
---|
| 145 | """
|
---|
| 146 | Checks if a bid is good enough to accept.
|
---|
| 147 | @param bid the bid to consider
|
---|
| 148 | @return true if the bid is good enough, false otherwise
|
---|
| 149 | """
|
---|
| 150 | if bid is None:
|
---|
| 151 | return False
|
---|
| 152 |
|
---|
| 153 | utilities = self._calculate_utilities(bid, verbose=True) # returns a tuple with (total, ours, theirs)
|
---|
| 154 | utilities_prev_offered_bid = self._calculate_utilities(self._last_offered_bid)
|
---|
| 155 |
|
---|
| 156 | # We evaluate the following boolean conditions and base our final decision on them
|
---|
| 157 | good_for_me = utilities[0] >= 0.6
|
---|
| 158 | time_spend = self._progress.get(time.time() * 1000) >= 0.8
|
---|
| 159 | better_than_last_offered = self._last_offered_bid and utilities_prev_offered_bid[0] <= utilities[0]
|
---|
| 160 | better_than_reservation_value = self._profile.getProfile().getReservationBid() and self._profile \
|
---|
| 161 | .getProfile().getReservationBid() <= good_for_me
|
---|
| 162 |
|
---|
| 163 | return ((good_for_me and time_spend) and better_than_reservation_value) or better_than_last_offered
|
---|
| 164 |
|
---|
| 165 | def _findBid(self) -> Bid:
|
---|
| 166 | # compose a list of all possible bids
|
---|
| 167 | domain = self._profile.getProfile().getDomain()
|
---|
| 168 | all_bids = AllBidsList(domain)
|
---|
| 169 | if not self.sorted_bids:
|
---|
| 170 | self.sorted_bids = sorted(all_bids, key=lambda x: self._profile.getProfile().getUtility(x), reverse=True)
|
---|
| 171 |
|
---|
| 172 | if self._progress.get(time.time() * 1000) < self.concede_range:
|
---|
| 173 | bid_index = random.randint(0, int(len(self.sorted_bids) * 0.005))
|
---|
| 174 |
|
---|
| 175 | bid = self.sorted_bids[bid_index]
|
---|
| 176 |
|
---|
| 177 | return bid
|
---|
| 178 | else:
|
---|
| 179 | delta = 0.1
|
---|
| 180 |
|
---|
| 181 | # Every x rounds, we sum the last x deltas to check what the opponents strategy is.
|
---|
| 182 | # From experiments, an agent that concedes will get a score around -0.5 to -0.3
|
---|
| 183 | # An agent that hard lines will get a score around 0 or bigger than 0
|
---|
| 184 | # Against conceding agents, we will be more conservative and take on an hard lining strategy
|
---|
| 185 | # Against hard lining agents, we will concede more.
|
---|
| 186 | # A lower value of concede_strategy means we concede more
|
---|
| 187 | # A higher value means we are more conservative
|
---|
| 188 | if len(self.received_bids) % 10 == 0:
|
---|
| 189 | opponent_trend = sum(self.opponent_delta[-10:])
|
---|
| 190 | if opponent_trend < -0.2:
|
---|
| 191 | self.concede_strategy += 1
|
---|
| 192 | else:
|
---|
| 193 | self.concede_strategy -= 1 if self._progress.get(time.time() * 1000) < 0.8 \
|
---|
| 194 | else 2 # start conceding more towards the end
|
---|
| 195 | # we make sure this constant falls between a reasonable range, so it doesn't get too crazy
|
---|
| 196 | self.concede_strategy = max(min(self.concede_strategy, 15), 3)
|
---|
| 197 | # concede range = time after we concede
|
---|
| 198 | start_index = int(
|
---|
| 199 | len(self.sorted_bids) * (self._progress.get(time.time() * 1000) - self.concede_range) / self.concede_strategy)
|
---|
| 200 | # start_index = 0
|
---|
| 201 | # delta = randomness parameter for the generation of the bids
|
---|
| 202 | end_index = int(len(self.sorted_bids) * (self._progress.get(time.time() * 1000) - self.concede_range + delta))
|
---|
| 203 |
|
---|
| 204 | bidding_range = self.sorted_bids[start_index:end_index]
|
---|
| 205 | potential_bids = []
|
---|
| 206 | # pick n random bids
|
---|
| 207 | n = 50
|
---|
| 208 | for _ in range(n):
|
---|
| 209 | potential_bids.append(bidding_range[random.randint(0, len(bidding_range) - 1)])
|
---|
| 210 |
|
---|
| 211 | # find the best one according to ur best utility
|
---|
| 212 | potential_bids = sorted(potential_bids,
|
---|
| 213 | key=lambda x: self._evaluate_utilities(self._calculate_utilities(x)),
|
---|
| 214 | reverse=True)
|
---|
| 215 | bid = potential_bids[0]
|
---|
| 216 |
|
---|
| 217 | return bid
|
---|
| 218 |
|
---|
| 219 | def _calculate_utilities(self, bid: Bid, verbose=False):
|
---|
| 220 | """
|
---|
| 221 | Returns a tuple of the utility of an bid. The tuple consists of (total utility, own utility, opponent utility)
|
---|
| 222 | """
|
---|
| 223 | if not bid:
|
---|
| 224 | return 0, 0
|
---|
| 225 | own_utility = self._profile.getProfile().getUtility(bid)
|
---|
| 226 | opponent_utility = self.opponent_model.getUtility(bid)
|
---|
| 227 | if verbose:
|
---|
| 228 | self.getReporter().log(logging.INFO,
|
---|
| 229 | 'Own utility ' + str(own_utility) + ' Opponent utility ' + str(opponent_utility))
|
---|
| 230 |
|
---|
| 231 | return own_utility, opponent_utility
|
---|
| 232 |
|
---|
| 233 | @staticmethod
|
---|
| 234 | def _evaluate_utilities(utilities: tuple[float, float]) -> float:
|
---|
| 235 | ratio = 0.6
|
---|
| 236 | return ratio * float(utilities[0]) + (1-ratio) * float(utilities[1])
|
---|
| 237 |
|
---|
| 238 | def _recognize_move(self):
|
---|
| 239 | """
|
---|
| 240 | Evaluates the last x bids received where x is equal to the window size.
|
---|
| 241 | It then calculates of this window was a conceding window or a non conceding window by calculating the delta
|
---|
| 242 | It adds the delta to a list and this list will then be used to adjust our concession rate.
|
---|
| 243 | """
|
---|
| 244 |
|
---|
| 245 | if len(self.received_bids) < self._window_size:
|
---|
| 246 | return
|
---|
| 247 |
|
---|
| 248 | start = len(self.received_bids) - self._window_size
|
---|
| 249 | half = len(self.received_bids) - int(self._window_size / 2)
|
---|
| 250 |
|
---|
| 251 | # function to map the bids to the estimated utility
|
---|
| 252 |
|
---|
| 253 | first_half = list(map(lambda x: self.opponent_model.getUtility(x), self.received_bids[start:half]))
|
---|
| 254 | second_half = list(map(lambda x: self.opponent_model.getUtility(x), self.received_bids[half:]))
|
---|
| 255 |
|
---|
| 256 | # now we calculate the average utility
|
---|
| 257 | first_avg = sum(first_half) / len(first_half)
|
---|
| 258 | second_avg = sum(second_half) / len(second_half)
|
---|
| 259 |
|
---|
| 260 | # It takes into account that an opponent model will naturally be a little conceding over time
|
---|
| 261 | # hence the bias correction
|
---|
| 262 | delta = float(second_avg - first_avg) + self._bias_correction
|
---|
| 263 | self.opponent_delta.append(delta)
|
---|