1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
|
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 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.
*/
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,
TurnIndicator,
AreaDescription,
Directions,
Items,
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
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
class Client(socket: Socket):
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 stdinReader = StdinLineReader()
private var timeLimit: Long = 0
private var lastTurnStart: Long = 0
private var lastExecutedTurn: Long = 0
assert(
lastTurnStart <= lastExecutedTurn,
"don't initialize with unexecuted 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")
def startClient(): Unit =
stdinReader.startReading()
// TODO: read data from server and store it in the turn description
// TODO: if turn isn't executed since lastturnstart, display the data
// and clean STDIN
// TODO: write data from stdin and send it to the server
// TODO: display timer to next turn end
while true do
sleep(POLL_INTERVAL)
this.readAndParseDataFromServer()
if this.lastExecutedTurn < this.lastTurnStart then
println(this.giveTurn())
stdinReader.newLine().foreach((s: String) =>
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]> ")
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.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)
case ServerLineState.TurnIndicator =>
this.serverLineState = ServerLineState.AreaDescription
case ServerLineState.AreaDescription =>
this.areaDescription = line
this.serverLineState = ServerLineState.Directions
case ServerLineState.Directions =>
this.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.serverLineState = ServerLineState.Entities
case ServerLineState.Entities =>
this.visibleEntities = line.split(LIST_SEPARATOR)
this.serverLineState = ServerLineState.ActionDescription
this.lastTurnStart = currentTimeMillis() / 1000
//bufferIndex = s"Houston, I think this shouldn't be so hard.\n".toVector.map(_.toByte).copyToArray(buffer)
//output.write(buffer, 0, bufferIndex)
//output.flush()
|