Index: geniuswebcore/geniusweb/simplerunner/NegoRunner.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/NegoRunner.py	(revision 87)
+++ geniuswebcore/geniusweb/simplerunner/NegoRunner.py	(revision 88)
@@ -17,9 +17,12 @@
 from geniusweb.protocol.NegoState import NegoState
 from geniusweb.simplerunner.ClassPathConnectionFactory import ClassPathConnectionFactory
+from geniusweb.simplerunner.Runner import Runner
+from geniusweb.simplerunner.gui.GUI import GUI
 
 
 class StdOutReporter (Reporter):
+
 	def log(self, level:int , msg:str, exc:Optional[BaseException]=None):
-		if level >= logging.WARNING :
+		if level >= logging.WARNING:
 			print(logging.getLevelName(level) + ":" + msg, file=sys.stderr)
 		else:
@@ -29,6 +32,4 @@
 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
@@ -47,95 +48,18 @@
 	'''
 
-	_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]) :
+	def main(args:List[str]):
 		'''
 		The main runner
 		
-		@param args should have 1 argument, the settings.json file to be used.
+		@param args should have 0 or 1 argument. If 0 arguments, the {@link GUI}
+		            is started. If one argoment, it should be a filename
+		            containing a settings.json file. That session is then run.
 		@throws IOException if problem occurs
 		'''
+		if len(args) == 0:
+			GUI.main([])
+			return 
+		
 		if len(args) != 1:
 			NegoRunner.showusage()
@@ -143,8 +67,8 @@
 		
 		serialized = Path(args[0]).read_text("utf-8")
-		settings:NegoSettings  = ObjectMapper().parse(json.loads(serialized),
-						NegoSettings) #type:ignore
+		settings:NegoSettings = ObjectMapper().parse(json.loads(serialized),
+						NegoSettings)  # type:ignore
 
-		runner = NegoRunner(settings,
+		runner = Runner(settings,
 				ClassPathConnectionFactory(), StdOutReporter(), 0)
 		runner.run()
@@ -156,8 +80,7 @@
 		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]])
+if __name__ == '__main__': 
+	NegoRunner.main(sys.argv[1:])
 	
Index: geniuswebcore/geniusweb/simplerunner/Runner.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/Runner.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/Runner.py	(revision 88)
@@ -0,0 +1,122 @@
+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 Runner:
+	'''
+	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
Index: geniuswebcore/geniusweb/simplerunner/gui/GUI.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/gui/GUI.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/gui/GUI.py	(revision 88)
@@ -0,0 +1,87 @@
+import logging
+import sys
+from typing import List, Optional
+
+from PyQt5 import QtCore
+from PyQt5.QtWidgets import QWidget, QTextEdit, QGridLayout, QPushButton, \
+	QApplication, QMainWindow
+from tudelft_utilities_logging.Reporter import Reporter
+
+from geniusweb.simplerunner.ClassPathConnectionFactory import ClassPathConnectionFactory
+from geniusweb.simplerunner.Runner import Runner
+from geniusweb.simplerunner.gui.SettingsPanel import SettingsPanel
+
+
+class GUI (QWidget):
+	'''
+	Simple GUI allowing users to create the settings interactively, much like on
+	the runserver
+	'''
+	# dataChannel needed to pipe output text from the runner
+	# into the PyQt system
+	dataChannel = QtCore.pyqtSignal(str)
+
+	def __init__(self):
+		super().__init__()
+		self.dataChannel.connect(self.receive)
+		self.resultarea = QTextEdit()
+		self.resultarea.setReadOnly(True)
+		self.resultarea.setText("Results will appear here after run")
+		# set dimension , 9, 40
+
+		layout = QGridLayout()
+		self.setLayout(layout)
+	
+		self.settingsPanel = SettingsPanel()
+		layout.addWidget(self.settingsPanel)
+		layout.addWidget(self.startPanel())
+
+	def startPanel(self) -> QWidget:
+		startpanel = QWidget()
+		layout = QGridLayout()
+		startpanel.setLayout(layout)
+		
+		startbutton = QPushButton()
+		startbutton.setText("Start Session")
+		layout.addWidget(startbutton)
+		layout.addWidget(self.resultarea)
+
+		this = self
+		startbutton.clicked.connect(this.runsession)  # type:ignore
+		return startpanel;
+
+	def runsession(self, b):
+		self.resultarea.setText("")
+		this = self
+		
+		class MyReporter(Reporter):
+
+			# override
+			def log(self, level:int, msg:str, thrown:Optional[BaseException]=None):
+				text = "[" + logging._levelToName[level] + "] " + msg + "\n"
+				# in python you can not turn a exception into a string
+				if thrown:
+					text = text + str(thrown) + "\n"
+				this.dataChannel.emit(text)
+
+		myreporter = MyReporter() 
+		runner = Runner(self.settingsPanel.getSettings(),
+			ClassPathConnectionFactory(), myreporter, 0)
+		runner.run()
+
+	def receive(self, text:str):
+		self.resultarea.append(text)
+
+	@staticmethod
+	def main(args:List[str]):
+		app = QApplication([])
+		
+		win = QMainWindow()
+		win.setWindowTitle("GeniusWeb SimpleRunner GUI")
+		win.setCentralWidget(GUI())
+		win.show()
+		sys.exit(app.exec_())
+
+
+if __name__ == '__main__':
+	GUI.main([])
Index: geniuswebcore/geniusweb/simplerunner/gui/MyFileChooser.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/gui/MyFileChooser.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/gui/MyFileChooser.py	(revision 88)
@@ -0,0 +1,32 @@
+from PyQt5.QtWidgets import QWidgetAction, QTextEdit, QPushButton, QGridLayout, \
+	QFileDialog, QWidget, QLineEdit
+
+
+class MyFileChooser (QWidget):
+	'''
+	one-line panel with on left the chosen file and on the right a "Browse"
+	button. If you click browse button you can browse for a file. You can also
+	just type the file location.
+	'''
+
+	def __init__(self):
+		super().__init__()
+		self.filenamearea = QLineEdit()
+		self.filenamearea.setText("select a file")
+		self.browsebutton = QPushButton()
+		self.browsebutton.setText("Browse")
+
+		layout = QGridLayout()
+		self.setLayout(layout)
+
+		layout.addWidget(self.filenamearea, 0, 0)
+		layout.addWidget(self.browsebutton, 0, 1)
+		self.browsebutton.clicked.connect(self.browseclick)
+
+	def browseclick(self):
+				fc = QFileDialog()
+				if fc.exec_() > 0:  # ? what does this function return?
+					self.filenamearea.setText(fc.selectedFiles()[0])
+
+	def getFile(self) -> str:
+		return self.filenamearea.text();
Index: geniuswebcore/geniusweb/simplerunner/gui/SelectionModel.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/gui/SelectionModel.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/gui/SelectionModel.py	(revision 88)
@@ -0,0 +1,54 @@
+from typing import List
+
+from PyQt5.QtCore import QAbstractTableModel, QModelIndex, QVariant
+from PyQt5.QtCore import Qt
+
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+
+class SelectionModel(QAbstractTableModel):
+	'''
+	The selected parties, profiles etc
+	'''
+
+	colnames = ["Party", "Parameters", "Profile"]
+
+	def __init__(self):
+		super().__init__()
+		self._teams:List[TeamInfo] = []
+		# pyqt does not use listeners
+
+	def getTeams(self) -> List[TeamInfo]:
+		return self._teams
+
+	def addTeam(self, team:TeamInfo):
+		self._teams.append(team)
+		self.beginResetModel()
+		self.endResetModel()
+
+	# Override
+	def rowCount(self, parent=QModelIndex()):
+		return len(self._teams) 
+
+	# Override
+	def columnCount(self, parent=QModelIndex()):
+		return 3 
+
+	def data(self, index, role=None):
+		if role == Qt.ItemDataRole.DisplayRole:
+			party:PartyWithProfile = self._teams[index.row()].getParties()[0]
+			if index.column() == 0:
+				return str(party.getParty().getPartyRef().getURI())
+			if index.column() == 1:
+				return str(party.getParty().getParameters())
+			if index.column() == 2:
+				return str(party.getProfile().getURI())
+			return "???"
+		return None  # without this you get checkboxes everywhere in table...
+
+	def headerData(self, column:int, orientation, role=Qt.ItemDataRole.DisplayRole):
+		if role != Qt.ItemDataRole.DisplayRole:
+			return QVariant()
+		if orientation == Qt.Orientation.Horizontal:
+			return QVariant(self.colnames[column]) 
Index: geniuswebcore/geniusweb/simplerunner/gui/SettingsPanel.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/gui/SettingsPanel.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/gui/SettingsPanel.py	(revision 88)
@@ -0,0 +1,157 @@
+import json
+import traceback
+
+from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel, QComboBox, QSpinBox, \
+	QFrame, QSplitter, QLineEdit, QPushButton, QTableView, QScrollArea, QVBoxLayout, \
+	QTextEdit, QMessageBox
+from pyson.ObjectMapper import ObjectMapper
+from uri.uri import URI
+
+from geniusweb.deadline.Deadline import Deadline
+from geniusweb.deadline.DeadlineRounds import DeadlineRounds
+from geniusweb.deadline.DeadlineTime import DeadlineTime
+from geniusweb.protocol.NegoSettings import NegoSettings
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.protocol.session.saop.SAOPSettings import SAOPSettings
+from geniusweb.references.Parameters import Parameters
+from geniusweb.references.PartyRef import PartyRef
+from geniusweb.references.PartyWithParameters import PartyWithParameters
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.references.ProfileRef import ProfileRef
+from geniusweb.simplerunner.gui.MyFileChooser import MyFileChooser
+from geniusweb.simplerunner.gui.SelectionModel import SelectionModel
+
+
+class SettingsPanel(QWidget):
+	
+	def __init__(self):
+		super().__init__()
+		self.jackson = ObjectMapper()
+		self.protocolcombo = QComboBox()
+		self.protocolcombo.addItems([ "SAOP" ])
+		self.timepanel = TimePanel()
+		self.nextparty = QLineEdit()
+		self.nextparameters = QLineEdit()
+		
+		self.nextprofile = MyFileChooser()
+		self.addButton = QPushButton()
+		self.addButton.setText("add")
+		self.selectedparties = SelectionModel()
+
+		layout = QGridLayout()
+		self.setLayout(layout)
+		
+		layout.addWidget(WithLabel("protocol", self.protocolcombo))
+		layout.addWidget(WithLabel("deadline", self.timepanel))
+		layout.addWidget(Separator())
+		
+		partic = QLabel()
+		partic.setText("participants")
+		layout.addWidget(partic)
+		
+		layout.addWidget(WithLabel("party:", self.nextparty))
+		layout.addWidget(WithLabel("parameters:", self.nextparameters))
+		layout.addWidget(WithLabel("profile:", self.nextprofile))
+		
+		layout.addWidget(Separator())
+
+		layout.addWidget(self.addButton)
+		
+		label = QLabel()
+		label.setText("Selected Profiles, parties for the session")
+		layout.addWidget(label)
+		
+		table = QTableView()
+		table.setModel(self.selectedparties)
+		layout.addWidget(table)
+		self.addButton.clicked.connect(self.addparty);
+
+	def addparty(self):
+		try:
+			self.addparty1()
+		except ValueError as e:
+			traceback.print_exc()
+			mbox = QMessageBox()
+			mbox.setText(str(e))
+			mbox.setWindowTitle("Failed to add party")
+			mbox.setIcon(QMessageBox.Icon.Warning)
+			mbox.exec_()
+ 
+	def addparty1(self):
+		try:
+			partyref = PartyRef(URI("pythonpath:" + self.nextparty.text()))
+		except Exception as e:
+			raise ValueError("Party not set correctly:" + str(e))
+
+		try:
+			parameters = self.jackson.parse(json.loads("{" + self.nextparameters.text() + "}"),
+					Parameters)
+		except Exception as e:
+			raise ValueError("Parameters not set correctly:" + str(e))
+
+		try:
+			profile = ProfileRef(URI("file:" + self.nextprofile.getFile()))
+		except Exception as  e:
+			raise ValueError("Profile not set correctly:" + str(e))
+
+		partywithparams = PartyWithParameters(partyref, 	parameters);
+		partywithprofile = PartyWithProfile(partywithparams, profile);
+		teaminfo = TeamInfo([partywithprofile])
+		self.selectedparties.addTeam(teaminfo)
+ 
+	def getSettings(self) -> NegoSettings:
+		return SAOPSettings(self.selectedparties.getTeams(),
+				self.timepanel.getDeadline())
+
+
+class WithLabel (QWidget):
+	'''
+	adds label to left of component
+	'''
+
+	def __init__(self, label:str, component:QWidget):
+		super().__init__()
+		layout = QGridLayout()
+		self.setLayout(layout)
+		
+		qlabel = QLabel()
+		qlabel.setText(label)
+
+		layout.addWidget(qlabel, 0, 0)
+		layout.addWidget(component, 0, 1)
+
+
+class TimePanel(QWidget):
+	DEFAULTDEADLINE = 10000  # millisecs
+	
+	def __init__(self):
+		super().__init__()
+		self.unitcombo = QComboBox()
+		self.unitcombo.addItems(["seconds", "rounds"])
+		self.time = QSpinBox()
+		self.time.setMinimum(1)
+		self.time.setMaximum(1000)
+		self.time.setValue(10)
+
+		layout = QGridLayout()
+		self.setLayout(layout)
+
+		layout.addWidget(self.time, 0, 0)
+		layout.addWidget(self.unitcombo, 0, 1)
+
+	def getDeadline(self) -> Deadline:
+		if self.unitcombo.currentIndex() == 0:  # time
+			return DeadlineTime(self.time.value() * 1000);
+		return DeadlineRounds(self.time.value(), self.DEFAULTDEADLINE)
+
+
+class Separator(QFrame):
+	'''
+	Equivalent of java's JSeparator: just a horizontal line for layout
+	'''
+	
+	def __init__(self):
+		super().__init__()
+		self.setFrameShape(QFrame.Shape.HLine)
+		self.setFrameShadow(QFrame.Shadow.Sunken)
+	
