package geniusweb.protocol.session.shaop; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import geniusweb.actions.Accept; import geniusweb.actions.Action; import geniusweb.actions.Comparison; import geniusweb.actions.ElicitComparison; import geniusweb.actions.EndNegotiation; import geniusweb.actions.Offer; import geniusweb.actions.PartyId; import geniusweb.inform.Agreements; import geniusweb.issuevalue.Bid; import geniusweb.progress.Progress; import geniusweb.progress.ProgressRounds; import geniusweb.protocol.ProtocolException; import geniusweb.protocol.partyconnection.ProtocolToPartyConn; import geniusweb.protocol.partyconnection.ProtocolToPartyConnections; import geniusweb.references.PartyWithProfile; /** * Immutable. */ public class SHAOPState extends BareSHAOPState { public static final double DEFAULT_ELICITATATION_COST = 0.01d; /** * see {@link BareSHAOPState}. */ /** * * @param actions see * {@link BareSHAOPState#BareSHAOPState(List, ProtocolToPartyConnections, Progress, SHAOPSettings, ProtocolException, int, Map, Map)}. * @param conns see * {@link BareSHAOPState#BareSHAOPState(List, ProtocolToPartyConnections, Progress, SHAOPSettings, ProtocolException, int, Map, Map)}. * @param progress see * {@link BareSHAOPState#BareSHAOPState(List, ProtocolToPartyConnections, Progress, SHAOPSettings, ProtocolException, int, Map, Map)}. * @param settings see * {@link BareSHAOPState#BareSHAOPState(List, ProtocolToPartyConnections, Progress, SHAOPSettings, ProtocolException, int, Map, Map)}. * @param e see * {@link BareSHAOPState#BareSHAOPState(List, ProtocolToPartyConnections, Progress, SHAOPSettings, ProtocolException, int, Map, Map)}. * @param teamNr see * {@link BareSHAOPState#BareSHAOPState(List, ProtocolToPartyConnections, Progress, SHAOPSettings, ProtocolException, int, Map, Map)}. * @param partytNumbers see * {@link BareSHAOPState#BareSHAOPState(List, ProtocolToPartyConnections, Progress, SHAOPSettings, ProtocolException, int, Map, Map)}. * @param spent see * {@link BareSHAOPState#BareSHAOPState(List, ProtocolToPartyConnections, Progress, SHAOPSettings, ProtocolException, int, Map, Map)}. */ @JsonCreator public SHAOPState(@JsonProperty("actions") List actions, @JsonProperty("connections") ProtocolToPartyConnections conns, @JsonProperty("progress") Progress progress, @JsonProperty("settings") SHAOPSettings settings, @JsonProperty("error") ProtocolException e, @JsonProperty("teamNr") int teamNr, @JsonProperty("partyNumbers") Map partytNumbers, @JsonProperty("totalSpent") Map spent) { super(actions, conns, progress, settings, e, teamNr, partytNumbers, spent); } /** * Creates the initial state from the given settings * * @param settings the {@link SHAOPSettings} */ public SHAOPState(SHAOPSettings settings) { this(Collections.emptyList(), null, null, settings, null, 0, null, null); } /** * * @param id the {@link PartyId} * @return the {@link PartyWithProfile} for that party */ public PartyWithProfile getSettings(PartyId id) { return settings.getAllParties().get(partyNumbers.get(id)); } /** * * @param party the {@link PartyId} to test * @return true iff party is a SHAOP party (not a COB party). */ public boolean isShaopParty(PartyId party) { return (partyNumbers.get(party) & 1) == 0; } /** * * @return the team leader {@link PartyId}s which are the SHAOP parties. */ private Set getLeaders() { return partyNumbers.keySet().stream().filter(id -> isShaopParty(id)) .collect(Collectors.toSet()); } /** * * @param party a Party Id * @return the PartyId of the party (COB and SHAOP are partners) */ public PartyId getPartner(PartyId party) { return connections.get(partyNumbers.get(party) ^ 1).getParty(); } @Override public boolean isFinal(long currentTimeMs) { List acts = getActions(); return super.isFinal(currentTimeMs) || !getAgreements().getMap().isEmpty() || (!acts.isEmpty() && acts.get(acts.size() - 1) instanceof EndNegotiation); } @Override public SHAOPSettings getSettings() { return settings; } /** * search back through the actions for an offer. We can not smarly limit the * search size because there can be an unlimited number of cob Comparison * actions anywhere in the list. * * @return Bid from the most recent offer, or null if no such offer */ private Bid getLastBid() { for (int n = actions.size() - 1; n >= 0; n--) { Action action = actions.get(n); if (action instanceof Offer) { return ((Offer) action).getBid(); } } return null; } /** * * @param connection the new {@link ProtocolToPartyConn} to a party * @return new SessionState with the new connection added. This call ignores * the progress (does not check isFinal) because SAOP uses this * during the setup where the deadline is not yet relevant. The * connection should be informed about the partyprofile but the * state currently indicate if that already happened or not. * IMPORTANT. The connections MUST be made in the same order as * {@link SHAOPSettings#getTeams()}. */ public SHAOPState with(ProtocolToPartyConn connection) { // Only called from the SHAOP initialization phase. ProtocolToPartyConnections newconns = connections.with(connection); Map newNumbers = new HashMap<>(partyNumbers); newNumbers.put(connection.getParty(), connections.size()); return new SHAOPState(actions, newconns, progress, settings, null, teamNr, newNumbers, totalSpent); } /** * * @param e the error that occured * @return a new state with the error set. */ public SHAOPState with(ProtocolException e) { return new SHAOPState(actions, connections, progress, settings, e, teamNr, partyNumbers, totalSpent); } /** * Sets the progress for this session. call is ignored if progress already * set. * * @param newprogress the new progress * @return new SAOPState with the progress set */ public SHAOPState with(Progress newprogress) { if (newprogress == null) { throw new IllegalArgumentException("newprogress must be not null"); } if (progress != null) { return this; } return new SHAOPState(actions, connections, newprogress, settings, error, teamNr, partyNumbers, totalSpent); } @Override public Agreements getAgreements() { Agreements agrees = new Agreements(); List acts = getActions(); if (acts.isEmpty()) return agrees; int requiredaccepts = getSettings().getTeams().size() - 1; // check that there are requiredaccepts and find the offer before that for (int n = acts.size() - 1; n >= 0; n--) { Action act = acts.get(n); if (act instanceof Comparison || act instanceof ElicitComparison) continue; if (requiredaccepts == 0 && act instanceof Offer) return agrees.with( new Agreements(((Offer) act).getBid(), getLeaders())); if (!(act instanceof Accept)) return agrees; requiredaccepts--; } return agrees; } /** * @param actor the actor that did this action. Can be used to check if * action is valid. NOTICE caller has to make sure the current * state is not final. * @param action the action that was proposed by actor. * @return new SessionState with the action added as last action. */ public SHAOPState with(PartyId actor, Action action) throws IllegalArgumentException { if (actor == null) { // this is a bug throw new IllegalArgumentException("actor must not be null"); } if (action == null) { throw new IllegalArgumentException("action is null"); } if (!actor.equals(action.getActor())) { throw new IllegalArgumentException( "act contains wrong credentials: " + action); } if (action instanceof ElicitComparison) { return withShaopInternalAction(actor, (ElicitComparison) action); } if (action instanceof Comparison) { return withCobInternalAction(actor, (Comparison) action); } return withShaopRealAction(actor, action); } private SHAOPState withCobInternalAction(PartyId actor, Comparison action) throws IllegalArgumentException { if (isShaopParty(actor)) throw new IllegalArgumentException( "Illegal action, actor is not a cob party:" + action); return withAction(action); } /** * * @param actor actor, supposedly SHAOP, did Elicit action * @param action an elicit action * @return new state * @throws ProtocolException */ private SHAOPState withShaopInternalAction(PartyId actor, ElicitComparison action) throws IllegalArgumentException { if (!isShaopParty(actor)) throw new IllegalArgumentException( "Illegal action, actor is not a shaop party:" + action); return withAction(action); } /** * update action, actorNr, and progress * * @param action the action that was done. Elicit actions do not update * progress * @return */ private SHAOPState withAction(Action action) { Progress newprogress = progress; Map newSpent = new HashMap<>(totalSpent); PartyId partyid = action.getActor(); List newactions = new LinkedList<>(getActions()); newactions.add(action); int newTeam = teamNr; if (action instanceof ElicitComparison) { Object cost = getPartyProfile(partyid).getParty().getParameters() .get("elicitationcost"); if (!(cost instanceof Double)) { cost = DEFAULT_ELICITATATION_COST; } Double oldSpent = totalSpent.get(partyid); if (oldSpent == null) { oldSpent = 0d; } newSpent.put(partyid, oldSpent + (Double) cost); } else if (action instanceof Comparison) { // nothing, turn remains with current SHAOP party } else { // shaop party does real action, move to next SHAOP party. newTeam = (teamNr + 1) % settings.getTeams().size(); // check if we completed a round if (newprogress instanceof ProgressRounds && newTeam == 0) { newprogress = ((ProgressRounds) newprogress).advance(); System.out.println("Progressed rounds " + newprogress + " after action " + action + ""); } } return new SHAOPState(newactions, connections, newprogress, settings, null, newTeam, partyNumbers, newSpent); } /** * party that did a real action * * @param actor * @param action * @return new state. * @throws ProtocolException */ private SHAOPState withShaopRealAction(PartyId actor, Action action) throws IllegalArgumentException { if (!isShaopParty(actor)) throw new IllegalArgumentException( "Only SHAOP party can execute " + action); // real action only allowed if party has the turn. if (!actor.equals(connections.get(2 * teamNr).getParty())) { throw new IllegalArgumentException("Party does not have the turn "); } // check protocol is followed for specific actions if (action instanceof Accept) { Bid bid = getLastBid(); if (bid == null) { throw new IllegalArgumentException( "Accept without a recent offer"); } if (!bid.equals(((Accept) action).getBid())) { throw new IllegalArgumentException( "Party accepts a bid differing from the last offer =" + bid + ", action=" + action + ")"); } } else if (action instanceof Offer) { // offer is always fine. We don't check if bid is actually in domain // or complete etc. We would need domain for that } else if (action instanceof EndNegotiation) { // just act, we will get into the final state then. } else { throw new IllegalArgumentException( "Action " + action + " is not allowed in SHAOP"); } return withAction(action); } }