package geniusweb.protocol.session.amop; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; 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 geniusweb.actions.Action; import geniusweb.actions.EndNegotiation; import geniusweb.actions.Offer; import geniusweb.actions.PartyId; import geniusweb.actions.Vote; import geniusweb.actions.Votes; import geniusweb.inform.Agreements; import geniusweb.inform.YourTurn; 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.protocol.session.SessionResult; import geniusweb.protocol.session.SessionState; import geniusweb.protocol.session.saop.SAOPSettings; import geniusweb.references.PartyWithProfile; import geniusweb.voting.CollectedVotes; public class AMOPState implements SessionState { /** * Phase determines what actions are allowed now. #isFinal terminates the * state. */ public enum Phase { INIT, OFFER, VOTE; public Phase next() { return this == OFFER ? VOTE : OFFER; } } private final Phase phase; private final AMOPSettings settings; private final Map partyprofiles; private final ProtocolToPartyConnections connections; private final Agreements agreements; private final List walkedAway; private final List actions; private final Progress progress; private final Map exceptions; /** * Creates the initial state from the given settings and progress=null * * @param settings the {@link SAOPSettings} */ public AMOPState(AMOPSettings settings) { this(Phase.INIT, Collections.emptyList(), new ProtocolToPartyConnections(Collections.emptyList()), null, settings, Collections.emptyMap(), new Agreements(), Collections.emptyMap(), Collections.emptyList()); } /** * @param phase The Phase * @param actions the legal actions that have been done in the * negotiation. first action is the oldest. This MUST * NOT contain illegal actions, otherwise we can not * decypher the actions list anymore and find the * proper phase boundaries. Instead parties doing * illegal actions must be killed. * @param conns the existing party connections. we assume ownership * of this so it should not be modified although * connections may of course break. * @param progress the {@link Progress} line. can be null if not yet * known * @param settings the {@link SAOPSettings} * @param partyprofiles map with the {@link PartyWithProfile} for connected * parties. null is equivalent to an empty map. * @param agreements the agreements reached. * @param e Possibly empty map of {@link ProtocolException}, the * keys are the party that failed to follow the * protocol. * @param walkedAway list of parties that walked away from the * negotiation. */ protected AMOPState(Phase phase, List actions, ProtocolToPartyConnections conns, Progress progress, AMOPSettings settings, Map partyprofiles, Agreements agreements, Map e, List walkedAway) { this.phase = phase; this.actions = actions; this.connections = conns; this.progress = progress; this.settings = settings; this.partyprofiles = partyprofiles; this.agreements = agreements; this.exceptions = e; this.walkedAway = walkedAway; } @Override public List getActions() { return Collections.unmodifiableList(actions); } @Override public Progress getProgress() { return progress; } public Map getPartyProfiles() { return Collections.unmodifiableMap(partyprofiles); } /** * @return all current/remaining active party connections. Finished/crashed * parties should be removed immediately. */ public ProtocolToPartyConnections getConnections() { return connections; } /** * * @param connection the new {@link ProtocolToPartyConn} * @param partyprofile the {@link PartyWithProfile} that is associated with * this state * @return new SessionState with the new connection added. This call ignores * the progress (does not check isFinal) because we uses this during * the setup where the deadline is not yet relevant. */ protected AMOPState with(ProtocolToPartyConn connection, PartyWithProfile partyprofile) { ProtocolToPartyConnections newconns = getConnections().with(connection); Map newprofiles = new HashMap<>( partyprofiles); newprofiles.put(connection.getParty(), partyprofile); return new AMOPState(Phase.INIT, actions, newconns, progress, settings, newprofiles, agreements, exceptions, walkedAway); } /** * @param id the {@link PartyId} of the party that failed. * @param e the {@link ProtocolException} that occured in the party * @return a new state with the error set. */ public AMOPState with(PartyId id, ProtocolException e) { Map newExc = exceptions; if (!exceptions.containsKey(id)) { newExc = new HashMap<>(exceptions); newExc.put(id, e); } return new AMOPState(phase, actions, connections, progress, settings, partyprofiles, agreements, newExc, walkedAway); } /** * Sets the progress for this session. Can be set only if progress=null. * Should be set in INIT phase. * * @param newprogress the new progress * @return new SAOPState with the progress set */ public AMOPState with(Progress newprogress) { if (progress != null || newprogress == null || phase != Phase.INIT) { throw new IllegalArgumentException( "progress must be null, newprogress must be not null and phase must be INIT"); } return new AMOPState(phase, actions, connections, newprogress, getSettings(), partyprofiles, agreements, exceptions, walkedAway); } @Override public Agreements getAgreements() { return agreements; } @Override public boolean isFinal(long currentTimeMs) { boolean pastDeadline = progress != null && progress.isPastDeadline(currentTimeMs); return phase != Phase.INIT && (pastDeadline || getActiveParties().size() < 2); } @Override public AMOPSettings getSettings() { return settings; } /** * @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. MUST NOT be null. * @param action the action that was proposed by actor. MUST NOT be null. * @return new SessionState with the action added as last action. * @throws ProtocolException if actor is violating the protocol * @throws RuntimeException if we hit a bug. */ public AMOPState with(PartyId actor, Action action) throws ProtocolException { if (!actor.equals(action.getActor())) { throw new ProtocolException( "act by " + actor + " contains wrong actorid: " + action, actor); } if (!getActiveParties().contains(actor)) throw new ProtocolException("Deactivated actor tried to act", actor); if (getPhaseActions().containsKey(actor)) throw new ProtocolException( "Attempt to act twice in phase:" + action, actor); List newactions = new LinkedList<>(getActions()); newactions.add(action); List newWalkedAway = walkedAway; // check protocol is followed for specific actions if (action instanceof Votes) { if (phase != Phase.VOTE) throw new ProtocolException( "Vote can only be placed in VOTE phase", actor); // Notice we don't check the votes. You can actually vote for // other bids. But such action would be useless as others can't vote // on it. } else if (action instanceof Offer) { if (phase != Phase.OFFER) { throw new ProtocolException( "Offer can only be placed in OFFER phase", actor); } } else if (action instanceof EndNegotiation) { newWalkedAway = new LinkedList<>(walkedAway); newWalkedAway.add(action.getActor()); } else { throw new ProtocolException( "Action " + action + " is not allowed in AMOP", actor); } return new AMOPState(phase, newactions, connections, progress, settings, partyprofiles, agreements, exceptions, newWalkedAway); } @Override public List getResults() { return Arrays.asList(new SessionResult(partyprofiles, getAgreements(), Collections.emptyMap(), null)); } /** * @return true iff all parties acted as required in the current Phase. We * search back until the last {@link YourTurn} if Phase=VOTE. / * {@link Vote} if Phase=OFFER and check that all current * connections have acted. */ public boolean isAllPartiesActed() { return getPhaseActions().keySet().containsAll(getActiveParties()); } /** * @return all actions of the current phase NOTE this assumes at least 1 * action is done in each phase, so that we can detect the previous * phase actions in the actions list. * */ public Map getPhaseActions() { Map newactions = new HashMap<>(); for (int n = actions.size() - 1; n >= 0; n--) { Action act = actions.get(n); if (act instanceof EndNegotiation) continue; if (phase == Phase.VOTE && !(act instanceof Votes)) break; if (phase == Phase.OFFER && !(act instanceof Offer)) break; newactions.put(act.getActor(), act); } return newactions; } /** * * @return all currently active parties. These are the parties that do not * yet have an {@link #agreements} nor caused {@link #exceptions} * nor {@link #walkedAway} */ public List getActiveParties() { List active = connections.stream().map(conn -> conn.getParty()) .collect(Collectors.toList()); active.removeAll(agreements.getMap().keySet()); active.removeAll(exceptions.keySet()); active.removeAll(walkedAway); return active; } public Phase getPhase() { return phase; } /** * * @return list of parties that walked away with {@link EndNegotiation} */ public List getWalkedAway() { return walkedAway; } /** * * @return new state with next phase selected and a updated list of * agreements. This must be called to end a phase, it's not * triggered by an action. If current phase is VOTE and next phase * OFFER then progress is also incremented. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public AMOPState nextPhase() { Agreements newagreements = agreements; Progress newprogress = progress; if (phase == Phase.VOTE) { // hacky cast newagreements = collectVotes((Map) getPhaseActions()); if (newprogress instanceof ProgressRounds) { newprogress = ((ProgressRounds) newprogress).advance(); } } return new AMOPState(phase.next(), actions, connections, newprogress, settings, partyprofiles, newagreements, exceptions, walkedAway); } /** * * @param allvotes the Votes of each party * @return all agreements that were reached from the given votes */ protected Agreements collectVotes(Map allvotes) { Agreements newagreements = agreements; while (true) { // power is 1 for each known party. Map powers = allvotes.keySet().stream() .collect(Collectors.toMap(p -> p, p -> 1)); Map> agrees = new CollectedVotes(allvotes, powers) .getMaxAgreements(); if (agrees.isEmpty()) break; // find the best one Bid maxbid = agrees.keySet().stream() .max(Comparator.comparingInt(bid -> agrees.get(bid).size())) .get(); newagreements = newagreements .with(new Agreements(maxbid, agrees.get(maxbid))); for (PartyId party : agrees.get(maxbid)) { allvotes.remove(party); } } return newagreements; } @Override public String toString() { return "AMOPState[" + phase + "," + settings + "," + partyprofiles + "," + connections + "," + agreements + "," + walkedAway + "," + actions + "," + progress + "," + exceptions + "," + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((actions == null) ? 0 : actions.hashCode()); result = prime * result + ((agreements == null) ? 0 : agreements.hashCode()); result = prime * result + ((connections == null) ? 0 : connections.hashCode()); result = prime * result + ((exceptions == null) ? 0 : exceptions.hashCode()); result = prime * result + ((partyprofiles == null) ? 0 : partyprofiles.hashCode()); result = prime * result + ((phase == null) ? 0 : phase.hashCode()); result = prime * result + ((progress == null) ? 0 : progress.hashCode()); result = prime * result + ((settings == null) ? 0 : settings.hashCode()); result = prime * result + ((walkedAway == null) ? 0 : walkedAway.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; AMOPState other = (AMOPState) obj; if (actions == null) { if (other.actions != null) return false; } else if (!actions.equals(other.actions)) return false; if (agreements == null) { if (other.agreements != null) return false; } else if (!agreements.equals(other.agreements)) return false; if (connections == null) { if (other.connections != null) return false; } else if (!connections.equals(other.connections)) return false; if (exceptions == null) { if (other.exceptions != null) return false; } else if (!exceptions.equals(other.exceptions)) return false; if (partyprofiles == null) { if (other.partyprofiles != null) return false; } else if (!partyprofiles.equals(other.partyprofiles)) return false; if (phase != other.phase) return false; if (progress == null) { if (other.progress != null) return false; } else if (!progress.equals(other.progress)) return false; if (settings == null) { if (other.settings != null) return false; } else if (!settings.equals(other.settings)) return false; if (walkedAway == null) { if (other.walkedAway != null) return false; } else if (!walkedAway.equals(other.walkedAway)) return false; return true; } }