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 getResults(self) -> List[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 |
|
---|