source: CSE3210/agent50/agent50.py

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

#6 Added CSE3210 parties

File size: 11.7 KB
Line 
1import logging
2import time
3from random import randint
4from tokenize import Number
5from typing import cast, Set
6from decimal import Decimal
7import math
8
9from geniusweb.actions.Accept import Accept
10from geniusweb.actions.Action import Action
11from geniusweb.actions.Offer import Offer
12from geniusweb.actions.PartyId import PartyId
13from geniusweb.bidspace.AllBidsList import AllBidsList
14from geniusweb.inform.ActionDone import ActionDone
15from geniusweb.inform.Finished import Finished
16from geniusweb.inform.Inform import Inform
17from geniusweb.inform.Settings import Settings
18from geniusweb.inform.YourTurn import YourTurn
19from geniusweb.issuevalue.Bid import Bid
20from geniusweb.issuevalue.Domain import Domain
21from geniusweb.issuevalue.Value import Value
22from geniusweb.issuevalue.ValueSet import ValueSet
23from geniusweb.party.Capabilities import Capabilities
24from geniusweb.party.DefaultParty import DefaultParty
25from geniusweb.profile.utilityspace import ValueSetUtilities
26from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
27from geniusweb.profileconnection.ProfileConnectionFactory import (
28 ProfileConnectionFactory,
29)
30from geniusweb.progress.ProgressRounds import ProgressRounds
31from tudelft_utilities_logging.Reporter import Reporter
32
33
34class Agent50(DefaultParty):
35 """
36 Template agent that offers random bids until a bid with sufficient utility is offered.
37 """
38
39 def __init__(self, reporter: Reporter = None):
40 super().__init__(reporter)
41 self.getReporter().log(logging.INFO, "party is initialized")
42 self._profile = None
43 self._last_received_bid: Bid = None
44 self._last_send_bid: Bid = None
45 self._incoming_bids: list[(Bid, Decimal)] = []
46 self._aspiration_level: Decimal = 1.0
47 self._asp_c = 0
48 self._ordered_issue_values: dict[str, list[str]] = {}
49 self._ordered_issue_values_is_initialized = False
50
51 def notifyChange(self, info: Inform):
52 """This is the entry point of all interaction with your agent after is has been initialised.
53
54 Args:
55 info (Inform): Contains either a request for action or information.
56 """
57
58 # a Settings message is the first message that will be send to your
59 # agent containing all the information about the negotiation session.
60 if isinstance(info, Settings):
61 self._settings: Settings = cast(Settings, info)
62 self._me = self._settings.getID()
63
64 # progress towards the deadline has to be tracked manually through the use of the Progress object
65 self._progress: ProgressRounds = self._settings.getProgress()
66
67 # the profile contains the preferences of the agent over the domain
68 self._profile = ProfileConnectionFactory.create(
69 info.getProfile().getURI(), self.getReporter()
70 )
71 # ActionDone is an action send by an opponent (an offer or an accept)
72 elif isinstance(info, ActionDone):
73 action: Action = cast(ActionDone, info).getAction()
74
75 # if it is an offer, set the last received bid
76 if isinstance(action, Offer):
77 self._last_received_bid = cast(Offer, action).getBid()
78
79 # YourTurn notifies you that it is your turn to act
80 elif isinstance(info, YourTurn):
81 action = self._myTurn()
82 if isinstance(self._progress, ProgressRounds):
83 self._progress = self._progress.advance()
84 self.getConnection().send(action)
85
86 # Finished will be send if the negotiation has ended (through agreement or deadline)
87 elif isinstance(info, Finished):
88 # terminate the agent MUST BE CALLED
89 self.terminate()
90 else:
91 self.getReporter().log(
92 logging.WARNING, "Ignoring unknown info " + str(info)
93 )
94
95 # lets the geniusweb system know what settings this agent can handle
96 # leave it as it is for this competition
97 def getCapabilities(self) -> Capabilities:
98 return Capabilities(
99 set(["SAOP"]),
100 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
101 )
102
103 # terminates the agent and its connections
104 # leave it as it is for this competition
105 def terminate(self):
106 self.getReporter().log(logging.INFO, "party is terminating:")
107 super().terminate()
108 if self._profile is not None:
109 self._profile.close()
110 self._profile = None
111
112
113
114 # Description of the agent
115 def getDescription(self) -> str:
116 return "Agent50"
117
118 # Turn handler for the trade-off agent
119 def _myTurn(self):
120 # On the first turn, initialize the ordered issue values
121 if not self._ordered_issue_values_is_initialized:
122 self.initializeIssueValues()
123
124 # Compute the utility value of the incoming bid
125 utility_value: Decimal = self._calculateUtilityValue(self._last_received_bid)
126
127 # log the incoming bid
128 self._incoming_bids.append((self._last_received_bid, utility_value))
129
130 # Check for a deadlock (when the utility value of the incoming bid is lower than that of the previous incoming bid)
131 # If so, decrease aspiration level (can be time-dependent) -> this is where the concession is made
132 if (
133 len(self._incoming_bids) > 1
134 and utility_value < self._incoming_bids[-2][1]
135 ):
136 self._decreaseAspirationLevel()
137
138 # Generate a bid at the current aspiration level and set it as the last sent bid
139 next_bid: Bid = self._generateBid()
140 self._last_send_bid = next_bid
141
142 # If the incoming bid is good enough, accept (when the received offer is better than last proposed offer)
143 if self._isAcceptable(self._last_received_bid):
144 action = Accept(self._me, self._last_received_bid)
145 else: # Else, offer the generated bid
146 action = Offer(self._me, next_bid)
147
148 return action
149
150 # Get the fraction of unique incoming bids
151 def _getFractionUniqueIncomingBids(self):
152 unique_received_bids = set([x[0] for x in self._incoming_bids])
153 return (
154 len(unique_received_bids) / len(self._incoming_bids)
155 if len(self._incoming_bids) >= 1
156 else 1
157 )
158
159 # Decreases the aspiration level depending on fraction of unique incoming bids and progress
160 def _decreaseAspirationLevel(self):
161 fraction_progress = self._progress.get(time.time() * 1000)
162 self._aspiration_level -= (
163 0.002 * (self._getFractionUniqueIncomingBids() + 0.5)
164 ) * (math.pow(fraction_progress, 2) + 1)
165
166 # Calculates the utility value of the given bid
167 def _calculateUtilityValue(self, bid: Bid) -> Decimal:
168 if not bid:
169 return 0
170 return self._profile.getProfile().getUtility(bid)
171
172 # Returns true iff the given bid is better than the last proposed bid and the reservation value (ACnext)
173 def _isAcceptable(self, bid: Bid) -> bool:
174 # Not acceptable if bid is None or there is no last send bid
175 if not bid or not self._last_send_bid:
176 return False
177
178 # Calculate utility value of the bid
179 uv = self._calculateUtilityValue(bid)
180
181 reservation_bid = self._profile.getProfile().getReservationBid()
182 # Check if utility value of bid is higher than last proposed bid and the reservation bid
183 if not reservation_bid:
184 return uv >= self._calculateUtilityValue(self._last_send_bid)
185 else:
186 return uv > self._calculateUtilityValue(
187 reservation_bid
188 ) and uv >= self._calculateUtilityValue(self._last_send_bid)
189
190 # Compute the variability of given issue
191 def _computeVariability(self, issue: str) -> Decimal:
192 # No variability if there is only one bid
193 if len(self._incoming_bids) < 2:
194 return 0
195
196 # Get the total of changes between successive bids
197 changes = 0
198 n = len(self._incoming_bids) - 1
199 for i in range(n):
200 if (
201 not self._incoming_bids[n - 1]
202 or not self._incoming_bids[n - 1][0]
203 or not self._incoming_bids[n - (i + 1)]
204 or not self._incoming_bids[n - (i + 1)][0]
205 ):
206 continue
207 if self._incoming_bids[n - i][0].getValue(issue) != self._incoming_bids[
208 n - (i + 1)
209 ][0].getValue(issue):
210 changes += 1
211
212 # Variability is total changes divided by total bids
213 return changes / n
214
215 # Bid generator for the trade-off agent
216 def _generateBid(self) -> Bid:
217 # Calculate the variance of each issue in the bids of the opposing party
218 variabilities = []
219 for issue in self._profile.getProfile().getDomain().getIssues():
220 variabilities.append((issue, self._computeVariability(issue)))
221
222 # Order issues in the most recent incoming bid by variance, descending
223 # (assumption: higher variance issues are less important for the opponent)
224 variabilities.sort(key=lambda x: x[1], reverse=True)
225 issues_sorted_by_variance = [v[0] for v in variabilities]
226 bid_values = (
227 self._last_received_bid.getIssueValues().copy()
228 if self._last_received_bid
229 else dict()
230 )
231
232 if len(bid_values) < len(variabilities):
233 bid_values = dict()
234 for issue in issues_sorted_by_variance:
235 bid_values[issue] = list(self._ordered_issue_values[issue])[-1]
236
237 new_proposed_bid = Bid(bid_values)
238
239 # Raise the value of each issue incrementally until the aspiration level has been reached
240 # Max out the first issue, if that does not reach the aspiration level, continue to the next issue
241 for issue in issues_sorted_by_variance:
242 ordered_values = list(self._ordered_issue_values[issue])
243 current_value = bid_values[issue]
244 index = ordered_values.index(current_value)
245 while (index < len(ordered_values)) & (
246 self._calculateUtilityValue(new_proposed_bid) < self._aspiration_level
247 ):
248 bid_values[issue] = ordered_values[index]
249 new_proposed_bid = Bid(bid_values)
250 index += 1
251
252 # Decrease aspiration level if new proposed bid is the same as last proposed bid
253 if self._last_send_bid and new_proposed_bid == self._last_send_bid:
254 self._decreaseAspirationLevel()
255
256 # Return the bid
257 return new_proposed_bid
258
259 # Initializes the data structure storing the issue values ordered by utility value (ascending)
260 def initializeIssueValues(self):
261 profile = self._profile.getProfile()
262
263 utilities_per_value_per_issue: dict[
264 str, ValueSetUtilities
265 ] = profile.getUtilities()
266
267 # Loop through all the issues in the domain
268 domain = profile.getDomain().getIssuesValues()
269 for issue in domain:
270 # Get the issue and utility values for the current issue
271 utility_values = utilities_per_value_per_issue[issue].getUtilities()
272 # Sort the issue values by their utility values, ascending
273 values_sorted_by_utility = dict(
274 sorted(utility_values.items(), key=lambda item: item[1])
275 )
276 # Store the results
277 self._ordered_issue_values[issue] = values_sorted_by_utility.keys()
278
279 # Change the state variable to note that the data structure has been initialized
280 self._ordered_issue_values_is_initialized = True
Note: See TracBrowser for help on using the repository browser.