[74] | 1 | import logging
|
---|
| 2 | import random
|
---|
| 3 | import time
|
---|
| 4 | from typing import cast, Dict, List, Union
|
---|
| 5 |
|
---|
| 6 | from geniusweb.actions.Accept import Accept
|
---|
| 7 | from geniusweb.actions.Action import Action
|
---|
| 8 | from geniusweb.actions.LearningDone import LearningDone
|
---|
| 9 | from geniusweb.actions.Offer import Offer
|
---|
| 10 | from geniusweb.actions.PartyId import PartyId
|
---|
| 11 | from geniusweb.inform.ActionDone import ActionDone
|
---|
| 12 | from geniusweb.inform.Finished import Finished
|
---|
| 13 | from geniusweb.inform.Inform import Inform
|
---|
| 14 | from geniusweb.inform.Settings import Settings
|
---|
| 15 | from geniusweb.inform.YourTurn import YourTurn
|
---|
| 16 | from geniusweb.issuevalue.Bid import Bid
|
---|
| 17 | from geniusweb.issuevalue.Value import Value
|
---|
| 18 | from geniusweb.party.Capabilities import Capabilities
|
---|
| 19 | from geniusweb.party.DefaultParty import DefaultParty
|
---|
| 20 | from geniusweb.profile.Profile import Profile
|
---|
| 21 | from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
|
---|
| 22 | from geniusweb.profileconnection.ProfileConnectionFactory import ProfileConnectionFactory
|
---|
| 23 | from geniusweb.profileconnection.ProfileInterface import ProfileInterface
|
---|
| 24 | from geniusweb.progress.Progress import Progress
|
---|
| 25 | from geniusweb.progress.ProgressRounds import ProgressRounds
|
---|
| 26 | from tudelft.utilities.immutablelist.ImmutableList import ImmutableList
|
---|
| 27 | from tudelft.utilities.immutablelist.Outer import Outer
|
---|
| 28 |
|
---|
| 29 | import numpy as np
|
---|
| 30 | from uri.uri import URI
|
---|
| 31 | from geniusweb.progress.ProgressRounds import ProgressRounds
|
---|
| 32 | from tudelft_utilities_logging.Reporter import Reporter
|
---|
| 33 |
|
---|
| 34 |
|
---|
| 35 | class Agent25(DefaultParty):
|
---|
| 36 | def __init__(self, reporter: Reporter = None):
|
---|
| 37 | super().__init__(reporter)
|
---|
| 38 |
|
---|
| 39 | # How linearly the agent concedes over time. 1 = linear concession.
|
---|
| 40 | # Concession parameter for ideal bid offer function.
|
---|
| 41 | self._offer_concession_param = 1.2
|
---|
| 42 | # Concession parameter for minimum acceptance function.
|
---|
| 43 | self._accept_concession_param = 1.2
|
---|
| 44 | # The amount of randomization in bids the agent makes. Usually values 0-10.
|
---|
| 45 | self._randomization_param = 3
|
---|
| 46 |
|
---|
| 47 | self._opponent_bid_utilities: List[float] = []
|
---|
| 48 | self._opponent_model: Dict[str, Dict[Value, int]] = {}
|
---|
| 49 |
|
---|
| 50 | self._best_bid = None
|
---|
| 51 |
|
---|
| 52 | self._all_bids: List[(float, Dict[str, Value])] = []
|
---|
| 53 |
|
---|
| 54 | self._min_utility: Union[float, None] = None
|
---|
| 55 | self._max_utility: Union[float, None] = None
|
---|
| 56 |
|
---|
| 57 | self._id: Union[PartyId, None] = None
|
---|
| 58 | self._last_received_bid: Union[Bid, None] = None
|
---|
| 59 | self._profile: Union[ProfileInterface, None] = None
|
---|
| 60 | self._session_progress: Union[Progress, None] = None
|
---|
| 61 | self._session_settings: Union[Settings, None] = None
|
---|
| 62 | self._uri: Union[URI, None] = None
|
---|
| 63 |
|
---|
| 64 | self.getReporter().log(logging.INFO, "Agent initialized")
|
---|
| 65 |
|
---|
| 66 | # Informs the GeniusWeb system what protocols the agent supports and what type of profile it has.
|
---|
| 67 | def getCapabilities(self) -> Capabilities:
|
---|
| 68 | return Capabilities({"SAOP"}, {'geniusweb.profile.utilityspace.LinearAdditive'})
|
---|
| 69 |
|
---|
| 70 | # Gives a description of the agent.
|
---|
| 71 | def getDescription(self) -> str:
|
---|
| 72 | return 'Custom agent created by CAI group 25.'
|
---|
| 73 |
|
---|
| 74 | # Handles all interaction between the agent and the session.
|
---|
| 75 | def notifyChange(self, info: Inform):
|
---|
| 76 |
|
---|
| 77 | # First message sent in the negotiation - informs the agent about details of the negotiation session.
|
---|
| 78 | if isinstance(info, Settings):
|
---|
| 79 | self._session_settings = cast(Settings, info)
|
---|
| 80 |
|
---|
| 81 | self._id = self._session_settings.getID()
|
---|
| 82 | self._session_progress = self._session_settings.getProgress()
|
---|
| 83 | self._uri: str = str(self._session_settings.getProtocol().getURI())
|
---|
| 84 |
|
---|
| 85 | if "Learn" == str(self._session_settings.getProtocol().getURI()):
|
---|
| 86 | self.getConnection().send(LearningDone(self._id))
|
---|
| 87 |
|
---|
| 88 | else:
|
---|
| 89 | self._profile = ProfileConnectionFactory.create(info.getProfile().getURI(), self.getReporter())
|
---|
| 90 | profile = self._profile.getProfile()
|
---|
| 91 |
|
---|
| 92 | # Finds all possible bids the agent can make and their corresponding utilities.
|
---|
| 93 | if isinstance(profile, UtilitySpace):
|
---|
| 94 | issues = list(profile.getDomain().getIssues())
|
---|
| 95 | values: List[ImmutableList[Value]] = [profile.getDomain().getValues(issue) for issue in issues]
|
---|
| 96 | all_bids: Outer = Outer[Value](values)
|
---|
| 97 |
|
---|
| 98 | for i in range(all_bids.size()):
|
---|
| 99 | bid = {}
|
---|
| 100 | for j in range(all_bids.get(i).size()):
|
---|
| 101 | bid[issues[j]] = all_bids.get(i).get(j)
|
---|
| 102 |
|
---|
| 103 | utility = float(profile.getUtility(Bid(bid)))
|
---|
| 104 | self._all_bids.append((utility, bid))
|
---|
| 105 |
|
---|
| 106 | # Sorts by highest utility first.
|
---|
| 107 | self._all_bids = sorted(self._all_bids, key=lambda x: x[0], reverse=True)
|
---|
| 108 |
|
---|
| 109 | self._max_utility = self._all_bids[0][0]
|
---|
| 110 | self._min_utility = self._all_bids[len(self._all_bids) - 1][0]
|
---|
| 111 |
|
---|
| 112 | # Indicates that the opponent has ended their turn by accepting your last bid or offering a new one.
|
---|
| 113 | elif isinstance(info, ActionDone):
|
---|
| 114 | action: Action = cast(ActionDone, info).getAction()
|
---|
| 115 |
|
---|
| 116 | if isinstance(action, Offer):
|
---|
| 117 | self._last_received_bid = cast(Offer, action).getBid()
|
---|
| 118 |
|
---|
| 119 | # Indicates that it is the agent's turn. The agent can accept the opponent's last bid or offer a new one.
|
---|
| 120 | elif isinstance(info, YourTurn):
|
---|
| 121 | action = self._execute_turn()
|
---|
| 122 |
|
---|
| 123 | if isinstance(self._session_progress, ProgressRounds):
|
---|
| 124 | self._session_progress = self._session_progress.advance()
|
---|
| 125 |
|
---|
| 126 | self.getConnection().send(action)
|
---|
| 127 |
|
---|
| 128 | # Indicates that the session is complete - either the time has expired or a bid has been agreed on.
|
---|
| 129 | elif isinstance(info, Finished):
|
---|
| 130 | finished = cast(Finished, info)
|
---|
| 131 | self.terminate()
|
---|
| 132 |
|
---|
| 133 | # Indicates that the information received was of an unknown type.
|
---|
| 134 | else:
|
---|
| 135 | self.getReporter().log(logging.WARNING, "Ignoring unknown info: " + str(info))
|
---|
| 136 |
|
---|
| 137 | # Terminates the agent and its connections.
|
---|
| 138 | def terminate(self):
|
---|
| 139 | self.getReporter().log(logging.INFO, "Agent is terminating...")
|
---|
| 140 |
|
---|
| 141 | super().terminate()
|
---|
| 142 |
|
---|
| 143 | if self._profile is not None:
|
---|
| 144 | self._profile.close()
|
---|
| 145 | self._profile = None
|
---|
| 146 |
|
---|
| 147 | ###############################################################
|
---|
| 148 | # Functions below determine the agent's negotiation strategy. #
|
---|
| 149 | ###############################################################
|
---|
| 150 |
|
---|
| 151 | # Processes the opponent's last bid and offers a new one if it isn't satisfactory.
|
---|
| 152 | def _execute_turn(self):
|
---|
| 153 | if self._last_received_bid is not None:
|
---|
| 154 | # Updates opponent preference profile model by incrementing issue values which appeared in the bid.
|
---|
| 155 | issues = self._last_received_bid.getIssues()
|
---|
| 156 | for issue in issues:
|
---|
| 157 | value = self._last_received_bid.getValue(issue)
|
---|
| 158 |
|
---|
| 159 | if issue in self._opponent_model and value in self._opponent_model[issue]:
|
---|
| 160 | self._opponent_model[issue][value] += 1
|
---|
| 161 | else:
|
---|
| 162 | if issue not in self._opponent_model:
|
---|
| 163 | self._opponent_model[issue] = {}
|
---|
| 164 |
|
---|
| 165 | self._opponent_model[issue][value] = 1
|
---|
| 166 |
|
---|
| 167 | # Creates normalized opponent profile with updated values
|
---|
| 168 | opponent_normalized_model: Dict[str, dict[Value, float]] = {}
|
---|
| 169 | for issue, value in self._opponent_model.items():
|
---|
| 170 | opponent_normalized_model[issue] = {}
|
---|
| 171 |
|
---|
| 172 | if len(self._opponent_model.get(issue).values()) > 0:
|
---|
| 173 | max_count = max(self._opponent_model.get(issue).values())
|
---|
| 174 |
|
---|
| 175 | for discrete_value, count in self._opponent_model.get(issue).items():
|
---|
| 176 | opponent_normalized_model[issue][discrete_value] = count / max_count
|
---|
| 177 |
|
---|
| 178 | # Calculates the predicted utility that the opponent gains from their last proposed bid.
|
---|
| 179 | opponent_utility = 0
|
---|
| 180 | for issue in self._last_received_bid.getIssues():
|
---|
| 181 | if issue in opponent_normalized_model:
|
---|
| 182 | value = self._last_received_bid.getValue(issue)
|
---|
| 183 | if value in opponent_normalized_model.get(issue):
|
---|
| 184 | opponent_utility += opponent_normalized_model.get(issue).get(value)
|
---|
| 185 | opponent_utility = opponent_utility / len(self._last_received_bid.getIssues())
|
---|
| 186 | self._opponent_bid_utilities.append(opponent_utility)
|
---|
| 187 |
|
---|
| 188 | # Predicts how much the opponent is conceding based on best-fit line gradient of previous proposed bids.
|
---|
| 189 | opponent_concession_estimate = -1.0
|
---|
| 190 | self._session_settings.getProgress().getTerminationTime()
|
---|
| 191 | if self._session_progress.get(int(time.time())) > 0.1:
|
---|
| 192 | variables = np.polyfit(
|
---|
| 193 | [x for x in range(0, 20)],
|
---|
| 194 | self._opponent_bid_utilities[
|
---|
| 195 | len(self._opponent_bid_utilities) - 21:
|
---|
| 196 | len(self._opponent_bid_utilities) - 1
|
---|
| 197 | ],
|
---|
| 198 | 1
|
---|
| 199 | )
|
---|
| 200 | opponent_concession_estimate = variables[0] * 10
|
---|
| 201 |
|
---|
| 202 | # Checks if opponent is hard-lining and adjusts strategy.
|
---|
| 203 | if abs(opponent_concession_estimate) < 0.001:
|
---|
| 204 | self._accept_concession_param = self._accept_concession_param * 0.9
|
---|
| 205 | self._offer_concession_param = self._offer_concession_param * 0.9
|
---|
| 206 |
|
---|
| 207 | if self._accept_bid(self._last_received_bid):
|
---|
| 208 | action = Accept(self._id, self._last_received_bid)
|
---|
| 209 |
|
---|
| 210 | else:
|
---|
| 211 | bid = self._create_bid()
|
---|
| 212 | action = Offer(self._id, bid)
|
---|
| 213 |
|
---|
| 214 | return action
|
---|
| 215 |
|
---|
| 216 | # Checks if a bid should be accepted.
|
---|
| 217 | def _accept_bid(self, bid: Bid) -> bool:
|
---|
| 218 | if bid is None:
|
---|
| 219 | return False
|
---|
| 220 |
|
---|
| 221 | profile: Profile = self._profile.getProfile()
|
---|
| 222 |
|
---|
| 223 | if isinstance(profile, UtilitySpace):
|
---|
| 224 | time_modifier = self._session_progress.get(int(time.time())) ** self._accept_concession_param
|
---|
| 225 |
|
---|
| 226 | # The minimum bid utility the agent can accept this round according to its strategy.
|
---|
| 227 | min_acceptance = 1.0 - time_modifier
|
---|
| 228 | min_acceptance = min_acceptance * (self._max_utility - self._min_utility)
|
---|
| 229 | min_acceptance = min_acceptance + self._min_utility
|
---|
| 230 |
|
---|
| 231 | #This tracks the best possibile bid we have received from the opposing agent
|
---|
| 232 | if self._best_bid == None or profile.getUtility(bid) > profile.getUtility(self._best_bid):
|
---|
| 233 | self._best_bid = bid
|
---|
| 234 | return profile.getUtility(bid) > min_acceptance
|
---|
| 235 |
|
---|
| 236 | raise Exception("Can not handle this type of profile")
|
---|
| 237 |
|
---|
| 238 | # Creates a new bid to offer.
|
---|
| 239 | def _create_bid(self) -> Bid:
|
---|
| 240 | time_modifier = self._session_progress.get(int(time.time())) ** self._offer_concession_param
|
---|
| 241 |
|
---|
| 242 | profile: Profile = self._profile.getProfile()
|
---|
| 243 |
|
---|
| 244 | # The target utility for the agent's bid this round according to its strategy.
|
---|
| 245 | ideal_utility = 1.0 - time_modifier
|
---|
| 246 | ideal_utility = ideal_utility * (self._max_utility - self._min_utility)
|
---|
| 247 | ideal_utility = ideal_utility + self._min_utility
|
---|
| 248 |
|
---|
| 249 | closest_bid = min(self._all_bids, key=lambda x: abs(x[0] - ideal_utility))
|
---|
| 250 | closest_bid_index = [y[0] for y in self._all_bids].index(closest_bid[0])
|
---|
| 251 |
|
---|
| 252 | # Bids we can make this round which give the agent a utility close to the ideal utility.
|
---|
| 253 | possible_bids = self._all_bids[
|
---|
| 254 | max(closest_bid_index - self._randomization_param * 2, 0):
|
---|
| 255 | min(closest_bid_index + self._randomization_param * 2, len(self._all_bids) - 1)
|
---|
| 256 | ]
|
---|
| 257 |
|
---|
| 258 | # Bids we can make this round sorted by expected opponent utility.
|
---|
| 259 | opponent_best_bids: List[(int, Dict[str, Value])] = []
|
---|
| 260 | for i in range(len(possible_bids)):
|
---|
| 261 | bid: Dict[str, Value] = possible_bids[i][1]
|
---|
| 262 | count = 0
|
---|
| 263 |
|
---|
| 264 | for issue, value in bid.items():
|
---|
| 265 | if issue in self._opponent_model and value in self._opponent_model[issue]:
|
---|
| 266 | count += self._opponent_model[issue][value]
|
---|
| 267 |
|
---|
| 268 | opponent_best_bids.append((count, bid))
|
---|
| 269 | opponent_best_bids = sorted(opponent_best_bids, key=lambda x: x[0], reverse=True)
|
---|
| 270 |
|
---|
| 271 | # Small randomization in case the opponent's profile model isn't perfect.
|
---|
| 272 | opponent_best_bids = opponent_best_bids[0: self._randomization_param]
|
---|
| 273 |
|
---|
| 274 | # Edge case for starting first - opponent's profile model isn't initialized.
|
---|
| 275 | if len(self._opponent_model) == 0:
|
---|
| 276 | final_bid = Bid(possible_bids[random.randint(0, len(possible_bids) - 1)][1])
|
---|
| 277 | else:
|
---|
| 278 | final_bid = Bid(opponent_best_bids[random.randint(0, len(opponent_best_bids) - 1)][1])
|
---|
| 279 |
|
---|
| 280 | if self._best_bid != None and profile.getUtility(final_bid) <= profile.getUtility(self._best_bid):
|
---|
| 281 | return self._best_bid
|
---|
| 282 | return final_bid |
---|