aboutsummaryrefslogtreecommitdiff
path: root/src/scalevalapokalypsi/Server/Server.scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/scalevalapokalypsi/Server/Server.scala')
-rw-r--r--src/scalevalapokalypsi/Server/Server.scala195
1 files changed, 195 insertions, 0 deletions
diff --git a/src/scalevalapokalypsi/Server/Server.scala b/src/scalevalapokalypsi/Server/Server.scala
new file mode 100644
index 0000000..13ca2f5
--- /dev/null
+++ b/src/scalevalapokalypsi/Server/Server.scala
@@ -0,0 +1,195 @@
+package scalevalapokalypsi.Server
+
+
+// TODO: TLS/SSL / import javax.net.ssl.SSLServerSocketFactory
+
+import java.lang.Thread.{currentThread, sleep}
+import java.io.IOException
+import java.net.{ServerSocket, Socket}
+import scalevalapokalypsi.constants.*
+import scalevalapokalypsi.Model.{Adventure,Entity,Player}
+import scalevalapokalypsi.utils.stringToByteArray
+
+import java.lang.System.currentTimeMillis
+import scala.util.Try
+
+
+/** `Server` exists to initialize a server for the game
+ * and run it with its method `startServer`.
+ *
+ * @param port the TCP port the server should listen on
+ * @param maxClients the maximum number of clients that may be in the game
+ * simultaneously.
+ * @param timeLimit the time limit clients should have to execute their turns.
+ * @param joinAfterStart whether new clients are accepted after the game has
+ * been started
+ */
+class Server(
+ port: Int,
+ maxClients: Int,
+ val timeLimit: Int,
+ val joinAfterStart: Boolean
+):
+
+ private val socket = ServerSocket(port)
+ private val clientGetter = ConnectionGetter(socket)
+ private val clients: Clients = Clients(maxClients)
+ private val buffer: Array[Byte] = Array.ofDim(1024)
+ private var bufferIndex = 0
+ private var adventure: Option[Adventure] = None
+ private var previousTurn = 0.0
+
+ /** Starts the server. Won't terminate under normal circumstances. */
+ def startServer(): Unit =
+ while true do
+ this.serverStep()
+ sleep(POLL_INTERVAL)
+
+ private def serverStep(): Unit =
+ this.clients.removeNonCompliant()
+ if this.adventure.isEmpty || this.joinAfterStart then
+ this.receiveNewClient()
+ this.readFromAll()
+ this.clients.foreach(_.interpretData())
+ this.writeClientDataToClients()
+ this.writeObservations()
+ if this.canExecuteTurns then
+ this.clients.inRandomOrder(_.act())
+ this.writeClientDataToClients()
+ this.writeObservations()
+ this.clients.foreach(c =>
+ this.writeToClient(this.turnStartInfo(c), c)
+ )
+ this.previousTurn = currentTimeMillis() / 1000
+ if this.adventure.isDefined && this.joinAfterStart then
+ this.clients.foreach( c => if c.isReadyForGameStart then
+ this.adventure.foreach(a =>
+ c.getName.foreach(n => a.addPlayer(n))
+ )
+ startGameForClient(c)
+ )
+ else if this.adventure.isEmpty && !this.clients.isEmpty && this.clients.forall(_.isReadyForGameStart) then
+ this.adventure = Some(Adventure(this.clients.names))
+ this.clients.foreach(startGameForClient(_))
+ this.previousTurn = currentTimeMillis() / 1000
+
+ /** Helper function to start the game for the specified client c.
+ * MAY ONLY BE USED IF `this.adventure` is Some!
+ * Apparently guard clauses are bad because they use return or something,
+ * but assertions should be fine, as long as they enforce the function
+ * contract?
+ */
+ private def startGameForClient(c: Client): Unit =
+ assert(this.adventure.isDefined)
+ c.gameStart()
+ val name = c.getName
+
+ val playerEntity: Option[Player] = name match
+ case Some(n) => this.adventure match
+ case Some(a) => a.getPlayer(n)
+ case None => None
+ case None => None
+ playerEntity.foreach(c.givePlayer(_))
+
+ this.writeToClient(
+ s"$timeLimit\r\n${this.turnStartInfo(c)}", c
+ )
+
+ this.clients.foreach(c =>
+ if c.player != playerEntity then
+ c.player.foreach(_.observe(s"${name.getOrElse("Unknown player")} joins the game."))
+ )
+
+
+ private def writeObservations(): Unit =
+ this.clients.foreach(c =>
+ val observations = c.player.map(_.readAndClearObservations())
+ observations.foreach(_.foreach((s: String) =>
+ this.writeToClient(s"$ACTION_NONBLOCKING_INDICATOR$s\r\n", c))
+ )
+ )
+
+ /** Helper function to determine if the next turn can be taken */
+ private def canExecuteTurns: Boolean =
+ val requirement1 = this.adventure.isDefined
+ val requirement2 = !this.clients.isEmpty // nice! you can just return
+ // to the game after everyone
+ // left and everything is just
+ // as before!
+ val allPlayersReady = this.clients.forall(_.isReadyToAct)
+ val requirement3 = (allPlayersReady
+ || currentTimeMillis() / 1000 >= previousTurn + timeLimit)
+ requirement1 && requirement2 && requirement3
+
+
+ /** Receives a new client and stores it in `clients`.
+ *
+ * @return describes if a client was added
+ */
+ private def receiveNewClient(): Boolean =
+ this.clientGetter.newClient() match
+ case Some(c) =>
+ clients.addClient(Client(c))
+ true
+ case None =>
+ false
+
+ private def turnStartInfo(client: Client): String =
+ val clientArea = client.player.map(_.location)
+ val areaDesc = clientArea
+ .map(_.description)
+ .getOrElse("You are floating in the middle of a soothing void.")
+ val directions = clientArea
+ .map(_.getNeighborNames.mkString(LIST_SEPARATOR))
+ .getOrElse("")
+ val items = clientArea
+ .map(_.getItemNames.mkString(LIST_SEPARATOR))
+ .getOrElse("")
+ val entities = client.player.map(c =>
+ c.location
+ .getEntityNames
+ .filter(c.name != _)
+ .mkString(LIST_SEPARATOR)
+ ).getOrElse("")
+ s"$TURN_INDICATOR\r\n$areaDesc\r\n$directions\r\n$items\r\n$entities\r\n"
+
+ /** Sends `message` to all clients
+ *
+ * @param message the message to send
+ */
+ private def writeToAll(message: String): Unit =
+ this.clients.mapAndRemove(c =>
+ val output = c.socket.getOutputStream
+ output.write(stringToByteArray(message))
+ output.flush()
+ )
+
+ private def writeToClient(message: String, client: Client): Unit =
+ try {
+ val output = client.socket.getOutputStream
+ output.write(stringToByteArray(message))
+ output.flush()
+ } catch {
+ case e: IOException => client.failedProtocol()
+ }
+
+ /** Sends every client's `dataToThisClient` to the client */
+ private def writeClientDataToClients(): Unit =
+ this.clients.mapAndRemove(c =>
+ val output = c.socket.getOutputStream
+ val data = c.dataToThisClient()
+ output.write(stringToByteArray(data))
+ output.flush()
+ )
+
+ /** Reads data sent by clients and stores it in the `Client`s of `clients` */
+ private def readFromAll(): Unit =
+ clients.mapAndRemove(c =>
+ val input = c.socket.getInputStream
+ while input.available() != 0 do
+ val bytesRead = input.read(buffer)
+ if bytesRead != -1 then
+ c.receiveData(buffer.take(bytesRead).toVector)
+ )
+
+end Server