diff --git a/.gitignore b/.gitignore index e697725e..8ed001d2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ logs results *.sbatch -*.log \ No newline at end of file +*.log +agents/group16_agent/utils/saved/*.plk + +.idea \ No newline at end of file diff --git a/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py b/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py index 12b71abc..f04a11b7 100644 --- a/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py +++ b/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py @@ -112,6 +112,7 @@ def notifyChange(self, data: Inform): self.profile = profile_connection.getProfile() self.domain = self.profile.getDomain() # compose a list of all possible bids + # RAFA: save all bids as a property self.all_bids = AllBidsList(self.domain) profile_connection.close() @@ -128,6 +129,7 @@ def notifyChange(self, data: Inform): self.other = actor # obtain the name of the opponent, cutting of the position ID. self.other_name = str(actor).rsplit("_", 1)[0] + # RAFA: load any previous data from file and adapt self.attempt_load_data() self.learn_from_past_sessions(self.data_dict["sessions"]) @@ -207,6 +209,7 @@ def opponent_action(self, action): # set bid as last received self.last_received_bid = bid + # RAFA: add opponent_best_bid, if existant update it if self.opponent_best_bid is None: self.opponent_best_bid = bid elif self.profile.getUtility(bid) > self.profile.getUtility(self.opponent_best_bid): diff --git a/agents/group16_agent/__init__.py b/agents/group16_agent/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/agents/group16_agent/__init__.py @@ -0,0 +1 @@ + diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py new file mode 100755 index 00000000..002718cd --- /dev/null +++ b/agents/group16_agent/group16_agent.py @@ -0,0 +1,459 @@ +import logging +from random import randint, uniform, choice +from statistics import variance, mean +from time import time +from typing import cast, List + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import ( + LinearAdditiveUtilitySpace, +) +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressTime import ProgressTime +from geniusweb.references.Parameters import Parameters +from numpy import floor +from tudelft_utilities_logging.ReportToLogger import ReportToLogger + +from .utils.opponent_model import OpponentModel +from .utils import wrapper + + +class Group16Agent(DefaultParty): + """ + Group16 agent implementing Gahboninho's strategy with session data integration. + + Key features: + - Uses variance in opponent bids to estimate concession willingness + - Calculates target utility using Ut = Umax - (Umax - Umin) * t formula + - Integrates session data from previous negotiations + - Uses random selection of bids above target utility + """ + + def __init__(self): + super().__init__() + self.find_bid_result = None + self.best_bid = None + self.bids_with_utilities = None + self.logger: ReportToLogger = self.getReporter() + + self.domain: Domain = None + self.parameters: Parameters = None + self.profile: LinearAdditiveUtilitySpace = None + self.progress: ProgressTime = None + self.me: PartyId = None + self.other: str = None + self.settings: Settings = None + self.storage_dir: str = None + self.got_opponent = False + + # Tracking bids and utilities + self.last_received_bid: Bid = None + self.opponent_model: OpponentModel = None + self.opponent = None + self.all_bids: AllBidsList = None + + # Gahboninho strategy parameters + self.round_count: int = 0 + self.opponent_utilities: List[float] = [] + self.concession_rate: float = 0.0 + self.opponent_utility_variance: float = 0.0 + self.probing_phase_complete: bool = False + + # Target utility parameters (Gahboninho) + self.max_target_utility: float = 0.95 + self.min_target_utility: float = 0.7 + + # For deadline management (DreamTeam) + self.avg_time_per_round = None + self.round_times = [] + self.last_time = None + + # Session tracking + self.utility_at_finish: float = 0.0 + self.did_accept: bool = False + + # Parameters from past sessions (DreamTeam) + self.force_accept_at_remaining_turns: float = 1.0 + self.force_accept_at_remaining_turns_light: float = 1.0 + self.top_bids_percentage: float = 1/300 + + self.logger.log(logging.INFO, "party is initialized") + + def notifyChange(self, data: Inform): + """MUST BE IMPLEMENTED + This is the entry point of all interaction with your agent after is has been initialised. + How to handle the received data is based on its class type. + + Args: + info (Inform): Contains either a request for action or information. + """ + + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(data, Settings): + self.settings = cast(Settings, data) + self.me = self.settings.getID() + + # progress towards the deadline has to be tracked manually through the use of the Progress object + self.progress = self.settings.getProgress() + + self.parameters = self.settings.getParameters() + self.storage_dir = self.parameters.get("storage_dir") + + # the profile contains the preferences of the agent over the domain + profile_connection = ProfileConnectionFactory.create( + data.getProfile().getURI(), self.getReporter() + ) + self.profile = profile_connection.getProfile() + self.domain = self.profile.getDomain() + + # Initialize list of all possible bids + self.all_bids = AllBidsList(self.domain) + + profile_connection.close() + + # ActionDone informs you of an action (an offer or an accept) + # that is performed by one of the agents (including yourself). + elif isinstance(data, ActionDone): + action = cast(ActionDone, data).getAction() + actor = action.getActor() + + # ignore action if it is our action + if actor != self.me: + # obtain the name of the opponent, cutting of the position ID. + self.other = str(actor).rsplit("_", 1)[0] + + # process action done by opponent + self.opponent_action(action) + # YourTurn notifies you that it is your turn to act + elif isinstance(data, YourTurn): + # execute a turn + self.my_turn() + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(data, Finished): + # Check if agreement reached + agreements = cast(Finished, data).getAgreements() + if len(agreements.getMap()) > 0: + agreed_bid = agreements.getMap()[self.me] + self.utility_at_finish = float(self.profile.getUtility(agreed_bid)) + self.logger.log(logging.INFO, f"Agreement reached with utility: {self.utility_at_finish}") + else: + self.utility_at_finish = 0.0 + self.logger.log(logging.INFO, "No agreement reached") + + self.save_data() + # terminate the agent MUST BE CALLED + self.logger.log(logging.INFO, "party is terminating:") + super().terminate() + else: + self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data)) + + def getCapabilities(self) -> Capabilities: + """MUST BE IMPLEMENTED + Method to indicate to the protocol what the capabilities of this agent are. + Leave it as is for the ANL 2022 competition + + Returns: + Capabilities: Capabilities representation class + """ + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + def send_action(self, action: Action): + """Sends an action to the opponent(s) + + Args: + action (Action): action of this agent + """ + self.getConnection().send(action) + + # give a description of your agent + def getDescription(self) -> str: + """MUST BE IMPLEMENTED + Returns a description of your agent. 1 or 2 sentences. + + Returns: + str: Agent description + """ + return "Team 16's GahboniTeam agent using adaptive concession based on opponent behavior" + + def opponent_action(self, action): + """Process an action that was received from the opponent. + + Args: + action (Action): action of opponent + """ + # if it is an offer, set the last received bid + if isinstance(action, Offer): + # create opponent model if it was not yet initialised + if self.opponent_model is None: + self.opponent_model = OpponentModel(self.domain) + + bid = cast(Offer, action).getBid() + + # Get our utility for this bid + our_utility = float(self.profile.getUtility(bid)) + + # update opponent model with bid and our utility + self.opponent_model.update(bid, our_utility) + + # Keep track of the best bid the opponent made + if self.best_bid is None or our_utility > float(self.profile.getUtility(self.best_bid)): + self.best_bid = bid + + # Track opponent utility for Gahboninho analysis + opponent_utility = self.opponent_model.get_predicted_utility(bid) + self.opponent_utilities.append(opponent_utility) + + # set bid as last received + self.last_received_bid = bid + + # After receiving at least 5 bids, calculate concession metrics + if len(self.opponent_utilities) >= 5 and not self.probing_phase_complete: + self.update_concession_metrics() + self.probing_phase_complete = True + # Continue updating metrics as we receive more bids + elif len(self.opponent_utilities) >= 5: + self.update_concession_metrics() + + def my_turn(self): + """This method is called when it is our turn. It should decide upon an action + to perform and send this action to the opponent. + """ + # Track timing for deadline management + current_time = time() + if self.last_time is not None: + round_time = current_time - self.last_time + self.round_times.append(round_time) + if len(self.round_times) >= 3: + self.avg_time_per_round = mean(self.round_times[-3:]) + self.last_time = current_time + + # Track round count for probing phase + self.round_count += 1 + + # Load opponent data from previous sessions + if self.other is not None and not self.got_opponent: + self.opponent = wrapper.get_opponent_data(self.parameters.get("storage_dir"), self.other) + # Apply opponent learned parameters using OpponentModel's learn_from_past_sessions + if self.opponent_model is not None and self.opponent.sessions: + # Use the existing learn_from_past_sessions method + self.opponent_model.learn_from_past_sessions(self.opponent.sessions) + # Copy learned parameters to main agent + self.force_accept_at_remaining_turns = self.opponent_model.force_accept_at_remaining_turns + self.force_accept_at_remaining_turns_light = self.opponent_model.force_accept_at_remaining_turns_light + self.top_bids_percentage = self.opponent_model.top_bids_percentage + self.got_opponent = True + + # check if the last received offer is good enough + if self.accept_condition(self.last_received_bid): + # if so, accept the offer + action = Accept(self.me, self.last_received_bid) + self.did_accept = True + self.logger.log(logging.INFO, f"Accepting bid with utility: {float(self.profile.getUtility(self.last_received_bid))}") + else: + # if not, find a bid to propose as counter offer + bid = self.find_bid() + action = Offer(self.me, bid) + self.logger.log(logging.INFO, f"Offering bid with utility: {float(self.profile.getUtility(bid))}") + + # send the action + self.send_action(action) + + def save_data(self): + """This method is called after the negotiation is finished. It can be used to store data + for learning capabilities. Note that no extensive calculations can be done within this method. + Taking too much time might result in your agent being killed, so use it for storage only. + """ + # Save opponent data for future sessions + if self.other is not None and self.opponent is not None: + wrapper.create_and_save_session_data( + opponent=self.opponent, + savepath=self.parameters.get("storage_dir"), + progress=self.progress.get(time() * 1000), + utility_at_finish=self.utility_at_finish, + did_accept=self.did_accept, + opponent_model=self.opponent_model + ) + else: + self.logger.log(logging.INFO, "No opponent data to save (opponent unknown or no model created)") + + self.got_opponent = False + data = "Data for learning (see README.md)" + with open(f"{self.storage_dir}/data.md", "w") as f: + f.write(data) + + def update_concession_metrics(self): + """Update metrics about the opponent's concession behavior + + This implements part of Gahboninho's approach to measure opponent's willingness to concede + based on variance in utilities and concession rate + """ + try: + # Calculate variance in opponent utilities + self.opponent_utility_variance = variance(self.opponent_utilities) + + # Calculate concession rate + # (simple approach: how much did opponent concede on average between consecutive bids) + total_decrease = 0.0 + decreases_count = 0 + + for i in range(1, len(self.opponent_utilities)): + diff = self.opponent_utilities[i-1] - self.opponent_utilities[i] + if diff > 0: # Only count actual concessions + total_decrease += diff + decreases_count += 1 + + self.concession_rate = total_decrease / max(1, decreases_count) + + self.logger.log(logging.INFO, f"Opponent utility variance: {self.opponent_utility_variance}") + self.logger.log(logging.INFO, f"Opponent concession rate: {self.concession_rate}") + + except Exception as e: + self.logger.log(logging.WARNING, f"Error calculating concession metrics: {e}") + + def calculate_target_utility(self, progress: float) -> float: + """Calculate target utility using Gahboninho's formula: + Ut = Umax - (Umax - Umin) * t + + Adjusted based on opponent's concession behavior + """ + # Base values + umax = self.max_target_utility + umin = self.min_target_utility + + # Adjust based on observed concession behavior + # If opponent doesn't concede much (low variance and rate), we should be more willing to concede + if self.opponent_utility_variance < 0.01 or self.concession_rate < 0.02: + # Low concession opponent - be more flexible + concession_factor = 1.1 + umin = max(0.65, umin - 0.05) # Lower our minimum acceptable utility + elif self.opponent_utility_variance > 0.03 or self.concession_rate > 0.05: + # High concession opponent - be more stubborn + concession_factor = 0.8 + umin = min(0.85, umin + 0.05) # Raise our minimum acceptable utility + else: + # Neutral concession behavior + concession_factor = 1.0 + + # Apply Gahboninho's formula with the concession factor + target = umax - (umax - umin) * progress * concession_factor + + # Cap the minimum + return max(target, umin) + + def accept_condition(self, bid: Bid) -> bool: + """Determine whether to accept opponent's bid + + Uses DreamTeam-style dynamic thresholds and Gahboninho's target utility + """ + if bid is None: + return False + + # Get our utility for this bid + utility = float(self.profile.getUtility(bid)) + + # Keep track of the best bid the opponent made so far + if self.best_bid is None or float(self.profile.getUtility(self.best_bid)) < utility: + self.best_bid = bid + + # Progress of the negotiation session between 0 and 1 (1 is deadline) + progress = self.progress.get(time() * 1000) + + # Calculate current target utility using Gahboninho's formula + target_utility = self.calculate_target_utility(progress) + + # DreamTeam-style dynamic thresholds + threshold = 0.98 + light_threshold = 0.95 + if self.avg_time_per_round is not None: + # Calculate thresholds based on average round time and remaining time + threshold = 1 - 1000 * self.force_accept_at_remaining_turns * self.avg_time_per_round / self.progress.getDuration() + light_threshold = 1 - 5000 * self.force_accept_at_remaining_turns_light * self.avg_time_per_round / self.progress.getDuration() + + # Accept conditions + conditions = [ + utility > 0.9, # Very good offer + utility >= target_utility, # Meets or exceeds our target + progress > threshold, # Very close to deadline + progress > light_threshold and utility >= target_utility - 0.05 # Approaching deadline with near-target utility + ] + return any(conditions) + + def find_bid(self) -> Bid: + """Find a bid to offer based on Gahboninho's strategy + + - Uses probing in early negotiation + - Uses target utility formula + - Randomly selects bids above target utility + - Considers opponent's best bid in late stages + """ + # Get current progress + progress = self.progress.get(time() * 1000) + + # During probing phase, use high utility bids + if self.round_count < 5: + # Steadily concede during probing phase from 0.95 to 0.9 + target_utility = max(0.9, 0.95 - self.round_count * 0.01) + else: + # After probing phase, use Gahboninho's formula + target_utility = self.calculate_target_utility(progress) + + # If we're near the deadline, consider using opponent's best bid + light_threshold = 0.95 + if self.avg_time_per_round is not None: + light_threshold = 1 - 5000 * self.force_accept_at_remaining_turns_light * self.avg_time_per_round / self.progress.getDuration() + + if progress > light_threshold and self.best_bid is not None: + best_bid_utility = float(self.profile.getUtility(self.best_bid)) + if best_bid_utility >= target_utility - 0.1: + return self.best_bid + + # Calculate bids with utilities if not done yet + if self.bids_with_utilities is None: + self.bids_with_utilities = [] + for index in range(self.all_bids.size()): + bid = self.all_bids.get(index) + bid_utility = float(self.profile.getUtility(bid)) + self.bids_with_utilities.append((bid, bid_utility)) + + # Sort by utility (highest first) + self.bids_with_utilities.sort(key=lambda tup: tup[1], reverse=True) + + # Gahboninho approach: Find all bids that meet or exceed target utility + eligible_bids = [] + for bid, utility in self.bids_with_utilities: + if utility >= target_utility: + eligible_bids.append((bid, utility)) + # Limit to a reasonable number for efficiency + if len(eligible_bids) >= 100: + break + + # If we have eligible bids, randomly select one + if eligible_bids: + return choice(eligible_bids)[0] + + # If no eligible bids found, fallback to a utility-based approach with expanding range + top_percentage = max(0.01, min(0.2, self.top_bids_percentage + progress * 0.19)) + expanded_top_bids = max(5, floor(self.all_bids.size() * top_percentage)) + next_bid = randint(0, min(expanded_top_bids, len(self.bids_with_utilities)) - 1) + + return self.bids_with_utilities[next_bid][0] \ No newline at end of file diff --git a/agents/group16_agent/utils/__init__.py b/agents/group16_agent/utils/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/agents/group16_agent/utils/__init__.py @@ -0,0 +1 @@ + diff --git a/agents/group16_agent/utils/opponent_model.py b/agents/group16_agent/utils/opponent_model.py new file mode 100644 index 00000000..1f1fee81 --- /dev/null +++ b/agents/group16_agent/utils/opponent_model.py @@ -0,0 +1,280 @@ +from collections import defaultdict +import logging +from typing import Dict, List, Tuple, Optional + +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value + + +class OpponentModel: + def __init__(self, domain: Domain): + self.offers = [] + self.domain = domain + + # Track best bid for us (highest utility for our agent) + self.best_bid_for_us = None + self.best_bid_utility = 0.0 + + # Issue estimators to track opponent preferences + self.issue_estimators = { + i: IssueEstimator(v) for i, v in domain.getIssuesValues().items() + } + + # Track opponent strategy type + self.bid_count = 0 + self.concession_rate = 0.0 + self.repeated_bids = defaultdict(int) + + # learn from previous mistakes + self.force_accept_at_remaining_turns = 1 + self.force_accept_at_remaining_turns_light = 1 + + self.hard_accept_at_turn_X = 1 + self.soft_accept_at_turn_X = 1 + self.top_bids_percentage = 1/300 + + # Track history of opponent utilities to analyze concession + self.opponent_utilities = [] + + def update(self, bid: Bid, our_utility: float = None): + """Update the opponent model with a new bid + + Args: + bid (Bid): New bid from opponent + our_utility (float, optional): Our utility for this bid + """ + # Track all received bids + self.offers.append(bid) + self.bid_count += 1 + + # Count repeated bids (use string representation for hashability) + bid_str = str(bid) + self.repeated_bids[bid_str] += 1 + + # Update best bid for us if applicable + if our_utility is not None and (self.best_bid_for_us is None or our_utility > self.best_bid_utility): + self.best_bid_for_us = bid + self.best_bid_utility = our_utility + + # Update all issue estimators + for issue_id, issue_estimator in self.issue_estimators.items(): + issue_estimator.update(bid.getValue(issue_id)) + + # Calculate opponent utility and track it + opponent_utility = self.get_predicted_utility(bid) + self.opponent_utilities.append(opponent_utility) + + # Update concession rate if we have at least 2 bids + if len(self.opponent_utilities) >= 2: + # Simple concession rate - average decrease in utility between consecutive bids + decreases = [] + for i in range(1, len(self.opponent_utilities)): + diff = self.opponent_utilities[i-1] - self.opponent_utilities[i] + if diff > 0: # Only count decreases in utility (actual concessions) + decreases.append(diff) + + if decreases: + self.concession_rate = sum(decreases) / len(decreases) + + def get_predicted_utility(self, bid: Bid) -> float: + """Predict the opponent's utility for a bid + + Args: + bid (Bid): Bid to evaluate + + Returns: + float: Predicted utility between 0 and 1 + """ + if len(self.offers) == 0 or bid is None: + return 0 + + # initiate + total_issue_weight = 0.0 + value_utilities = [] + issue_weights = [] + + for issue_id, issue_estimator in self.issue_estimators.items(): + # get the value that is set for this issue in the bid + value: Value = bid.getValue(issue_id) + + # collect both the predicted weight for the issue and + # predicted utility of the value within this issue + value_utilities.append(issue_estimator.get_value_utility(value)) + issue_weights.append(issue_estimator.weight) + + total_issue_weight += issue_estimator.weight + + # normalise the issue weights such that the sum is 1.0 + if total_issue_weight == 0.0: + issue_weights = [1 / len(issue_weights) for _ in issue_weights] + else: + issue_weights = [iw / total_issue_weight for iw in issue_weights] + + # calculate predicted utility by multiplying all value utilities with their issue weight + predicted_utility = sum( + [iw * vu for iw, vu in zip(issue_weights, value_utilities)] + ) + + return predicted_utility + + def get_opponent_type(self) -> str: + """Identify opponent negotiation strategy type + + Returns: + str: Strategy type (HARDHEADED, CONCEDER, or UNKNOWN) + """ + if self.bid_count < 3: + return "UNKNOWN" + + # Check for hardheaded opponent (low concession rate, many repeated bids) + unique_bids = len(self.repeated_bids) + bid_repetition_ratio = unique_bids / self.bid_count + + if self.concession_rate < 0.02 or bid_repetition_ratio < 0.5: + return "HARDHEADED" + elif self.concession_rate > 0.05: + return "CONCEDER" + else: + return "NEUTRAL" + + def get_concession_rate(self) -> float: + """Get the opponent's concession rate + + Returns: + float: Concession rate (higher means more concessions) + """ + return self.concession_rate + + def get_top_issues(self, n: int = 3) -> List[Tuple[str, float]]: + """Get the top n most important issues for the opponent + + Args: + n (int, optional): Number of issues to return. Defaults to 3. + + Returns: + List[Tuple[str, float]]: List of (issue_id, weight) tuples + """ + # Get normalized issue weights + total_weight = sum(est.weight for est in self.issue_estimators.values()) + + if total_weight == 0: + # Equal weights if no data yet + equal_weight = 1.0 / len(self.issue_estimators) + issues = [(issue_id, equal_weight) for issue_id in self.issue_estimators.keys()] + else: + # Normalize weights + issues = [(issue_id, est.weight / total_weight) + for issue_id, est in self.issue_estimators.items()] + + # Sort by weight and return top n + issues.sort(key=lambda x: x[1], reverse=True) + return issues[:n] + + # TODO: Add methods to save/load opponent data for persistent learning + + def learn_from_past_sessions(self, sessions: list): + hard_accept_levels = [0, 0, 1, 1.1] + soft_accept_levels = [0, 1, 1.1] + top_bids_levels = [1 / 300, 1 / 100, 1 / 30] + + # fully failed + failed_sessions_count = 0 + for session in sessions: + # Check if session failed (utility == 0) + if isinstance(session, dict) and session.get("utilityAtFinish", 1) == 0: + failed_sessions_count += 1 + + # low utility + low_utility_sessions_count = 0 + for session in sessions: + # Check if session had low utility (utility < 0.5) + if isinstance(session, dict) and session.get("utilityAtFinish", 1) < 0.5: + low_utility_sessions_count += 1 + + # hard accept based on previous failed sessions + if failed_sessions_count >= len(hard_accept_levels): + accept_index = len(hard_accept_levels) - 1 + else: + accept_index = failed_sessions_count + #self.hard_accept_at_turn_X = hard_accept_levels[accept_index] + self.force_accept_at_remaining_turns = hard_accept_levels[accept_index] + + # soft + if failed_sessions_count >= len(soft_accept_levels): + light_accept_index = len(soft_accept_levels) - 1 + else: + light_accept_index = failed_sessions_count + #self.soft_accept_at_turn_X = soft_accept_levels[light_accept_index] + self.force_accept_at_remaining_turns_light = soft_accept_levels[light_accept_index] + + # Set top_bids_percentage based on low utility sessions + if low_utility_sessions_count >= len(top_bids_levels): + top_bids_index = len(top_bids_levels) - 1 + else: + top_bids_index = low_utility_sessions_count + self.top_bids_percentage = top_bids_levels[top_bids_index] + +class IssueEstimator: + def __init__(self, value_set: DiscreteValueSet): + if not isinstance(value_set, DiscreteValueSet): + raise TypeError( + "This issue estimator only supports issues with discrete values" + ) + + self.bids_received = 0 + self.max_value_count = 0 + self.num_values = value_set.size() + self.value_trackers = defaultdict(ValueEstimator) + self.weight = 0 + + def update(self, value: Value): + self.bids_received += 1 + + # get the value tracker of the value that is offered + value_tracker = self.value_trackers[value] + + # register that this value was offered + value_tracker.update() + + # update the count of the most common offered value + self.max_value_count = max([value_tracker.count, self.max_value_count]) + + # update predicted issue weight + # the intuition here is that if the values of the receiverd offers spread out over all + # possible values, then this issue is likely not important to the opponent (weight == 0.0). + # If all received offers proposed the same value for this issue, + # then the predicted issue weight == 1.0 + equal_shares = self.bids_received / self.num_values + self.weight = (self.max_value_count - equal_shares) / ( + self.bids_received - equal_shares + ) + + # recalculate all value utilities + for value_tracker in self.value_trackers.values(): + value_tracker.recalculate_utility(self.max_value_count, self.weight) + + def get_value_utility(self, value: Value): + if value in self.value_trackers: + return self.value_trackers[value].utility + + return 0 + + +class ValueEstimator: + def __init__(self): + self.count = 0 + self.utility = 0 + + def update(self): + self.count += 1 + + def recalculate_utility(self, max_value_count: int, weight: float): + if weight < 1: + mod_value_count = ((self.count + 1) ** (1 - weight)) - 1 + mod_max_value_count = ((max_value_count + 1) ** (1 - weight)) - 1 + + self.utility = mod_value_count / mod_max_value_count + else: + self.utility = 1 diff --git a/agents/group16_agent/utils/wrapper.py b/agents/group16_agent/utils/wrapper.py new file mode 100644 index 00000000..4818be8e --- /dev/null +++ b/agents/group16_agent/utils/wrapper.py @@ -0,0 +1,101 @@ +# python objects to store opponent information +import pandas as pd +import os + + +class Opponent: + ''' + Used to store opponent information that we would like to stay persistent across sessions/encounters + ''' + def __init__(self, result=0, finalUtility=0, offerVariance=[], name="", sessions=None): + self.result = result + self.finalUtility = finalUtility + self.offerVariance = offerVariance + self.name = name + self.sessions = sessions if sessions is not None else [] + + def add_session(self, session_data): + """Add a new session data entry to the sessions list""" + if self.sessions is None: + self.sessions = [] + self.sessions.append(session_data) + + def save(self, savepath): + save_opponent_data(savepath, self) + + def normalize(self): + # not the behaviour, will take all the raw data and make it into statistics + return self.result + + +def get_opponent_data(savepath, name): + file_path = savepath + name+".plk" + if not os.path.exists(savepath): + os.makedirs(savepath) + try: + opponent = pd.read_pickle(file_path) + if not isinstance(opponent, Opponent): + raise ValueError("Deserialized object is not of type Opponent") + opponent.normalize() + except (FileNotFoundError, ValueError, Exception): + print(f"File not found or invalid data. Creating default Opponent for {name}. at path: ", savepath) + opponent = Opponent(name=name) + pd.to_pickle(opponent, file_path) + + return opponent + + +def save_opponent_data(savepath, opponent): + if(opponent == None): + print("we have a problem opponent is None") + return + if isinstance(opponent, Opponent): + print(f"we are saving {opponent.name}") + file_path = savepath + opponent.name + ".plk" + if not os.path.exists(savepath): + os.makedirs(savepath) + pd.to_pickle(opponent, file_path) + else: + print("Non opponent saved") + + +def create_and_save_session_data(opponent, savepath, progress, utility_at_finish, did_accept, opponent_model=None): + """Create and save session data for an opponent + + Args: + opponent: The Opponent object to update + savepath: Path to save the opponent data + progress: Current negotiation progress + utility_at_finish: Utility of the final agreement (0 if no agreement) + did_accept: Whether we accepted the opponent's offer + opponent_model: Optional OpponentModel to get current parameters from + """ + if opponent is None: + print("Cannot save session data: opponent is None") + return + + # Get default values + min_util = 0.7 # Threshold for a good deal + top_bids_percentage = 1/300 + force_accept_at_remaining_turns = 1 + + # Get values from opponent model if available + if opponent_model: + top_bids_percentage = getattr(opponent_model, 'top_bids_percentage', top_bids_percentage) + force_accept_at_remaining_turns = getattr(opponent_model, 'force_accept_at_remaining_turns', force_accept_at_remaining_turns) + + # Create session data dictionary + session_data = { + "progressAtFinish": progress, + "utilityAtFinish": utility_at_finish, + "didAccept": did_accept, + "isGood": utility_at_finish >= min_util, + "topBidsPercentage": top_bids_percentage, + "forceAcceptAtRemainingTurns": force_accept_at_remaining_turns + } + + # Add session data to opponent + opponent.add_session(session_data) + + # Save opponent data + save_opponent_data(savepath, opponent) diff --git a/run.py b/run.py index 5a294c95..adeed6a6 100644 --- a/run.py +++ b/run.py @@ -17,13 +17,21 @@ # You need to specify a time deadline (is milliseconds (ms)) we are allowed to negotiate before we end without agreement settings = { "agents": [ + #{ + # "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", + # "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, + #}, { - "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", - "parameters": {"storage_dir": "agent_storage/DreamTeam109Agent"}, + "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, }, + # { + # "class": "agents.template_agent.template_agent.TemplateAgent", + # "parameters": {"storage_dir": "agent_storage/TemplateAgent"}, + # }, { - "class": "agents.template_agent.template_agent.TemplateAgent", - "parameters": {"storage_dir": "agent_storage/TemplateAgent"}, + "class": "agents.group16_agent.group16_agent.Group16Agent", + "parameters": {"storage_dir": "agent_storage/Group16Agent"}, }, ], "profiles": ["domains/domain00/profileA.json", "domains/domain00/profileB.json"], diff --git a/run_tournament.py b/run_tournament.py index 41dd7242..f30ec794 100644 --- a/run_tournament.py +++ b/run_tournament.py @@ -18,33 +18,37 @@ tournament_settings = { "agents": [ { - "class": "agents.template_agent.template_agent.TemplateAgent", - "parameters": {"storage_dir": "agent_storage/TemplateAgent"}, - }, - { - "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", - }, - { - "class": "agents.conceder_agent.conceder_agent.ConcederAgent", - }, - { - "class": "agents.hardliner_agent.hardliner_agent.HardlinerAgent", - }, - { - "class": "agents.linear_agent.linear_agent.LinearAgent", - }, - { - "class": "agents.random_agent.random_agent.RandomAgent", - }, - { - "class": "agents.stupid_agent.stupid_agent.StupidAgent", - }, - { - "class": "agents.CSE3210.agent2.agent2.Agent2", - }, - { - "class": "agents.CSE3210.agent3.agent3.Agent3", - }, + "class": "agents.group16_agent.group16_agent.Group16Agent", + "parameters": {"storage_dir": "agent_storage/Group16Agent"}, + }, + { + "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, + }, + # { + # "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", + # }, + # { + # "class": "agents.conceder_agent.conceder_agent.ConcederAgent", + # }, + # { + # "class": "agents.hardliner_agent.hardliner_agent.HardlinerAgent", + # }, + # { + # "class": "agents.linear_agent.linear_agent.LinearAgent", + # }, + # { + # "class": "agents.random_agent.random_agent.RandomAgent", + # }, + # { + # "class": "agents.stupid_agent.stupid_agent.StupidAgent", + # }, + # { + # "class": "agents.CSE3210.agent2.agent2.Agent2", + # }, + # { + # "class": "agents.CSE3210.agent3.agent3.Agent3", + # }, { "class": "agents.CSE3210.agent7.agent7.Agent7", }, @@ -57,63 +61,63 @@ { "class": "agents.CSE3210.agent18.agent18.Agent18", }, - { - "class": "agents.CSE3210.agent19.agent19.Agent19", - }, - { - "class": "agents.CSE3210.agent22.agent22.Agent22", - }, - { - "class": "agents.CSE3210.agent24.agent24.Agent24", - }, - { - "class": "agents.CSE3210.agent25.agent25.Agent25", - }, - { - "class": "agents.CSE3210.agent26.agent26.Agent26", - }, - { - "class": "agents.CSE3210.agent27.agent27.Agent27", - }, - { - "class": "agents.CSE3210.agent29.agent29.Agent29", - }, - { - "class": "agents.CSE3210.agent32.agent32.Agent32", - }, - { - "class": "agents.CSE3210.agent33.agent33.Agent33", - }, - { - "class": "agents.CSE3210.agent41.agent41.Agent41", - }, - { - "class": "agents.CSE3210.agent43.agent43.Agent43", - }, - { - "class": "agents.CSE3210.agent50.agent50.Agent50", - }, - { - "class": "agents.CSE3210.agent52.agent52.Agent52", - }, - { - "class": "agents.CSE3210.agent55.agent55.Agent55", - }, - { - "class": "agents.CSE3210.agent58.agent58.Agent58", - }, - { - "class": "agents.CSE3210.agent61.agent61.Agent61", - }, - { - "class": "agents.CSE3210.agent64.agent64.Agent64", - }, - { - "class": "agents.CSE3210.agent67.agent67.Agent67", - }, - { - "class": "agents.CSE3210.agent68.agent68.Agent68", - }, + # { + # "class": "agents.CSE3210.agent19.agent19.Agent19", + # }, + # { + # "class": "agents.CSE3210.agent22.agent22.Agent22", + # }, + # { + # "class": "agents.CSE3210.agent24.agent24.Agent24", + # }, + # { + # "class": "agents.CSE3210.agent25.agent25.Agent25", + # }, + # { + # "class": "agents.CSE3210.agent26.agent26.Agent26", + # }, + # { + # "class": "agents.CSE3210.agent27.agent27.Agent27", + # }, + # { + # "class": "agents.CSE3210.agent29.agent29.Agent29", + # }, + # { + # "class": "agents.CSE3210.agent32.agent32.Agent32", + # }, + # { + # "class": "agents.CSE3210.agent33.agent33.Agent33", + # }, + # { + # "class": "agents.CSE3210.agent41.agent41.Agent41", + # }, + # { + # "class": "agents.CSE3210.agent43.agent43.Agent43", + # }, + # { + # "class": "agents.CSE3210.agent50.agent50.Agent50", + # }, + # { + # "class": "agents.CSE3210.agent52.agent52.Agent52", + # }, + # { + # "class": "agents.CSE3210.agent55.agent55.Agent55", + # }, + # { + # "class": "agents.CSE3210.agent58.agent58.Agent58", + # }, + # { + # "class": "agents.CSE3210.agent61.agent61.Agent61", + # }, + # { + # "class": "agents.CSE3210.agent64.agent64.Agent64", + # }, + # { + # "class": "agents.CSE3210.agent67.agent67.Agent67", + # }, + # { + # "class": "agents.CSE3210.agent68.agent68.Agent68", + # }, ], "profile_sets": [ ["domains/domain00/profileA.json", "domains/domain00/profileB.json"],