source: CSE3210/agent55/agent55.py

Last change on this file was 74, checked in by wouter, 21 months ago

#6 Added CSE3210 parties

File size: 18.9 KB
Line 
1import logging
2import time
3from random import randint, uniform
4from typing import cast
5from math import log10, floor
6from geniusweb.actions.Accept import Accept
7from geniusweb.actions.Action import Action
8from geniusweb.actions.Offer import Offer
9from geniusweb.bidspace.AllBidsList import AllBidsList
10from geniusweb.bidspace.BidsWithUtility import BidsWithUtility
11from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
12from geniusweb.bidspace.Interval import Interval
13from geniusweb.inform.ActionDone import ActionDone
14from geniusweb.inform.Finished import Finished
15from geniusweb.inform.Inform import Inform
16from geniusweb.inform.Settings import Settings
17from geniusweb.inform.YourTurn import YourTurn
18from geniusweb.issuevalue.Bid import Bid
19from geniusweb.party.Capabilities import Capabilities
20from geniusweb.party.DefaultParty import DefaultParty
21from geniusweb.profileconnection.ProfileConnectionFactory import ProfileConnectionFactory
22from geniusweb.progress.ProgressRounds import ProgressRounds
23from tudelft_utilities_logging.Reporter import Reporter
24import heapq
25from decimal import *
26from .Group55OpponentModel import FrequencyOpponentModel
27
28
29class Agent55(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._utilspace: LinearAdditive = None
37 self._bidutils = None
38 self.getReporter().log(logging.INFO, "party is initialized")
39 self._profile = None
40 self._lastReceivedBid: Bid = None
41
42 """
43 this will create the opponent model
44 """
45 self.opponentModel = FrequencyOpponentModel.create()
46
47 """
48 baselineAcceptableUtility is a utility value for which we accept immediately
49 """
50 self.baselineAcceptableUtility = 0.95
51
52 """
53 hardballOpponentUtilityDelta is the opponent utility change value over which an opponnent is considered to be playing hardball
54 """
55 self.hardballOpponentUtilityDelta = -0.005
56
57 """
58 timePassedAccept is a fixed amount of time passed in the negotiation after which we accept
59 """
60 self.timePassedAccept = 0.95
61
62 """
63 timePassedConcede is a fixed amount of time passed in the negotiation when our agent starts conceding more
64 """
65 self.timePassedConcede = 0.75
66
67 """
68 These two variables will show the average utility-change of their and our agent, throughout their offerings.
69 This excludes the jump from no offer to the initial offer. Note that the first bid this will return None, so
70 there must be a check for this.
71 """
72 self.theirAverageUtilityChangeByTheirBids = None
73 self.ourAverageUtilityChangeByTheirBids = None
74
75 """
76 These variables help with the calculation of 'theirAverageUtilityChangeByTheirBids' and
77 'ourAverageUtilityChangeByTheirBids'.
78 """
79 self.sumChangeOurUtilitiesByTheirBids = 0
80 self.sumChangeTheirUtilitiesByTheirBids = 0
81 self.ourUtilityLastTimeByTheirBids = 0
82 self.theirUtilityLastTimeByTheirBids = 0
83
84 """
85 Matas: These variables enable our bidding strategy
86 """
87 self.ourBestBids = []
88 self.opponentsBestBids = []
89 self.roundsSinceBidRecalibration = 0
90 self.reCalibrateEveryRounds = 10
91 self.randomBidDiscoveryAttemptsPerTurn = 500
92 self.acceptableUtilityNormalizationWidth = 0.1
93 self.utilityThresholdAdjustmentStep = 0.1
94 self.percentOfTimeWeUseOpponentsBestBidIfItIsBetter = 0.7
95 self.paddingForUsingRandomBid = 0.1
96 self.amountOfBestBidsToKeep = 50
97 self.bidsToKeepBasedOnProgressScale = 0.3
98 self.opponentNicenessConceedingContributionScale = 0.3
99
100 def notifyChange(self, info: Inform):
101 """This is the entry point of all interaction with your agent after is has been initialised.
102
103 Args:
104 info (Inform): Contains either a request for action or information.
105 """
106
107 # a Settings message is the first message that will be send to your
108 # agent containing all the information about the negotiation session.
109 if isinstance(info, Settings):
110 self._settings: Settings = cast(Settings, info)
111 self._me = self._settings.getID()
112
113 # progress towards the deadline has to be tracked manually through the use of the Progress object
114 self._progress: ProgressRounds = self._settings.getProgress()
115
116 # the profile contains the preferences of the agent over the domain
117 self._profile = ProfileConnectionFactory.create(
118 info.getProfile().getURI(), self.getReporter()
119 )
120
121 # create and initialize opponent-model
122 profile = self._profile.getProfile()
123 self.opponentModel = self.opponentModel.With(
124 profile.getDomain(), profile.getReservationBid())
125
126 # ActionDone is an action send by an opponent (an offer or an accept)
127 elif isinstance(info, ActionDone):
128 action: Action = cast(ActionDone, info).getAction()
129
130 # if it is an offer, set the last received bid
131 if isinstance(action, Offer):
132 self._lastReceivedBid = cast(Offer, action).getBid()
133
134 """
135 Important caveat: anytime we do an offer the program also passes this part and updates the
136 last_received bid with the offer we made.
137
138 The reason that their variable is called 'lastReceivedBid' is that we access it during our turn and
139 during our turn this is always the last bid done by the opponent.
140
141 For this reason, we first check if the Action does not contain our id before updating the
142 opponent model.
143 """
144 if cast(Offer, action).getActor() is not self._me:
145 self.opponentModel = self.opponentModel.WithAction(
146 action, self._progress)
147 self._updateOpponentModel()
148
149 # YourTurn notifies you that it is your turn to act
150 elif isinstance(info, YourTurn):
151 action = self._myTurn()
152 if isinstance(self._progress, ProgressRounds):
153 self._progress = self._progress.advance()
154 self.getConnection().send(action)
155
156 # Finished will be send if the negotiation has ended (through agreement or deadline)
157 elif isinstance(info, Finished):
158 # terminate the agent MUST BE CALLED
159 self.terminate()
160 else:
161 self.getReporter().log(
162 logging.WARNING, "Ignoring unknown info " + str(info)
163 )
164
165 # lets the geniusweb system know what settings this agent can handle
166 # leave it as it is for this competition
167 def getCapabilities(self) -> Capabilities:
168 return Capabilities(
169 set(["SAOP"]),
170 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
171 )
172
173 # terminates the agent and its connections
174 # leave it as it is for this competition
175 def terminate(self):
176 self.getReporter().log(logging.INFO, "party is terminating:")
177 super().terminate()
178 if self._profile is not None:
179 self._profile.close()
180 self._profile = None
181
182
183
184 # give a description of your agent
185
186 def getDescription(self) -> str:
187 return "Agent55"
188
189 # execute a turn
190 def _myTurn(self):
191 self._updateUtilSpace()
192
193 # Generate a bid according to our current acceptable utility
194 (aGoodBid, nashProduct) = self._generateAGoodBid()
195
196 # Update our best bid store and fetch the best bid
197 (currentBestOurBid, currentBestOurBidNashProduct) = self._updateBidsAndGetBestBid(
198 self.ourBestBids, aGoodBid, nashProduct, floor(self.amountOfBestBidsToKeep * (1 - self._progress.get(time.time() * 1000)) * self.bidsToKeepBasedOnProgressScale))
199
200 currentBestBid = currentBestOurBid
201 currentBestBidNashProduct = currentBestOurBidNashProduct
202
203 # If we have a bid from the opponent, store it in the opponent's best bid store
204 (currentBestTheirBid, currentBestTheirBidNashProduct) = (None, 0)
205 if self._lastReceivedBid is not None:
206 (currentBestTheirBid, currentBestTheirBidNashProduct) = self._updateBidsAndGetBestBid(
207 self.opponentsBestBids, self._lastReceivedBid, self._getNashProduct(self._lastReceivedBid), 1)
208
209 # print("Our best stored bid: {}, their best stored bid: {}".format(
210 # currentBestOurBidNashProduct, currentBestTheirBidNashProduct))
211
212 # Pick which best bid we are using as base. Slight random bias towards our best bid. Also the opponent best bid must be more favorable to us.
213 if currentBestOurBidNashProduct < currentBestTheirBidNashProduct \
214 and self.percentOfTimeWeUseOpponentsBestBidIfItIsBetter > uniform(0, 1) \
215 and self._profile.getProfile().getUtility(currentBestTheirBid) > self.opponentModel.getUtility(currentBestTheirBid):
216
217 currentBestBid = currentBestTheirBid
218 currentBestBidNashProduct = currentBestTheirBidNashProduct
219
220 # Use a newly generated bid instead of offering an optimal one with a random chance that is higher at the beginning and lower at the end.
221 # Moreover, use the freshly generated bids if we are conceding.
222 if currentBestBid is None or self._progress.get(time.time() * 1000) + self.paddingForUsingRandomBid < uniform(0, 1) or self._progress.get(time.time() * 1000) > self.timePassedConcede:
223 currentBestBid = aGoodBid
224 currentBestBidNashProduct = nashProduct
225
226 if self._isAcceptable(self._lastReceivedBid, currentBestBid):
227 # if so, accept the offer
228 action = Accept(self._me, self._lastReceivedBid)
229 else:
230 # if not, propose a counter offer
231
232 action = Offer(self._me, currentBestBid)
233
234 # send the action
235 return action
236
237 def _isOpponentPlayingHardball(self) -> bool:
238 if self.theirAverageUtilityChangeByTheirBids is None:
239 return False
240
241 return self.theirAverageUtilityChangeByTheirBids > self.hardballOpponentUtilityDelta
242
243 def _getHardballFactor(self) -> Decimal:
244 timeLeft = self._progress.get(time.time() * 1000)
245
246 # high hardball factor before conceding time
247 if timeLeft <= self.timePassedConcede:
248 return 20
249
250 # opponent is not playing hardball so we can concede less
251 if not self._isOpponentPlayingHardball():
252 return 14
253
254 return 8
255
256 def _getAcceptableUtility(self) -> Decimal:
257 timePassed = self._progress.get(time.time() * 1000)
258 timeLeft = 1 - timePassed
259 # the higher the factor the less we concede
260 hardballFactor = self._getHardballFactor()
261
262 return Decimal(log10(timeLeft) / hardballFactor + self.baselineAcceptableUtility)
263
264 # method that checks if we should accept an offer
265 def _isAcceptable(self, lastReceivedBid: Bid, proposedBid: Bid) -> bool:
266 if lastReceivedBid is None or proposedBid is None:
267 return False
268
269 profile = self._profile.getProfile()
270
271 if profile.getUtility(lastReceivedBid) >= profile.getUtility(proposedBid):
272 return True
273
274 return self._isGood(lastReceivedBid)
275
276 # method that checks if an offer is considered good
277 def _isGood(self, lastReceivedBid: Bid) -> bool:
278 if lastReceivedBid is None:
279 return False
280
281 progress = self._progress.get(time.time() * 1000)
282
283 if progress >= self.timePassedAccept:
284 return True
285
286 profile = self._profile.getProfile()
287 utility = profile.getUtility(lastReceivedBid)
288
289 if utility >= self.baselineAcceptableUtility:
290 return True
291
292 return utility >= self._getAcceptableUtility()
293
294 def _generateAGoodBid(self) -> tuple[Bid, Decimal]:
295 # Use the expexted opponent utility to set a range to find a bid that is acceptable to us
296
297 # Starting points
298 acceptableUtility = self._getAcceptableUtility()
299 maxUtility = 1
300
301 # Decrease our max utility if the opponent is taking losses according to our model
302 if self._progress.get(time.time() * 1000) > self.timePassedConcede:
303 maxUtility -= (Decimal(self._progress.get(time.time() * 1000)) *
304 Decimal(self.opponentNicenessConceedingContributionScale) * (1 - self.theirUtilityLastTimeByTheirBids))
305
306 # Normalize in case we decrease maxUtil by too much.
307 if maxUtility <= acceptableUtility:
308 acceptableUtility = maxUtility - \
309 Decimal(self.acceptableUtilityNormalizationWidth)
310
311 # Attempt to generate a bid, and adjust our utility thresholds if necessary
312 while maxUtility <= 1 or acceptableUtility >= 0:
313 generatedBid, nash = self._generateAGoodBidGivenMinMaxUtil(
314 acceptableUtility, maxUtility)
315 if generatedBid is None:
316
317 # Adjust thresholds. First expand the max utility, then reduce the min utility.
318 if maxUtility < 1:
319 maxUtility = min(
320 maxUtility + Decimal(self.utilityThresholdAdjustmentStep), 1)
321 else:
322 acceptableUtility = max(
323 acceptableUtility - Decimal(self.utilityThresholdAdjustmentStep), 0)
324
325 else:
326 return generatedBid, nash
327
328 # All atempts have failed. Generate a random bid.
329 return self._generateRandomBid()
330
331 def _generateAGoodBidGivenMinMaxUtil(self, acceptableUtility, maxUtility) -> tuple[Bid, Decimal]:
332 currentAvailableBids = self._bidutils.getBids(
333 Interval(acceptableUtility, Decimal(maxUtility))
334 )
335
336 # If no available bids, we can't generate a bid.
337 if currentAvailableBids.size() == 0:
338 return None, 0
339
340 goodBid = currentAvailableBids.get(
341 randint(0, currentAvailableBids.size() - 1))
342 nash = self._getNashProduct(goodBid)
343
344 return goodBid, nash
345
346 def _updateBidsAndGetBestBid(self, bestBids, bestBidFromThisTurn, nashProduct, nBestBids) -> Bid:
347 self.roundsSinceBidRecalibration += 1
348
349 # Must at least pick one option
350 if nBestBids < 1:
351 nBestBids = 1
352
353 # After a certain amount of rounds has passed, we recallibrate our bid storage
354 if self.roundsSinceBidRecalibration >= self.reCalibrateEveryRounds:
355 self.roundsSinceBidRecalibration = 0
356
357 # Update and prune
358 updatedRaw = [self._popAndUpdate(bestBids)
359 for i in range(min(len(bestBids), self.amountOfBestBidsToKeep))]
360
361 bestBids.clear()
362 [heapq.heappush(bestBids, x)
363 for x in updatedRaw]
364
365 # Invert the nash product since heapq is a min queue
366 invertedNashProduct = 1 - nashProduct
367 heapq.heappush(bestBids,
368 (invertedNashProduct, MaxHeapObj(bestBidFromThisTurn)))
369
370 # Pick a bid close to the Nash Equilibrium
371 toPickFrom = heapq.nsmallest(min(nBestBids, len(bestBids)), bestBids)
372
373 (currentBestInvertedNashProduct,
374 currentBestBid) = toPickFrom[randint(0, len(toPickFrom) - 1)]
375
376 # Invert nash product and return
377 return currentBestBid.val, 1 - currentBestInvertedNashProduct
378
379 def _getNashProduct(self, bid) -> Decimal:
380 utility = self._profile.getProfile().getUtility(bid)
381 opponentUtility = self.opponentModel.getUtility(bid)
382 return utility * opponentUtility
383
384 def _popAndUpdate(self, bestBids):
385 x = heapq.heappop(bestBids)
386 return (self._getNashProduct(x[1].val), x[1])
387
388 def _updateUtilSpace(self) -> LinearAdditive:
389 newutilspace = self._profile.getProfile()
390 if not newutilspace == self._utilspace:
391 self._utilspace = newutilspace
392 self._bidutils = BidsWithUtility.create(self._utilspace)
393 return self._utilspace
394
395 def _generateRandomBid(self) -> tuple[Bid, Decimal]:
396 domain = self._profile.getProfile().getDomain()
397 all_bids = AllBidsList(domain)
398 bid = None
399
400 # Try to generate a good random bid
401 for _ in range(self.randomBidDiscoveryAttemptsPerTurn):
402 candidate = all_bids.get(randint(0, all_bids.size() - 1))
403 if self._isGood(candidate):
404 bid = candidate
405 break
406
407 # If no good ones found within the allocated attempt count, pick at random
408 if bid is None:
409 bid = all_bids.get(randint(0, all_bids.size() - 1))
410
411 nash = self._getNashProduct(bid)
412
413 return bid, nash
414
415 """
416 This method maintains all extensions of the opponent model. Everytime the opponent makes an offer, this gets
417 updated. Currently the method maintains the following extensions:
418 * theirAverageUtilityChangeByTheirBids
419 * ourAverageUtilityChangeByTheirBids
420 """
421
422 def _updateOpponentModel(self):
423
424 ###This block calculates: ourAverageUtilityChangeByTheirBids and TheirAverageUtilityChangeByTheirBids ########
425
426 ourUtilityThisBid = self._profile.getProfile().getUtility(self._lastReceivedBid)
427 theirUtilityThisBid = self.opponentModel.getUtility(
428 self._lastReceivedBid)
429 bidCount = self.opponentModel._totalBids
430
431 # if it's the first offer
432 if bidCount == 1:
433 self.ourUtilityLastTimeByTheirBids = ourUtilityThisBid
434 self.theirUtilityLastTimeByTheirBids = theirUtilityThisBid
435 else:
436 ourDifference = ourUtilityThisBid - self.ourUtilityLastTimeByTheirBids
437 theirDifference = theirUtilityThisBid - self.theirUtilityLastTimeByTheirBids
438
439 self.sumChangeOurUtilitiesByTheirBids += ourDifference
440 self.sumChangeTheirUtilitiesByTheirBids += theirDifference
441
442 self.theirAverageUtilityChangeByTheirBids = self.sumChangeTheirUtilitiesByTheirBids / \
443 (bidCount - 1)
444 self.ourAverageUtilityChangeByTheirBids = self.sumChangeOurUtilitiesByTheirBids / \
445 (bidCount - 1)
446
447 self.ourUtilityLastTimeByTheirBids = ourUtilityThisBid
448 self.theirUtilityLastTimeByTheirBids = theirUtilityThisBid
449 ###End of calculation: ourAverageUtilityChangeByTheirBids and TheirAverageUtilityChangeByTheirBids ########
450
451# helper for heap
452
453
454class MaxHeapObj(object):
455 def __init__(self, val): self.val = val
456 def __lt__(self, other): return True
457 def __eq__(self, other): return True
Note: See TracBrowser for help on using the repository browser.