diff options
Diffstat (limited to 'src/scalevalapokalypsi/Client')
-rw-r--r-- | src/scalevalapokalypsi/Client/Client.scala | 178 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Client/ReceivedLineParser.scala | 27 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Client/StdinLineReader.scala | 31 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Client/Turn.scala | 32 |
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 |