package bargainingchips.players; import static java.lang.Math.pow; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.BlockingQueue; import bargainingchips.Bundle; import bargainingchips.NegotiationContext; import bargainingchips.OutcomeSpace; import bargainingchips.actions.Bid; import bargainingchips.actions.FinalAccept; import bargainingchips.actions.Offer; import bargainingchips.actions.OfferBy; import bargainingchips.actions.OfferType; import bargainingchips.actions.PreAccept; import bargainingchips.analysis.BundleUtilityPoint; import bargainingchips.analysis.BundleUtilitySpace; import bargainingchips.messaging.coordination.CoordinationMessage; import bargainingchips.messaging.status.StatusMessage; import bargainingchips.outcomes.Outcome; import bargainingchips.players.Agent; import bargainingchips.utilityfunctions.UtilityFunction; import bargainingchips.players.history.BidEvent; import bargainingchips.players.history.BidEventHistory; import bargainingchips.players.history.BidEventHistoryKeeper; //import agents.bayesianopponentmodel.BayesianOpponentModelScalable; //import agents.bayesianopponentmodel.OpponentModelUtilSpace; //import bargainingchips.players.TitForTatNegotiationAgent.BayesianOpponentModelScalable; /** * @version 1 * * This TitForTatNegotiatingAgent adapts {@link /NegotiatorGUI/src/main/java/agents/anac/y2011/Nice_Tit_for_Tat} [1], * according to Bargaining Chips definitions. Tit-for-Tat strategy has been successful in reaching Nash equilibrium, * a pessimistic solution (not the optimistic Pareto solution) in repeated infinite Prisoners' Dilemma games assuming * the definition given for `rationality'. Although, not all humans and also all rational humans behaves under that * definition for rationality, but this class is as a test base skeleton for other agents which uses Tit-For-Tat. * * [1] T. Baarslag, K. Hindriks, C. Jonker, A tit for tat negotiation strategy for real-time bilateral negotiations, * Complex Automated Negotiations, 229-233 (2013). * * * @author Faria Nassiri-Mofakham * */ public abstract class NiceTitForTatNegotiationAgent extends HistoryAgent { private static final double TIME_USED_TO_DETERMINE_OPPONENT_STARTING_POINT = 0.01; private Offer lastReceivedOffer = null; /** * Indicates the number of times we have offered the best bid by the * opponent so far (which means we are very close to the deadline and wanted * to accept) */ private int offeredOpponentBestBid = 0; private long DOMAINSIZE; private List space; //private BayesianOpponentModelScalable opponentModel; private double myNashUtility; private double initialGap; private final boolean TEST_EQUIVALENCE = false; private Random random100; public NiceTitForTatNegotiationAgent(String name, UtilityFunction u, NegotiationContext nc, BlockingQueue in, BlockingQueue out, BlockingQueue cin, BlockingQueue cout) { super(name, u, nc, in, out, cin, cout); } @Override public void init() { super.init(); space = outcomeSpace.getAllBids(); //getNumberOfPossibleBids(); //prepareOpponentModel(); DOMAINSIZE = space.size(); if (TEST_EQUIVALENCE) { random100 = new Random(100); } else { random100 = new Random(); } log("Domain size: " + DOMAINSIZE); } //** protected void prepareOpponentModel() { // opponentModel = new BayesianOpponentModelScalable( // for OPPONENT MODEL // (AdditiveUtilitySpace) utilitySpace); } @Override // public Bundle chooseCounterBid() { protected Bundle getNextBid() { // OutcomeSpace outcomeSpace = negotiationContext.getOutcomeSpace(); // return outcomeSpace.getBidNearUtility(getTargetUtility(), u, this); // Bundle opponentLastBid = lastReceivedOffer; Bundle opponentLastBid = getOpponentLastBid(); double time = getTime(); log("---------- t = " + time + "----------\n"); // // If we have time, we receiveMessage the opponent model // if (canUpdateBeliefs(time)) { // for OPPONENT MODEL // updateBeliefs(opponentLastBid); // updateMyNashUtility(); // } double myUtilityOfOpponentLastBid = getUtility(opponentLastBid); double maximumOfferedUtilityByOpponent = opponentHistory .getMaximumUtility(); double minimumOfferedUtilityByOpponent = opponentHistory .getMinimumUtility(); double minUtilityOfOpponentFirstBids = getMinimumUtilityOfOpponentFirstBids( myUtilityOfOpponentLastBid); double opponentConcession = maximumOfferedUtilityByOpponent - minUtilityOfOpponentFirstBids; /** * Measures how far away the opponent is from myNashUtility. 0 = * farthest, 1 = closest */ double opponentConcedeFactor = Math.min(1, opponentConcession / (myNashUtility - minUtilityOfOpponentFirstBids)); double myConcession = opponentConcedeFactor * (1 - myNashUtility); double myCurrentTargetUtility = 1 - myConcession; log("Min/Max utility offered by opponent was " + round2(minimumOfferedUtilityByOpponent) + "/" + round2(maximumOfferedUtilityByOpponent) + ". (Right now, he offers me " + round2(myUtilityOfOpponentLastBid) + ")\n" + "The minimum of his first few bids is " + round2(minUtilityOfOpponentFirstBids) + " so his concession is " + round2(opponentConcession) + ", which is at " + percentage(opponentConcedeFactor) + "%\n" + "So I concede the same factor and my concession is " + round2(myConcession) + ". Therefore, my current target util is " + round2(myCurrentTargetUtility)); initialGap = 1 - minUtilityOfOpponentFirstBids; double gapToNash = Math.max(0, myCurrentTargetUtility - myNashUtility); double bonus = getBonus(); double tit = bonus * gapToNash; myCurrentTargetUtility -= tit; log("The gap to Nash is then " + round2(gapToNash) + ". I will add another bonus of " + round2(tit) + " (=" + percentage(bonus) + "%)" + " to have a target util of " + myCurrentTargetUtility); List myBids = getBidsOfUtility(myCurrentTargetUtility); Bundle myBid = getBestBidForOpponent(myBids); myBid = makeAppropriate(myBid); log(" so I choose a bid with util " + getUtility(myBid)); return myBid; } /** * Decides if we have enough time to receiveMessage opponent model and Nash * point. On large domains, this may take 3 seconds. * * @param time */ private boolean canUpdateBeliefs(double time) { // in the last seconds we don't want to lose any time if (time > 0.99) return false; // in a big domain, we stop updating half-way if (isDomainBig()) if (time > 0.5) return false; return true; } /** * Returns a small bonus multiplier to our target utility if we have to be * fast. * * discount = 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 bonus = 0.5 0.3 * 0.1 * * Normal domains: time = 0.91 to 0.96 bonus = 0.0 to 1.0 * * Big domains: time = 0.85 to 0.9 bonus = 0.0 to 1.0 */ private double getBonus() { double discountFactor = outcomeSpace.getDiscountFactor(); double discountBonus = 0.5 - 0.4 * discountFactor; if (discountFactor < 1) log("Discount = " + discountFactor + ", so we set discount bonus to " + percentage(discountBonus) + "%."); boolean isBigDomain = DOMAINSIZE > 3000; double timeBonus = 0; double time = getTime(); double minTime = 0.91; if (isBigDomain) minTime = 0.85; if (time > minTime) { if (isBigDomain) log("We have a big domain of size " + DOMAINSIZE + ", so we start the time bonus from t = " + minTime); timeBonus = Math.min(1, 20 * (time - minTime)); log("t = " + round2(time) + ", so we set time bonus to " + percentage(timeBonus) + "%."); } double bonus = Math.max(discountBonus, timeBonus); if (bonus < 0) bonus = 0; if (bonus > 1) bonus = 1; return bonus; } private void updateMyNashUtility() throws Exception { BundleUtilitySpace bs=null; try { bs = new BundleUtilitySpace(outcomeSpace,outcomeSpace, true, false); } catch (Exception e1) { e1.printStackTrace(); }//null; myNashUtility = 0.7; try { double nashMultiplier = getNashMultiplier(initialGap); // if (DOMAINSIZE < 200000) { // by OPPONENT MODEL // bs = new BundleUtilitySpace(outcomeSpace, // new OpponentModelUtilSpace(opponentModel), true, false); BundleUtilityPoint nash = bs.getNash(); if (nash != null && nash.getUtilityA() != null) myNashUtility = nash.getUtilityA(); myNashUtility *= nashMultiplier; // don't ask for too much or too little if (myNashUtility > 1) myNashUtility = 1; if (myNashUtility < 0.5) myNashUtility = 0.5; } catch (Exception e) { e.printStackTrace(); } } /** * If the gap is big (e.g. 0.8) the multiplier is smaller than 1 (e.g. 0.92) * If the gap is small (e.g. 0.2) the multiplier is bigger than 1 (e.g. * 1.28) */ private double getNashMultiplier(double gap) { double mult = 1.4 - 0.6 * gap; if (mult < 0) mult = 0; return mult; } private void updateBeliefs(Bundle opponentLastBid) { // try { // by OPPONENT MODEL // opponentModel.updateBeliefs(opponentLastBid); // } catch (Exception e) { // e.printStackTrace(); // } } /** * Gives us our utility of the starting point of our opponent. This is used * as a reference whether the opponent concedes or not. */ private double getMinimumUtilityOfOpponentFirstBids( double myUtilityOfOpponentLastBid) { BidEventHistory firstBids = opponentHistory.filterBetweenTime(0, TIME_USED_TO_DETERMINE_OPPONENT_STARTING_POINT); double firstBidsMinUtility; if (firstBids.size() == 0) firstBidsMinUtility = opponentHistory.getFirstBidEvent() .getMyUtility(); else firstBidsMinUtility = firstBids.getMinimumUtility(); return firstBidsMinUtility; } /** * Prevents the agent from overshooting. Replaces the planned bid (i.e, {@link Bundle}) by * something more appropriate if there was a bid by the opponent that is * better than our planned bid. */ private Bundle makeAppropriate(Bundle myPlannedBid) { Bundle bestBidByOpponent = opponentHistory.getBestBid(); // Bid by opponent is better than our planned bid double bestUtilityByOpponent = getUtility(bestBidByOpponent); double myPlannedUtility = getUtility(myPlannedBid); if (bestUtilityByOpponent >= myPlannedUtility) { log("Opponent previously made a better offer to me than my planned util " + myPlannedUtility + ", namely " + bestUtilityByOpponent + ", so I make that bid instead."); return bestBidByOpponent; } return myPlannedBid; } @Override public Bundle chooseOpeningBid() { try { return outcomeSpace.getMaxBidPossible(u); } catch (Exception e) { e.printStackTrace(); return outcomeSpace.getRandom(random100); // get a bid in random } } @Override public Bundle chooseFirstCounterBid() { if (TEST_EQUIVALENCE) { // try { // opponentModel.updateBeliefs( // using OPPONENT MODEL // opponentHistory.getLastBidDetails().getBid()); // } catch (Exception e) { // e.printStackTrace(); // } } return chooseOpeningBid(); } @Override public boolean isAcceptable(Bundle plannedBid) { double time = getTime(); Bundle opponentBid = getOpponentLastBid(); Bundle myNextBid = plannedBid; if (utility(opponentBid) >= utility(myNextBid)) return true; if (time < 0.98) return false; double offeredUtility = opponentHistory.getLastBidEvent() .getMyUtility(); double now = time; double timeLeft = 1 - now; log("<======= new AC ========= (t = " + round3(now) + ", remaining: " + round3(timeLeft) + ")"); // if we will still see a lot of bids (more than enoughBidsToCome), we // do not accept BidEventHistory recentBids = opponentHistory .filterBetweenTime(now - timeLeft, now); int recentBidsSize = recentBids.size(); int enoughBidsToCome = 10; if (isDomainBig()) enoughBidsToCome = 40; if (recentBidsSize > enoughBidsToCome) { log("I expect to see " + recentBidsSize + " more bids. I will only consider accepting when it falls below " + enoughBidsToCome); return false; } // v2.0 double window = timeLeft; // double window = 5 * timeLeft; BidEventHistory recentBetterBids = opponentHistory.filterBetween( offeredUtility, 1, now - window, now); int n = recentBetterBids.size(); double p = timeLeft / window; if (p > 1) p = 1; double pAllMiss = Math.pow(1 - p, n); if (n == 0) pAllMiss = 1; double pAtLeastOneHit = 1 - pAllMiss; double avg = recentBetterBids.getAverageUtility(); double expectedUtilOfWaitingForABetterBid = pAtLeastOneHit * avg; log("In the previous time window of size " + window + " I have seen " + n + " better offers (out of a total of " + recentBidsSize + ") than " + round2(offeredUtility) + ", with avg util " + round2(avg)); log("p = " + p + ", so pAtLeastOneHit = " + pAtLeastOneHit); log("expectedUtilOfWaitingForABetterBid = " + round2(expectedUtilOfWaitingForABetterBid)); log("Acceptable? " + (offeredUtility > expectedUtilOfWaitingForABetterBid)); log("========================>\n"); if (offeredUtility > expectedUtilOfWaitingForABetterBid) return true; return false; } @Override protected Offer sendOffer() { Offer myAction = null; // Nothing received yet if (lastReceivedOffer == null) { Bundle myBid = chooseOpeningBid(); //getNextBid(); remember(new Offer(myBid,OfferType.BID)); return new Bid(myBid); } // Our last bid got accepted. We are also accepting (and we should notify the coordinator). if (lastReceivedOffer.isPreAccept()) return new FinalAccept(lastReceivedOffer); Bundle lastReceivedBid = lastReceivedOffer.getBundle(); // Accept if (isAcceptable(lastReceivedBid)) return new PreAccept(lastReceivedOffer); // Counter offer based actions else { // we are the second if (getMyLastBid() == null) { Bundle firstCounterBid = chooseFirstCounterBid(); // Check to see if we want to accept the first offer if (isAcceptable(firstCounterBid)) { myAction = makeAcceptAction(); return myAction; } else { myAction = new Offer(firstCounterBid,OfferType.BID); remember(myAction); return new Bid(firstCounterBid); } } // We make a normal counter-offer Bundle counterBid = chooseCounterBid(); // Check to see if we want to accept if (isAcceptable(counterBid)) { myAction = makeAcceptAction(); return myAction; } else { myAction = new Offer(counterBid, OfferType.BID); remember(myAction); return new Bid(counterBid); } // if (myAction == null) // { // need this code to check equivalence // myAction = new Offer(counterBid, OfferType.BID); // // remember(myAction); // return new Bid(counterBid); // } } //return new Bid(chooseCounterBid()); } @SuppressWarnings("unused") @Override public Offer makeAcceptAction() { log("We are in the final phase because the AC accepted. We have offered the best bid of our opponent " + offeredOpponentBestBid + " time(s)."); // However, if it wants to accept and we have some time left, we try to // replace it with the best offer so far Bundle opponentBid = getOpponentLastBid(); double offeredUtility = utility(opponentBid); double now = getTime(); double timeLeft = 1 - now; BidEventHistory recentBids = opponentHistory .filterBetweenTime(now - timeLeft, now); int expectedBids = recentBids.size(); Bundle bestBid = opponentHistory.getBestBid(); double bestBidUtility = utility(bestBid); log("We expect to see " + expectedBids + " more bids. The AC wants to accept the offer of utility " + round2(offeredUtility) + " (max offered = " + round2(bestBidUtility) + ")."); if (TEST_EQUIVALENCE) { // we expect to see more bids, that are strictly better than what we // are about to offer if (!(expectedBids > 1 && bestBidUtility > offeredUtility)) { return new PreAccept(new Offer(opponentBid,OfferType.BID)); } } else { // we expect to see more bids, that are strictly better than what we // are about to offer if (expectedBids > 1 && bestBidUtility > offeredUtility && offeredOpponentBestBid <= 3) { log("Which is not enough, so we will not accept now. Instead, we offer the opponent's best bid so far, which gives us utility " + utility(bestBid)); offeredOpponentBestBid++; return new Bid(bestBid); } else { log("Therefore, we will accept now."); return new PreAccept(new Offer(opponentBid,OfferType.BID)); } } return null; } private double utility(Bundle b) { try { return u.getUtility(b); } catch (Exception e) { e.printStackTrace(); return 0; } } protected static void log(String s) { } /** * Rounds to two decimals */ public static double round2(double x) { return Math.round(100 * x) / 100d; } /** * Rounds to 3 decimals */ public static double round3(double x) { return Math.round(1000 * x) / 1000d; } public static double percentage(double x) { return Math.round(1000 * x) / 10d; } /** * Get all bids in a utility range. */ private List getBidsOfUtility(double lowerBound, double upperBound) { // In big domains, we need to find only this amount of bids around the // target. Should be at least 2. final int limit = 2; List bidsInRange = new ArrayList(); for (Bundle b : space) try { double util = u.getUtility(b); if (util >= lowerBound && util <= upperBound) bidsInRange.add(b); // In big domains, break early if (isDomainBig() && bidsInRange.size() >= limit) return bidsInRange; } catch (Exception e) { e.printStackTrace(); } return bidsInRange; } /** * Get all bids in a utility range. */ private List getBidsOfUtility(double target) { if (target > 1) target = 1; double min = target * 0.98; double max = target + 0.04; do { max += 0.01; List bids = getBidsOfUtility(min, max); int size = bids.size(); log(size + " bids found in [" + round2(min) + ", " + round2(max) + "]"); // We need at least 2 bids. Or if max = 1, then we settle for 1 bid. if (size > 1 || (max >= 1 && size > 0)) { log(size + " bids of target utility " + round2(target) + " found in [" + round2(min) + ", " + round2(max) + "]"); return bids; } } while (max <= 1); // Weird if this happens ArrayList best = new ArrayList(); best.add(chooseOpeningBid()); return best; } private boolean isDomainBig() { return DOMAINSIZE > 10000; } private Bundle getBestBidForOpponent(List bids) { // We first make a bid history for the opponent, then pick the best one. BidEventHistory possibleBidHistory = new BidEventHistory(); for (Bundle b : bids) { double utility; try { utility =0; //utility = opponentModel.getNormalizedUtility(b); BidEvent bidEvent = new BidEvent(b, utility, 0); possibleBidHistory.add(bidEvent); } catch (Exception e) { e.printStackTrace(); } } // Pick the top 3 to 20 bids, depending on the domain size int n = (int) Math.round(bids.size() / 10.0); if (n < 3) n = 3; if (n > 20) n = 20; BidEventHistory bestN = possibleBidHistory.getBestBidHistory(n); BidEvent randomBestN = bestN.getRandom(random100); log("Random bid chosen out of the top " + n + " of " + bids.size() + " bids, with opp util: " + round2(randomBestN.getMyUtility())); return randomBestN.getBid(); } // @Override // public SupportedNegotiationSetting getSupportedNegotiationSetting() { // return SupportedNegotiationSetting.getLinearUtilitySpaceInstance(); // } @Override public String toDescription() { return name + " with u = " + u + " using " + this.getClass().getSimpleName() + " strategy "; } }