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 |
---|