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
|
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,
ActionsAndSong,
TurnIndicator,
AreaDescription,
Directions,
Items,
Entities
/** Creates a new client.
*
* @param name the name the client and its player should have
* @param ip the ip of the server to connect to
* @param 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 // TODO: is really never true when it should
private var timeLimit: Long = 0
private var lastTurnStart: Long = 0
private var lastExecutedTurn: Long = 0
private var isSinging: Boolean = false
private val bufferedActions: Buffer[String] = Buffer.empty
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()
this.displayActions()
if
this.lastExecutedTurn < this.lastTurnStart &&
!this.isSinging
then
print(this.giveTurn())
// TODO: we probably want to quit at EOF
stdinReader.newLine().foreach((s: String) =>
this.isSinging = false
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 bufferAction(action: String): Unit =
this.bufferedActions += action
private def displayActions(): Unit =
val somethingToShow = this.bufferedActions.nonEmpty
if somethingToShow then
if !this.isSinging then
this.bufferedActions.foreach(println(_))
this.bufferedActions.clear()
if !this.isSinging && this.canAct && somethingToShow then
print(this.actionGetterIndicator)
private def startSong(verse: String): Unit =
this.isSinging = true
print(s"\nLaula: “$verse”\n> ")
// TODO: this is sometimes in front of actions, use an indicator to test if newline before actions?
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.ActionsAndSong =>
if line.headOption.exists(_.toString == SING_INDICATOR) then
this.startSong(line.tail)
else if line.headOption.contains(ACTION_BLOCKING_INDICATOR) then
this.canAct = false
this.bufferAction(line.tail)
else if line.nonEmpty then
this.bufferAction((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.ActionsAndSong
this.lastTurnStart = currentTimeMillis() / 1000
end parseLineFromServer
end Client
|