aboutsummaryrefslogtreecommitdiff
path: root/src/scalevalapokalypsi/Client/Client.scala
blob: 1ca4e7e19be953b53727fe4e2d7d285e132a6cd1 (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
package scalevalapokalypsi.Client

import java.net.{Socket,InetSocketAddress}
import scala.io.Source
import scala.sys.process.stdout
import scalevalapokalypsi.constants.*
import scalevalapokalypsi.utils.{stringToByteArray,getNCharsFromSocket}
import scalevalapokalypsi.Client.{ReceivedLineParser,RoomState}
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()
	socket.connect(new InetSocketAddress(ip, port), INITIAL_CONN_TIMEOUT)
	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 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 lineToSing: Option[String] = None
	private val bufferedActions: Buffer[String] = Buffer.empty
	assert(
		lastTurnStart <= lastExecutedTurn,
		"don't initialize with unexecuted turn"
	)
	private val turnInfo = RoomState()
	private var gameOver = false

	/** Takes a client step and optionally returns an in-game event for UI
	  *
	  * @param clientInput one line of client input if any
	  * @return            an event describing new changes in the game state
	  */
	def clientStep(clientInput: Option[String]): GameEvent =

			this.readAndParseDataFromServer()

			val actions = this.getNewActions()

			val roomState = if
				this.lastExecutedTurn < this.lastTurnStart &&
				this.lineToSing.isEmpty
			then
				this.giveTurn()
				Some(this.turnInfo)
			else
				None

			for line <- clientInput do
				this.lineToSing = None
				output.write(stringToByteArray(s"$line\r\n"))

			val timeOfTurnEnd = this.lastTurnStart + this.timeLimit
			val timeToTurnEnd = -currentTimeMillis()/1000 + timeOfTurnEnd
			GameEvent(
				actions,
				roomState,
				this.lineToSing,
				this.canAct,
				Some(timeToTurnEnd).filter(p => this.lastTurnStart != 0),
				!this.gameOver
			)

	end clientStep
	

	private def readAndParseDataFromServer(): Unit =
		var availableBytes = input.available()
		while availableBytes != 0 do
			val bytesRead = input.read(buffer, 0, availableBytes)
			if bytesRead != -1 then
				parseDataFromServer(buffer.take(bytesRead))
			availableBytes = input.available()
	
	private def giveTurn(): Unit =
		this.canAct = true
		this.lastExecutedTurn = currentTimeMillis / 1000
	
	private def bufferAction(action: String): Unit =
		this.bufferedActions += action

	private def getNewActions(): Option[Vector[String]] =
		val somethingToShow = this.bufferedActions.nonEmpty
		if somethingToShow && this.lineToSing.isEmpty then
			val res = this.bufferedActions.toVector
			this.bufferedActions.clear()
			Some(res)
		else
			None

	private def startSong(verse: String): Unit =
		this.lineToSing = Some(verse)


	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

		if line == GAME_END_INDICATOR then
			this.gameOver = true

		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 =>
				val dirs = line.split(LIST_SEPARATOR)
				if dirs(0) == "" && dirs.length == 1 then
					this.turnInfo.possibleDirections = Array.empty
				else
					this.turnInfo.possibleDirections = dirs
				this.serverLineState = ServerLineState.Items

			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