[88] | 1 | from typing import List, Dict, Optional
|
---|
| 2 |
|
---|
| 3 | from geniusweb.actions.Action import Action
|
---|
| 4 | from geniusweb.actions.PartyId import PartyId
|
---|
| 5 | from geniusweb.inform.Agreements import Agreements
|
---|
| 6 | from geniusweb.progress.Progress import Progress
|
---|
| 7 | from geniusweb.progress.ProgressRounds import ProgressRounds
|
---|
| 8 | from geniusweb.protocol.ProtocolException import ProtocolException
|
---|
| 9 | from geniusweb.protocol.session.SessionResult import SessionResult
|
---|
| 10 | from geniusweb.protocol.session.SessionState import SessionState
|
---|
| 11 | from geniusweb.protocol.session.mopac.MOPACSettings import MOPACSettings
|
---|
| 12 | from geniusweb.protocol.session.mopac.PartyStates import PartyStates
|
---|
| 13 | from geniusweb.protocol.session.mopac.phase.OfferPhase import OfferPhase
|
---|
| 14 | from geniusweb.protocol.session.mopac.phase.OptInPhase import OptInPhase
|
---|
| 15 | from geniusweb.protocol.session.mopac.phase.Phase import Phase, PHASE_MAXTIME, \
|
---|
| 16 | PHASE_MINTIME
|
---|
| 17 | from geniusweb.references.PartyWithProfile import PartyWithProfile
|
---|
| 18 | from geniusweb.utils import val, toTuple
|
---|
| 19 |
|
---|
| 20 |
|
---|
| 21 | class MOPACState (SessionState):
|
---|
| 22 | '''
|
---|
| 23 | Keeps track of the current {@link Phase}. Adds initializing stuff and
|
---|
| 24 | time/deadline checking. This state does not contain connections, this assumes
|
---|
| 25 | that someone else handles (i.e. {@link NegoProtocol} that and the connections
|
---|
| 26 | with the parties and that negotiation events are just pumped in from there.
|
---|
| 27 | <p>
|
---|
| 28 | This object is a bit tricky. It has two states
|
---|
| 29 | <ol>
|
---|
| 30 | <li>The initial state, where phase=null and connections are being made to all
|
---|
| 31 | parties. At this point, problems can not be handled nicely yet because there
|
---|
| 32 | are no PartyId's for problematic parties
|
---|
| 33 | <li>The working state, where all parties are connected and all problems can
|
---|
| 34 | be connected to an offending party.
|
---|
| 35 | </ol>
|
---|
| 36 | '''
|
---|
| 37 |
|
---|
| 38 | # private final Phase phase; // maybe null while initializing
|
---|
| 39 | # private final MOPACSettings settings;
|
---|
| 40 | # private final Map<PartyId, PartyWithProfile> partyprofiles;
|
---|
| 41 | # private final List<Action> actions;
|
---|
| 42 | # private final Progress progress;
|
---|
| 43 | #
|
---|
| 44 | # /**
|
---|
| 45 | # * Creates the initial state from the given settings and progress=null
|
---|
| 46 | # *
|
---|
| 47 | # * @param settings the {@link SAOPSettings}
|
---|
| 48 | # */
|
---|
| 49 | # public MOPACState(MOPACSettings settings) {
|
---|
| 50 | # this(null, Arrays.asList(), null, settings, Collections.emptyMap());
|
---|
| 51 | #
|
---|
| 52 | # }
|
---|
| 53 |
|
---|
| 54 | def __init__(self, phase:Optional[Phase] ,
|
---|
| 55 | actions:List[Action] ,
|
---|
| 56 | progress:Optional[Progress] ,
|
---|
| 57 | settings:MOPACSettings,
|
---|
| 58 | partyprofiles:Dict[PartyId, PartyWithProfile] ):
|
---|
| 59 | '''
|
---|
| 60 | @param phase The Phase, or null if still initializing. Phase is
|
---|
| 61 | set something only after we have connections.
|
---|
| 62 | @param actions the legal actions that have been done in the
|
---|
| 63 | negotiation. {@link PartyStates#getActions()} is
|
---|
| 64 | called to collect the actions after the phase is
|
---|
| 65 | finished.first action in list is the oldest. This
|
---|
| 66 | will not contain actions that immediately led to an
|
---|
| 67 | agreement, because {@link PartyStates} removes the
|
---|
| 68 | actions that led to an agreement. This MUST NOT
|
---|
| 69 | contain illegal actions. Parties doing illegal
|
---|
| 70 | actions are killed and the offending action ends up
|
---|
| 71 | in the stacktrace. Previous actions of crashed
|
---|
| 72 | parties remain standing and valid.
|
---|
| 73 | @param progress the {@link Progress} line. can be null if not yet
|
---|
| 74 | known. null happens because during initialization
|
---|
| 75 | phase the protocol first add all connections. During
|
---|
| 76 | this time, parties may already enter the
|
---|
| 77 | failed/exception state.
|
---|
| 78 | @param settings the {@link SAOPSettings}
|
---|
| 79 | @param partyprofiles map with the {@link PartyWithProfile} for connected
|
---|
| 80 | parties. null is equivalent to an empty map.
|
---|
| 81 | '''
|
---|
| 82 | self._phase = phase
|
---|
| 83 | self._actions = actions
|
---|
| 84 | self._progress = progress
|
---|
| 85 | self._settings = settings
|
---|
| 86 | self._partyprofiles = partyprofiles
|
---|
| 87 |
|
---|
| 88 | def initPhase(self,newprogress:Progress , now:int) -> "MOPACState" :
|
---|
| 89 | '''
|
---|
| 90 | Sets the progress for this session and initial phase. Must be called
|
---|
| 91 | after all parties have been connected with
|
---|
| 92 | {@link #with(PartyId, PartyWithProfile)}.
|
---|
| 93 |
|
---|
| 94 | @param newprogress the initial {@link Progress} typically matching the
|
---|
| 95 | settings deadline object
|
---|
| 96 | @param now current time ms since 1970
|
---|
| 97 |
|
---|
| 98 | @return state with the initial partystates , progress set.
|
---|
| 99 | '''
|
---|
| 100 | if self._progress != None or newprogress == None or self._phase != None:
|
---|
| 101 | raise ValueError(
|
---|
| 102 | "progress must be null, newprogress must be not null and phase must be INIT")
|
---|
| 103 |
|
---|
| 104 | partyStates = PartyStates(self._getPowers())
|
---|
| 105 | firstPhase = OfferPhase(partyStates,
|
---|
| 106 | now + MOPACState._getAvailablePhaseTime(newprogress, now),
|
---|
| 107 | self._settings.getVotingEvaluator())
|
---|
| 108 | return MOPACState(firstPhase, self._actions, newprogress, self._settings,
|
---|
| 109 | self._partyprofiles)
|
---|
| 110 |
|
---|
| 111 | def getActions(self)->List[Action]:
|
---|
| 112 | return list(self._actions)
|
---|
| 113 |
|
---|
| 114 | def getProgress(self)->Optional[Progress] :
|
---|
| 115 | return self._progress
|
---|
| 116 |
|
---|
| 117 | def getAgreements(self)-> Agreements:
|
---|
| 118 | return val(self._phase).getPartyStates().getAgreements();
|
---|
| 119 |
|
---|
| 120 | def getSettings(self)->MOPACSettings :
|
---|
| 121 | return self._settings
|
---|
| 122 |
|
---|
| 123 | def getPartyProfiles(self)->Dict[PartyId, PartyWithProfile]:
|
---|
| 124 | return dict(self._partyprofiles)
|
---|
| 125 |
|
---|
| 126 | def isFinal(self, now:int)->bool:
|
---|
| 127 | return self._phase != None and val(self._phase).isFinal(now) \
|
---|
| 128 | and not self.isNewPhasePossible(now)
|
---|
| 129 |
|
---|
| 130 | def getResult(self)->SessionResult :
|
---|
| 131 | return SessionResult(self._partyprofiles, self.getAgreements(),
|
---|
| 132 | {}, None)
|
---|
| 133 |
|
---|
| 134 | @staticmethod
|
---|
| 135 | def _getAvailablePhaseTime(aprogress:Progress , now:int) ->int:
|
---|
| 136 | '''
|
---|
| 137 | @param progress the Progress that needs to be checked
|
---|
| 138 | @param now current time ms since 1970
|
---|
| 139 | @return the max possible duration in ms of a phase considering the
|
---|
| 140 | progress.
|
---|
| 141 | '''
|
---|
| 142 | return min(int(aprogress.getTerminationTime().timestamp()*1000) - now,
|
---|
| 143 | PHASE_MAXTIME)
|
---|
| 144 |
|
---|
| 145 | def With(self, id:PartyId , partyprofile:PartyWithProfile ) -> "MOPACState" :
|
---|
| 146 | '''
|
---|
| 147 | @param id the new {@link PartyId}
|
---|
| 148 | @param partyprofile the {@link PartyWithProfile} that is associated with
|
---|
| 149 | this state
|
---|
| 150 | @return new {@link MOPACState} with the new party added. This call
|
---|
| 151 | ignores the progress (does not check isFinal) because we uses
|
---|
| 152 | this during the setup where the deadline is not yet relevant.
|
---|
| 153 | '''
|
---|
| 154 | if self._phase != None:
|
---|
| 155 | raise ValueError(
|
---|
| 156 | "Adding connections only allowed while initializing");
|
---|
| 157 |
|
---|
| 158 | newprofiles = dict(self._partyprofiles)
|
---|
| 159 | newprofiles[id]= partyprofile
|
---|
| 160 | return MOPACState(None, self._actions,self._progress, self._settings, newprofiles)
|
---|
| 161 |
|
---|
| 162 | def WithException(self, e:ProtocolException) ->"MOPACState" :
|
---|
| 163 | '''
|
---|
| 164 | @param e the {@link ProtocolException} that occured
|
---|
| 165 | @return a new state with the error set. You MUST have called
|
---|
| 166 | {@link #initPhase(Progress, long)} before using this
|
---|
| 167 | '''
|
---|
| 168 | return MOPACState(val(self._phase).WithException(e), self._actions, self._progress,
|
---|
| 169 | self._settings,self._partyprofiles)
|
---|
| 170 |
|
---|
| 171 | def nextPhase(self, now:int)->"MOPACState" :
|
---|
| 172 | '''
|
---|
| 173 | Start the next phase. If new phase is OfferPhase, we increase progress.
|
---|
| 174 | Actions is reset to empty. does nothing if not
|
---|
| 175 | {@link #isNewPhasePossible(long)}
|
---|
| 176 |
|
---|
| 177 | @param now current time
|
---|
| 178 | @return new {@link MOPACState} with phase initialized for next phase.
|
---|
| 179 | '''
|
---|
| 180 | remainingNegoTime = int(val(self._progress).getTerminationTime().timestamp()*1000) - now
|
---|
| 181 | newphase = val(self._phase).next(now, min(remainingNegoTime, PHASE_MAXTIME))
|
---|
| 182 |
|
---|
| 183 | return MOPACState(newphase, self._actions,
|
---|
| 184 | MOPACState._increment(val(self._progress), val(self._phase)),
|
---|
| 185 | self.getSettings(), self._partyprofiles)
|
---|
| 186 |
|
---|
| 187 | def isNewPhasePossible(self, now:int)->bool:
|
---|
| 188 | '''
|
---|
| 189 | When this is called, all parties should have acted.
|
---|
| 190 |
|
---|
| 191 | @param now current time
|
---|
| 192 | @return true if there are still >2 parties active and we have enough
|
---|
| 193 | time for a new phase.
|
---|
| 194 | '''
|
---|
| 195 | # System.out.println("phase=" + phase);
|
---|
| 196 | newprogress = MOPACState._increment(val(self._progress), val(self._phase))
|
---|
| 197 | if newprogress.isPastDeadline(now + PHASE_MINTIME):
|
---|
| 198 | return False
|
---|
| 199 |
|
---|
| 200 | return len(val(self._phase).getPartyStates().getNegotiatingParties()) >= 2\
|
---|
| 201 | and self._getAvailablePhaseTime(newprogress,
|
---|
| 202 | now) > PHASE_MINTIME
|
---|
| 203 |
|
---|
| 204 | def getPhase(self)->Phase :
|
---|
| 205 | return val(self._phase)
|
---|
| 206 |
|
---|
| 207 | def WithAction(self, actor:PartyId, action:Action, now:int)->"MOPACState" :
|
---|
| 208 | '''
|
---|
| 209 | Check if action is allowed. Add action to the list of actions. Notice,
|
---|
| 210 | this does NOT check if we need to step to the next phase, because
|
---|
| 211 | deciding that is also depending on time-outs.
|
---|
| 212 |
|
---|
| 213 | @param actor the actor that did this action. Can be used to check if
|
---|
| 214 | action is valid. NOTICE caller has to make sure the current
|
---|
| 215 | state is not final. MUST NOT be null.
|
---|
| 216 | @param action the action that was proposed by actor. MUST NOT be null.
|
---|
| 217 | @param now the current time in ms since 1970, see
|
---|
| 218 | {@link System#currentTimeMillis()}
|
---|
| 219 | @return new {@link MOPACState} with the action checked and registered. If
|
---|
| 220 | the action is not allowed, the new state may be that the actor is
|
---|
| 221 | in the exception list.
|
---|
| 222 | '''
|
---|
| 223 | return MOPACState(val(self._phase).With(actor, action, now), self._actions, self._progress,
|
---|
| 224 | self._settings, self._partyprofiles)
|
---|
| 225 |
|
---|
| 226 | @staticmethod
|
---|
| 227 | def _increment(aprogress:Progress , aphase:Phase ) ->Progress :
|
---|
| 228 | '''
|
---|
| 229 | @param aprogress the progress that might need to be advanced
|
---|
| 230 | @param aphase the phase
|
---|
| 231 | @return the next progress. Progress round advances if phase is
|
---|
| 232 | {@link OptIn}.
|
---|
| 233 | '''
|
---|
| 234 | if isinstance(aprogress,ProgressRounds) and isinstance(aphase,OptInPhase):
|
---|
| 235 | return aprogress.advance()
|
---|
| 236 | return aprogress
|
---|
| 237 |
|
---|
| 238 | def _getPowers(self)-> Dict[PartyId, int]:
|
---|
| 239 | '''
|
---|
| 240 | @return the power of all parties as set in their parameters, default =1.
|
---|
| 241 | bad power values (non-integer, or <1) are ignored.
|
---|
| 242 | '''
|
---|
| 243 | map:Dict[PartyId,int] = {}
|
---|
| 244 | for pid in self._partyprofiles:
|
---|
| 245 | if self._partyprofiles[pid].getParty().getParameters().containsKey("power"):
|
---|
| 246 | power = self._partyprofiles[pid].getParty().getParameters()\
|
---|
| 247 | .get("power")
|
---|
| 248 | if not isinstance(power, int) or power < 1:
|
---|
| 249 | power = 1
|
---|
| 250 | else:
|
---|
| 251 | power=1
|
---|
| 252 | map[pid]=power
|
---|
| 253 | return map
|
---|
| 254 |
|
---|
| 255 | def __repr__(self)->str:
|
---|
| 256 | return "MOPACState[" + str(self._phase) + "," + str(self._settings)\
|
---|
| 257 | + "," + str(self._partyprofiles) + "," + str(self._progress) + "]"
|
---|
| 258 |
|
---|
| 259 | def finishPhase(self)->"MOPACState" :
|
---|
| 260 | '''
|
---|
| 261 | @return a wrapped-up state, with all parties doen an action or kicked,
|
---|
| 262 | and agreements collected
|
---|
| 263 | '''
|
---|
| 264 | newphase = val(self._phase).finish()
|
---|
| 265 | newactions = list(self._actions)
|
---|
| 266 | newactions=newactions+ newphase.getPartyStates().getActions()
|
---|
| 267 | return MOPACState(newphase, newactions, self._progress, self._settings,
|
---|
| 268 | self._partyprofiles)
|
---|
| 269 |
|
---|
| 270 | def __hash__(self):
|
---|
| 271 | return hash((tuple(self._actions), toTuple(self._partyprofiles),
|
---|
| 272 | self._phase, self._progress, self._settings))
|
---|
| 273 |
|
---|
| 274 | def __eq__(self, other):
|
---|
| 275 | return isinstance(other, self.__class__) \
|
---|
| 276 | and self._actions==other._actions\
|
---|
| 277 | and self._partyprofiles==other._partyprofiles\
|
---|
| 278 | and self._phase==other._phase\
|
---|
| 279 | and self._progress==other._progress\
|
---|
| 280 | and self._settings==other._settings
|
---|
| 281 |
|
---|