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