1 | import logging
|
---|
2 | import random
|
---|
3 | import time
|
---|
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.party.Capabilities import Capabilities
|
---|
17 | from geniusweb.party.DefaultParty import DefaultParty
|
---|
18 | from geniusweb.profileconnection.ProfileConnectionFactory import (
|
---|
19 | ProfileConnectionFactory,
|
---|
20 | )
|
---|
21 |
|
---|
22 | from .MyOpponentModel import MyOpponentModel
|
---|
23 | from geniusweb.progress.ProgressRounds import ProgressRounds
|
---|
24 | from tudelft_utilities_logging.Reporter import Reporter
|
---|
25 |
|
---|
26 |
|
---|
27 | class Agent11(DefaultParty):
|
---|
28 | """
|
---|
29 | Template agent that offers random bids until a bid with sufficient utility is offered.
|
---|
30 | """
|
---|
31 |
|
---|
32 | def __init__(self, reporter: Reporter = None):
|
---|
33 | super().__init__(reporter)
|
---|
34 | self.getReporter().log(logging.INFO, "party is initialized")
|
---|
35 | self._profile = None
|
---|
36 | self._last_received_bid = None
|
---|
37 |
|
---|
38 | self.sorted_bids = None
|
---|
39 | self.opponent_model = None
|
---|
40 | self.concede_range = 0.7
|
---|
41 | self._second_to_last_received_bid = None
|
---|
42 | self._last_offered_bid = None
|
---|
43 | self._window_size = 20
|
---|
44 | self._bias_correction = 0.015
|
---|
45 | self.concede_count = 0
|
---|
46 | self.non_concede_count = 0
|
---|
47 | self.concede_strategy = 10
|
---|
48 | self.received_bids = []
|
---|
49 | # list that tries to keep track of the movement of the opponent bids
|
---|
50 | self.opponent_delta = []
|
---|
51 |
|
---|
52 | def notifyChange(self, info: Inform):
|
---|
53 | """This is the entry point of all interaction with your agent after is has been initialised.
|
---|
54 |
|
---|
55 | Args:
|
---|
56 | info (Inform): Contains either a request for action or information.
|
---|
57 | """
|
---|
58 |
|
---|
59 | # a Settings message is the first message that will be send to your
|
---|
60 | # agent containing all the information about the negotiation session.
|
---|
61 | if isinstance(info, Settings):
|
---|
62 | self._settings: Settings = cast(Settings, info)
|
---|
63 | self._me = self._settings.getID()
|
---|
64 |
|
---|
65 | # progress towards the deadline has to be tracked manually through the use of the Progress object
|
---|
66 | self._progress = self._settings.getProgress()
|
---|
67 |
|
---|
68 | # the profile contains the preferences of the agent over the domain
|
---|
69 | self._profile = ProfileConnectionFactory.create(
|
---|
70 | info.getProfile().getURI(), self.getReporter()
|
---|
71 | )
|
---|
72 | # initialize opponent model
|
---|
73 | self.opponent_model = MyOpponentModel.create().With(self._profile.getProfile().getDomain(), None)
|
---|
74 | # self.opponent_model = FrequencyOpponentModel.create().With(self._profile.getProfile().getDomain(), None)
|
---|
75 |
|
---|
76 | # ActionDone is an action send by an opponent (an offer or an accept)
|
---|
77 | elif isinstance(info, ActionDone):
|
---|
78 | action: Action = cast(ActionDone, info).getAction()
|
---|
79 |
|
---|
80 | # if it is an offer, set the last received bid
|
---|
81 | if isinstance(action, Offer) and action.getActor() != self._me:
|
---|
82 | self._last_received_bid = cast(Offer, action).getBid()
|
---|
83 | self.received_bids.append(self._last_received_bid)
|
---|
84 |
|
---|
85 | # YourTurn notifies you that it is your turn to act
|
---|
86 | elif isinstance(info, YourTurn):
|
---|
87 | action = self._myTurn()
|
---|
88 | if isinstance(self._progress, ProgressRounds):
|
---|
89 | self._progress = self._progress.advance()
|
---|
90 | self.getConnection().send(action)
|
---|
91 |
|
---|
92 | # Finished will be send if the negotiation has ended (through agreement or deadline)
|
---|
93 | elif isinstance(info, Finished):
|
---|
94 | # terminate the agent MUST BE CALLED
|
---|
95 | self.terminate()
|
---|
96 | else:
|
---|
97 | self.getReporter().log(
|
---|
98 | logging.WARNING, "Ignoring unknown info " + str(info)
|
---|
99 | )
|
---|
100 |
|
---|
101 | # lets the geniusweb system know what settings this agent can handle
|
---|
102 | # leave it as it is for this competition
|
---|
103 | def getCapabilities(self) -> Capabilities:
|
---|
104 | return Capabilities(
|
---|
105 | set(["SAOP"]),
|
---|
106 | set(["geniusweb.profile.utilityspace.LinearAdditive"]),
|
---|
107 | )
|
---|
108 |
|
---|
109 | # terminates the agent and its connections
|
---|
110 | # leave it as it is for this competition
|
---|
111 | def terminate(self):
|
---|
112 | self.getReporter().log(logging.INFO, "party is terminating:")
|
---|
113 | super().terminate()
|
---|
114 | if self._profile is not None:
|
---|
115 | self._profile.close()
|
---|
116 | self._profile = None
|
---|
117 |
|
---|
118 | # give a description of your agent
|
---|
119 | def getDescription(self) -> str:
|
---|
120 | return "Agent11"
|
---|
121 |
|
---|
122 | # execute a turn
|
---|
123 | def _myTurn(self):
|
---|
124 | progress = self._progress.get(time.time() * 1000)
|
---|
125 | # register bet with the opponent model
|
---|
126 | if self._last_received_bid:
|
---|
127 | self.opponent_model = self.opponent_model.WithAction(Offer(None, self._last_received_bid), progress)
|
---|
128 |
|
---|
129 | self._recognize_move()
|
---|
130 | # Check if last received offer is good enough and more than 80% passed
|
---|
131 | if self._isGood(self._last_received_bid):
|
---|
132 | action = Accept(self._me, self._last_received_bid)
|
---|
133 | else:
|
---|
134 | # Opponents bid was not good enough, we make a bid.
|
---|
135 | bid = self._findBid()
|
---|
136 |
|
---|
137 | self._secondToLast_offered_bid = self._last_offered_bid
|
---|
138 | self._last_offered_bid = bid
|
---|
139 |
|
---|
140 | action = Offer(self._me, bid)
|
---|
141 |
|
---|
142 | return action
|
---|
143 |
|
---|
144 | def _isGood(self, bid: Bid) -> bool:
|
---|
145 | """
|
---|
146 | Checks if a bid is good enough to accept.
|
---|
147 | @param bid the bid to consider
|
---|
148 | @return true if the bid is good enough, false otherwise
|
---|
149 | """
|
---|
150 | if bid is None:
|
---|
151 | return False
|
---|
152 |
|
---|
153 | utilities = self._calculate_utilities(bid, verbose=True) # returns a tuple with (total, ours, theirs)
|
---|
154 | utilities_prev_offered_bid = self._calculate_utilities(self._last_offered_bid)
|
---|
155 |
|
---|
156 | # We evaluate the following boolean conditions and base our final decision on them
|
---|
157 | good_for_me = utilities[0] >= 0.6
|
---|
158 | time_spend = self._progress.get(time.time() * 1000) >= 0.8
|
---|
159 | better_than_last_offered = self._last_offered_bid and utilities_prev_offered_bid[0] <= utilities[0]
|
---|
160 | better_than_reservation_value = self._profile.getProfile().getReservationBid() and self._profile \
|
---|
161 | .getProfile().getReservationBid() <= good_for_me
|
---|
162 |
|
---|
163 | return ((good_for_me and time_spend) and better_than_reservation_value) or better_than_last_offered
|
---|
164 |
|
---|
165 | def _findBid(self) -> Bid:
|
---|
166 | # compose a list of all possible bids
|
---|
167 | domain = self._profile.getProfile().getDomain()
|
---|
168 | all_bids = AllBidsList(domain)
|
---|
169 | if not self.sorted_bids:
|
---|
170 | self.sorted_bids = sorted(all_bids, key=lambda x: self._profile.getProfile().getUtility(x), reverse=True)
|
---|
171 |
|
---|
172 | if self._progress.get(time.time() * 1000) < self.concede_range:
|
---|
173 | bid_index = random.randint(0, int(len(self.sorted_bids) * 0.005))
|
---|
174 |
|
---|
175 | bid = self.sorted_bids[bid_index]
|
---|
176 |
|
---|
177 | return bid
|
---|
178 | else:
|
---|
179 | delta = 0.1
|
---|
180 |
|
---|
181 | # Every x rounds, we sum the last x deltas to check what the opponents strategy is.
|
---|
182 | # From experiments, an agent that concedes will get a score around -0.5 to -0.3
|
---|
183 | # An agent that hard lines will get a score around 0 or bigger than 0
|
---|
184 | # Against conceding agents, we will be more conservative and take on an hard lining strategy
|
---|
185 | # Against hard lining agents, we will concede more.
|
---|
186 | # A lower value of concede_strategy means we concede more
|
---|
187 | # A higher value means we are more conservative
|
---|
188 | if len(self.received_bids) % 10 == 0:
|
---|
189 | opponent_trend = sum(self.opponent_delta[-10:])
|
---|
190 | if opponent_trend < -0.2:
|
---|
191 | self.concede_strategy += 1
|
---|
192 | else:
|
---|
193 | self.concede_strategy -= 1 if self._progress.get(time.time() * 1000) < 0.8 \
|
---|
194 | else 2 # start conceding more towards the end
|
---|
195 | # we make sure this constant falls between a reasonable range, so it doesn't get too crazy
|
---|
196 | self.concede_strategy = max(min(self.concede_strategy, 15), 3)
|
---|
197 | # concede range = time after we concede
|
---|
198 | start_index = int(
|
---|
199 | len(self.sorted_bids) * (self._progress.get(time.time() * 1000) - self.concede_range) / self.concede_strategy)
|
---|
200 | # start_index = 0
|
---|
201 | # delta = randomness parameter for the generation of the bids
|
---|
202 | end_index = int(len(self.sorted_bids) * (self._progress.get(time.time() * 1000) - self.concede_range + delta))
|
---|
203 |
|
---|
204 | bidding_range = self.sorted_bids[start_index:end_index]
|
---|
205 | potential_bids = []
|
---|
206 | # pick n random bids
|
---|
207 | n = 50
|
---|
208 | for _ in range(n):
|
---|
209 | potential_bids.append(bidding_range[random.randint(0, len(bidding_range) - 1)])
|
---|
210 |
|
---|
211 | # find the best one according to ur best utility
|
---|
212 | potential_bids = sorted(potential_bids,
|
---|
213 | key=lambda x: self._evaluate_utilities(self._calculate_utilities(x)),
|
---|
214 | reverse=True)
|
---|
215 | bid = potential_bids[0]
|
---|
216 |
|
---|
217 | return bid
|
---|
218 |
|
---|
219 | def _calculate_utilities(self, bid: Bid, verbose=False):
|
---|
220 | """
|
---|
221 | Returns a tuple of the utility of an bid. The tuple consists of (total utility, own utility, opponent utility)
|
---|
222 | """
|
---|
223 | if not bid:
|
---|
224 | return 0, 0
|
---|
225 | own_utility = self._profile.getProfile().getUtility(bid)
|
---|
226 | opponent_utility = self.opponent_model.getUtility(bid)
|
---|
227 | if verbose:
|
---|
228 | self.getReporter().log(logging.INFO,
|
---|
229 | 'Own utility ' + str(own_utility) + ' Opponent utility ' + str(opponent_utility))
|
---|
230 |
|
---|
231 | return own_utility, opponent_utility
|
---|
232 |
|
---|
233 | @staticmethod
|
---|
234 | def _evaluate_utilities(utilities: tuple[float, float]) -> float:
|
---|
235 | ratio = 0.6
|
---|
236 | return ratio * float(utilities[0]) + (1-ratio) * float(utilities[1])
|
---|
237 |
|
---|
238 | def _recognize_move(self):
|
---|
239 | """
|
---|
240 | Evaluates the last x bids received where x is equal to the window size.
|
---|
241 | It then calculates of this window was a conceding window or a non conceding window by calculating the delta
|
---|
242 | It adds the delta to a list and this list will then be used to adjust our concession rate.
|
---|
243 | """
|
---|
244 |
|
---|
245 | if len(self.received_bids) < self._window_size:
|
---|
246 | return
|
---|
247 |
|
---|
248 | start = len(self.received_bids) - self._window_size
|
---|
249 | half = len(self.received_bids) - int(self._window_size / 2)
|
---|
250 |
|
---|
251 | # function to map the bids to the estimated utility
|
---|
252 |
|
---|
253 | first_half = list(map(lambda x: self.opponent_model.getUtility(x), self.received_bids[start:half]))
|
---|
254 | second_half = list(map(lambda x: self.opponent_model.getUtility(x), self.received_bids[half:]))
|
---|
255 |
|
---|
256 | # now we calculate the average utility
|
---|
257 | first_avg = sum(first_half) / len(first_half)
|
---|
258 | second_avg = sum(second_half) / len(second_half)
|
---|
259 |
|
---|
260 | # It takes into account that an opponent model will naturally be a little conceding over time
|
---|
261 | # hence the bias correction
|
---|
262 | delta = float(second_avg - first_avg) + self._bias_correction
|
---|
263 | self.opponent_delta.append(delta)
|
---|