public Dispatcher(String remoteHost, int downstreamPort, int upstreamPort, int majorVersion, int minorVersion,
			int revisionVersion, String userName, int tankColorRed, int tankColorGreen, int tankColorBlue) {
		this.remoteHost = remoteHost;
		this.downstreamPort = downstreamPort;
		assert (downstreamPort < 65536);
		this.upstreamPort = upstreamPort;
		assert (upstreamPort < 65536);
		this.majorVersion = majorVersion;
		this.minorVersion = minorVersion;
		this.revisionVersion = revisionVersion;
		this.userName = userName;
		this.tankColorRed = tankColorRed;
		this.tankColorGreen = tankColorGreen;
		this.tankColorBlue = tankColorBlue;

		try {

			// set up upstream connection
			upstreamSocket = new Socket(remoteHost, upstreamPort);
			upstreamInput = new BufferedReader(new InputStreamReader(upstreamSocket.getInputStream()));
			upstreamOutput = new BufferedWriter(new OutputStreamWriter(upstreamSocket.getOutputStream()));

			// build connect upstream channel message
			JSONObject connectUpstreamChannelMessage = buildConnectUpstreamChannelMessage(userName, tankColorRed,
					tankColorGreen, tankColorBlue, majorVersion, minorVersion, revisionVersion);

			// send connect upstream message
			QuickLog.debug("Connecting upstream: " + connectUpstreamChannelMessage.toString());
			upstreamOutput.write(connectUpstreamChannelMessage.toString());
			upstreamOutput.newLine();
			upstreamOutput.flush();

			// get response
			QuickLog.debug("DEBUG: Awaiting response...");
			String connectUpstreamResponse = upstreamInput.readLine();
			QuickLog.debug("DEBUG: Got response: " + connectUpstreamResponse);

			// parse response
			JSONObject connectionAcceptedJSON = new JSONObject(connectUpstreamResponse)
					.getJSONObject("connectionAccepted");
			int UID = connectionAcceptedJSON.getInt("withUID");
			String returnedUserName = connectionAcceptedJSON.getString("forUser");
			assert (returnedUserName == userName && UID > 0);

			// set up downstream connection
			Socket downstreamSocket = new Socket(remoteHost, downstreamPort);
			BufferedReader downstreamInput = new BufferedReader(
					new InputStreamReader(downstreamSocket.getInputStream()));
			BufferedWriter downstreamOutput = new BufferedWriter(new OutputStreamWriter(
					downstreamSocket.getOutputStream()));

			// build connect downstream channel message
			JSONObject connectDownstreamChannelMessage = buildConnectDownstreamChannelMessage(UID, majorVersion,
					minorVersion, revisionVersion);

			// send connect downstream message
			QuickLog.debug("Connecting downstream: " + connectDownstreamChannelMessage.toString());
			downstreamOutput.write(connectDownstreamChannelMessage.toString());
			downstreamOutput.newLine();
			downstreamOutput.flush();

			// get response
			QuickLog.debug("Awaiting response...");
			String connectDownstreamResponse = downstreamInput.readLine();
			QuickLog.debug("Got response: " + connectDownstreamResponse);

			// parse connect response and fetch initial game state
			JSONObject initialGameState = new JSONObject(connectDownstreamResponse).getJSONObject("welcomeToGame");
			QuickLog.info("Welcome to game: " + initialGameState.toString());

			// fetch rules object from initial game state
			JSONObject jsonRules = initialGameState.getJSONObject("rules");
			assert (jsonRules != null);

			// set up processor using rules from initial game state
			Rules gameRules = new Rules(jsonRules.getDouble("moveSpeed"), jsonRules.getDouble("rotationSpeed"),
					jsonRules.getDouble("turretRotationSpeed"), jsonRules.getDouble("fireInterval"),
					jsonRules.getDouble("ballisticsTravelSpeed"), jsonRules.getInt("fieldOfView"),
					jsonRules.getInt("turretFieldOfView"), jsonRules.getInt("hp"), jsonRules.getInt("ballisticDamage"),
					jsonRules.getInt("enemyHitScore"), jsonRules.getInt("enemyKillScore"),
					jsonRules.getInt("tankStatusUpdateRate"));
			processor = new Processor(this, gameRules);

			processor.start();

			// wait for the one-time 'game will start' message
			JSONObject gameWillStart = new JSONObject(downstreamInput.readLine()).getJSONObject("gameWillStart");
			QuickLog.info("Got gameWillStart: " + gameWillStart);

			// get the map dimensions
			// FIXME: we don't deal with non-square game maps
			int mapSize = gameWillStart.getJSONObject("onMap").getJSONObject("withSize").getInt("width");
			processor.setMapSize(mapSize);
			DebugGUI gui = new DebugGUI(processor);

			String serverResponse;

			// listen for input (blocking)
			while ((serverResponse = downstreamInput.readLine()) != null) {

				JSONObject serverResponseJSON = new JSONObject(serverResponse);

				if (serverResponseJSON.has("tankStatusUpdate")) {

					// parse context object out of raw JSON context
					JSONObject statusUpdate = serverResponseJSON.getJSONObject("tankStatusUpdate");

					Tank ownTank = new Tank(statusUpdate.getJSONObject("position").getDouble("x"), statusUpdate
							.getJSONObject("position").getDouble("y"), statusUpdate.getDouble("direction"),
							statusUpdate.getDouble("turretDirection"), statusUpdate.getBoolean("isMoving"),
							statusUpdate.getBoolean("isRotating"), statusUpdate.getBoolean("isTurretRotating"));

					// process tank status update
					processor.processTankStatusUpdate(ownTank);
					gui.repaint();

				} else {

					// TODO: handle downstream server errors

					QuickLog.error("Could not process status update: " + serverResponseJSON);

				}

			}

		} catch (IOException e) {
			e.printStackTrace();
		} catch (JSONException e) {
			e.printStackTrace();
		} catch (NullPointerException e) {
			e.printStackTrace();
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
		}

		// make sure the threaded processor will stop
		processor.setRunning(false);
	}