diff options
author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-07 21:41:53 +0200 |
---|---|---|
committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-07 21:41:53 +0200 |
commit | def8975617e5c6da431e41cc889d167b0f2e2bb0 (patch) | |
tree | f538102c17d81b596834fad7f47528fe2fc04b19 | |
parent | 891b6f877ba848a3f78851a757734ebd1f066798 (diff) | |
download | scalevalapokalypsi-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.scala | 33 | ||||
-rw-r--r-- | src/main/scala/Server/Character.scala | 7 | ||||
-rw-r--r-- | src/main/scala/Server/Client.scala | 5 | ||||
-rw-r--r-- | src/main/scala/Server/Server.scala | 83 | ||||
-rw-r--r-- | src/main/scala/main.scala | 2 |
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") |