aboutsummaryrefslogtreecommitdiff
path: root/src/scalevalapokalypsi/Client
diff options
context:
space:
mode:
Diffstat (limited to 'src/scalevalapokalypsi/Client')
-rw-r--r--src/scalevalapokalypsi/Client/Client.scala178
-rw-r--r--src/scalevalapokalypsi/Client/ReceivedLineParser.scala27
-rw-r--r--src/scalevalapokalypsi/Client/StdinLineReader.scala31
-rw-r--r--src/scalevalapokalypsi/Client/Turn.scala32
4 files changed, 268 insertions, 0 deletions
diff --git a/src/scalevalapokalypsi/Client/Client.scala b/src/scalevalapokalypsi/Client/Client.scala
new file mode 100644
index 0000000..41b1003
--- /dev/null
+++ b/src/scalevalapokalypsi/Client/Client.scala
@@ -0,0 +1,178 @@
+package scalevalapokalypsi.Client
+
+import java.lang.Thread.sleep
+import java.net.Socket
+import scala.io.Source
+import scala.sys.process.stdout
+import scalevalapokalypsi.constants.*
+import scalevalapokalypsi.utils.{stringToByteArray,getNCharsFromSocket}
+import scalevalapokalypsi.Client.{ReceivedLineParser,StdinLineReader,Turn}
+import scala.concurrent.Future
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.util.{Try, Success, Failure}
+import scala.collection.mutable.Buffer
+import java.lang.System.currentTimeMillis
+
+
+/** A helper enum for `Client` to keep track of communications with the server
+ */
+enum ServerLineState:
+ case WaitingForTimeLimit,
+ ActionDescription,
+ TurnIndicator,
+ AreaDescription,
+ Directions,
+ Items,
+ Entities
+
+
+/** Creates a new client.
+ *
+ * @param name the name the client and its player should have
+ * @ip the ip of the server to connect to
+ * @port the port of the server to connect to
+ * @return the client created, if all was successful
+ */
+def newClient(name: String, ip: String, port: Int): Option[Client] =
+ val socket = Socket(ip, port)
+ val output = socket.getOutputStream
+ val input = socket.getInputStream
+ val initMsg = s"$GAME_VERSION\r\n$name\r\n"
+ output.write(stringToByteArray(initMsg))
+ val msgLen = (PROTOCOL_VERSION_GOOD + "\r\n").length
+ val versionResponse = getNCharsFromSocket(input, msgLen)
+ if versionResponse == Some(s"$PROTOCOL_VERSION_GOOD\r\n") then
+ Some(Client(socket))
+ else
+ None
+
+
+
+/** Main class for the client: handles communication with the server
+ * and the player. Should be initialized with `newClient`.
+ *
+ * @param socket the socket the client uses
+ */
+class Client(socket: Socket):
+
+ /** Essential IO variables */
+ private val input = socket.getInputStream
+ private val output = socket.getOutputStream
+ private val buffer: Array[Byte] = Array.ofDim(MAX_MSG_SIZE)
+ private var bufferIndex = 0
+ private val serverLineParser = ReceivedLineParser()
+ private val stdinReader = StdinLineReader()
+
+ private var serverLineState = ServerLineState.WaitingForTimeLimit
+
+ /** Variables about the status of the current turn for the client */
+ private var canAct = false
+ private var timeLimit: Long = 0
+ private var lastTurnStart: Long = 0
+ private var lastExecutedTurn: Long = 0
+ assert(
+ lastTurnStart <= lastExecutedTurn,
+ "don't initialize with unexecuted turn"
+ )
+ private val turnInfo = Turn()
+
+
+ /** Starts the client. This shouldn't terminate. */
+ def startClient(): Unit =
+
+ stdinReader.startReading()
+
+ while true do
+ sleep(POLL_INTERVAL)
+
+ this.readAndParseDataFromServer()
+
+ if this.lastExecutedTurn < this.lastTurnStart then
+ print(this.giveTurn())
+
+ stdinReader.newLine().foreach((s: String) =>
+ output.write(stringToByteArray(s+"\r\n"))
+ )
+
+ end startClient
+
+
+ private def readAndParseDataFromServer(): Unit =
+ var availableBytes = input.available()
+ while availableBytes != 0 do
+ val bytesRead = input.read(buffer, 0, availableBytes)
+ if bytesRead != -1 then
+ // TODO: unsafe conversion
+ parseDataFromServer(buffer.take(bytesRead))
+ availableBytes = input.available()
+
+ private def giveTurn(): String =
+ this.canAct = true
+ this.lastExecutedTurn = currentTimeMillis / 1000
+ s"\n\n${this.turnInfo}\n${this.actionGetterIndicator}"
+
+ private def displayAction(action: String): Unit =
+ println(s"$action")
+ if this.canAct then
+ print(this.actionGetterIndicator)
+
+ private def actionGetterIndicator =
+ val timeOfTurnEnd = this.lastTurnStart + this.timeLimit
+ val timeToTurnEnd = -currentTimeMillis()/1000 + timeOfTurnEnd
+ s"[$timeToTurnEnd]> "
+
+
+
+ private def parseDataFromServer(data: Array[Byte]): Unit =
+ this.serverLineParser.in(data)
+ var nextLine: Option[String] = Some("")
+ while nextLine.isDefined do
+ nextLine = this.serverLineParser
+ .nextLine()
+ nextLine
+ .foreach(this.parseLineFromServer(_))
+
+
+ private def parseLineFromServer(line: String) =
+
+ if line == TURN_INDICATOR then
+ this.serverLineState = ServerLineState.TurnIndicator
+
+ serverLineState match
+
+ case ServerLineState.WaitingForTimeLimit =>
+ val time = line.toLongOption
+ time match
+ case Some(t) => this.timeLimit = t
+ case None => print("Invalid time limit, oh no!!!")
+ this.serverLineState = ServerLineState.TurnIndicator
+ this.lastTurnStart = currentTimeMillis / 1000
+
+ case ServerLineState.ActionDescription =>
+ if line.nonEmpty && line.head == ACTION_BLOCKING_INDICATOR then
+ this.canAct = false
+ this.displayAction(line.tail)
+
+ case ServerLineState.TurnIndicator =>
+ this.serverLineState = ServerLineState.AreaDescription
+
+ case ServerLineState.AreaDescription =>
+ this.turnInfo.areaDescription = line
+ this.serverLineState = ServerLineState.Directions
+
+ case ServerLineState.Directions =>
+ this.turnInfo.possibleDirections = line.split(LIST_SEPARATOR)
+ this.serverLineState = ServerLineState.Items // TODO: maybe use a list instead?
+
+ case ServerLineState.Items =>
+ this.turnInfo.visibleItems = line.split(LIST_SEPARATOR)
+ this.serverLineState = ServerLineState.Entities
+
+ case ServerLineState.Entities =>
+ this.turnInfo.visibleEntities = line.split(LIST_SEPARATOR)
+ this.serverLineState = ServerLineState.ActionDescription
+ this.lastTurnStart = currentTimeMillis() / 1000
+
+ end parseLineFromServer
+
+end Client
diff --git a/src/scalevalapokalypsi/Client/ReceivedLineParser.scala b/src/scalevalapokalypsi/Client/ReceivedLineParser.scala
new file mode 100644
index 0000000..dfcc2d2
--- /dev/null
+++ b/src/scalevalapokalypsi/Client/ReceivedLineParser.scala
@@ -0,0 +1,27 @@
+package scalevalapokalypsi.Client
+
+import scala.collection.mutable.Buffer
+import scalevalapokalypsi.constants.*
+
+/** A class for checking asynchronously for received lines */
+class ReceivedLineParser:
+
+ private var serverLineState = ServerLineState.ActionDescription
+
+ private var bufferedData: Buffer[Byte] = Buffer.empty // TODO: suboptimal DS
+
+ /** Add received data */
+ def in(data: Array[Byte]): Unit =
+ this.bufferedData ++= data
+
+ /** Read a line from the received data */
+ def nextLine(): Option[String] =
+ val indexOfCRLF = this.bufferedData.indexOfSlice(CRLF)
+ if indexOfCRLF == -1 then
+ None
+ else
+ val splitData = this.bufferedData.splitAt(indexOfCRLF)
+ this.bufferedData = Buffer.from(splitData(1).drop(CRLF.length))
+ Some(String(splitData(0).toArray))
+
+end ReceivedLineParser
diff --git a/src/scalevalapokalypsi/Client/StdinLineReader.scala b/src/scalevalapokalypsi/Client/StdinLineReader.scala
new file mode 100644
index 0000000..6ba8761
--- /dev/null
+++ b/src/scalevalapokalypsi/Client/StdinLineReader.scala
@@ -0,0 +1,31 @@
+package scalevalapokalypsi.Client
+
+import scala.concurrent.Future
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.io.StdIn.readLine
+import scala.util.{Try, Success, Failure}
+
+/** This class is for taking new lines from stdin when they are available.
+ * reading starts when either newLine or clear or startReading are called.
+ */
+class StdinLineReader:
+
+ private var nextLine: Future[String] = Future.failed(Exception())
+
+ /** Returns a new line of input if there are any. */
+ def newLine(): Option[String] =
+ this.nextLine.value match
+ case Some(Success(s)) =>
+ this.startReading()
+ Some(s)
+ case Some(Failure(e)) =>
+ this.startReading()
+ None
+ case None => None
+
+ /** Discards the line that is currently being read and restarts reading */
+ def startReading(): Unit =
+ this.nextLine = Future(readLine())
+
+
+end StdinLineReader
diff --git a/src/scalevalapokalypsi/Client/Turn.scala b/src/scalevalapokalypsi/Client/Turn.scala
new file mode 100644
index 0000000..30101c5
--- /dev/null
+++ b/src/scalevalapokalypsi/Client/Turn.scala
@@ -0,0 +1,32 @@
+package scalevalapokalypsi.Client
+
+/** `Turn`s represent information the client has got about a turn.
+ * This class exists essentially so that the client has somewhere
+ * to store data about turns and something to format that data with.
+ */
+class Turn:
+
+ /** Description of the area the player controlled by the client is in
+ * at the end of the turn. */
+ var areaDescription: String = ""
+
+ /** Directions the player controlled by the client can go to. */
+ var possibleDirections: Array[String] = Array.empty
+
+ /** Items the player controlled by the client can see. */
+ var visibleItems: Array[String] = Array.empty
+
+ /** Entities the player controlled by the client can see. */
+ var visibleEntities: Array[String] = Array.empty
+
+ override def toString: String =
+ val itemDesc = "You can see the following items: " +
+ this.visibleItems.mkString(", ")
+ val entityDesc = "The following entities reside in the room: " +
+ this.visibleEntities.mkString(", ")
+ val directionDesc = "There are exits to " +
+ this.possibleDirections.mkString(", ")
+ (s"$areaDescription\n$directionDesc\n" +
+ s"\n$itemDesc\n$entityDesc")
+
+end Turn