aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-07 21:41:53 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-07 21:41:53 +0200
commitdef8975617e5c6da431e41cc889d167b0f2e2bb0 (patch)
treef538102c17d81b596834fad7f47528fe2fc04b19
parent891b6f877ba848a3f78851a757734ebd1f066798 (diff)
downloadscalevalapokalypsi-def8975617e5c6da431e41cc889d167b0f2e2bb0.tar.gz
scalevalapokalypsi-def8975617e5c6da431e41cc889d167b0f2e2bb0.zip
Added possibility to join in the middle of the game & fixed bugs and inaccuracies:
* Adding the player didn't add it to the starting area. Fixed. * Refactored some code a bit by extracting functions * Changed the areas of `Adventure` to be private
-rw-r--r--src/main/scala/Model/Adventure.scala33
-rw-r--r--src/main/scala/Server/Character.scala7
-rw-r--r--src/main/scala/Server/Client.scala5
-rw-r--r--src/main/scala/Server/Server.scala83
-rw-r--r--src/main/scala/main.scala2
5 files changed, 94 insertions, 36 deletions
diff --git a/src/main/scala/Model/Adventure.scala b/src/main/scala/Model/Adventure.scala
index c13ff47..01f5d13 100644
--- a/src/main/scala/Model/Adventure.scala
+++ b/src/main/scala/Model/Adventure.scala
@@ -14,13 +14,13 @@ class Adventure(val playerNames: Vector[String]):
/** the name of the game */
val title = "A Forest Adventure"
- /*private*/ val middle = Area("Forest", "You are somewhere in the forest. There are a lot of trees here.\nBirds are singing.")
- /*private*/ val northForest = Area("Forest", "You are somewhere in the forest. A tangle of bushes blocks further passage north.\nBirds are singing.")
- /*private*/ val southForest = Area("Forest", "The forest just goes on and on.")
- /*private*/ val clearing = Area("Forest Clearing", "You are at a small clearing in the middle of forest.\nNearly invisible, twisted paths lead in many directions.")
- /*private*/ val tangle = Area("Tangle of Bushes", "You are in a dense tangle of bushes. It's hard to see exactly where you're going.")
- /*private*/ val home = Area("Home", "Home sweet home! Now the only thing you need is a working remote control.")
- /*private*/ val destination = home
+ private val middle = Area("Forest", "You are somewhere in the forest. There are a lot of trees here.\nBirds are singing.")
+ private val northForest = Area("Forest", "You are somewhere in the forest. A tangle of bushes blocks further passage north.\nBirds are singing.")
+ private val southForest = Area("Forest", "The forest just goes on and on.")
+ private val clearing = Area("Forest Clearing", "You are at a small clearing in the middle of forest.\nNearly invisible, twisted paths lead in many directions.")
+ private val tangle = Area("Tangle of Bushes", "You are in a dense tangle of bushes. It's hard to see exactly where you're going.")
+ private val home = Area("Home", "Home sweet home! Now the only thing you need is a working remote control.")
+ private val destination = home
middle.setNeighbors(Vector("north" -> northForest, "east" -> tangle, "south" -> southForest, "west" -> clearing))
northForest.setNeighbors(Vector("east" -> tangle, "south" -> middle, "west" -> clearing))
@@ -38,7 +38,24 @@ class Adventure(val playerNames: Vector[String]):
))
val players: Map[String, Entity] = Map()
- playerNames.foreach(name => players += name -> Entity(name, middle))
+ playerNames.foreach(this.addPlayer(_))
+
+ /** Adds a player entity with the specified name to the game.
+ *
+ * @param name the name of the player entity to add
+ * @return the created player entity
+ */
+ def addPlayer(name: String): Entity =
+ val newPlayer = Entity(name, middle)
+ middle.addEntity(newPlayer)
+ players += name -> newPlayer
+ newPlayer
+
+ /** Gets the player entity with the specified name.
+ *
+ * @param name name of the player to find
+ * @return the player, if one with the name was found
+ */
def getPlayer(name: String): Option[Entity] = this.players.get(name)
/** Returns a message that is to be displayed to the player at the beginning of the game. */
diff --git a/src/main/scala/Server/Character.scala b/src/main/scala/Server/Character.scala
deleted file mode 100644
index 174b7b0..0000000
--- a/src/main/scala/Server/Character.scala
+++ /dev/null
@@ -1,7 +0,0 @@
-package o1game.Server
-
-import scala.collection.mutable.Buffer
-
-class GameCharacter(val name: String):
- private val items: Buffer[String] = Buffer.empty // TODO: Item class
- // TODO: A lot of other things too
diff --git a/src/main/scala/Server/Client.scala b/src/main/scala/Server/Client.scala
index d9bb529..d9fa684 100644
--- a/src/main/scala/Server/Client.scala
+++ b/src/main/scala/Server/Client.scala
@@ -24,7 +24,10 @@ class Client(val socket: Socket):
*
* @return false if there has been a protocol violation, true otherwise
*/
- def isIntactProtocolWise: Boolean = protocolIsIntact
+ 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
*
diff --git a/src/main/scala/Server/Server.scala b/src/main/scala/Server/Server.scala
index 6fa284c..2f78e6b 100644
--- a/src/main/scala/Server/Server.scala
+++ b/src/main/scala/Server/Server.scala
@@ -8,6 +8,9 @@ import java.net.{ServerSocket, Socket}
import o1game.constants.*
import o1game.Model.Adventure
import o1game.Model.Entity
+import scala.util.Try
+
+import scala.util.Try
/** Converts this string to an array of bytes (probably for transmission).
*
@@ -18,9 +21,22 @@ def stringToByteArray(str: String): Array[Byte] =
str.toVector.map(_.toByte).toArray
/** `Server` exists to initialize a server for the game
- * and run it with its method `startServer`.
- */
-class Server(port: Int, maxClients: Int, val timeLimit: Int):
+ * and run it with its method `startServer`.
+ *
+ * @param port the TCP port the server should listen on
+ * @param maxClients the maximum number of clients that may be in the game
+ * simultaneously.
+ * @param timeLimit the time limit clients should have to execute their turns.
+ * @param joinAfterStart whether new clients are accepted after the game has
+ * been started
+ */
+class Server(
+ port: Int,
+ maxClients: Int,
+ val timeLimit: Int,
+ val joinAfterStart: Boolean
+):
+
private val socket = ServerSocket(port)
private val clientGetter = ConnectionGetter(socket)
private val clients: Clients = Clients(maxClients)
@@ -31,25 +47,45 @@ class Server(port: Int, maxClients: Int, val timeLimit: Int):
/** Starts the server. Won't terminate under normal circumstances. */
def startServer(): Unit =
while true do
+ this.serverStep()
sleep(POLL_INTERVAL)
+
+ private def serverStep(): Unit =
+ if this.adventure.isEmpty || this.joinAfterStart then
this.receiveNewClient()
- this.readFromAll()
- this.writeClientDataToClients()
- this.clients.removeNonCompliant()
- this.clients.foreach(_.interpretData())
- if this.adventure.isEmpty && !this.clients.isEmpty && this.clients.forall(_.isReadyForGameStart) then
- this.adventure = Some(Adventure(this.clients.names))
- this.clients.foreach( c =>
- c.gameStart()
- val name = c.getName
- val entity: Option[Entity] = name match
- case Some(n) => this.adventure match
- case Some(a) => a.getPlayer(n)
- case None => None
- case None => None
- entity.map(c.giveEntity(_))
+ this.readFromAll()
+ this.writeClientDataToClients()
+ this.clients.removeNonCompliant()
+ this.clients.foreach(_.interpretData())
+ if this.adventure.isDefined && this.joinAfterStart then
+ this.clients.foreach( c => if c.isReadyForGameStart then
+ this.adventure.foreach(a =>
+ c.getName.foreach(n => a.addPlayer(n))
)
- this.writeToAll(s"$timeLimit")
+ startGameForClient(c)
+ )
+ else if this.adventure.isEmpty && !this.clients.isEmpty && this.clients.forall(_.isReadyForGameStart) then
+ this.adventure = Some(Adventure(this.clients.names))
+ this.clients.foreach(startGameForClient(_))
+
+ /** Helper function to start the game for the specified client c.
+ * MAY ONLY BE USED IF `this.adventure` is Some!
+ * Apparently guard clauses are bad because they use return or something,
+ * but assertions should be fine, as long as they enforce the function
+ * contract?
+ */
+ private def startGameForClient(c: Client): Unit =
+ assert(this.adventure.isDefined)
+ c.gameStart()
+ val name = c.getName
+ val entity: Option[Entity] = name match
+ case Some(n) => this.adventure match
+ case Some(a) => a.getPlayer(n)
+ case None => None
+ case None => None
+ entity.map(c.giveEntity(_))
+ this.writeToClient(s"$timeLimit", c)
+
/** Receives a new client and stores it in `clients`.
@@ -75,6 +111,15 @@ class Server(port: Int, maxClients: Int, val timeLimit: Int):
output.flush()
)
+ private def writeToClient(message: String, client: Client): Unit =
+ val success = Try( () =>
+ val output = client.socket.getOutputStream
+ output.write(stringToByteArray(message))
+ output.flush()
+ )
+ if success.isFailure then
+ client.failedProtocol()
+
/** Sends every client's `dataToThisClient` to the client */
private def writeClientDataToClients(): Unit =
this.clients.mapAndRemove(c =>
diff --git a/src/main/scala/main.scala b/src/main/scala/main.scala
index da13025..145dc1c 100644
--- a/src/main/scala/main.scala
+++ b/src/main/scala/main.scala
@@ -9,7 +9,7 @@ import scala.io.StdIn.readLine
print("Please choose:\n1) Client.Client\n2) Server\n> ")
readLine().toIntOption match
case Some(1) => Client("127.0.0.1", 2267).startClient()
- case Some(2) => Server(2267, 5, 30).startServer()
+ case Some(2) => Server(2267, 5, 30, true).startServer()
case _ => println("Invalid input")