blob: 940888b5a9dfa12134363465edc9ea26667b7879 (
plain)
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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
|
package scalevalapokalypsi.Server
import java.net.Socket
import scala.math.{min,max}
import scalevalapokalypsi.constants.*
import scalevalapokalypsi.utils.*
import ServerProtocolState.*
import scalevalapokalypsi.Model.Action
import scalevalapokalypsi.Model.Entities.Player
import java.lang.System.currentTimeMillis
import scala.collection.mutable.Buffer
// Note to graders etc
// This class has an interesting design choice:
// It does not write data directly to the client.
// This is because with some multithreaded implementation of
// Server.scala it could lead to race conditions etc where
// several writes happened at once.
//
// Thus, this class never writes to the client anything, and
// all TCP communication is the responsibility of the server.
// Because of lack of time though, I had to rely on a very dirty
// trick - the client has a workaround way to actually write data
// to the client by queuing it to the server via the method
// `dataToThisClient`.
class Client(val socket: Socket):
private var incompleteMessage: Array[Byte] =
Array.fill(MAX_MSG_SIZE)(0.toByte)
private var incompleteMessageIndex = 0
private var protocolState = WaitingForVersion
private var outData: String = ""
private var character: Option[Player] = None
private var protocolIsIntact = true
private var name: Option[String] = None
private var nextAction: Option[Action] = None
private var turnUsed = false
private var singStartTime: Option[Long] = None
private var versesToSing: Vector[String] = Vector.empty
private val versesSung: Buffer[String] = Buffer.empty
private var verseIndex = 0
def clientHasSong = this.singStartTime.isDefined
def startSongIfNeeded(): Unit =
if this.player.exists(_.isSinging) && !this.clientHasSong then
val verses = this.player.flatMap(_.getVerses)
verses.foreach(v =>
this.versesToSing = v
this.verseIndex = 0
this.singStartTime = Some(currentTimeMillis() / 1000)
this.startVerse()
)
/** Starts the next verse for the remote client,
* use only when you have checked that there are verses left to sing!
*/
def startVerse(): Unit =
val verse = this.versesToSing.lift(this.verseIndex)
verse.foreach(v =>
this.addDataToSend(s"${SING_INDICATOR}$v")
)
this.verseIndex += 1
/** Calculates the amount of bytes available for future incoming messages */
def spaceAvailable: Int = this.incompleteMessage.size - incompleteMessageIndex - 1
/** Tests whether the client has behaved according to protocol.
*
* @return false if there has been a protocol violation, true otherwise
*/
def isIntactProtocolWise: Boolean = this.protocolIsIntact
/** Marks that this client misbehaved in eyes of the protocol */
def failedProtocol(): Unit = this.protocolIsIntact = false
/** Tests whether this client is initialized and ready to start the game
*
* @return true if the client is ready to join the game
*/
def isReadyForGameStart: Boolean =
this.protocolState == WaitingForGameStart
/** Signals this client that it's joining the game. This is important so
* that this object knows to update its protocol state.
*/
def gameStart(): Unit = this.protocolState = InGame
/** Returns the player this client controls in the model.
*
* @return an option containing the player
*/
def player: Option[Player] = this.character
/** Tells this client object that it controls the specified player.
*
* @param player the player this client is to control
*/
def givePlayer(player: Player): Unit =
this.character = Some(player)
/** Gets the name of this client, which should match the name of the player
* that is given to this client. Not very useful if the client hasn't yet
* received the name or if it already has an player.
*
* @return the name of this client
*/
def getName: Option[String] = this.name
/** Sets `data` as received for the client.
*
* @return false means there was not enough space to receive the message
*/
def receiveData(data: Vector[Byte]): Boolean =
if data.length > this.spaceAvailable then
false
else
for i <- 0 until min(data.length, this.spaceAvailable) do
this.incompleteMessage(this.incompleteMessageIndex+i) = data(i)
this.incompleteMessageIndex += data.length
true
/** Returns data that should be sent to this client.
* The data is cleared when calling.
*/
def dataToThisClient(): String =
val a = this.outData
this.outData = ""
a
/** Specifies that the data should be buffered for
* sending to this client
*
* @param data data to buffer for sending
*/
private def addDataToSend(data: String): Unit =
this.outData += s"$data\r\n"
/** Returns one line of data if there are any line breaks.
* Removes the parsed data from the message buffering area.
*/
private def nextLine(): Option[String] =
var nextCRLF = this.incompleteMessage.indexOf(CRLF(0))
if this.incompleteMessage(nextCRLF + 1) != CRLF(1) then nextCRLF = -1
if nextCRLF != -1 then
val message = this.incompleteMessage.take(nextCRLF)
val rest = this.incompleteMessage.drop(nextCRLF + 2)
this.incompleteMessage = rest ++ Array.fill(nextCRLF + 1)(0.toByte)
this.incompleteMessageIndex = 0
byteArrayToString(message)
else
None
/** Makes the client play its turn */
def giveTurn(): Unit =
this.turnUsed = false
/** Checks whether the client has chosen its next action
*
* @return whether the client is ready to act */
def hasActed: Boolean = this.turnUsed
/** Causes the client to interpret the data it has received */
def interpretData(): Unit =
LazyList.continually(this.nextLine())
.takeWhile(_.isDefined)
.flatten
.foreach(s => interpretLine(s))
/** Makes the client execute the action specified by `line`.
* If there is a protocol error, the function changes
* the variable `protocolIsIntact` to false.
*
* @param line the line to interpret
*/
private def interpretLine(line: String): Unit =
this.protocolIsIntact = this.protocolState match
case WaitingForVersion =>
if line == GAME_VERSION then
addDataToSend(s"$PROTOCOL_VERSION_GOOD")
this.protocolState = WaitingForClientName
true
else
addDataToSend(s"$PROTOCOL_VERSION_BAD")
false
case WaitingForClientName =>
this.name = Some(line)
this.protocolState = WaitingForGameStart
true
case WaitingForGameStart => true
case InGame =>
this.executeLine(line)
true
/** Buffers the action for execution or executes it immediately if it
* doesn't take a turn */
private def executeLine(line: String) =
if !this.turnUsed then
this.singStartTime match
case Some(t) =>
if this.verseIndex == this.versesToSing.length then
val timeQuality =
5.0*this.versesToSing.length /
max(5.0, currentTimeMillis()/1000 - t)
val songToSing = this.versesToSing.mkString("")
.toLowerCase
val songSung = this.versesSung.mkString("").toLowerCase
val quality =
timeQuality * (
1.0 - hammingDistance(songToSing, songSung)
.toFloat
/ songToSing.length.toFloat
)
this.player.foreach(_.applySingEffect(quality.toFloat))
this.singStartTime = None
else
this.versesSung += line
this.startVerse()
case None if isPrintable(line) =>
val action = Action(line)
val takesATurn = this.character.exists(p => action.execute(p))
if takesATurn then
this.addDataToSend(s"$ACTION_BLOCKING_INDICATOR")
this.turnUsed = true
case None =>
() // There were some illegal chars but whatever
end executeLine
end Client
|