Index: geniuswebcore/geniusweb/actions/Action.py
===================================================================
--- geniuswebcore/geniusweb/actions/Action.py	(revision 66)
+++ geniuswebcore/geniusweb/actions/Action.py	(revision 67)
@@ -11,5 +11,8 @@
 			"geniusweb.actions.Offer.Offer",\
 			"geniusweb.actions.Accept.Accept",\
-			"geniusweb.actions.LearningDone.LearningDone"])
+			"geniusweb.actions.LearningDone.LearningDone",\
+			"geniusweb.actions.Vote.Vote",\
+			"geniusweb.actions.Votes.Votes",\
+			])
 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
 class Action (ABC):
Index: geniuswebcore/geniusweb/actions/ActionWithBid.py
===================================================================
--- geniuswebcore/geniusweb/actions/ActionWithBid.py	(revision 66)
+++ geniuswebcore/geniusweb/actions/ActionWithBid.py	(revision 67)
@@ -17,6 +17,6 @@
 		@param bid the bid. FIXME use Bid object and strong typing
 		'''
-		super().__init__(actor);
-		self._bid = bid;
+		super().__init__(actor)
+		self._bid = bid
 
 	def getBid(self) -> Bid: 
Index: geniuswebcore/geniusweb/actions/Vote.py
===================================================================
--- geniuswebcore/geniusweb/actions/Vote.py	(revision 67)
+++ geniuswebcore/geniusweb/actions/Vote.py	(revision 67)
@@ -0,0 +1,56 @@
+from geniusweb.actions.ActionWithBid import ActionWithBid
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.issuevalue.Bid import Bid
+
+class Vote ( ActionWithBid ):
+	'''
+	A vote is an indication by a party that it conditionally accepts a bid. It's
+	then up to the protocol to determine the outcome.
+	'''
+	
+	def __init__(self, actor:PartyId, bid:Bid ,minPower :int , maxPower:int):
+		'''
+		@param id       the {@link PartyId} that does the action
+		@param bid      the bid that is voted on
+		@param minPower the minimum power this bid must get in order for the vote
+		                to be valid. Power is the sum of the powers of the
+		                parties that are in the deal. If power=1 for all
+		                participants (usually the default) power can be
+		                interpreted as number of votes
+		@param maxPower the maximum power this bid must get in order for the vote
+		                 to be valid. See {@link #minPower}
+		'''
+		super().__init__(actor, bid)
+		self._minPower = minPower
+		self._maxPower = maxPower
+		if bid == None or minPower == None or minPower < 1 or maxPower == None or maxPower < minPower:
+			raise ValueError("Vote must have non-null bid and minVotes, and minPower must be >=1 and maxPower must be >=minPower")
+
+	def getMinPower(self)  -> int:
+		'''
+		@return the minimum power this bid must get in order for the vote to be
+		        valid.
+		'''
+		return self._minPower
+
+	def getMaxPower(self)->int:
+		'''
+		@return the max power this bid must get in order for the vote to be
+		        valid.
+		'''
+		return self._maxPower;
+
+	#Override
+	def __repr__(self) -> str:
+		return "Vote[" + repr(self.getActor()) + "," + repr(self.getBid()) + ","\
+			 + repr(self._minPower) + "," + repr(self._maxPower) + "]"
+
+	def __hash__(self):
+		return hash((self._actor, self._bid, self._minPower, self._maxPower))
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and \
+			super().__eq__(other) and self._minPower==other._minPower \
+			and self._maxPower==other._maxPower
+
+	
Index: geniuswebcore/geniusweb/actions/Votes.py
===================================================================
--- geniuswebcore/geniusweb/actions/Votes.py	(revision 67)
+++ geniuswebcore/geniusweb/actions/Votes.py	(revision 67)
@@ -0,0 +1,85 @@
+from __future__ import annotations
+from typing import Set, Optional, List
+
+from geniusweb.actions.AbstractAction import AbstractAction
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.actions.Vote import Vote
+from geniusweb.issuevalue.Bid import Bid
+
+
+class Votes (AbstractAction):
+    '''
+    Indicates that a party conditionally agrees with any of the {@link Vote}s
+    provided.
+    '''
+    def __init__(self, actor: PartyId, votes:Set[Vote]):
+        '''
+        @param id    party id
+        @param votes the {@link Vote}s that the party can agree on, if the
+                     condition of the Vote holds. Every {@link Vote#getActor()}
+                     should equal the id. There may be at most 1 vote per
+                     {@link Bid}.
+        '''
+        super().__init__(actor)
+        if votes == None:
+            raise ValueError("votes must be not null");
+        self._votes = votes.copy()
+        for vote in self._votes:
+            if not vote.getActor()== actor:
+                raise ValueError("All votes must come from "\
+                        + str(actor) + " but found " + str(vote))
+
+        # check for duplicate Vote's, possibly with different powers
+        votedfor:List[Bid] = []
+        for vote in votes:
+            bid = vote.getBid()
+            if bid in votedfor:
+                raise ValueError("Votes contains multiple Vote's for "+ str(bid))
+            votedfor.append(bid)
+
+
+    def isExtending(self, otherVotes:Votes ) -> bool:
+        '''
+        Test if Votes extends other votes. Extending means that for each vote on
+        bid B with power P in othervotes, this contains also a vote for bid B
+        with power at most P.
+        
+        @param otherVotes the {@link Votes}, usually from a previous round, that
+                          this should extend.
+        @return true iff this extends the otherVotes.
+        '''
+        if not otherVotes.getActor()==self.getActor():
+            return False
+        for vote in otherVotes.getVotes():
+            myvote:Optional[Vote] = self.getVote(vote.getBid())
+            if myvote == None or myvote.getMinPower() > vote.getMinPower()   or myvote.getMaxPower() < vote.getMaxPower(): # type:ignore
+                return False
+
+        return True
+
+
+    def getVote(self, bid:Bid ) -> Optional[Vote] :
+        '''
+        @param bid the bid that we may have a vote for
+        @return myvote for bid, or null if no vote for that bid;
+        '''
+        for vote in self._votes:
+            if vote.getBid() == bid:
+                return vote;
+        return None;
+
+    def getVotes(self) -> Set[Vote]:
+        return self._votes.copy()
+
+    
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and \
+            self._actor==other._actor and self._votes==other._votes
+
+    def __hash__(self):
+        return hash((self._actor, tuple(self._votes)))
+    
+    def __repr__(self) -> str :
+        return "Votes[" + str(self._actor) + "," +\
+             str(self._votes) + "]"
+
Index: geniuswebcore/geniusweb/bidspace/AllBidsList.py
===================================================================
--- geniuswebcore/geniusweb/bidspace/AllBidsList.py	(revision 67)
+++ geniuswebcore/geniusweb/bidspace/AllBidsList.py	(revision 67)
@@ -0,0 +1,43 @@
+from typing import Dict, List
+
+from tudelft.utilities.immutablelist.AbstractImmutableList import AbstractImmutableList
+from tudelft.utilities.immutablelist.ImmutableList import ImmutableList
+from tudelft.utilities.immutablelist.Outer import Outer
+
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.issuevalue.Domain import Domain
+from geniusweb.issuevalue.Value import Value
+
+
+class AllBidsList (AbstractImmutableList[Bid]):
+    '''
+    A list containing all complete bids in the space. This is an
+    {@link ImmutableList} so it can contain all bids without pre-computing them.
+    '''
+
+    def __init__(self, domain:Domain):
+        '''
+        This object contains s list containing all bids in the space. This is an
+        ImmutableList so it can contain all bids without pre-computing them.
+        
+        @param domain the {@link Domain}
+        '''
+        if domain == None:
+            raise ValueError("domain=null");
+        self._issues = list(domain.getIssues())
+
+        values:List[ImmutableList[Value]] = [domain.getValues(issue) for issue in self._issues]
+        self._allValuePermutations = Outer[Value](values)
+ 
+    def  get(self, index:int) -> Bid:
+        nextValues = self._allValuePermutations.get(index);
+
+        issueValues:Dict[str, Value]  = {}
+        for n in range(len(self._issues)):
+            issueValues[self._issues[n]] = nextValues.get(n)
+
+        return Bid(issueValues);
+
+    def size(self) ->int:
+        return self._allValuePermutations.size();
+
Index: geniuswebcore/geniusweb/connection/ConnectionEnd.py
===================================================================
--- geniuswebcore/geniusweb/connection/ConnectionEnd.py	(revision 66)
+++ geniuswebcore/geniusweb/connection/ConnectionEnd.py	(revision 67)
@@ -1,7 +1,10 @@
 from abc import ABC
-from typing import TypeVar, Generic, List
+from typing import TypeVar, Generic, List, Optional
 
 from tudelft.utilities.listener.Listenable import Listenable
+from uri.uri import URI
+
 from geniusweb.references.Reference import Reference
+
 
 INTYPE = TypeVar('INTYPE')
@@ -40,5 +43,5 @@
 		
 		@param data the data to be sent.
-		@throws IOException if the data failed to be sent.
+		@throws ConnectionError if the data failed to be sent.
 		'''
 	
@@ -48,9 +51,8 @@
 		'''
 
-	def getRemoteURI(self)->str: 
+	def getRemoteURI(self)->URI: 
 		'''
 		@return the URI of the remote endpoint that makes up the connection. This
 		         is a URI that uniquely identifies the remote object.
-		         HACK for now we return str instead of URI.
 		'''
 
@@ -61,5 +63,5 @@
 		before closing the connection.
 		'''
-	def getError(self) -> Exception:
+	def getError(self) -> Optional[Exception]:
 		'''
 		@return the latest internal error, or null if no error occured. If the
Index: geniuswebcore/geniusweb/connection/ConnectionFactory.py
===================================================================
--- geniuswebcore/geniusweb/connection/ConnectionFactory.py	(revision 67)
+++ geniuswebcore/geniusweb/connection/ConnectionFactory.py	(revision 67)
@@ -0,0 +1,36 @@
+from abc import ABC, abstractmethod
+from typing import TypeVar, Generic
+
+from geniusweb.connection.ConnectionEnd import ConnectionEnd
+from geniusweb.references.Reference import Reference
+
+
+INTYPE = TypeVar('INTYPE')
+OUTTYPE = TypeVar('OUTTYPE')
+
+class ConnectionFactory(ABC,  Generic[INTYPE,OUTTYPE]):
+	'''
+	factory that can turn a {@link Reference} into a connection to that
+	reference.
+	
+	
+	@param <INTYPE>  the type of incoming messages. Incoming messages are
+	                 received through Listener#not. Incoming messages are usually
+	                 asynchronous.
+	@param <OUTTYPE> the type of outgoing messages. Outgoing messages can be sent
+	                 directly with #send.
+	'''
+	@abstractmethod
+	def connect(self, reference:Reference) -> ConnectionEnd[INTYPE, OUTTYPE]:
+		'''
+		@param reference the {@link Reference} to a {@link Connectable}
+		@return connection to provided reference
+		@throws ConnectionError         if the connection can not be made because
+		                                of a network or address related problem.
+		                                This points to something non-trivially
+		                                that may require user intervention to fix
+		                                something.
+		@throws NoResourcesNowException if the connection can not be made at this
+		                                moment because of lack of resources on
+		                                the server. This suggest to retry later.
+		'''
Index: geniuswebcore/geniusweb/events/AbstractEvent.py
===================================================================
--- geniuswebcore/geniusweb/events/AbstractEvent.py	(revision 67)
+++ geniuswebcore/geniusweb/events/AbstractEvent.py	(revision 67)
@@ -0,0 +1,35 @@
+from abc import ABC
+import time
+from geniusweb.events.NegotiationEvent import NegotiationEvent
+
+
+
+class AbstractEvent (NegotiationEvent, ABC):
+
+	def __init__(self, now:int=round(1000*time.time())):
+		'''
+		@param now the current time to use. see {@link System#currentTimeMillis()}.
+		Defaults to current time.
+		'''
+		if now == None or now < 0:
+			raise ValueError("time must be non-null and positive")
+
+		self._time = now;
+
+
+	def getTime(self) ->int:
+		return self._time
+
+
+	def __repr__(self) ->str:
+		return str(self.__class__.__name__) + "[" + str(self._time) + "]";
+
+	def __hash__(self):
+		return hash(self._time)
+
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and \
+			self._time==other._time
+
+
Index: geniuswebcore/geniusweb/events/ActionEvent.py
===================================================================
--- geniuswebcore/geniusweb/events/ActionEvent.py	(revision 67)
+++ geniuswebcore/geniusweb/events/ActionEvent.py	(revision 67)
@@ -0,0 +1,34 @@
+from geniusweb.actions.Action import Action
+from geniusweb.events.AbstractEvent import AbstractEvent
+class ActionEvent ( AbstractEvent):
+	'''
+	Event where a party did an {@link Action}
+	'''
+
+	def __init__(self, action:Action ,	time:int) :
+		'''
+		@param action the {@link Action} that was done
+		@param time   the current time to use. in millisec since 1970.
+		'''
+		super().__init__(time)
+		if not action:
+			raise ValueError("Action must not be null")
+		self._action = action
+
+	def getAction(self)->Action :
+		'''
+		@return action done by some party in the system.
+		'''
+		return self._action
+
+	
+	def __repr__(self):
+		return type(self).__name__ + "[" + str(self.getTime())\
+			 + "," + str(self._action) + "]"
+
+	def __hash__(self):
+		return hash((self._action, self._time))
+
+	def __eq__(self, other):
+		return super().__eq__(other) and \
+			self._actions==other._action
Index: geniuswebcore/geniusweb/events/CurrentState.py
===================================================================
--- geniuswebcore/geniusweb/events/CurrentState.py	(revision 67)
+++ geniuswebcore/geniusweb/events/CurrentState.py	(revision 67)
@@ -0,0 +1,21 @@
+from geniusweb.events.ProtocolEvent import ProtocolEvent
+class CurrentState( ProtocolEvent ):
+	'''
+	Reports a NegoState as the latest state of the protocol. The negoState is
+	abstract because the state is not something that can be handled at this place
+	in the dependency hierarchy. The "real" implementations are inside the
+	protocol.
+	<p>
+	When a protocol reports this event, it indicates that this state can be
+	logged to the loggers for output to the users (publication on the webpages
+	and reporting to parent protocols eg a tournamentrunner). Usually protocols
+	are updating this only after a session completes. There may be quite some
+	overhead triggered by emission of these events, such as rewriting files and
+	notifying end users behind websockets. Protocols MUST report this at least
+	once to report the final isFinished() state.
+	<p>
+	The logged state may be used to re-start a protocol if it crashes later in an
+	unfinished state.
+	'''
+	# all should implement NegoState getState();
+
Index: geniuswebcore/geniusweb/events/NegotiationEvent.py
===================================================================
--- geniuswebcore/geniusweb/events/NegotiationEvent.py	(revision 67)
+++ geniuswebcore/geniusweb/events/NegotiationEvent.py	(revision 67)
@@ -0,0 +1,26 @@
+from abc import abstractmethod, ABC
+
+from pyson.JsonSubTypes import JsonSubTypes
+
+
+@JsonSubTypes([ "ActionEvent","SessionStarted","TournamentStarted"])
+class NegotiationEvent (ABC):
+	'''
+	 reports some event happened in the negotiation system. Generally, parties are
+	 informed about events in the system, but this depends on the protocol.
+	 '''
+	@abstractmethod
+	def getTime(self)->int:
+		'''
+		@return the time at which the event happened on the server, measured in
+		        milliseconds, between the start time and midnight, January 1,
+		        1970 UTCas. See also {@link System#currentTimeMillis()}. <br>
+		
+		        If the event is about an {@link Action} done by a negotiation
+		        party on another machine, this time refers to the time the action
+		        was handled on the server (which may differ from the clock time
+		        on the other machine). <br>
+		
+		        Note: we do not use RFC 3339 because we need millisecond
+		        accuracy.
+		'''
Index: geniuswebcore/geniusweb/events/ProtocolEvent.py
===================================================================
--- geniuswebcore/geniusweb/events/ProtocolEvent.py	(revision 67)
+++ geniuswebcore/geniusweb/events/ProtocolEvent.py	(revision 67)
@@ -0,0 +1,9 @@
+from geniusweb.events.AbstractEvent import AbstractEvent
+
+class ProtocolEvent (AbstractEvent):
+	'''
+	an event triggered by the protocol.
+	'''
+	
+	def ProtocolEvent(self, now:int):
+		super().__init__(now)
Index: geniuswebcore/geniusweb/events/SessionStarted.py
===================================================================
--- geniuswebcore/geniusweb/events/SessionStarted.py	(revision 67)
+++ geniuswebcore/geniusweb/events/SessionStarted.py	(revision 67)
@@ -0,0 +1,28 @@
+from typing import List
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.events.ProtocolEvent import ProtocolEvent
+class SessionStarted (ProtocolEvent):
+
+	def __init__(self, sessionNumber:int, parties: List[PartyId], time:int):
+		super().__init__(time)
+		self._sessionNumber = sessionNumber
+		self._parties = parties
+
+	def getParties(self) -> List[PartyId]:
+		return self._parties
+
+	def getSessionNumber(self)->int:
+		return self._sessionNumber
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and \
+			super().__eq__(other) and self._parties==other._parties \
+				and self._sessionNumber==other._sessionNumber
+
+	def __hash__(self):
+		return hash((self._time, self._sessionNumber, tuple(self._parties)))
+		
+	def __repr__(self)->str:
+		return "SessionStarted[" + str(self._sessionNumber) + "," +\
+			str(self._parties) + "," + str(self.getTime())+ "]"
+
Index: geniuswebcore/geniusweb/events/TournamentStarted.py
===================================================================
--- geniuswebcore/geniusweb/events/TournamentStarted.py	(revision 67)
+++ geniuswebcore/geniusweb/events/TournamentStarted.py	(revision 67)
@@ -0,0 +1,64 @@
+package geniusweb.events;
+
+public class TournamentStarted extends ProtocolEvent {
+	private Long numberOfSessions;
+
+	@SuppressWarnings("unused") // deserialization
+	private TournamentStarted() {
+		numberOfSessions = null;
+	}
+
+	/**
+	 * 
+	 * @param numberOfSessions the total number of sessions in the tournament
+	 */
+	public TournamentStarted(Long numberOfSessions) {
+		this.numberOfSessions = numberOfSessions;
+	}
+
+	/**
+	 * 
+	 * @param numberOfSessions the total number of sessions in the tournament
+	 * @param now              the current timestamp
+	 */
+	public TournamentStarted(Long numberOfSessions, Long now) {
+		super(now);
+		this.numberOfSessions = numberOfSessions;
+	}
+
+	public Long getNumberOfSessions() {
+		return numberOfSessions;
+	}
+
+	@Override
+	public String toString() {
+		return "TournamentStarted[" + getTime() + "," + numberOfSessions + "]";
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + ((numberOfSessions == null) ? 0
+				: numberOfSessions.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		TournamentStarted other = (TournamentStarted) obj;
+		if (numberOfSessions == null) {
+			if (other.numberOfSessions != null)
+				return false;
+		} else if (!numberOfSessions.equals(other.numberOfSessions))
+			return false;
+		return true;
+	}
+
+}
Index: geniuswebcore/geniusweb/inform/Agreements.py
===================================================================
--- geniuswebcore/geniusweb/inform/Agreements.py	(revision 66)
+++ geniuswebcore/geniusweb/inform/Agreements.py	(revision 67)
@@ -7,4 +7,5 @@
 from geniusweb.actions.PartyId import PartyId
 from geniusweb.issuevalue.Bid import Bid
+from geniusweb.utils import toStr
 
 
@@ -28,5 +29,5 @@
 		return self._agreements
 
-	def With(self, other:Agreements ) ->Agreements :
+	def With(self, other:"Agreements" ) ->"Agreements" :
 		'''
 		* @param other {@link Agreements}
@@ -50,5 +51,5 @@
 
 	def __repr__(self): 
-		return "Agreements" + str(self._agreements)
+		return "Agreements" + toStr(self._agreements)
 	
 	def __eq__(self, other):
Index: geniuswebcore/geniusweb/inform/Inform.py
===================================================================
--- geniuswebcore/geniusweb/inform/Inform.py	(revision 66)
+++ geniuswebcore/geniusweb/inform/Inform.py	(revision 67)
@@ -1,11 +1,16 @@
 from abc import ABC
+
 from pyson.JsonSubTypes import JsonSubTypes
+from pyson.JsonTypeInfo import Id, As
 from pyson.JsonTypeInfo import JsonTypeInfo
-from pyson.JsonTypeInfo import Id,As
+
 
 @JsonSubTypes(["geniusweb.inform.Settings.Settings",\
 			"geniusweb.inform.YourTurn.YourTurn",\
 			"geniusweb.inform.ActionDone.ActionDone",\
-			"geniusweb.inform.Finished.Finished"])
+			"geniusweb.inform.Finished.Finished",\
+			"geniusweb.inform.OptIn.OptIn",\
+			"geniusweb.inform.Voting.Voting"\
+			])
 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
 class Inform(ABC):
Index: geniuswebcore/geniusweb/inform/OptIn.py
===================================================================
--- geniuswebcore/geniusweb/inform/OptIn.py	(revision 67)
+++ geniuswebcore/geniusweb/inform/OptIn.py	(revision 67)
@@ -0,0 +1,40 @@
+from typing import List
+
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.actions.Votes import Votes
+from geniusweb.inform.Inform import Inform
+
+
+class OptIn (Inform):
+    '''
+    Informs party that it's time to Opt-in.
+
+    '''
+    def __init__(self, votes:List[Votes] ) :
+        '''
+        @param votes a list of votes. There may be only one votes per party.
+        '''
+        self._votes = votes;
+
+        parties:List[PartyId] = []
+        for vots in votes:
+            partyid = vots.getActor()
+            if partyid in parties:
+                raise ValueError("OptIn contains multiple Votes for party " + str(partyid))
+            parties.append(partyid)
+
+    def getVotes(self)->List[Votes] :
+        '''
+        @return list of Votes that can be opted in to
+        '''
+        return list(self._votes);
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self._votes==other._votes
+
+    def __hash__(self):
+        return hash(tuple(self._votes))
+    
+    def __repr__(self) -> str :
+        return "OptIn[" + str(self._votes) + "]"
+
Index: geniuswebcore/geniusweb/inform/Voting.py
===================================================================
--- geniuswebcore/geniusweb/inform/Voting.py	(revision 67)
+++ geniuswebcore/geniusweb/inform/Voting.py	(revision 67)
@@ -0,0 +1,39 @@
+from typing import Dict, List
+
+from geniusweb.actions.Offer import Offer
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.inform.Inform import Inform
+
+
+class Voting (Inform):
+    '''
+    Informs a party that it's time to do voting.
+    '''
+    def __init__(self, offers:List[Offer] , powers:Dict[PartyId,int] ):
+        self._offers = offers
+        self._powers = powers
+
+    def getOffers(self) -> List[Offer] :
+        '''
+        @return list of bids to be voted on
+        '''
+        return list(self._offers);
+
+    def getPowers(self) -> Dict[PartyId, int]:
+        '''
+        @return map with the power of each party in the negotiation. This map may
+                contain parties that are already finished, eg they reached an
+                 agreement or walked away.
+        '''
+        return dict(self._powers)
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and \
+            self._offers==other._offers and self._powers==other._powers
+
+    def __hash__(self):
+        return hash((tuple(self._offers), tuple(self._powers.items())))
+
+    def __repr__(self)->str :
+        return "Voting[" + str(self._offers) + "," + str(self._powers) + "]";
+
Index: geniuswebcore/geniusweb/issuevalue/Bid.py
===================================================================
--- geniuswebcore/geniusweb/issuevalue/Bid.py	(revision 66)
+++ geniuswebcore/geniusweb/issuevalue/Bid.py	(revision 67)
@@ -1,4 +1,5 @@
-from typing import Any, Dict, Union
+from typing import Any, Dict, Union, Set
 from geniusweb.issuevalue.Value import Value
+from geniusweb.utils import toStr
 
 NoneType=type(None)
@@ -7,6 +8,8 @@
     def __init__(self, issuevalues:Dict[str, Value]):
         '''
-        FIXME use strict typing
+        @param  a map of issue, Value pairs. The String is the issue name.
         '''
+        if not isinstance(issuevalues,dict):
+            raise ValueError("issuevalues must be dict, got "+str(issuevalues))
         for (iss,val) in issuevalues.items():
             if not isinstance(iss, str):
@@ -17,8 +20,13 @@
 
     def getIssueValues(self) -> Dict[str, Value]:
+        '''
+        @param issue name of the issue
+        @return the value for the given issue, or null if there is no value for
+                the given issue.
+        '''
         return self._issuevalues
     
-    def getIssues(self):
-        return self._issuevalues.keys()
+    def getIssues(self)->Set[str]:
+        return set(self._issuevalues.keys())
     
     
@@ -33,4 +41,23 @@
         return self._issuevalues[issue];
     
+    def merge(self,  otherbid:"Bid")->"Bid" :
+        '''
+        this this partial bid with another partial bid.
+        
+        @param otherbid another partial bid.
+        @return a bid with the combined values of both partial bids.
+        @throws IllegalArgumentException if issues overlap.
+        '''
+        ourissues = set(self._issuevalues.keys())
+        ourissues = ourissues.intersection(otherbid.getIssues())
+        if len(ourissues)!=0:
+            raise ValueError(\
+                "Otherbid contains issues that are already set:"\
+                            + str(ourissues))
+            
+        newvalues = dict(self._issuevalues)
+        newvalues.update(otherbid._issuevalues)
+        return Bid(newvalues)
+    
     def __eq__(self, other):
         return isinstance(other, self.__class__) and \
@@ -38,5 +65,5 @@
 
     def __repr__(self) -> str:
-        return "Bid" + repr( self._issuevalues);
+        return "Bid" + toStr(self._issuevalues);
     
     def __hash__(self):
Index: geniuswebcore/geniusweb/issuevalue/DiscreteValue.py
===================================================================
--- geniuswebcore/geniusweb/issuevalue/DiscreteValue.py	(revision 66)
+++ geniuswebcore/geniusweb/issuevalue/DiscreteValue.py	(revision 67)
@@ -19,5 +19,5 @@
 
 	def __repr__(self): 
-		return str(self._value)
+		return '"'+str(self._value)+'"'
 	
 	def __eq__(self, other):
Index: geniuswebcore/geniusweb/issuevalue/DiscreteValueSet.py
===================================================================
--- geniuswebcore/geniusweb/issuevalue/DiscreteValueSet.py	(revision 66)
+++ geniuswebcore/geniusweb/issuevalue/DiscreteValueSet.py	(revision 67)
@@ -1,5 +1,5 @@
 from copy import copy
 from decimal import Decimal
-from typing import List
+from typing import List, Collection
 
 from geniusweb.issuevalue.DiscreteValue import DiscreteValue
@@ -10,18 +10,21 @@
 class DiscreteValueSet (ValueSet):
 
-	def __init__(self, values: List[DiscreteValue]) :
-		self._values= copy(values)
+	def __init__(self, values: Collection[DiscreteValue]) :
+		self._values:List[DiscreteValue]=[]
+		for val in values:
+			if val not in self._values:
+				self._values.append(val)
 
 	def getValues(self) -> List[DiscreteValue]:
-		return copy(self._values)
+		return list(self._values)
 
 	def contains(self, value:Value ) ->bool:
 		return value in self._values
 
-	def get(self,index:Decimal) -> DiscreteValue :
+	def get(self,index:int) -> DiscreteValue :
 		return self._values[int(index)]
 
-	def size(self) -> Decimal :
-		return Decimal(len(self._values))
+	def size(self) -> int :
+		return len(self._values)
 
 	def __repr__(self):
@@ -30,6 +33,7 @@
 
 	def __eq__(self, other):
-		return isinstance(other, self.__class__) and self._values==other._values
+		return isinstance(other, self.__class__) and set(self._values)==set(other._values)
 
 	def __hash__(self):
-		return hash((tuple(self._values))) 
+		# don't use tuple because order is irrelevant.
+		return sum([hash(val) for val in self._values]) 
Index: geniuswebcore/geniusweb/issuevalue/Domain.py
===================================================================
--- geniuswebcore/geniusweb/issuevalue/Domain.py	(revision 66)
+++ geniuswebcore/geniusweb/issuevalue/Domain.py	(revision 67)
@@ -9,5 +9,5 @@
 class Domain:
 	'''
-	A dmain description. 
+	A domain description. 
 	'''
 
Index: geniuswebcore/geniusweb/issuevalue/NumberValueSet.py
===================================================================
--- geniuswebcore/geniusweb/issuevalue/NumberValueSet.py	(revision 66)
+++ geniuswebcore/geniusweb/issuevalue/NumberValueSet.py	(revision 67)
@@ -8,5 +8,7 @@
 
 class NumberValueSet ( ValueSet ):
-
+	'''
+	number range from low to high with given step size
+	'''
 	def __init__(self,  range:Range ):
 		if not isinstance(range, Range):
Index: geniuswebcore/geniusweb/profile/Profile.py
===================================================================
--- geniuswebcore/geniusweb/profile/Profile.py	(revision 66)
+++ geniuswebcore/geniusweb/profile/Profile.py	(revision 67)
@@ -1,7 +1,9 @@
 from abc import ABC
+from typing import Optional
 
 from pyson.JsonSubTypes import JsonSubTypes
 from pyson.JsonTypeInfo import Id, As
 from pyson.JsonTypeInfo import JsonTypeInfo
+
 from geniusweb.issuevalue.Bid import Bid
 from geniusweb.issuevalue.Domain import Domain
@@ -29,5 +31,5 @@
 		'''
 		
-	def getReservationBid(self) -> Bid :
+	def getReservationBid(self) -> Optional[Bid] :
 		'''
 		@return a (hypothetical) bid that is the best alternative to a
@@ -35,5 +37,5 @@
 		        accepted. If a negotiation does not reach an agreement, the party
 		        can get this offer somewhere else. This replaces the older notion
-		        of a "reservation value" and is more general. If null, there is
+		        of a "reservation value" and is more general. If None, there is
 		        no reservation bid and any agreement is better than no agreement.
 		        This bid can be partial.
Index: geniuswebcore/geniusweb/profile/utilityspace/DiscreteValueSetUtilities.py
===================================================================
--- geniuswebcore/geniusweb/profile/utilityspace/DiscreteValueSetUtilities.py	(revision 66)
+++ geniuswebcore/geniusweb/profile/utilityspace/DiscreteValueSetUtilities.py	(revision 67)
@@ -1,5 +1,5 @@
 from copy import copy
 from decimal import Decimal
-from typing import Dict, cast
+from typing import Dict, cast, Optional
 
 from pyson.JsonGetter import JsonGetter
@@ -10,4 +10,5 @@
 from geniusweb.issuevalue.ValueSet import ValueSet
 from geniusweb.profile.utilityspace.ValueSetUtilities import ValueSetUtilities
+from geniusweb.utils import toStr
 
 
@@ -47,5 +48,5 @@
 
 	#Override
-	def getUtility(self,  value:Value) -> Decimal:
+	def getUtility(self,  value:Optional[Value]) -> Decimal:
 		if not value in self._valueUtilities:
 			return Decimal("0")
@@ -74,5 +75,5 @@
 
 	def __repr__(self):
-		return "DiscreteValueSetUtilities" + str(self._valueUtilities)
+		return "DiscreteValueSetUtilities" + toStr(self._valueUtilities)
 	
 
Index: geniuswebcore/geniusweb/profile/utilityspace/LinearAdditiveUtilitySpace.py
===================================================================
--- geniuswebcore/geniusweb/profile/utilityspace/LinearAdditiveUtilitySpace.py	(revision 66)
+++ geniuswebcore/geniusweb/profile/utilityspace/LinearAdditiveUtilitySpace.py	(revision 67)
@@ -2,5 +2,5 @@
 from decimal import Decimal
 import re
-from typing import Dict, cast, Union
+from typing import Dict, cast, Union, Optional
 
 from pyson.JsonGetter import JsonGetter
@@ -12,4 +12,5 @@
 from geniusweb.profile.utilityspace.ValueSetUtilities import ValueSetUtilities
 
+from geniusweb.utils import toStr
 
 class LinearAdditiveUtilitySpace (LinearAdditive): 
@@ -28,5 +29,5 @@
 
 	def __init__(self,  domain:Domain , name:str,  issueUtilities: Dict[str, ValueSetUtilities],\
-			issueWeights: Dict[str, Decimal] , reservationBid:Union[Bid,type(None)] = None ):
+			issueWeights: Dict[str, Decimal] , reservationBid:Optional[Bid] = None ):
 		'''
 		@param domain  the {@link Domain} in which this profile is defined.
@@ -64,5 +65,5 @@
 			raise ValueError(\
 				"One of the ValueSetUtilities in issueUtilitiesis null:"\
-				+ issueUtilities)
+				+ str(issueUtilities))
 
 		if None in issueWeights.values():
@@ -88,22 +89,22 @@
 
 		for issue in  issueUtilities:
-			message:str = issueUtilities.get(issue).isFitting(domain.getValues(issue));
+			message:str = issueUtilities.get(issue).isFitting(domain.getValues(issue)) #type:ignore
 			if message != None:
 				raise ValueError(message);
 
-		total:Decimal = sum(issueWeights.values())
+		total:Decimal = sum(issueWeights.values()) #type:ignore
 		if total!=Decimal(1):
 			raise ValueError("The sum of the weights ("
 					+ str(issueWeights.values()) + ") must be 1")
 		
-		if reservationBid != None:
-			message:str = domain.isFitting(reservationBid)
-			if message:
-				raise ValueError("reservationbid is not fitting domain: " + message)
+		if reservationBid:
+			message1:Optional[str] = domain.isFitting(reservationBid)
+			if message1:
+				raise ValueError("reservationbid is not fitting domain: " + message1)
 
 
 	#Override
 	def getUtility(self, bid:Bid ) -> Decimal:
-		return sum([ self._util(iss, bid.getValue(iss)) for iss in self._issueWeights.keys() ])
+		return sum([ self._util(iss, bid.getValue(iss)) for iss in self._issueWeights.keys() ]) #type:ignore
 
 	#Override
@@ -113,9 +114,10 @@
 	#Override
 	def __repr__(self):
-		return "LinearAdditive[" + str(self._issueUtilities) + "," + \
-			str(self._issueWeights) + "," + str(self._reservationBid) + "]"
+		return "LinearAdditive[" + toStr(self._issueUtilities) + "," + \
+			toStr(self._issueWeights) + "," + str(self._reservationBid) + "]"
+
 
 	#Override
-	def getReservationBid(self) -> Bid :
+	def getReservationBid(self) -> Optional[Bid]: 
 		return self._reservationBid;
 	
@@ -152,11 +154,13 @@
 		return copy(self._issueWeights)
 
-	def _util(self, issue:str, value:Value ) ->Decimal:
+	def _util(self, issue:str, value:Optional[Value] ) ->Decimal:
 		'''
 		@param issue the issue to get weighted util for
-		@param value the issue value to use (typically coming from a bid)
+		@param value the issue value to use (typically coming from a bid). Can be None
 		@return weighted util of just the issue value:
 		        issueUtilities[issue].utility(value) * issueWeights[issue)
 		'''
+		if not value:
+			return Decimal(0)
 		return self._issueWeights[issue] * self._issueUtilities[issue].getUtility(value)
 
Index: geniuswebcore/geniusweb/profile/utilityspace/NumberValueSetUtilities.py
===================================================================
--- geniuswebcore/geniusweb/profile/utilityspace/NumberValueSetUtilities.py	(revision 66)
+++ geniuswebcore/geniusweb/profile/utilityspace/NumberValueSetUtilities.py	(revision 67)
@@ -45,5 +45,5 @@
 
 	#Override
-	def getUtility(self, value:Value ) -> Decimal :
+	def getUtility(self, value:Optional[Value] ) -> Decimal :
 		if not isinstance(value, NumberValue):
 			return Decimal("0");
Index: geniuswebcore/geniusweb/profile/utilityspace/ValueSetUtilities.py
===================================================================
--- geniuswebcore/geniusweb/profile/utilityspace/ValueSetUtilities.py	(revision 66)
+++ geniuswebcore/geniusweb/profile/utilityspace/ValueSetUtilities.py	(revision 67)
@@ -19,5 +19,5 @@
 	'''
 
-	def getUtility(self, value:Value )->Decimal:
+	def getUtility(self, value:Optional[Value] )->Decimal:
 		'''
 		@param value the {@link Value} to get the utility for
Index: geniuswebcore/geniusweb/profileconnection/FileProfileConnector.py
===================================================================
--- geniuswebcore/geniusweb/profileconnection/FileProfileConnector.py	(revision 66)
+++ geniuswebcore/geniusweb/profileconnection/FileProfileConnector.py	(revision 67)
@@ -21,5 +21,5 @@
 		'''
 		serialized = Path(filename).read_text("utf-8")
-		self._profile:Profile = self._pyson.parse(json.loads(serialized), Profile)
+		self._profile:Profile = self._pyson.parse(json.loads(serialized), Profile) #type:ignore
 	
 	#Override
Index: geniuswebcore/geniusweb/profileconnection/ProfileConnectionFactory.py
===================================================================
--- geniuswebcore/geniusweb/profileconnection/ProfileConnectionFactory.py	(revision 66)
+++ geniuswebcore/geniusweb/profileconnection/ProfileConnectionFactory.py	(revision 67)
@@ -1,4 +1,4 @@
 from tudelft_utilities_logging.Reporter import Reporter
-from uri import URI  # type:ignore
+from uri.uri import URI  # type:ignore
 
 from geniusweb.profileconnection.FileProfileConnector import FileProfileConnector
Index: geniuswebcore/geniusweb/profileconnection/WebSocketContainer.py
===================================================================
--- geniuswebcore/geniusweb/profileconnection/WebSocketContainer.py	(revision 66)
+++ geniuswebcore/geniusweb/profileconnection/WebSocketContainer.py	(revision 67)
@@ -2,5 +2,5 @@
 import threading
 
-from uri import URI  # type:ignore
+from uri.uri import URI  # type:ignore
 from websocket._app import WebSocketApp  # type:ignore
 
Index: geniuswebcore/geniusweb/profileconnection/WebsocketProfileConnector.py
===================================================================
--- geniuswebcore/geniusweb/profileconnection/WebsocketProfileConnector.py	(revision 66)
+++ geniuswebcore/geniusweb/profileconnection/WebsocketProfileConnector.py	(revision 67)
@@ -7,5 +7,5 @@
 from tudelft.utilities.listener.DefaultListenable import DefaultListenable
 from tudelft_utilities_logging.Reporter import Reporter
-from uri import URI  # type:ignore
+from uri.uri import URI  # type:ignore
 import websocket  # type:ignore
 
Index: geniuswebcore/geniusweb/progress/ProgressFactory.py
===================================================================
--- geniuswebcore/geniusweb/progress/ProgressFactory.py	(revision 66)
+++ geniuswebcore/geniusweb/progress/ProgressFactory.py	(revision 67)
@@ -21,5 +21,5 @@
 		if isinstance(deadline,DeadlineRounds):
 			return ProgressRounds( deadline.getRounds(),
-					0, datetime.fromtimestamp(nowms + deadline.getDuration()))
+					0, datetime.fromtimestamp( (nowms + deadline.getDuration())/1000.))
 		else:
-			return ProgressTime(deadline.getDuration(),datetime.fromtimestamp(nowms))
+			return ProgressTime(deadline.getDuration(),datetime.fromtimestamp(nowms/1000.))
Index: geniuswebcore/geniusweb/progress/ProgressRounds.py
===================================================================
--- geniuswebcore/geniusweb/progress/ProgressRounds.py	(revision 66)
+++ geniuswebcore/geniusweb/progress/ProgressRounds.py	(revision 67)
@@ -74,5 +74,5 @@
 			currentTimeMs > int(1000 * datetime.timestamp(self._endtime))
 
-	def advance(self) -> ProgressRounds :
+	def advance(self) -> "ProgressRounds" :
 		'''
 		@return new ProgressRounds with round 1 advanced (or this, if
Index: geniuswebcore/geniusweb/protocol/CurrentNegoState.py
===================================================================
--- geniuswebcore/geniusweb/protocol/CurrentNegoState.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/CurrentNegoState.py	(revision 67)
@@ -0,0 +1,31 @@
+import time
+
+from geniusweb.events.CurrentState import CurrentState
+from geniusweb.protocol.NegoState import NegoState
+
+
+class CurrentNegoState (CurrentState):
+	'''
+	Implementation of CurrentState event. This contains NegoState and therefore
+	can not be defined in the events module.
+	'''
+
+	def __init__(self,  state:NegoState, now:int=round(1000*time.time())) :
+		super().__init__(now)
+		self._state = state;
+
+
+	def getState(self) -> NegoState:
+		return self._state
+
+		
+	def __repr__(self):
+		return "CurrentNegoState[" + str(self.getTime())\
+			 + "," + str(self._state) + "]"
+
+	def __hash__(self):
+		return hash((self._state, self._time))
+
+	def __eq__(self, other):
+		return super().__eq__(other) and \
+			self._state==other._state
Index: geniuswebcore/geniusweb/protocol/NegoProtocol.py
===================================================================
--- geniuswebcore/geniusweb/protocol/NegoProtocol.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/NegoProtocol.py	(revision 67)
@@ -0,0 +1,97 @@
+from typing import TYPE_CHECKING
+from pyson.JsonSubTypes import JsonSubTypes
+from tudelft.utilities.listener.Listenable import Listenable
+
+from geniusweb.events.ProtocolEvent import ProtocolEvent
+if TYPE_CHECKING:
+	from geniusweb.protocol.NegoState import NegoState
+from geniusweb.protocol.partyconnection.ProtocolToPartyConnFactory import ProtocolToPartyConnFactory
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.references.ProtocolRef import ProtocolRef
+
+
+@JsonSubTypes(["SessionProtocol"])
+class NegoProtocol (Listenable[ProtocolEvent]):
+	'''
+	Abstract interface to all negotiation protocols, both single session and
+	tournaments. Generally a protocol handles events on the provided connections
+	according to the rules set by the protocol. The rules are explained by
+	{@link #getDescription()}. A protocol reports the progress through its
+	{@link Listenable} interface. <br>
+	<h2>General information</h2>
+	
+	A protocol is mutable because the incoming connections cause state changes.
+	But it is recommended to push changing properties into the state so that the
+	complete state can be recovered and analysed.
+	<p>
+	Because a protocol contains an internal state, it can be used only once.
+	<p>
+	The protocol can emit a {@link CurrentState} event at any time. It should do
+	so at least once, to log the final outcome of the nego.
+	<p>
+	
+	Normally the constructor will receive a {@link ConnectionFactory} through
+	which it can resolve received {@link Reference}s. <br>
+	First call to instances <b>must be</b>
+	{@link #start(ProtocolToPartyConnFactory)}.
+	
+	<h2>Ensure time deadline</h2>
+	
+	The protocol also needs to keep an eye on the deadline and take appropriate
+	actions when the deadline is reached. <br>
+	<p>
+	All protocol implementations must ensure that the deadline is kept and that
+	the session ends at the agreed time {@link Deadline#getDuration()}. This is
+	to ensure that the negotiation ends and resources are freed up at or before
+	some known time.
+	'''
+	def start(self, connectionfactory:ProtocolToPartyConnFactory):
+		'''
+		Start the protocol: make connection with parties and follow the protocol.
+		This must be called once and should be the first call after construction.
+		<p>
+		
+		The protocol implementation should not start any real work (eg making
+		connections) before this point. That also allows us to construct protocol
+		instances just to fetch the description.
+		<p>
+		
+		All errors are to be handled through {@link SessionState#getResult()}
+		except for bugs that use {@link RuntimeException} like
+		{@link IllegalArgumentException}s. <br>
+		The protocol usually uses the incoming connections to keep running. It
+		does not need to run in a separate thread or so.
+		
+		
+		@param connectionfactory the {@link ProtocolToPartyConnFactory} that
+		                         allows the protocol to connect with the
+		                        {@link Reference}s in the settings
+		'''
+
+	def getDescription(self)-> str:
+		'''
+		@return a complete description of how this protocol behaves. Presented to
+		        the end users who should know negotiation basics but not all
+		        technical terms.
+		'''
+	def getState(self)->"NegoState" :
+		'''
+		@return current state: the results of all sessions run so far and how to
+		        continue from that point. Changes over time as the session
+		        proceeds. Errors are also stored in the state.
+		'''
+
+	 
+	def getRef(self) -> ProtocolRef :
+		'''
+		@return the {@link ProtocolRef} for this protocol
+		'''
+
+	def addParticipant(self, party:PartyWithProfile ):
+		'''
+		Add a party after the protocol has started. Only some protocols can
+		handle this call. Usual protocols take the settings with their
+		constructor.
+		@param party the {@link PartyWithProfile} to be added.
+		'''
+
Index: geniuswebcore/geniusweb/protocol/NegoSettings.py
===================================================================
--- geniuswebcore/geniusweb/protocol/NegoSettings.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/NegoSettings.py	(revision 67)
@@ -0,0 +1,33 @@
+from abc import ABC, abstractmethod
+
+from pyson.JsonSubTypes import JsonSubTypes
+from pyson.JsonTypeInfo import JsonTypeInfo, As, Id
+from tudelft_utilities_logging.Reporter import Reporter
+
+from geniusweb.protocol.NegoProtocol import NegoProtocol
+
+
+@JsonSubTypes( ["geniusweb.protocol.session.saop.SAOPSettings.SAOPSettings" ])
+@JsonTypeInfo(use = Id.NAME, include = As.WRAPPER_OBJECT)
+class NegoSettings(ABC):
+	'''
+	Interface for negotiation settings. A negotiation can be either a single
+	session or a tournament
+	'''
+	@abstractmethod
+	def getMaxRunTime(self) -> float:
+		'''
+		@return the maximum run time (seconds). In deterministic runs this can be
+		        an exact number (#sessions * runtime per session) but this
+		        interface also allows more random tournament protocols. The
+		        protocol should stick closely with the maximum it provides to
+		        enable planning of tournaments properly.
+		'''
+
+	@abstractmethod
+	def getProtocol(self,  logger:Reporter)->NegoProtocol :
+		'''
+		@param logger the logger where the protocol can log events to.
+		@return an initialized and ready to use {@link NegoProtocol} that can
+		        handle this Negotiation.
+		'''
Index: geniuswebcore/geniusweb/protocol/NegoState.py
===================================================================
--- geniuswebcore/geniusweb/protocol/NegoState.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/NegoState.py	(revision 67)
@@ -0,0 +1,35 @@
+from abc import ABC
+
+from pyson.JsonSubTypes import JsonSubTypes
+from pyson.JsonTypeInfo import Id, As
+from pyson.JsonTypeInfo import JsonTypeInfo
+
+from geniusweb.protocol.NegoSettings import NegoSettings
+
+
+@JsonSubTypes([ "geniusweb.protocol.session.saop.SAOPState.SAOPState"])
+@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
+class NegoState (ABC):
+	'''
+	The current state of the session/tournament. Must be (de)serializabl;e so
+	that it can be restarted if a crash occurs. Notice that this restart
+	functionality is not yet available.
+	<p>
+	In general the state contains all the information to control the flow of the
+	negotiation: who did what, are we finished, etc. This object may be stored to
+	record the final result as well
+	'''
+	def getSettings(self) -> NegoSettings: 
+		'''
+		@return the settings that were used to create this Nego
+		'''
+
+	def isFinal(self, currentTimeMsLint)-> bool :
+		'''
+		@param currentTimeMs the current time in ms since 1970, see
+		                      {@link System#currentTimeMillis()}
+		@return true iff this is the final state. A state can be final because
+		        the protocol decided so, eg a deal was achieved, the deadline was
+		        reached or someone made a protocol error. If true, no more state
+		        changes can occur, including no more protocol errors.
+		'''
Index: geniuswebcore/geniusweb/protocol/ProtocolException.py
===================================================================
--- geniuswebcore/geniusweb/protocol/ProtocolException.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/ProtocolException.py	(revision 67)
@@ -0,0 +1,35 @@
+from typing import Optional
+
+from geniusweb.actions.PartyId import PartyId
+
+
+class ProtocolException (Exception) :
+	'''
+	thrown if a Party violates the protocols (that includes disconnecting).
+	'''
+
+	def __init__(self, message:str, party:Optional[PartyId] ,  e:Optional[Exception]=None):
+		'''
+		ProtocolException is special, in that it does not auto-fill the
+		stacktrace. This is needed because usually a ProtocolException is caused
+		by a party doing a bad action. Creating a stacktrace pointing to the
+		class reporting the protocol exception (usually, the protocol handler)
+		makes no sense as the protocol handler is doing the correct job there.
+		
+		
+		@param message the error message
+		@param party   offending/failing {@link PartyId}. In exceptional cases this can
+			be null, eg if it can not be determined which party
+			failed. Null should be avoided if possible at all.
+		@param e       the cause of the error, none if no cause
+		'''
+		super().__init__(str(party) + ":" + message)
+		self.__cause__=e
+		self._party = party
+
+
+	def getParty(self) -> Optional[PartyId] :
+		'''
+		@return offending party, either the {@link PartyId} or a {@link PartyRef}
+		'''
+		return self._party;
Index: geniuswebcore/geniusweb/protocol/partyconnection/ProtocolToPartyConn.py
===================================================================
--- geniuswebcore/geniusweb/protocol/partyconnection/ProtocolToPartyConn.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/partyconnection/ProtocolToPartyConn.py	(revision 67)
@@ -0,0 +1,19 @@
+from abc import ABC, abstractmethod
+
+from geniusweb.actions.Action import Action
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.connection.ConnectionEnd import ConnectionEnd
+from geniusweb.inform.Inform import Inform
+
+
+class ProtocolToPartyConn (ConnectionEnd[Action, Inform], ABC):
+	'''
+	Connection of the protocol with a party. Note, this is the opposite direction
+	of a connection of the party with the protocol.
+	'''
+
+	@abstractmethod
+	def getParty(self)-> PartyId :
+		'''
+		@return the partyId of the party that this connects to.
+		'''
Index: geniuswebcore/geniusweb/protocol/partyconnection/ProtocolToPartyConnFactory.py
===================================================================
--- geniuswebcore/geniusweb/protocol/partyconnection/ProtocolToPartyConnFactory.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/partyconnection/ProtocolToPartyConnFactory.py	(revision 67)
@@ -0,0 +1,32 @@
+from abc import abstractmethod
+from typing import List
+
+from geniusweb.actions.Action import Action
+from geniusweb.connection.ConnectionFactory import ConnectionFactory
+from geniusweb.inform.Inform import Inform
+from geniusweb.protocol.partyconnection.ProtocolToPartyConn import ProtocolToPartyConn
+from geniusweb.references.Reference import Reference
+
+
+class ProtocolToPartyConnFactory(ConnectionFactory[Action, Inform] ):
+	'''
+	An extended {@link ConnectionFactory} that store PartyIDs with the
+	connections, making it possible to find back the appropriate connection given
+	a PartyID. Also, it allows the connection factory to actually assign the
+	PartyIDs.
+	'''
+
+	@abstractmethod
+	def connectAll(self, references: List[Reference] ) -> List[ProtocolToPartyConn]:
+		'''
+		Connects a set of References in one go, following a modified Banker's
+		algorthm.
+		
+		@param references the list of references to connect with
+		@return a List of connections, one fore each Reference.
+		@throws ConnectionError         if a connection is failing irrecoverably.
+		@throws NoResourcesNowException is there are insufficient resources now.
+		                                This also includes a suggested moment to
+		                                retry.
+		'''
+	
Index: geniuswebcore/geniusweb/protocol/partyconnection/ProtocolToPartyConnections.py
===================================================================
--- geniuswebcore/geniusweb/protocol/partyconnection/ProtocolToPartyConnections.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/partyconnection/ProtocolToPartyConnections.py	(revision 67)
@@ -0,0 +1,91 @@
+from __future__ import annotations
+from typing import List, Optional, Dict
+
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.inform.Inform import Inform
+from geniusweb.protocol.partyconnection.ProtocolToPartyConn import ProtocolToPartyConn
+
+
+class ProtocolToPartyConnections: #implements Iterable<ProtocolToPartyConn> 
+	'''
+	Contains all parties with their connections. immutable
+	'''
+
+	def __init__(self, connections:List[ProtocolToPartyConn] ):
+		self._connections = connections
+
+	def get(self, pid:PartyId) -> Optional[ProtocolToPartyConn] :
+		'''
+		@param id the {@link PartyId} needed
+		@return a connection with that party, or null if no such connection.
+		'''
+		for conn in self._connections:
+			if pid==conn.getParty():
+				return conn
+		return None
+
+	def allunique(self) ->bool:
+		'''
+		@return true iff all parties have one connection only
+		'''
+		return len(self._getParties()) == len(self._connections)
+
+	def broadcast(self, info:Inform) -> Dict[PartyId, BaseException]: 
+		'''
+		Broadcast info to all parties. Notice that the broadcast immediately
+		aborts if an error occurs and remaining parties will not receive the
+		event. Therefore it is recommended to instead send to all parties
+		individually to ensure all parties are handled individually.
+		
+		@param info the {@link Inform} to broadcast
+		@return a Map&lt;PartyId, IOException&gt; where possible exceptions for
+		        each party are stored. Normally a protocol would kick these
+		        parties.
+		'''
+		exceptions:Dict[PartyId, BaseException] = {}
+		for conn in self._connections:
+			try:
+				conn.send(info);
+			except Exception as e:
+				exceptions[conn.getParty()]=e
+		return exceptions
+
+
+	def size(self) ->int:
+		'''
+		@return number of connections available
+		'''
+
+		return len(self._connections)
+
+	def getConn(self, i:int ) ->ProtocolToPartyConn :
+		'''
+		@param i the connection number
+		@return the ith connection
+		'''
+		return self._connections[i]
+
+	def __iter__(self):
+		return iter(self._connections)
+
+	def With(self, conn:ProtocolToPartyConn) ->"ProtocolToPartyConnections" :
+		newconnections:List[ProtocolToPartyConn] = list(self._connections)
+		newconnections.append(conn)
+		return ProtocolToPartyConnections(newconnections)
+
+	def __repr__(self)->str:
+		return "ConnectionWithParties" + str(self._connections)
+
+	def __hash__(self) -> int:
+		return hash(tuple(self._connections))
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) \
+			 and  self._connections == other._connections
+
+
+	def _getParties(self) -> List[PartyId]:
+		'''
+		@return list (set) of parties currently in the connections.
+		'''
+		return list(set([ con.getParty() for con in self._connections ]))
Index: geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py	(revision 67)
@@ -0,0 +1,226 @@
+from __future__ import annotations
+from abc import abstractmethod
+from typing import List, Dict, Optional, TypeVar, Generic
+
+from geniusweb.actions.Action import Action
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.progress.Progress import Progress
+from geniusweb.protocol.ProtocolException import ProtocolException
+from geniusweb.protocol.session.SessionSettings import SessionSettings
+from geniusweb.protocol.session.SessionState import SessionState
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.utils import toTuple
+
+
+S = TypeVar('S',bound=SessionSettings)   
+P = TypeVar('P',bound="DefaultSessionState")
+
+
+class DefaultSessionState (SessionState, Generic[S,P]):
+	'''
+	The default current state of the session. immutable.
+	
+	@param <P> the actual SessionState object
+	@param <S> the actual SessionSettings object
+	'''
+
+	# 	error: Optional[ProtocolException]
+	# 	progress: Progress
+	# 	partyprofiles: Optional[Dict[PartyId, PartyWithProfile]]
+	# 	settings: S
+	
+
+	def __init__(self, actions:List[Action], connections:List[PartyId],
+			progress:Optional[Progress], settings:S,
+			partyprofiles:Optional[Dict[PartyId, PartyWithProfile]]=None, 
+			error:Optional[ProtocolException]=None):
+		'''
+		@param actions       value for actions done so far. None equals to empty
+		                     list
+		@param conns         the currently existing connections. Can be empty. If
+		                     None it is assumed to be empty. Each connection
+		                     represents another party. Normally there is exactly
+		                     1 connection for every party. The protocol should
+		                     check this.
+		@param progr         the {@link Progress} that governs this session. Can
+		                     be null if session did not yet start.
+		@param settings      the settings used for the session
+		@param partyprofiles map with the {@link PartyWithProfile} for connected
+		                     parties. None is equivalent to an empty map.
+		@param e             the exception that occured, usually None. All errors
+		                     occuring due to faulty {@link Action}s translate to
+		                     {@link ProtocolException}s. All errors in our own
+		                     code are bugs (not ProtocolExceptions) and should
+		                     result in a throw and terminate the session.
+		'''
+
+		self._partyprofiles = dict(partyprofiles) if partyprofiles else {}		
+
+		self._connections = list(connections) if connections else []
+
+		self._actions = list(actions) if actions else []
+
+		if len(self._connections) != len(set(self._connections)):
+			raise ValueError("There can not be multiple connections for a party:"
+							+ str(self._connections))
+
+		if not settings:
+			raise ValueError("Settings must be not null");
+		self._progress = progress;
+		self._settings = settings;
+		self._error = error;
+
+	@abstractmethod
+	def With(self,actions1:List[Action] ,  conns:List[PartyId],
+			progress1:Optional[Progress] , settings1:S,
+			partyprofiles1:Dict[PartyId, PartyWithProfile] , e: Optional[ProtocolException]) -> P :
+		'''
+		Construct a new session state, where the DefaultSessionState changes
+		while the rest of the state remains unchanged. Notice the return type P.
+		
+		@param actions1       the new {@link Action}s
+		@param conns          the new connected {@link PartyId}s
+		@param progress1      the new {@link Progress}
+		@param settings1      the new {@link SessionSettings}. Normally this is
+		                      constant during a session.
+		@param partyprofiles1 the new {@link PartyWithProfile}s for all parties.
+		                      Normally this remains constant during a session.
+		@param e              an error that occured that caused the session to
+		                      reach its terminated/final state. If an error does
+		                      not cause termination, only a warning should be
+		                      logged.
+		@return the new state of the derived sessionstate.
+		'''
+
+	def getConnections(self)->List[PartyId] :
+		'''
+		@return existing connections.
+		'''
+		return list(self._connections)
+
+	def getPartyProfiles(self)->Dict[PartyId, PartyWithProfile] :
+		'''
+		@return map with {@link PartyWithProfile} for the parties. May be an
+        incomplete map, as more parties with their profiles may be set
+        only later.
+		'''
+		return dict(self._partyprofiles)
+
+	def getActions(self)->List[Action] :
+		'''
+		@return unmodifyable list of actions done so far.
+		'''
+		return list(self._actions)
+
+	def getProgress(self)->Optional[Progress] :
+		return self._progress
+
+	def getSettings(self)->S :
+		return self._settings
+
+	def isFinal(self,currentTimeMs:int) ->bool :
+		return self._error != None or \
+			(self._progress!=None and self._progress.isPastDeadline(currentTimeMs)) #type:ignore
+
+	def getError(self)-> Optional[ProtocolException]:
+		'''
+		@return an error that occured that caused the session to
+              reach its terminated/final state. If an error does
+              not cause termination, only a warning should be
+              logged. None if no error occured.
+		'''
+		return self._error
+
+	def WithDeadlineReached(self)->P :
+		return self.With(self._actions, self._connections, self._progress, self._settings, 
+				self._partyprofiles, self._error) 
+
+	def WithoutParty(self, party:PartyId )->P :
+		assert isinstance(party, PartyId)
+		newconn = list(self._connections)
+		newconn.remove(party);
+		return self.With(self._actions, newconn, self._progress, \
+				self._settings, self._partyprofiles, self._error)
+
+	def WithException(self, e:ProtocolException)-> P:
+		'''
+		@param e the error that occured
+		@return a new state with the error set/updated.
+		'''
+		assert isinstance(e, ProtocolException)
+		return self.With(self._actions, self._connections, self._progress, 
+						self._settings, self._partyprofiles, e)
+
+	def WithParty(self, connection:PartyId,  partyprofile:PartyWithProfile) -> P :
+		assert isinstance(connection, PartyId)
+		assert isinstance(partyprofile, PartyWithProfile)
+		newconns = list(self.getConnections())
+		newconns.append(connection)
+		newprofiles = dict(self.getPartyProfiles())
+		newprofiles[connection]= partyprofile
+		return self.With(self.getActions(), newconns, self.getProgress(), self.getSettings(),
+				newprofiles, None)
+
+	def WithProgress(self,newprogress:Progress )->P :
+		'''
+		Sets the new progress for this session.
+		
+		@param newprogress the new progress
+		@return new SAOPState with the progress set to new value
+		'''
+		assert isinstance(newprogress, Progress)
+		if not newprogress:
+			raise ValueError("newprogress must be not null");
+		return self.With(self._actions, self._connections, newprogress, self._settings, 
+						self._partyprofiles,self._error)
+
+	def WithAction(self,actor:PartyId , action:Action )->P :
+		'''
+		@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 SAOPState with the action added as last action.
+		'''
+		msg = self.checkAction(actor, action)
+		if msg:
+			raise ValueError(msg)
+
+		newactions = list(self.getActions())
+		newactions.append(action)
+		return self.With(newactions, self._connections, self._progress, 
+				self._settings, self._partyprofiles,self._error)
+
+	def checkAction(self, actor:PartyId , action:Action) -> Optional[str]:
+		'''
+		@param actor  the known real actor that did this action
+		@param action an {@link Action}
+		@return null if action seems ok, or message explaining why not.
+		'''
+		if not actor :
+			return "actor must not be null"
+		if not action:
+			return "action is null"
+		if actor != action.getActor():
+			return "act contains wrong credentials: " + str(action)
+		return None
+
+	def __repr__(self)->str:
+		return type(self).__name__ + "[" + str(self._actions) + ","\
+				+ str(self._connections) + "," + str(self._progress) + "," + \
+				str(self._settings) + "," + str(self._error);
+
+	def __hash__(self):
+		return hash((tuple(self._actions), tuple(self._connections),
+					toTuple(self._partyprofiles),
+					self._progress, self._settings, self._error))
+
+	def __eq__(self, other) -> bool:
+		return isinstance(other, self.__class__) and \
+			self._partyprofiles == other._partyprofiles and \
+			self._actions==other._actions and \
+			self._connections == other._connections and \
+			self._progress == other._progress and \
+			self._settings == other._settings and \
+			self._error == other._error
+
Index: geniuswebcore/geniusweb/protocol/session/SessionProtocol.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/SessionProtocol.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/session/SessionProtocol.py	(revision 67)
@@ -0,0 +1,9 @@
+from pyson.JsonSubTypes import JsonSubTypes
+from geniusweb.protocol.NegoProtocol import NegoProtocol
+
+@JsonSubTypes([ "SAOP" ])
+class SessionProtocol (NegoProtocol):
+    '''
+    Interface for all session protocols.
+    '''
+
Index: geniuswebcore/geniusweb/protocol/session/SessionResult.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/SessionResult.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/session/SessionResult.py	(revision 67)
@@ -0,0 +1,81 @@
+from typing import Dict, Optional
+
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.inform.Agreements import Agreements
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.utils import toStr, toTuple
+
+
+class SessionResult:
+	'''
+	All results collected from running a session. Normally this does not contain
+	all the details, only the final outcome and other global notes.
+	'''
+	def __init__(self, participants : Dict[PartyId, PartyWithProfile],
+			agreements:Agreements , penalties:Dict[PartyId, float] ,
+			error:Optional[Exception]):
+		'''
+		@param participants the list oof {@link PartyWithProfile}. Should never
+		                    be null. Some of them may have entered later of left
+		                    early. This list should contain them all.
+		@param agreements   the final Agreements.
+		@param penalties    the penalties in [0,1] for each participant.
+		@param error        a fatal error that terminated the session. Non-fatal
+		                    errors (warnings) are not to be reported. Null if no
+		                    fatal error occured (session ended following the
+		                    protocol). Normally, a fatal error results in no
+		                    agreement but there might be protocols that allow a
+		                    session to end with an error but stick with a already
+		                    reached agreement as well.
+		'''
+		self._participants = dict(participants)
+		self._agreements = agreements
+		self._penalties = dict(penalties)
+		self._error = error
+
+	def getParticipants(self) ->Dict[PartyId, PartyWithProfile] :
+		'''
+		@return the map with for each {@link PartyId} the
+		        {@link PartyWithProfile}. Should never be null. Some of them may
+		        have entered later of left early. This list should contain them
+		        all.
+		'''
+		return dict(self._participants)
+
+	def getAgreements(self) -> Agreements :
+		'''
+		@return the final {@link Agreements} of the session. May be empty, not
+		        null
+		'''
+		return self._agreements;
+
+	def getPenalties(self ) -> Dict[PartyId, float] :
+		'''
+	 	@return Map of penalties,
+		'''
+		return dict(self._penalties)
+
+	def getError(self) -> Optional[Exception] :
+		'''
+		@return a fatal error that terminated the session. Non-fatal errors
+		        (warnings) are not to be reported. Null if no fatal error occured
+		        (session ended following the protocol). Normally, a fatal error
+		        results in no agreement but there might be protocols that allow a
+		        session to end with an error but stick with a already reached
+		        agreement as well.
+		'''
+		return self._error
+
+	def __repr__(self)->str:
+		return "SessionResult[" + toStr(self._participants) + "," + str(self._agreements) + ","\
+				+ toStr(self._penalties) + "," + str(self._error) + "]"
+	
+	def __hash__(self):
+		return hash(( toTuple(self._participants ), toTuple(self._penalties ), self._agreements))
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and \
+			self._participants == other._participants and \
+			self._agreements == other._agreements and\
+			self._penalties == other._penalties and \
+			self._error == other._error 
Index: geniuswebcore/geniusweb/protocol/session/SessionSettings.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/SessionSettings.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/session/SessionSettings.py	(revision 67)
@@ -0,0 +1,51 @@
+from typing import List
+
+from tudelft_utilities_logging.Reporter import Reporter
+
+from geniusweb.protocol.NegoSettings import NegoSettings
+from geniusweb.protocol.session.SessionProtocol import SessionProtocol
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+
+class SessionSettings(NegoSettings):
+	'''
+	interface for settings for session protocols. Immutable.
+	'''
+	def getTeams(self)->List[TeamInfo] :
+		'''
+		Get the {@link TeamInfo}s.
+		
+		@return list of {@link PartyWithProfile} items.
+		'''
+
+	def getTeamSize(self) -> int:
+		'''
+		@return the size of each {@link TeamInfo}. Usually 1 (SAOP etc) or
+		        sometimes 2 (SHAOP). Used eg by tournament runner to determine
+		        proper team construction.
+		'''
+
+	def With(self, partyprofteam:TeamInfo ) -> "SessionSettings" :
+		'''
+		Allows modification of SessionSettings to include a party. This is needed
+		for tournament auto-configuration of sessions.
+		
+		@param partyprofteam the {@link TeamInfo} to be added
+		@return new modified SessionSettings object
+		'''
+	
+	def getAllParties(self) -> List[PartyWithProfile]:
+		'''
+		@return all parties from all teams, as a flattened ordered list. The
+	        order : start with all parties from team 1, then all from team 2,
+	        etc. The specific order of the parties from a team depends on the
+	        protocol.
+		'''
+
+	def getProtocol(self,  logger:Reporter)->SessionProtocol :
+		'''
+		@param logger the logger where the protocol can log events to.
+		@return the an initialized and ready to use {@link SessionProtocol} that
+		    can handle this Negotiation.
+		'''
Index: geniuswebcore/geniusweb/protocol/session/SessionState.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/SessionState.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/session/SessionState.py	(revision 67)
@@ -0,0 +1,66 @@
+from abc import abstractmethod
+from typing import List, Optional
+
+
+from geniusweb.actions.Action import Action
+from geniusweb.inform.Agreements import Agreements
+from geniusweb.progress.Progress import Progress
+from geniusweb.protocol.NegoState import NegoState
+from geniusweb.protocol.session.SessionResult import SessionResult
+from geniusweb.protocol.session.SessionSettings import SessionSettings
+
+
+class SessionState(NegoState):
+	'''
+	The current state of the session. E.g. typically contains the actions so far
+	and the parties currently connected. <br>
+	The state checks if transitions (Actions from the party) are following the
+	protocol, and thus implement most of the protocol . <br>
+	
+	If protocol errors occur, these should be stored in the state and the state
+	should become {@link #isFinal(long)}. Throwing should happen only in case of
+	a bug.<br>
+	
+	Implementations should be immutable (to ensure thread safety, testability
+	etc).
+	
+	States must be serializable so that listeners can follow what is going on in
+	the protocol. As uaual, mark non-serializable fields as transient.
+	'''
+	@abstractmethod
+	def getSettings(self)->SessionSettings:
+		'''
+		@return the SessionSettings
+		'''
+	
+	@abstractmethod
+	def getActions(self)->List[Action]: 
+		'''
+		@return unmodifyable list of actions done so far, in the order in which
+		they arrived. List of list allows implementations to add some
+		extra structure to the actions, eg one list per phase
+		'''
+	
+	@abstractmethod
+	def getProgress(self) -> Optional[Progress]:
+		'''
+		@return the progress of the session. Can return null if the session did
+		        not even start yet. Notice that the protocol determines when a
+		        session officially starts (eg, in SAOP it starts after all
+		        parties were connected succesfully).
+		'''
+	
+	@abstractmethod
+	def getAgreements(self)->Agreements :
+		'''
+		@return the current standing agreements
+		    An agreement does not necessarily mean {@link #isFinal(long)}.
+		'''
+	
+	@abstractmethod
+	def getResult(self)->Optional[SessionResult]:
+		'''
+		@return the {@link SessionResult} which is a short report of the final
+		        outcome. Assumes {@link #isFinal(long)}. result may be undefined
+		        if not.
+		'''
Index: geniuswebcore/geniusweb/protocol/session/TeamInfo.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/TeamInfo.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/session/TeamInfo.py	(revision 67)
@@ -0,0 +1,65 @@
+from typing import List
+from pyson.JsonTypeInfo import Id, As
+from pyson.JsonTypeInfo import JsonTypeInfo
+
+from geniusweb.protocol.tournament.Team import Team
+from geniusweb.references.Parameters import Parameters
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+
+@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
+class TeamInfo :
+	'''
+	Contains a functional team, eg in SHAOP a team would be a SHAOP together with
+	his COB partner. In SAOP this is just a SAOP party.
+	'''
+
+	def __init__(self,  parties: List[PartyWithProfile]):
+		self._parties = parties
+
+	 
+	def getParties(self) ->List[PartyWithProfile] :
+		'''
+		@return the list of all parties involved, which is the parties in all
+		teams combined.
+		'''
+		return list(self._parties)
+
+
+	def getSize(self)-> int:
+		'''
+		@return the number of parties in the team.
+		'''
+		return len(self._parties)
+
+	def __repr__(self)->str:
+		return "TeamInfo[" +str( self._parties) + "]"
+
+	def __hash__(self):
+		return hash(tuple(self._parties))
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and \
+            self._parties == other._parties
+
+	def With(self, parameters:Parameters ) ->"TeamInfo" :
+		'''
+		@param parameters a set of {@link Parameters} to be merged with the
+		                  existing parameters of the first team member.
+		@return updated TeamInfo
+		'''
+		updatedparties = list(self._parties)
+		party1 = updatedparties[0]
+		newparams1 = party1.getParty().getParameters().WithParameters(parameters)
+		party1update = PartyWithProfile(
+				party1.getParty().With(newparams1), party1.getProfile())
+		updatedparties[0]= party1update
+		return TeamInfo(updatedparties)
+
+
+	def getTeam(self)->Team:
+		'''
+		@return the Team, without the profiles
+		'''
+		return Team ([party.getParty() for party in self._parties])
+
Index: geniuswebcore/geniusweb/protocol/session/saop/SAOP.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/saop/SAOP.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/session/saop/SAOP.py	(revision 67)
@@ -0,0 +1,342 @@
+from datetime import datetime
+import logging
+from threading import Timer
+import threading
+import time
+import traceback
+from typing import Optional, List
+
+from tudelft.utilities.listener.DefaultListenable import DefaultListenable
+from tudelft.utilities.listener.Listener import Listener
+from tudelft.utilities.repository.NoResourcesNowException import NoResourcesNowException
+from tudelft_utilities_logging.Reporter import Reporter
+from uri.uri import URI
+
+from geniusweb.actions.Action import Action
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.events.ProtocolEvent import ProtocolEvent
+from geniusweb.inform.ActionDone import ActionDone
+from geniusweb.inform.Finished import Finished
+from geniusweb.inform.Inform import Inform
+from geniusweb.inform.Settings import Settings
+from geniusweb.inform.YourTurn import YourTurn
+from geniusweb.progress.ProgressFactory import ProgressFactory
+from geniusweb.protocol.CurrentNegoState import CurrentNegoState
+from geniusweb.protocol.NegoState import NegoState
+from geniusweb.protocol.ProtocolException import ProtocolException
+from geniusweb.protocol.partyconnection.ProtocolToPartyConn import ProtocolToPartyConn
+from geniusweb.protocol.partyconnection.ProtocolToPartyConnFactory import ProtocolToPartyConnFactory
+from geniusweb.protocol.partyconnection.ProtocolToPartyConnections import ProtocolToPartyConnections
+from geniusweb.protocol.session.SessionProtocol import SessionProtocol
+from geniusweb.protocol.session.SessionState import SessionState
+from geniusweb.protocol.session.saop.SAOPState import SAOPState
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.references.ProtocolRef import ProtocolRef
+from geniusweb.references.Reference import Reference
+from geniusweb.utils import val
+
+
+class SAOP (DefaultListenable[ProtocolEvent], SessionProtocol):
+	'''
+	The protocol runs as follows
+	<ol>
+	<li>The protocol tries to start all parties. If not all parties start, the
+	parties are freed up and another attempt is done to start all parties some
+	time later.
+	<li>All parties are sent the {@link SessionSettings}. Only parties specified 
+	initially in the settings do participate. 
+	<li>The session deadline clock now starts ticking.
+	<li>All parties are sent their settings.
+	<li>All parties get YourTurn in clockwise order. A party must do exactly one
+	action after it received YourTurn.
+	<li>The negotiation continues until an agreement is reached (all parties
+	agreed to the last bid), the {@link Deadline} is reached, or a party fails to
+	adhere to the protocol.
+	<li>If the session times out, the connections are cut and the negotiation
+	completes without errors and without agreement.
+	</ol>
+	<p>
+	This logs to "Protocol" logger if there are issues
+	<p>
+	This object is mutable: the internal state changes as parties interact with
+	the protocol.
+	<p>
+	Thread safe: all entry points are synchronized.
+	'''
+	_TIME_MARGIN = 20; # ms extra delay after deadline
+	_MINDURATION = 100;
+	_MIN_SLEEP_TIME = 1000;
+	_MAX_SLEEP_TIME = 60000;
+	_SAOP = ProtocolRef(URI("SAOP"))
+
+	_isFinishedInfoSent = False 
+	_deadlinetimer:Optional[Timer] = None
+	_synclock = threading.RLock()
+
+	def __init__(self, state:SAOPState , logger:Reporter ,
+			connects:ProtocolToPartyConnections=ProtocolToPartyConnections([]) ) :
+		'''
+		@param state  normally the initial state coming from SAOPSettings
+		@param logger the Reporter to log to
+		@param connects the connections to the parties. Defaults to no-connections.
+		'''
+		super().__init__()
+		if state == None:
+			raise ValueError("state must be not null")
+
+		if state.getSettings().getDeadline().getDuration() < self._MINDURATION :
+			raise ValueError("Duration must be at least " + str(self._MINDURATION))
+
+		self._log = logger
+		self._state = state
+		self._conns = connects
+
+	def start(self, connectionfactory:ProtocolToPartyConnFactory ):
+		with self._synclock: 
+			try:
+				self._connect(connectionfactory)
+				self._setDeadline()
+				self._setupParties()
+				self._nextTurn()
+			except Exception as e:
+				traceback.print_exc() #for debugging. 
+				self._handleError("Failed to start up session", None, e)
+
+	def getDescription(self)->str:
+		return "All parties get YourTurn in clockwise order, after which they can do their next action. "\
+				+ "No new participants after start. End after prescribed deadline or when some bid is unanimously Accepted."\
+				+ "Parties can only act on their own behalf and only when it is their turn."
+
+	def addParticipant(self, party:PartyWithProfile ):
+		raise ValueError("Dynamic joining a negotiation is not allowed in SAOP")
+
+	def getState(self)-> SessionState :
+		return self._state
+
+	def getRef(self)->ProtocolRef :
+		return self._SAOP
+
+	#*******************************************************************
+	# private functions. Some are protected only, for testing purposes
+	# ******************************************************************
+
+	def _connect(self, connectionfactory:ProtocolToPartyConnFactory):
+		'''
+		step 1 in protocol: connect all involved parties and start the clock.
+		This always "succeeds" with a valid (but possibly final) state
+		<p>
+		This is 'protected' to allow junit testing, this code is not a 'public'
+		part of the interface.
+		
+		@param connectionfactory the connectionfactory for making party
+		                         connections
+		
+		@throws InterruptedException if the connection procedure is unterrupted
+		
+		@throws IOException          if this fails to properly conect to the
+		                             parties, eg interrupted or server not
+		                             responding..
+		'''
+		with self._synclock:
+			participants = self._state.getSettings().getAllParties();
+			parties:List[Reference] = [party.getParty().getPartyRef() for party in participants ]
+			connections:Optional[List[ProtocolToPartyConn] ] = None
+			self._log.log(logging.INFO, "SAOP connect " + str(parties));
+			while not connections:
+				try:
+					connections = connectionfactory.connectAll(parties)
+				except NoResourcesNowException as e:
+					waitms = (e.getLater().timestamp() - time.time())*1000
+					self._log.log(logging.INFO,
+							"No resources available to run session, waiting"+ str(waitms))
+					time.sleep(min(self._MAX_SLEEP_TIME,
+								max(self._MIN_SLEEP_TIME, waitms)))
+
+			for i in range(len(participants)):
+				# now we bookkeep the connections ourselves,
+				# and update the state to keep in sync.
+				self._conns = self._conns.With(connections[i])
+				self._setState(self._state.WithParty(connections[i].getParty(),
+						participants[i]))
+
+	def _setDeadline(self):
+		'''
+		Set state to proper deadline. Starts the timer tasks. This tasks triggers
+		a call to handleError when the session times out.
+		'''
+		with self._synclock:
+			now = time.time()
+			deadline = self._state.getSettings().getDeadline()
+			self._setState(self._state.WithProgress(ProgressFactory.create(deadline, 1000*now)))
+			
+			# set timer TIME_MARGIN after real deadline to ensure we're not too early
+			duration=(deadline.getDuration() + self._TIME_MARGIN)/1000.0
+			self._deadlinetimer = Timer( duration, self._timertask)
+			self._deadlinetimer.start()
+			self._log.log(logging.INFO, "SAOP deadline set to "
+					+ datetime.utcfromtimestamp(duration+now).strftime('%Y/%m/%d %H:%M:%S'))
+
+	def _timertask(self):
+		if not self._state.isFinal(1000.*time.time()):
+			self._log.log(logging.CRITICAL,
+					"BUG. Deadline timer has triggered but state is not final")
+		self._log.log(logging.INFO,
+				"SAOP deadline reached. Terminating session.")
+		self._finish()
+		
+
+	def _setupParties(self) :
+		'''
+		step 2 in protocol: listen to connections and send settings to the
+		parties.
+		<p>
+		This is 'protected' to allow junit testing, this code is not a 'public'
+		part of the interface.
+		
+		@throws ProtocolException if a party does not follow the protocol
+		'''
+		this=self
+		class MyListener(Listener[Action]):
+			def __init__(self, conn):
+				self.conn=conn
+			def notifyChange(self, action: Action):
+				this._actionRequest(self.conn,action)
+
+		with self._synclock:
+			for conn in  self._conns:
+				conn.addListener(MyListener(conn))
+	
+			for connection in self._conns:
+				try :
+					self._sendSettings(connection)
+				except ConnectionError as e:
+					raise ProtocolException("Failed to initialize",
+							connection.getParty(), e)
+	
+
+	def _sendSettings(self, connection:ProtocolToPartyConn):
+		'''
+		Inform a party about its settings
+		
+		@param connection
+		@throws ConnectionError if party got disconnected
+		'''
+		with self._synclock:
+			partyid = connection.getParty()
+			profile = self._state.getPartyProfiles()[partyid].getProfile()
+			params = self._state.getPartyProfiles()[partyid].getParty()\
+					.getParameters();
+			if not profile:
+				raise ValueError(
+						"Missing profile for party " + str(connection.getReference()))
+			connection.send(Settings(connection.getParty(), profile, self.getRef(),
+					val(self._state.getProgress()), params))
+
+	def   _actionRequest(self, partyconn:ProtocolToPartyConn , action:Action): 
+		'''
+		This is called when one of the party connections does an action.
+		Synchronized so that we always handle only 1 action at a time.
+		
+		@param partyconn the connection on which the action came in.
+		@param action    the {@link Action} taken by some party
+		'''
+		with self._synclock:
+			if not action:
+				err = partyconn.getError();
+				if not err:
+					err = ProtocolException("Party sent a null action",
+							partyconn.getParty())
+				self._handleError(str(partyconn) + "Protocol error", partyconn.getParty(),
+						err)
+				return
+	
+			try:
+				if partyconn.getParty() != self._state._getNextActor() :
+					# party does not have the turn.
+					raise ProtocolException(
+							"Party acts without having the turn",
+							partyconn.getParty());
+
+				# FIXME? this ignores possible broadcast errors
+				self._conns.broadcast(ActionDone(action))
+				self._setState(self._state.WithAction(partyconn.getParty(), action))
+				if not self._state.isFinal(int(1000*time.time())):
+					self._nextTurn()
+			except Exception as  e:
+				self._handleError("failed to handle action " + str(action),
+						partyconn.getParty(), e)
+
+
+	def _nextTurn(self):
+		'''
+		Signal next participant it's his turn
+		
+		
+		@throws IOException
+		'''
+		with self._synclock:
+			party = self._state._getNextActor()
+			try:
+				self._conns.get(party).send(YourTurn())
+			except ConnectionError as e:
+				self._handleError("failed to send YourTurn", party, e)
+
+	def _handleError(self, message:str, party:Optional[PartyId], e:Exception):
+		'''
+		Update state to include the given error and finishes up the session.
+		
+		@param message The message to attach to the error
+		@param party   the party where the error occured
+		@param e       the exception that occured.
+		'''
+		with self._synclock:
+			if isinstance(e,  ProtocolException):
+				self._setState(self._state.WithException( e))
+			else:
+				self._setState(self._state.WithException(ProtocolException(message, party, e)))
+			if party:
+				self._setState(self._state.WithoutParty(party))
+			self._log.log(logging.WARNING, "SAOP protocol intercepted error due to party "\
+					+ str(party) + ":" + message, e)
+
+	def _setState(self, newstate:SAOPState) :
+		'''
+		Sets the new state. If the new state is final, the finish-up procedure is
+		executed.
+		
+		@param newstate the new state.
+		'''
+		with self._synclock:
+			now = int(1000*time.time())
+			if self._state.isFinal(now):
+				self._finish()
+				return
+			self._state = newstate;
+			if newstate.isFinal(now):
+				self._finish()
+
+	def _finish(self):
+		'''
+		Called when we reach final state. Cancels deadline timer. Send finished
+		info to all parties, notify current nego state as final and set
+		{@link #isFinishedInfoSent}. Double calls are automatically ignored.
+		'''
+		with self._synclock:
+			if self._deadlinetimer :
+				self._deadlinetimer.cancel()
+				self._deadlinetimer = None
+			if self._isFinishedInfoSent:
+				return;
+			self._isFinishedInfoSent=True
+			finished = Finished(self._state.getAgreements());
+			for conn in self._conns:
+				self._sendFinish(conn, finished)
+			self.notifyListeners(CurrentNegoState(self._state))
+
+	def _sendFinish(self, connection:ProtocolToPartyConn , finished:Inform ):
+		try:
+			connection.send(finished)
+			connection.close()
+		except Exception as e:
+			self._log.log(logging.INFO, "Failed to send Finished to " + str(connection), e)
+
Index: geniuswebcore/geniusweb/protocol/session/saop/SAOPSettings.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/saop/SAOPSettings.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/session/saop/SAOPSettings.py	(revision 67)
@@ -0,0 +1,73 @@
+from typing import List
+
+from tudelft_utilities_logging.Reporter import Reporter
+
+from geniusweb.deadline.Deadline import Deadline
+from geniusweb.protocol.session.SessionProtocol import SessionProtocol
+from geniusweb.protocol.session.SessionSettings import SessionSettings
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+
+class SAOPSettings(SessionSettings):
+
+	def __init__(self,  participants:List[TeamInfo], deadline:Deadline ): 
+		'''
+		@param participants the list of {@link PartyWithProfile} in clockwise
+		                    order. There must be at least 2 to run the SAOP
+		                    protocol. But SAOP can be initialized with less, for
+		                    use in TournamentSettings.
+		@param deadline     the deadline of the negotiation
+		'''
+		self._participants = participants;
+		self._deadline = deadline;
+		if participants == None:
+			raise ValueError("participants must not be null")
+		if deadline == None:
+			raise ValueError("deadline must not be null")
+
+	def getMaxRunTime(self) -> float:
+		return self._deadline.getDuration() / 1000.
+
+	def getProtocol(self, logger:Reporter ) -> SessionProtocol :
+		# avoid circular imports
+		from geniusweb.protocol.session.saop.SAOP import SAOP
+		from geniusweb.protocol.session.saop.SAOPState import SAOPState
+		return SAOP(SAOPState([],[],None, self), logger)
+	
+	def getParticipants(self) -> List[TeamInfo] :
+		return list(self._participants)
+
+	def getTeams(self) -> List[TeamInfo] :
+		return list(self._participants)
+
+	def getDeadline(self)->Deadline :
+		'''
+		@return the deadline for this negotiation
+		'''
+		return self._deadline
+
+	def  getAllParties(self) -> List[PartyWithProfile] : 
+		return [particip.getParties()[0] for particip in self._participants ]
+
+	def getTeamSize(self) -> int:
+		return 1
+
+	def __repr__(self)->str:
+		return "SAOPSettings" + str(self._participants) + "," + str(self._deadline) + "]"
+
+	def __hash__(self):
+		return hash((self._deadline, tuple(self._participants)))
+	
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) \
+			 and  self._deadline == other._deadline and self._participants==other._participants
+
+	def With(self, team:TeamInfo) -> SessionSettings :
+		if team.getSize() != 1:
+			raise ValueError("Team must be size 1 but found " + str(team));
+		newparts = list(self._participants)
+		newparts.append(team)
+		return SAOPSettings(newparts, self._deadline)
+
Index: geniuswebcore/geniusweb/protocol/session/saop/SAOPState.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/saop/SAOPState.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/session/saop/SAOPState.py	(revision 67)
@@ -0,0 +1,156 @@
+from __future__ import annotations
+
+from typing import List, Dict, Optional, TYPE_CHECKING
+
+from geniusweb.actions.Accept import Accept
+from geniusweb.actions.Action import Action
+from geniusweb.actions.EndNegotiation import EndNegotiation
+from geniusweb.actions.Offer import Offer
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.inform.Agreements import Agreements
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.progress.Progress import Progress
+from geniusweb.progress.ProgressRounds import ProgressRounds
+from geniusweb.protocol.ProtocolException import ProtocolException
+from geniusweb.protocol.session.DefaultSessionState import DefaultSessionState
+from geniusweb.protocol.session.SessionResult import SessionResult
+from geniusweb.protocol.session.saop.SAOPSettings import SAOPSettings
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+#CHECK WHY DO THE TYPE VARIABLES HAVE TO BE SWAPPED HERE?
+class SAOPState (DefaultSessionState[ "SAOPSettings","SAOPState"]): 
+	'''
+	Immutable.
+	'''
+
+	def __init__(self, actions:List[Action] , connections:List[PartyId] ,
+			progress:Optional[Progress] , settings: SAOPSettings ,
+			partyprofiles:Dict[PartyId, PartyWithProfile]={}, 
+			error:Optional[ProtocolException]=None) :
+		'''
+		
+		@param actions       the actions done by the parties
+		@param conns         the existing party connections. If null, default
+		                     empty list is used. Can be less than 2 parties in
+		                     the first phases of the setup or after parties
+		                     disconnected.
+		@param progress      the {@link Progress} line. can be null
+		@param settings      the {@link SAOPSettings}
+		@param partyprofiles map with the {@link PartyWithProfile} for connected
+		                     parties. null is equivalent to an empty map.
+		@param e             the {@link ProtocolException}, or null if none
+		                     occurred.
+		'''
+		super().__init__(actions, connections, progress, settings, partyprofiles, error) 
+
+	def With(self, actions:List[Action] , conns: List[PartyId],
+			progr:Optional[Progress] , settings: SAOPSettings,
+			partyprofiles:Dict[PartyId, PartyWithProfile] ,  e:Optional[ProtocolException]) -> "SAOPState" :
+		assert isinstance(actions, list)
+		#assert isinstance(settings, SAOPSettings)
+		assert isinstance(partyprofiles, dict)
+		return SAOPState(actions, conns, progr, settings, partyprofiles, e);
+
+
+	def getAgreements(self) ->Agreements :
+		agree = Agreements()
+		acts = self.getActions()
+		nparticipants = len(self.getConnections())
+		if nparticipants < 2 or len(acts) < nparticipants:
+			return agree;
+		offer = acts[len(acts) - nparticipants]
+		if not isinstance(offer, Offer):
+			return agree;
+		bid = offer.getBid()
+
+		# check that the last n-1 are accepts.
+		allaccept = all( [ (isinstance(act, Accept) and bid==act.getBid())\
+					for act in acts[-(nparticipants-1):]] )
+		if allaccept:
+			agree = Agreements({party: bid for party in  self._getParties()})
+		return agree
+
+	def _getParties(self)->List[PartyId] :
+		'''
+		@return all currently connected parties.
+		'''
+		return self.getConnections()
+
+	def isFinal(self,  currentTimeMs:int)->bool:
+		acts = self.getActions()
+		return super().isFinal(currentTimeMs) \
+				or self.getAgreements().getMap()!={} \
+				or (not acts==[] and isinstance(acts[- 1], EndNegotiation))
+
+	def WithAction(self, actor:PartyId , action:Action ) -> "SAOPState":
+		return super().WithAction(actor, action).WithProgress(self.advanceProgress())
+
+	def checkAction(self, actor:PartyId, action:Action ) -> Optional[str]:
+		msg = super().checkAction(actor, action)
+		if msg:
+			return msg
+
+		if actor != self._getNextActor():
+			return "Party does not have the turn "
+
+		# check protocol is followed for specific actions
+		if isinstance(action, Accept):
+			bid = self._getLastBid()
+			if not bid:
+				return "Accept without a recent offer";
+
+			if bid != action.getBid():
+				return "Party accepts a bid differing from the last offer ="\
+						+ str(bid) + ", action=" + str(action) + ")"
+			return None
+		elif isinstance(action,Offer):
+			return None
+		elif isinstance(action, EndNegotiation):
+			return None
+		return "Action " + str(action) + " is not allowed in SAOP"
+
+
+	def _getLastBid(self)-> Optional[ Bid ]:
+		'''
+		Check up to nparticipants-1 steps back if there was an offer.
+	 
+		@return Bid from the most recent offer, or null if no such offer
+		'''
+		nparticipants = len(self.getConnections())
+		acts = self.getActions()
+		n = len(acts) - 1
+		while n > len(acts) - nparticipants and n >= 0:
+			action = acts[n]
+			if isinstance(action ,Offer):
+				return action.getBid()
+			n=n-1
+		return None
+
+	def _getNextActor(self)->PartyId :
+		'''
+		@return the next actor in the current state. Assumes 1 action per actor
+		        every time. NOTE if party disconnects this would jump wildly but
+		        nego stops then anyway
+		'''		
+		return self.getConnections()[len(self.getActions()) % len(self.getConnections())]
+
+	def advanceProgress(self)-> Progress :
+		'''
+		@return new progress state
+		'''		
+		newprogress = self.getProgress()
+		assert newprogress
+		if isinstance(newprogress,ProgressRounds) and self._isLastActor():
+			newprogress = newprogress.advance()
+		return newprogress
+
+	def _isLastActor(self) -> bool:
+		'''
+		@return true if the current actor is the last actor in the list
+		'''
+		nparticipants = len(self.getConnections())
+		return len(self.getActions()) % nparticipants == nparticipants - 1;
+
+	def getResult(self) -> SessionResult:
+		return SessionResult(self.getPartyProfiles(), self.getAgreements(),
+				{}, self.getError())
Index: geniuswebcore/geniusweb/protocol/tournament/Team.py
===================================================================
--- geniuswebcore/geniusweb/protocol/tournament/Team.py	(revision 67)
+++ geniuswebcore/geniusweb/protocol/tournament/Team.py	(revision 67)
@@ -0,0 +1,32 @@
+from typing import List
+from geniusweb.references.PartyWithParameters import PartyWithParameters
+class Team:
+	'''
+	A team is a group of parties, each with their own parameters. Each will be
+	coupled to a {@link ProfileList} in the generation phase of the protocol
+	'''
+	def __init__(self, parties: List[PartyWithParameters]):
+		'''
+		@param ps a list of {@link PartyWithParameters}. Each party is always
+		          associated with the given parameters. The order of them is
+		          important as it will be matched with the order in the
+		          {@link ProfileList}.
+		'''
+		self._parties = list(parties)
+
+	def	getParties(self) -> List[PartyWithParameters] :
+		'''
+		@return list of all team {@link PartyWithParameters}.
+		'''
+		return list(self._parties)
+
+	def __hash__(self):
+		return hash(tuple(self._parties))
+
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and  self._parties == other._parties
+
+	def __repr__(self):
+		return repr(self._parties)
+
Index: geniuswebcore/geniusweb/references/Parameters.py
===================================================================
--- geniuswebcore/geniusweb/references/Parameters.py	(revision 66)
+++ geniuswebcore/geniusweb/references/Parameters.py	(revision 67)
@@ -2,8 +2,10 @@
 
 from copy import deepcopy
-from typing import Dict, Any
+from email._header_value_parser import Parameter
+from typing import Dict, Any, List, Optional
 
 from pyson.JsonGetter import JsonGetter
 from pyson.JsonValue import JsonValue
+from geniusweb.utils import toStr
 
 
@@ -27,10 +29,12 @@
 		'''
 		@param key the key to get the value for.
-		@return the raw value
+		@return the raw value, or none if key not in parameters.
 		'''
+		if not key in self._params:
+			return None
 		return self._params[key]
 	
 	@JsonValue()
-	def getParameters(self):
+	def getParameters(self) -> Dict[str, Any]:
 		return deepcopy(self._params)
 
@@ -51,5 +55,5 @@
 
 
-	def With(self, key:str, val:object) -> Parameters :
+	def With(self, key:str, val:object) -> "Parameters" :
 		'''
 		@param key the key. Key may already in params.
@@ -62,5 +66,50 @@
 		return Parameters(newparams)
 
+	def WithParameters(self, parameters:"Parameters"):
+		'''
+		@param parameters the parameters to be added/overridden
+		@return new Parameters, with new parameters ADDED to existing. The new
+			 parameters override existing ones.
+		'''
+		newparams = dict(self._params)
+		newparams.update(parameters._params)
+		return Parameters(newparams)
 
+	def getType(self, paramname:str, classType):
+		'''
+		Getter with type-check
+		
+		@param <T>       the expected type
+		@param paramname the parameter name that must be in the map
+		@param classType the expected value type for this parameter
+		@return object of requested type.
+		@throws IllegalArgumentException if the object is not available.
+	'''
+		if not paramname in self._params \
+				or not type(self._params[paramname])==classType: 
+			raise ValueError(" Missing a parameter "+ paramname + " with a " + str(classType) + " value");
+		return self._params[paramname]
+	
+	def getDouble(self, name:str, defaultval:Optional[float], minv:Optional[float],
+			 maxv:Optional[float]) -> Optional[float]: 
+		'''
+		@param name       the parameter name
+		@param defaultval the default value to return if parameter is not
+		                  available or not inside the given range. Can be null
+		@param minv        the minimum value, or null if not applicable
+		@param maxv        the maximum value, or null if not applicable
+		@return the double value contained inside the parameter with the given
+		        name, or defaultval if the parameter does not exist, does not
+		        contain a double, or is outside [min, max].
+		'''
+		if  not name in self._params or not type(self._params[name])==float:
+			return defaultval;
+		val:float = self._params[name]
+		if minv  and val < minv:
+			return defaultval;
+		if maxv  and val > maxv:
+			return defaultval;
+		return val;
+	
 	def __hash__(self):
 		return sum( [ hash(k) + 31 * hash(v) for (k,v) in self._params.items()])
@@ -71,4 +120,4 @@
 
 	def __repr__(self):
-		return str(self._params)
+		return toStr(self._params)
 
Index: geniuswebcore/geniusweb/references/PartyRef.py
===================================================================
--- geniuswebcore/geniusweb/references/PartyRef.py	(revision 67)
+++ geniuswebcore/geniusweb/references/PartyRef.py	(revision 67)
@@ -0,0 +1,32 @@
+from pyson.JsonValue import JsonValue
+from uri.uri import URI
+
+from geniusweb import party
+from geniusweb.references.Reference import Reference
+
+
+class PartyRef (Reference):
+	'''
+	A URI reference to create a new instance of a party on a party factory
+	server. These are used to describe sesion and tournament settings.
+	'''
+
+
+	def __init__(self, party:URI ):
+		if not isinstance(party, URI):
+			raise ValueError("expected URI, but got "+repr(party))
+		self._party = party
+
+	@JsonValue()
+	def getURI(self)-> URI :
+		return self._party;
+
+	def __repr__(self) ->str:
+		return "PartyRef[" + str(self._party) + "]"
+
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and self._party==other._party
+
+	def __hash__(self):
+		return hash(self._party)
Index: geniuswebcore/geniusweb/references/PartyWithParameters.py
===================================================================
--- geniuswebcore/geniusweb/references/PartyWithParameters.py	(revision 67)
+++ geniuswebcore/geniusweb/references/PartyWithParameters.py	(revision 67)
@@ -0,0 +1,38 @@
+from __future__ import annotations
+from geniusweb.references.Parameters import Parameters
+from geniusweb.references.PartyRef import PartyRef
+
+class PartyWithParameters:
+	'''
+	Container holding a partyref with parameters
+	'''
+
+	def __init__(self, partyref:PartyRef , parameters:Parameters):
+		self._partyref = partyref
+		self._parameters = parameters
+
+	def getPartyRef(self) -> PartyRef :
+		return self._partyref
+
+	def getParameters(self) ->Parameters :
+		return self._parameters;
+
+
+	def With(self, parameters2:Parameters) ->"PartyWithParameters":
+		'''
+		@param parameters2 the additional parameters
+		@return this party but with the given parameters added to our parameters
+		'''
+		return PartyWithParameters(self._partyref, self._parameters.WithParameters(parameters2));
+
+	def __repr__(self)->str:
+		if self._parameters.isEmpty():
+			return str(self._partyref)
+		return str(self._partyref) + str(self._parameters)
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and \
+			self._partyref==other._partyref and self._parameters==other._parameters
+
+	def __hash__(self):
+		return hash((self._parameters, self._partyref))
Index: geniuswebcore/geniusweb/references/PartyWithProfile.py
===================================================================
--- geniuswebcore/geniusweb/references/PartyWithProfile.py	(revision 67)
+++ geniuswebcore/geniusweb/references/PartyWithProfile.py	(revision 67)
@@ -0,0 +1,37 @@
+from geniusweb.references.PartyWithParameters import PartyWithParameters
+from geniusweb.references.ProfileRef import ProfileRef
+
+
+class PartyWithProfile:
+
+	def __init__(self,   party:PartyWithParameters, profile:ProfileRef ) :
+		if party == None or profile == None:
+			raise ValueError("party and profile must be not None")
+
+		self._party = party
+		self._profile = profile
+
+	def getParty(self)->PartyWithParameters :
+		'''
+		@return the {@link PartyWithParameters}. never null.
+		'''
+		return self._party
+
+	def getProfile(self)-> ProfileRef:
+		'''
+		@return the profile setting for this party. Never null.
+		'''
+		return self._profile
+
+	def __hash__(self):
+		'''
+		support for using this in dict etc
+		'''
+		return hash((self._party, self._profile))
+
+	def __repr__(self)->str:
+		return "PartyWithProfile[" + str(self._party) + "," + str(self._profile) + "]"
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and \
+			self._party==other._party and self._profile==other._profile
Index: geniuswebcore/geniusweb/references/ProfileRef.py
===================================================================
--- geniuswebcore/geniusweb/references/ProfileRef.py	(revision 66)
+++ geniuswebcore/geniusweb/references/ProfileRef.py	(revision 67)
@@ -1,5 +1,5 @@
 from pyson.JsonGetter import JsonGetter
 from pyson.JsonValue import JsonValue
-from uri import URI # type: ignore
+from uri.uri import URI # type: ignore
 
 from geniusweb.references.Reference import Reference
Index: geniuswebcore/geniusweb/references/ProtocolRef.py
===================================================================
--- geniuswebcore/geniusweb/references/ProtocolRef.py	(revision 66)
+++ geniuswebcore/geniusweb/references/ProtocolRef.py	(revision 67)
@@ -1,5 +1,5 @@
 from pyson.JsonGetter import JsonGetter
 from pyson.JsonValue import JsonValue
-from uri import URI # type: ignore
+from uri.uri import URI # type: ignore
 
 from geniusweb.references.Reference import Reference
Index: geniuswebcore/geniusweb/references/Reference.py
===================================================================
--- geniuswebcore/geniusweb/references/Reference.py	(revision 66)
+++ geniuswebcore/geniusweb/references/Reference.py	(revision 67)
@@ -3,5 +3,5 @@
 from typing import TypeVar, Generic, List
 from pyson.JsonValue import JsonValue
-from uri import URI          # type: ignore
+from uri.uri import URI          # type: ignore
 
 class Reference (ABC):
Index: geniuswebcore/geniusweb/simplerunner/BasicConnection.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/BasicConnection.py	(revision 67)
+++ geniuswebcore/geniusweb/simplerunner/BasicConnection.py	(revision 67)
@@ -0,0 +1,169 @@
+from collections import deque
+from multiprocessing import Queue
+from threading import Thread, Condition
+import threading
+import traceback
+from typing import TypeVar, Generic, List, Optional
+
+from tudelft.utilities.listener.Listener import Listener
+from uri.uri import URI
+
+from geniusweb.connection.ConnectionEnd import ConnectionEnd
+from geniusweb.references.Reference import Reference
+from geniusweb.simplerunner.BlockingQueue import BlockingQueue
+
+
+S=TypeVar("S")
+
+class Info (Generic[S]):
+	'''
+	Wrapper around data so that we can put Null end EOS in a
+	{@link ArrayBlockingQueue}
+	@param <S> the type of contained data.
+	'''
+
+class Data (Info[S]):
+
+	def __init__(self, data:S):
+		self._data = data
+
+	def get(self)->S:
+		return self._data;
+
+	def __repr__(self):
+		return str(self._data)
+
+class EOS (Info[S]):
+	'''
+	End of stream.
+	'''
+	def __repr__(self):
+		return "EOS"
+	
+# I use this single instance everywhere
+THE_EOS:EOS = EOS()
+
+
+IN = TypeVar('IN')
+OUT = TypeVar('OUT')
+
+class BasicConnection(ConnectionEnd[IN, OUT]):
+	'''
+	A basic connection that implements connection with direct calls
+	
+	@param <IN>  the type of the incoming data
+	@param <OUT> the type of outgoing data
+	'''
+
+	def __init__(self, reference:Reference , uri:URI ):
+		'''
+		@param reference Reference that was used to create this connection.
+		@param uri       the URI of the remote endpoint that makes up the
+		                 connection. This is a URI that uniquely identifies the
+		                 remote object
+		'''
+		self._reference = reference
+		self._uri = uri
+		self._listeners:List[Listener[IN]] = []
+		self._synclock = threading.RLock()
+		self._error:Optional[Exception]=None
+	
+		# to be initialized
+		self._handler:Optional[Listener[OUT]] = None
+		self._messages = BlockingQueue[Info](4)
+
+
+	def init(self, newhandler:Listener[OUT] ) :
+		'''
+		To be called to hook up the other side that will handle a send action
+		from us. Must be called first.
+		
+		@param newhandler a Listener&lt;OUT&gt; that can handle send actions.
+		'''
+		if self._handler: 
+			raise ValueError("already initialized")
+		self._handler = newhandler
+		this=self
+
+		class MyHandlerThread(Thread):
+			'''
+			thread that handles this._messages until EOS is hit.
+			It runs in scope of init and uses 'this'
+			'''
+			def run(self):
+				try:
+					while (True):
+						#print("INTO"+str(self))
+						mess = this._messages.take()
+						#print("OUT"+str(self))
+						if mess==THE_EOS:
+							break;
+						this._handler.notifyChange(mess.get())
+				except Exception as e:
+					this.setError(e)
+				this._handler = None
+				print("BasicConnection closed");
+		
+		handlerThread=MyHandlerThread()
+		handlerThread.start();
+
+	def setError(self,  e:Exception):
+		'''
+		Error condition occurs. Record error and close connection
+		
+		@param e
+		'''
+		with self._synclock:
+			if not self._error:
+				# maybe log instead?
+				traceback.print_exc()
+				self._error = e
+				self.close()
+
+	def send(self, data:OUT ) :
+		with self._synclock:
+			if not self._handler:
+				raise ValueError(
+					"BasicConnection has not been initialized or was closed")
+			# it seems there is no InterruptedException possible in python.
+			self._messages.put(Data(data))
+
+	def getReference(self) -> Reference :
+		return self._reference
+
+	def getRemoteURI(self)->URI: 
+		return self._uri
+
+	def close(self):
+		with self._synclock:
+			print("flushing and terminating " + str(self))
+			if not self._handler or self._messages.contains(THE_EOS):
+				return
+			# it seems there is no InterruptedException possible in python.
+			self._messages.put(THE_EOS)
+
+	def __repr__(self):
+		return "BasicConnection[" + str(self._reference) + "]"
+
+	def getError(self)->Optional[Exception]:
+		return self._error;
+
+	def isOpen(self)->bool: 
+		'''
+		 @return true iff this connection is open. Returns false also when then
+ 			handler is in the close-down process
+		'''
+		return self._handler != None and not self._messages.contains(THE_EOS)
+
+	#****************** implements listenable ****************
+	# override because notifyListeners should throw exceptions.
+	def addListener(self, l:Listener[IN]):
+		self._listeners.append(l)
+
+	def removeListener(self, l:Listener[IN] ) :
+		self._listeners.remove(l)
+
+	def notifyListeners(self, data:IN ) :
+		for l in self._listeners:
+			l.notifyChange(data)
+
Index: geniuswebcore/geniusweb/simplerunner/BlockingQueue.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/BlockingQueue.py	(revision 67)
+++ geniuswebcore/geniusweb/simplerunner/BlockingQueue.py	(revision 67)
@@ -0,0 +1,33 @@
+from collections import deque
+import threading
+from typing import Generic, TypeVar
+
+
+T=TypeVar('T') # contained element type
+
+class BlockingQueue(Generic[T]):
+    '''
+    Like threading.Queue but using collections.queue
+    which allows us to extend the functioanlity
+    '''
+    def __init__(self, capacity: int):
+        self._pushing = threading.Semaphore(capacity)
+        self._pulling = threading.Semaphore(0)
+        self._data:deque = deque()
+ 
+    def put(self, element: T):
+        self._pushing.acquire()
+        self._data.append(element)
+        self._pulling.release()
+ 
+    def take(self) -> T:
+        self._pulling.acquire()
+        self._pushing.release()
+        return self._data.popleft()
+        
+    def size(self) -> int:
+        return len(self._data)
+    
+    def contains(self, elt:T) -> bool:
+        return elt in self._data
+    
Index: geniuswebcore/geniusweb/simplerunner/ClassPathConnectionFactory.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/ClassPathConnectionFactory.py	(revision 67)
+++ geniuswebcore/geniusweb/simplerunner/ClassPathConnectionFactory.py	(revision 67)
@@ -0,0 +1,187 @@
+import importlib
+import re
+from typing import List
+
+from tudelft.utilities.listener.Listener import Listener
+from uri.uri import URI
+
+from geniusweb.actions.Action import Action
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.inform.Inform import Inform
+from geniusweb.party.Party import Party
+from geniusweb.protocol.partyconnection.ProtocolToPartyConn import ProtocolToPartyConn
+from geniusweb.protocol.partyconnection.ProtocolToPartyConnFactory import ProtocolToPartyConnFactory
+from geniusweb.references.Reference import Reference
+from geniusweb.simplerunner.BasicConnection import BasicConnection 
+from geniusweb.utils import val
+
+
+class BasicConnectionWithParty (BasicConnection[Action, Inform],  ProtocolToPartyConn):
+
+	_id: PartyId 
+
+	def __init__(self, reference:Reference ,uri: URI):
+		super().__init__(reference, uri)
+		self._id = PartyId(re.sub("\.", "_", str(uri).replace("pythonpath:", "")))
+
+	def getParty(self)->PartyId :
+		return self._id
+
+	def __repr__(self)->str:
+		return "WP" + super().__repr__()
+
+
+class ConnectionPair: 
+	'''
+	Contains the connection from protocol to party and from party to protocol.
+	These two are dependent on each other, and must close together.
+	'''
+	serialcounter = 1 # class variable. Keeps track of used valies.
+	_SCHEME = "pythonpath"
+	_PROTOCOLURI = URI("protocol:protocol")
+
+	_party2protocol: BasicConnection[Inform, Action]
+	_protocol2party: BasicConnectionWithParty
+
+	def __init__(self, reference:Reference ) :
+		'''
+		@param reference the party class path reference4
+		'''
+		# set up the whole other party including the connection to it.
+		classpath = self._getClassPath(reference.getURI())
+		party = self._instantiate(classpath)
+		this=self
+
+		# if call to protocol would fail, we should also close the
+		# protocol2party connection. But we assume that will work fine.
+		class myBC(BasicConnection[Inform, Action]):
+			def __init__(self):
+				super().__init__(reference, ConnectionPair._PROTOCOLURI)
+				
+			def close(self):
+				if self.isOpen():
+					super().close()
+					this._protocol2party.close()
+	
+		self._party2protocol = myBC()
+		
+		# if callback from protocol to party fails, then also close the
+		# other direction.
+		class myBCP(BasicConnectionWithParty):
+			def __init__(self):
+				super().__init__(reference, \
+					URI("pythonpath:" + reference.getURI().getPath()\
+						+ "." + str(ConnectionPair.serialcounter)))
+				ConnectionPair.serialcounter +=  1
+				
+			def close(self):
+				if self.isOpen():
+					super().close()
+					this._party2protocol.close()
+	
+		
+		self._protocol2party = myBCP()
+
+		class party2protocolListener(Listener[Action]):
+			def notifyChange(self, data: Action):
+				this._protocol2party.notifyListeners(data)
+		self._party2protocol.init(party2protocolListener())
+
+		class protocol2partyListener(Listener[Inform]):
+			def notifyChange(self, data: Inform):
+				this._party2protocol.notifyListeners(data)
+		self._protocol2party.init(protocol2partyListener())
+
+		party.connect(self._party2protocol)
+
+
+	def getOpenConnections(self)->List[BasicConnection] :
+		'''
+		@return the open connections
+		'''
+		open:List[BasicConnection]  = []
+		if self._party2protocol.isOpen():
+			open.append(self._party2protocol)
+		if self._protocol2party.isOpen():
+			open.append(self._protocol2party)
+		return open
+
+	def getProtocolToPartyConn(self)->BasicConnectionWithParty :
+		return self._protocol2party
+	
+	
+	def _getPartyToProtocolConn(self) ->BasicConnection :
+		'''
+		Internal use for testing
+		'''
+		return self._party2protocol
+	
+	def _instantiate(self, classpath:str)->Party :
+		'''
+		@param classpath to a full.pythonpath.classname as string. 
+				The class must be on the python path.
+				the 'full.pythonpath' must be the module "filename",
+				the "classname" is the name of the class inside the module file.
+		@return instance of the given {@link Party} on the classpath
+		@throws ValueError if the Party can not be instantiated, eg
+		         because of a ClassCastException,
+		         ClassNotFoundException,
+		         IllegalAccessException etc. Such an
+		         exception is considered a bug by the
+		         programmer, because this is a
+		         stand-alone runner.
+		'''
+		print("Connecting with party '" + classpath + "'")
+		try:
+			
+			[path, cla] = classpath.rsplit(".",1)
+			mod=importlib.import_module(path)
+			partyclass = getattr(mod, cla)
+			return partyclass()
+		except Exception as e:
+			raise ValueError(
+					"Failed to create connection to party " + classpath,e)
+		importlib
+
+	def _getClassPath(self,  uri:URI) -> str:
+		assert isinstance(uri, URI)
+		if self._SCHEME != uri.getScheme():
+			raise ValueError("Required the " + self._SCHEME
+					+ " protocol but found " + uri.getScheme())
+		path = uri.getPath()
+		if not path:
+			raise ValueError(
+					"Expected pythonpath not present in " + str(uri))
+		return path
+	
+
+
+class ClassPathConnectionFactory (ProtocolToPartyConnFactory):
+	'''
+	A connectionfactory that only accepts URLs of the form
+	<code>pythonpath:org/my/package/class</code>
+	'''
+	_allConnections: List[ConnectionPair]  = []
+
+	def connect(self, reference:Reference ) ->ProtocolToPartyConn :
+		'''
+		@param reference the party class path reference4
+			'''
+		connpair = ConnectionPair(reference)
+		self._allConnections.append(connpair)
+		return connpair.getProtocolToPartyConn()
+
+	def connectAll(self,  references:List[Reference] ) ->List[ProtocolToPartyConn] :
+		connections:List[ProtocolToPartyConn] = []
+		for partyref in references:
+			connections.append(self.connect(partyref))
+		return connections
+
+	def getOpenConnections(self)->List[BasicConnection] :
+		'''
+		@return list of connections that are still open.
+		'''
+		openconns:List[BasicConnection] = []
+		for connpair in self._allConnections:
+			openconns.extend(connpair.getOpenConnections())
+		return openconns
Index: geniuswebcore/geniusweb/simplerunner/NegoRunner.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/NegoRunner.py	(revision 67)
+++ geniuswebcore/geniusweb/simplerunner/NegoRunner.py	(revision 67)
@@ -0,0 +1,163 @@
+import json
+import logging
+from pathlib import Path
+import sys
+import time
+import traceback
+from typing import List, Optional
+
+from pyson.ObjectMapper import ObjectMapper
+from tudelft.utilities.listener.Listener import Listener
+from tudelft_utilities_logging.Reporter import Reporter
+
+from geniusweb.events.ProtocolEvent import ProtocolEvent
+from geniusweb.protocol.CurrentNegoState import CurrentNegoState
+from geniusweb.protocol.NegoProtocol import NegoProtocol
+from geniusweb.protocol.NegoSettings import NegoSettings
+from geniusweb.protocol.NegoState import NegoState
+from geniusweb.simplerunner.ClassPathConnectionFactory import ClassPathConnectionFactory
+
+
+class StdOutReporter (Reporter):
+	def log(self, level:int , msg:str, exc:Optional[BaseException]=None):
+		if level >= logging.WARNING :
+			print(logging.getLevelName(level) + ":" + msg, file=sys.stderr)
+		else:
+			print(logging.getLevelName(level) + ":" + msg)
+
+
+class NegoRunner:
+	'''
+	WARNING DO NOT USE. NOT WORKING YET. 
+	
+	A simple tool to run a negotiation stand-alone, without starting the servers.
+	All referred files and classes need to be stored locally (or be in the
+	dependency list if you use maven).
+	<p>
+	<em>IMPORTANT</em> SimpleRunner has a number of restrictions, compared to a
+	run using a runserver and partyserver
+	<ul>
+	<li>With stand-alone runner, your parties are run together in a single
+	classloader. The main implication is that there may arise version conflicts
+	between parties.
+	<li>Stand-alone runner does NOT enforce the time deadline. Parties may
+	continue running indefinitely and thus bog down the JVM and stalling
+	tournaments.
+	</ul>
+	'''
+
+	_properlyStopped:bool = False
+	_LOOPTIME = 200 # ms
+	_FINALWAITTIME = 5000 # ms
+
+	def __init__(self, settings:NegoSettings ,
+			connectionfactory:ClassPathConnectionFactory , logger:Reporter ,
+			maxruntime:int):
+		'''
+		@param settings          the {@link NegoSettings}
+		@param connectionfactory the {@link ProtocolToPartyConnFactory}
+		@param logger            the {@link Reporter} to log problems
+		@param maxruntime        limit in millisecs. Ignored if 0
+		'''
+		if settings == None  or connectionfactory == None:
+			raise ValueError("Arguments must be not null");
+		self._settings = settings;
+		self._log = logger;
+		self._protocol = settings.getProtocol(self._log);
+		self._connectionfactory = connectionfactory;
+		self._maxruntime = maxruntime;
+		self._jackson=ObjectMapper()
+
+	def isProperlyStopped(self)->bool:
+		'''
+		@return true if the runner has finished
+		'''
+		return self._properlyStopped
+
+	def run(self):
+		this=self
+		class protocolListener(Listener[ProtocolEvent]):
+			def notifyChange(self, evt: ProtocolEvent):
+				this._handle(evt)
+
+		self._protocol.addListener(protocolListener())
+		self._protocol.start(self._connectionfactory)
+		remainingtime = self._maxruntime;
+		while not self._properlyStopped and  (self._maxruntime == 0 or remainingtime > 0):
+			time.sleep(self._LOOPTIME/1000.)
+			remainingtime -= self._LOOPTIME
+		self._log.log(logging.INFO, "Waiting for connection closure")
+
+		remainingtime = self._FINALWAITTIME;
+		while remainingtime > 0 and\
+			len(self._connectionfactory.getOpenConnections())!=0:
+				time.sleep(self._LOOPTIME/1000.)
+				remainingtime -= self._LOOPTIME
+			
+		openconn  = self._connectionfactory.getOpenConnections()
+		if len(openconn)!=0:
+			self._log.log(logging.WARNING, "Connections " + str(openconn)\
+					+ " did not close properly at end of run")
+		self._log.log(logging.INFO, "end run")
+
+	def _handle(self, evt:ProtocolEvent ):
+		if isinstance(evt , CurrentNegoState) and \
+			evt.getState().isFinal(1000*time.time()):
+				self._stop()
+
+	def _stop(self):
+		self._logFinal(logging.INFO, self._protocol.getState())
+		self._properlyStopped =True
+
+	def _logFinal(self, level:int , state: NegoState):
+		'''
+		Separate so that we can intercept this when mocking, as this will crash
+		on mocks because {@link #jackson} can not handle mocks.
+		
+		@param level the log {@link Level}, eg logging.WARNING
+		@param state the {@link NegoState} to log
+		'''
+		try:
+			self._log.log(level, "protocol ended normally: "
+					+ json.dumps(self._jackson.toJson(self._protocol.getState())))
+		except Exception as e: # catch json issues
+			traceback.print_exc()
+
+	def getProtocol(self) ->NegoProtocol :
+		'''
+		@return protocol that runs/ran the session.
+		'''
+		return self._protocol
+
+	@staticmethod
+	def main( args:List[str]) :
+		'''
+		The main runner
+		
+		@param args should have 1 argument, the settings.json file to be used.
+		@throws IOException if problem occurs
+		'''
+		if len(args) != 1:
+			NegoRunner.showusage()
+			return;
+		
+		serialized = Path(args[0]).read_text("utf-8")
+		settings:NegoSettings  = ObjectMapper().parse(json.loads(serialized),
+						NegoSettings) #type:ignore
+
+		runner = NegoRunner(settings,
+				ClassPathConnectionFactory(), StdOutReporter(), 0)
+		runner.run()
+
+	@staticmethod
+	def showusage():
+		print("GeniusWeb stand-alone runner.")
+		print("first argument should be <settings.json>.")
+		print("The settings.json file should contain the NegoSettings.")
+		print("See the settings.json example file and the GeniusWeb wiki pages. ")
+
+	
+
+if __name__ == '__main__':   
+	NegoRunner.main([sys.argv[1]])
+	
Index: geniuswebcore/geniusweb/utils.py
===================================================================
--- geniuswebcore/geniusweb/utils.py	(revision 67)
+++ geniuswebcore/geniusweb/utils.py	(revision 67)
@@ -0,0 +1,29 @@
+'''
+A few simple but often needed tools.
+'''
+from typing import TypeVar, Optional
+
+
+def toStr(d:dict)->str:
+    '''
+    prettyprint dict, using str (not repr) for elements in dict.
+    This tostring function looks similar to how java prints it,
+    '''
+    return "{"+ (", ".join([str(k)+"="+str(v) for (k,v) in d.items()]))+"}"
+
+
+def toTuple(d:dict) -> tuple:
+    '''
+    Converts dict into tuples of tuples. Used mainly to compute hash
+    '''
+    return tuple([(k,v) for (k,v) in d.items()])
+
+T = TypeVar('T')
+def val(v:Optional[T])->T:
+    '''
+    @return the value contained in the optional.
+    Raises exception if the value is None. 
+    '''
+    if not v:
+        raise ValueError("Value is not set")
+    return v 
