aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-14 19:25:19 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-14 19:25:19 +0200
commita43812ed462630850edbf29bda182fbf1e5e1263 (patch)
tree20e84b60adee6fa31ceb970ffccffa5b6b583d86
parentc87263e9e493fe6c130f5ad6a523871c08987f4c (diff)
downloadscalevalapokalypsi-a43812ed462630850edbf29bda182fbf1e5e1263.tar.gz
scalevalapokalypsi-a43812ed462630850edbf29bda182fbf1e5e1263.zip
Immediate printing of actions & no prompt on blocking action & refactoring
-rw-r--r--protocol.txt2
-rw-r--r--src/main/scala/Client/Client.scala206
-rw-r--r--src/main/scala/Client/ReceivedLineParser.scala27
-rw-r--r--src/main/scala/Client/StdinLineReader.scala31
-rw-r--r--src/main/scala/Client/Turn.scala32
-rw-r--r--src/main/scala/Server/Client.scala14
-rw-r--r--src/main/scala/Server/constants.scala2
-rw-r--r--src/main/scala/utils.scala19
8 files changed, 205 insertions, 128 deletions
diff --git a/protocol.txt b/protocol.txt
index 15cc75c..7b13ab2 100644
--- a/protocol.txt
+++ b/protocol.txt
@@ -4,7 +4,7 @@ Server: [good/version old]
Server: [time limit in int/secs]CRLF # signifies game start
Before turn:
-N x [Description of action during previous turn]CRLF
+N x [Action blocker indicator][Description of action during previous turn]CRLF
At start of turn:
Server: [turn indicator]CRLF
[Description of area]CRLF
diff --git a/src/main/scala/Client/Client.scala b/src/main/scala/Client/Client.scala
index 2503cdf..e9d3074 100644
--- a/src/main/scala/Client/Client.scala
+++ b/src/main/scala/Client/Client.scala
@@ -3,69 +3,19 @@ package o1game.Client
import java.lang.Thread.sleep
import java.net.Socket
import scala.io.Source
-import scala.io.StdIn.readLine
import scala.sys.process.stdout
-import java.io.InputStream
import o1game.constants.*
-import o1game.utils.stringToByteArray
+import o1game.utils.{stringToByteArray,getNCharsFromSocket}
+import o1game.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 scala.collection.immutable.LazyList
import java.lang.System.currentTimeMillis
-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
-def getNCharsFromSocket(input: InputStream, n: Int): Option[String] =
- val buffer: Array[Byte] = Array.ofDim(n)
- var i = 0
- var failed = false
- while i < n && !failed do
- val res = input.read(buffer, i, n - i)
- if res < 0 then failed = true
- i += res
- // TODO: better error handling
- if failed then None else Some(String(buffer))
-
-/** This class is for taking new lines from stdin when they are available.
- * reading starts when either newLine or clear or startReading are called.
+/** A helper enum for `Client` to keep track of communications with the server
*/
-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.nextLine = Future(readLine())
- Some(s)
- case Some(Failure(e)) =>
- this.nextLine = Future(readLine())
- None
- case None => None
-
- /** Discards the line that is currently being read and restarts reading */
- def clear(): Unit =
- this.nextLine = Future(readLine())
-
- /** Equivalent to clear */
- def startReading(): Unit = this.clear()
-
-end StdinLineReader
-
enum ServerLineState:
case WaitingForGameStart,
ActionDescription,
@@ -76,81 +26,60 @@ enum ServerLineState:
Entities
-//def indexOfCRLF(data: IndexedSeq[Byte]): Int =
-// val LF = data.indexOf(10)
-// data.get(LF + 1).filter(_ == 13).filter(a => LF == -1).getOrElse(-1)
-//
-//def splitAtCRLF(data: IndexedSeq[Byte]): Vector[]
-
-class ServerDataParser:
-
- private var serverLineState = ServerLineState.ActionDescription
-
- private var bufferedData: Buffer[Byte] = Buffer.empty // TODO: suboptimal DS
+/** 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
- def in(data: Array[Byte]): Unit =
- this.bufferedData ++= 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 ServerDataParser
+/** 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 var serverLineState = ServerLineState.WaitingForGameStart
- private val serverLineParser = ServerDataParser()
+ private val serverLineParser = ReceivedLineParser()
private val stdinReader = StdinLineReader()
- private var timeLimit: Long = 0
+ private var serverLineState = ServerLineState.WaitingForGameStart
+
+ /** 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()
- // TODO: extract these to a separate area object for the client
- private var actions: Buffer[String] = Buffer.empty
- private var areaDescription: String = ""
- private var possibleDirections: Array[String] = Array.empty
- private var visibleItems: Array[String] = Array.empty
- private var visibleEntities: Array[String] = Array.empty
-
-
- 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.lastExecutedTurn = currentTimeMillis / 1000
- val actionDesc = this.actions.mkString("\n")
- this.actions = Buffer.empty
- 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"\n$actionDesc\n\n$areaDescription\n$directionDesc\n" +
- s"\n$itemDesc\n$entityDesc\n")
-
+ /** Starts the client. This shouldn't terminate. */
def startClient(): Unit =
+
stdinReader.startReading()
while true do
@@ -165,10 +94,34 @@ class Client(socket: Socket):
output.write(stringToByteArray(s+"\r\n"))
)
- if this.timeLimit != 0 && this.lastTurnStart != 0 then
- val timeOfTurnEnd = this.lastTurnStart + this.timeLimit
- val timeToTurnEnd = -currentTimeMillis()/1000 + timeOfTurnEnd
- print(s"\r[$timeToTurnEnd]> ")
+ 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(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)
@@ -179,35 +132,46 @@ class Client(socket: Socket):
nextLine
.foreach(this.parseLineFromServer(_))
+
private def parseLineFromServer(line: String) =
+
if line == TURN_INDICATOR then
this.serverLineState = ServerLineState.TurnIndicator
+
serverLineState match
+
case ServerLineState.WaitingForGameStart =>
val time = line.toLongOption
time match
case Some(t) => this.timeLimit = t
case None => print("Invalid time limit, oh no!!!")
this.serverLineState = ServerLineState.ActionDescription
+
case ServerLineState.ActionDescription =>
- this.actions.append(line)
+ if line.head == ACTION_BLOCKING_INDICATOR then
+ this.canAct = false
+ this.displayAction(line.tail)
+
case ServerLineState.TurnIndicator =>
this.serverLineState = ServerLineState.AreaDescription
+
case ServerLineState.AreaDescription =>
- this.areaDescription = line
+ this.turnInfo.areaDescription = line
this.serverLineState = ServerLineState.Directions
+
case ServerLineState.Directions =>
- this.possibleDirections = line.split(LIST_SEPARATOR)
+ this.turnInfo.possibleDirections = line.split(LIST_SEPARATOR)
this.serverLineState = ServerLineState.Items // TODO: maybe use a list instead?
+
case ServerLineState.Items =>
- this.visibleItems = line.split(LIST_SEPARATOR)
+ this.turnInfo.visibleItems = line.split(LIST_SEPARATOR)
this.serverLineState = ServerLineState.Entities
+
case ServerLineState.Entities =>
- this.visibleEntities = line.split(LIST_SEPARATOR)
+ this.turnInfo.visibleEntities = line.split(LIST_SEPARATOR)
this.serverLineState = ServerLineState.ActionDescription
this.lastTurnStart = currentTimeMillis() / 1000
+ end parseLineFromServer
- //bufferIndex = s"Houston, I think this shouldn't be so hard.\n".toVector.map(_.toByte).copyToArray(buffer)
- //output.write(buffer, 0, bufferIndex)
- //output.flush()
+end Client
diff --git a/src/main/scala/Client/ReceivedLineParser.scala b/src/main/scala/Client/ReceivedLineParser.scala
new file mode 100644
index 0000000..7cbf935
--- /dev/null
+++ b/src/main/scala/Client/ReceivedLineParser.scala
@@ -0,0 +1,27 @@
+package o1game.Client
+
+import scala.collection.mutable.Buffer
+import o1game.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/main/scala/Client/StdinLineReader.scala b/src/main/scala/Client/StdinLineReader.scala
new file mode 100644
index 0000000..42a1f40
--- /dev/null
+++ b/src/main/scala/Client/StdinLineReader.scala
@@ -0,0 +1,31 @@
+package o1game.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/main/scala/Client/Turn.scala b/src/main/scala/Client/Turn.scala
new file mode 100644
index 0000000..6b78811
--- /dev/null
+++ b/src/main/scala/Client/Turn.scala
@@ -0,0 +1,32 @@
+package o1game.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
diff --git a/src/main/scala/Server/Client.scala b/src/main/scala/Server/Client.scala
index cd557c6..323b78c 100644
--- a/src/main/scala/Server/Client.scala
+++ b/src/main/scala/Server/Client.scala
@@ -89,7 +89,7 @@ class Client(val socket: Socket):
* @param data data to buffer for sending
*/
private def addDataToSend(data: String): Unit =
- this.outData += s"$data\r\n"
+ this.outData += s"$data"
/** Returns one line of data if there are any line breaks.
@@ -109,6 +109,7 @@ class Client(val socket: Socket):
/** Makes the client play its turn */
def act(): Unit =
+ this.addDataToSend(ACTION_BLOCKING_INDICATOR.toString)
this.nextAction.foreach(this.executeAction(_))
this.nextAction = None
@@ -134,11 +135,11 @@ class Client(val socket: Socket):
this.protocolIsIntact = this.protocolState match
case WaitingForVersion =>
if line == GAME_VERSION then
- addDataToSend(PROTOCOL_VERSION_GOOD)
+ addDataToSend(s"$PROTOCOL_VERSION_GOOD\r\n")
this.protocolState = WaitingForClientName
true
else
- addDataToSend(PROTOCOL_VERSION_BAD)
+ addDataToSend(s"$PROTOCOL_VERSION_BAD\r\n")
false
case WaitingForClientName =>
this.name = Some(line)
@@ -158,13 +159,14 @@ class Client(val socket: Socket):
) then
this.nextAction = Some(action)
else if this.nextAction.isEmpty then
- executeAction(action)
+ this.addDataToSend(ACTION_NONBLOCKING_INDICATOR.toString)
+ this.executeAction(action)
/** Executes the specified action and buffers its description for sending */
private def executeAction(action: Action) =
this.character.flatMap(action.execute(_)) match
- case Some(s) => this.addDataToSend((s))
- case None => this.addDataToSend("You can't do that")
+ case Some(s) => this.addDataToSend(s"$s\r\n")
+ case None => this.addDataToSend("You can't do that\r\n")
end Client
diff --git a/src/main/scala/Server/constants.scala b/src/main/scala/Server/constants.scala
index a9e4502..083db4e 100644
--- a/src/main/scala/Server/constants.scala
+++ b/src/main/scala/Server/constants.scala
@@ -6,6 +6,8 @@ val CRLF: Vector[Byte] = Vector(13.toByte, 10.toByte)
val POLL_INTERVAL = 100 // millisec.
val GAME_VERSION = "0.1.0"
val TURN_INDICATOR = ">"
+val ACTION_BLOCKING_INDICATOR='.'
+val ACTION_NONBLOCKING_INDICATOR='+'
val LIST_SEPARATOR=";"
diff --git a/src/main/scala/utils.scala b/src/main/scala/utils.scala
index f230be7..cfca568 100644
--- a/src/main/scala/utils.scala
+++ b/src/main/scala/utils.scala
@@ -1,5 +1,7 @@
package o1game.utils
+import java.io.InputStream
+
/** Converts this string to an array of bytes (probably for transmission).
*
* @param str the string to convert
@@ -7,3 +9,20 @@ package o1game.utils
*/
def stringToByteArray(str: String): Array[Byte] =
str.toVector.map(_.toByte).toArray
+
+/** Reads n characters from the given InputStream blockingly.
+ *
+ * @param input the InputStream to read from
+ * @param n the number of bytes to read
+ * @return The read result, or None in case of failure
+ */
+def getNCharsFromSocket(input: InputStream, n: Int): Option[String] =
+ val buffer: Array[Byte] = Array.ofDim(n)
+ var i = 0
+ var failed = false
+ while i < n && !failed do
+ val res = input.read(buffer, i, n - i)
+ if res < 0 then failed = true
+ i += res
+ // TODO: better error handling
+ if failed then None else Some(String(buffer))