from typing import List, Dict, Optional
from geniusweb.actions.Action import Action
from geniusweb.actions.PartyId import PartyId
from geniusweb.inform.Agreements import Agreements
from geniusweb.progress.Progress import Progress
from geniusweb.progress.ProgressRounds import ProgressRounds
from geniusweb.protocol.ProtocolException import ProtocolException
from geniusweb.protocol.session.SessionResult import SessionResult
from geniusweb.protocol.session.SessionState import SessionState
from geniusweb.protocol.session.mopac.MOPACSettings import MOPACSettings
from geniusweb.protocol.session.mopac.PartyStates import PartyStates
from geniusweb.protocol.session.mopac.phase.OfferPhase import OfferPhase
from geniusweb.protocol.session.mopac.phase.OptInPhase import OptInPhase
from geniusweb.protocol.session.mopac.phase.Phase import Phase, PHASE_MAXTIME, \
PHASE_MINTIME
from geniusweb.references.PartyWithProfile import PartyWithProfile
from geniusweb.utils import val, toTuple
class MOPACState (SessionState):
'''
Keeps track of the current {@link Phase}. Adds initializing stuff and
time/deadline checking. This state does not contain connections, this assumes
that someone else handles (i.e. {@link NegoProtocol} that and the connections
with the parties and that negotiation events are just pumped in from there.
This object is a bit tricky. It has two states
- The initial state, where phase=null and connections are being made to all
parties. At this point, problems can not be handled nicely yet because there
are no PartyId's for problematic parties
- The working state, where all parties are connected and all problems can
be connected to an offending party.
'''
# private final Phase phase; // maybe null while initializing
# private final MOPACSettings settings;
# private final Map partyprofiles;
# private final List actions;
# private final Progress progress;
#
# /**
# * Creates the initial state from the given settings and progress=null
# *
# * @param settings the {@link SAOPSettings}
# */
# public MOPACState(MOPACSettings settings) {
# this(null, Arrays.asList(), null, settings, Collections.emptyMap());
#
# }
def __init__(self, phase:Optional[Phase] ,
actions:List[Action] ,
progress:Optional[Progress] ,
settings:MOPACSettings,
partyprofiles:Dict[PartyId, PartyWithProfile] ):
'''
@param phase The Phase, or null if still initializing. Phase is
set something only after we have connections.
@param actions the legal actions that have been done in the
negotiation. {@link PartyStates#getActions()} is
called to collect the actions after the phase is
finished.first action in list is the oldest. This
will not contain actions that immediately led to an
agreement, because {@link PartyStates} removes the
actions that led to an agreement. This MUST NOT
contain illegal actions. Parties doing illegal
actions are killed and the offending action ends up
in the stacktrace. Previous actions of crashed
parties remain standing and valid.
@param progress the {@link Progress} line. can be null if not yet
known. null happens because during initialization
phase the protocol first add all connections. During
this time, parties may already enter the
failed/exception state.
@param settings the {@link SAOPSettings}
@param partyprofiles map with the {@link PartyWithProfile} for connected
parties. null is equivalent to an empty map.
'''
self._phase = phase
self._actions = actions
self._progress = progress
self._settings = settings
self._partyprofiles = partyprofiles
def initPhase(self,newprogress:Progress , now:int) -> "MOPACState" :
'''
Sets the progress for this session and initial phase. Must be called
after all parties have been connected with
{@link #with(PartyId, PartyWithProfile)}.
@param newprogress the initial {@link Progress} typically matching the
settings deadline object
@param now current time ms since 1970
@return state with the initial partystates , progress set.
'''
if self._progress != None or newprogress == None or self._phase != None:
raise ValueError(
"progress must be null, newprogress must be not null and phase must be INIT")
partyStates = PartyStates(self._getPowers())
firstPhase = OfferPhase(partyStates,
now + MOPACState._getAvailablePhaseTime(newprogress, now),
self._settings.getVotingEvaluator())
return MOPACState(firstPhase, self._actions, newprogress, self._settings,
self._partyprofiles)
def getActions(self)->List[Action]:
return list(self._actions)
def getProgress(self)->Optional[Progress] :
return self._progress
def getAgreements(self)-> Agreements:
return val(self._phase).getPartyStates().getAgreements();
def getSettings(self)->MOPACSettings :
return self._settings
def getPartyProfiles(self)->Dict[PartyId, PartyWithProfile]:
return dict(self._partyprofiles)
def isFinal(self, now:int)->bool:
return self._phase != None and val(self._phase).isFinal(now) \
and not self.isNewPhasePossible(now)
def getResult(self)->SessionResult :
return SessionResult(self._partyprofiles, self.getAgreements(),
{}, None)
@staticmethod
def _getAvailablePhaseTime(aprogress:Progress , now:int) ->int:
'''
@param progress the Progress that needs to be checked
@param now current time ms since 1970
@return the max possible duration in ms of a phase considering the
progress.
'''
return min(int(aprogress.getTerminationTime().timestamp()*1000) - now,
PHASE_MAXTIME)
def With(self, id:PartyId , partyprofile:PartyWithProfile ) -> "MOPACState" :
'''
@param id the new {@link PartyId}
@param partyprofile the {@link PartyWithProfile} that is associated with
this state
@return new {@link MOPACState} with the new party added. This call
ignores the progress (does not check isFinal) because we uses
this during the setup where the deadline is not yet relevant.
'''
if self._phase != None:
raise ValueError(
"Adding connections only allowed while initializing");
newprofiles = dict(self._partyprofiles)
newprofiles[id]= partyprofile
return MOPACState(None, self._actions,self._progress, self._settings, newprofiles)
def WithException(self, e:ProtocolException) ->"MOPACState" :
'''
@param e the {@link ProtocolException} that occured
@return a new state with the error set. You MUST have called
{@link #initPhase(Progress, long)} before using this
'''
return MOPACState(val(self._phase).WithException(e), self._actions, self._progress,
self._settings,self._partyprofiles)
def nextPhase(self, now:int)->"MOPACState" :
'''
Start the next phase. If new phase is OfferPhase, we increase progress.
Actions is reset to empty. does nothing if not
{@link #isNewPhasePossible(long)}
@param now current time
@return new {@link MOPACState} with phase initialized for next phase.
'''
remainingNegoTime = int(val(self._progress).getTerminationTime().timestamp()*1000) - now
newphase = val(self._phase).next(now, min(remainingNegoTime, PHASE_MAXTIME))
return MOPACState(newphase, self._actions,
MOPACState._increment(val(self._progress), val(self._phase)),
self.getSettings(), self._partyprofiles)
def isNewPhasePossible(self, now:int)->bool:
'''
When this is called, all parties should have acted.
@param now current time
@return true if there are still >2 parties active and we have enough
time for a new phase.
'''
# System.out.println("phase=" + phase);
newprogress = MOPACState._increment(val(self._progress), val(self._phase))
if newprogress.isPastDeadline(now + PHASE_MINTIME):
return False
return len(val(self._phase).getPartyStates().getNegotiatingParties()) >= 2\
and self._getAvailablePhaseTime(newprogress,
now) > PHASE_MINTIME
def getPhase(self)->Phase :
return val(self._phase)
def WithAction(self, actor:PartyId, action:Action, now:int)->"MOPACState" :
'''
Check if action is allowed. Add action to the list of actions. Notice,
this does NOT check if we need to step to the next phase, because
deciding that is also depending on time-outs.
@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.
@param now the current time in ms since 1970, see
{@link System#currentTimeMillis()}
@return new {@link MOPACState} with the action checked and registered. If
the action is not allowed, the new state may be that the actor is
in the exception list.
'''
return MOPACState(val(self._phase).With(actor, action, now), self._actions, self._progress,
self._settings, self._partyprofiles)
@staticmethod
def _increment(aprogress:Progress , aphase:Phase ) ->Progress :
'''
@param aprogress the progress that might need to be advanced
@param aphase the phase
@return the next progress. Progress round advances if phase is
{@link OptIn}.
'''
if isinstance(aprogress,ProgressRounds) and isinstance(aphase,OptInPhase):
return aprogress.advance()
return aprogress
def _getPowers(self)-> Dict[PartyId, int]:
'''
@return the power of all parties as set in their parameters, default =1.
bad power values (non-integer, or <1) are ignored.
'''
map:Dict[PartyId,int] = {}
for pid in self._partyprofiles:
if self._partyprofiles[pid].getParty().getParameters().containsKey("power"):
power = self._partyprofiles[pid].getParty().getParameters()\
.get("power")
if not isinstance(power, int) or power < 1:
power = 1
else:
power=1
map[pid]=power
return map
def __repr__(self)->str:
return "MOPACState[" + str(self._phase) + "," + str(self._settings)\
+ "," + str(self._partyprofiles) + "," + str(self._progress) + "]"
def finishPhase(self)->"MOPACState" :
'''
@return a wrapped-up state, with all parties doen an action or kicked,
and agreements collected
'''
newphase = val(self._phase).finish()
newactions = list(self._actions)
newactions=newactions+ newphase.getPartyStates().getActions()
return MOPACState(newphase, newactions, self._progress, self._settings,
self._partyprofiles)
def __hash__(self):
return hash((tuple(self._actions), toTuple(self._partyprofiles),
self._phase, self._progress, self._settings))
def __eq__(self, other):
return isinstance(other, self.__class__) \
and self._actions==other._actions\
and self._partyprofiles==other._partyprofiles\
and self._phase==other._phase\
and self._progress==other._progress\
and self._settings==other._settings