diff options
author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-07 22:52:25 +0200 |
---|---|---|
committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-07 22:52:25 +0200 |
commit | 239571e3408a3187953bef1dd5d516461bad0e31 (patch) | |
tree | 67c062a635affc90c08a92ac61adfed2445985d8 | |
parent | def8975617e5c6da431e41cc889d167b0f2e2bb0 (diff) | |
download | scalevalapokalypsi-239571e3408a3187953bef1dd5d516461bad0e31.tar.gz scalevalapokalypsi-239571e3408a3187953bef1dd5d516461bad0e31.zip |
Added turns, time limits and turn order randomization (feature/bug?)
-rw-r--r-- | src/main/scala/Model/Action.scala | 8 | ||||
-rw-r--r-- | src/main/scala/Model/Area.scala | 14 | ||||
-rw-r--r-- | src/main/scala/Model/Entity.scala | 8 | ||||
-rw-r--r-- | src/main/scala/Model/Item.scala | 2 | ||||
-rw-r--r-- | src/main/scala/Server/Client.scala | 47 | ||||
-rw-r--r-- | src/main/scala/Server/Clients.scala | 8 | ||||
-rw-r--r-- | src/main/scala/Server/Server.scala | 25 |
7 files changed, 97 insertions, 15 deletions
diff --git a/src/main/scala/Model/Action.scala b/src/main/scala/Model/Action.scala index 84704ce..11b0bc8 100644 --- a/src/main/scala/Model/Action.scala +++ b/src/main/scala/Model/Action.scala @@ -10,6 +10,14 @@ class Action(input: String): private val verb = commandText.takeWhile( _ != ' ' ) private val modifiers = commandText.drop(verb.length).trim + def takesATurnFor(actor: Entity): Boolean = + this.verb match + case "rest" => true + case "go" => actor.location.hasNeighbor(modifiers) + case "get" => actor.location.hasItem(this.modifiers) + case "drop" => actor.canDrop(this.modifiers) + case other => false + /** Causes the given player to take the action represented by this object, assuming * that the command was understood. Returns a description of what happened as a result * of the action (such as “You go west.”). The description is returned in an `Option` diff --git a/src/main/scala/Model/Area.scala b/src/main/scala/Model/Area.scala index 97c75bf..f5b5289 100644 --- a/src/main/scala/Model/Area.scala +++ b/src/main/scala/Model/Area.scala @@ -17,7 +17,16 @@ class Area(val name: String, var description: String): /** Returns the area that can be reached from this area by moving in the given direction. The result * is returned in an `Option`; `None` is returned if there is no exit in the given direction. */ - def neighbor(direction: String) = this.neighbors.get(direction) + def neighbor(direction: String): Option[Area] = + this.neighbors.get(direction) + + /** Tells whether this area has a neighbor in the given direction. + * + * @param direction the direction to check + * @return whether there is a neighbor in the direction + */ + def hasNeighbor(direction: String): Boolean = + this.neighbors.contains(direction) /** Adds an exit from this area to the given area. The neighboring area is reached by moving in * the specified direction from this area. */ @@ -44,6 +53,9 @@ class Area(val name: String, var description: String): def addItems(items: IterableOnce[Item]) = items.iterator.foreach(i => this.items += i.name -> i) + def hasItem(itemName: String) = this.items.contains(itemName) + + /** Removes the specified item if it exists. * * @param itemName the name of the item to remove diff --git a/src/main/scala/Model/Entity.scala b/src/main/scala/Model/Entity.scala index eb19606..c18ffea 100644 --- a/src/main/scala/Model/Entity.scala +++ b/src/main/scala/Model/Entity.scala @@ -47,6 +47,14 @@ class Entity(val name: String, initialLocation: Area): s"You drop the $itemName" case None => "You don't have that!" + /** Tells whether this entity can drop the specified item + * (if an action were to specify so). + * + * @param itemName the name to check + * @return whether this entity has this item and can drop it + */ + def canDrop(itemName: String): Boolean = this.inventory.contains(itemName) + /** Causes the player to rest for a short while (this has no substantial effect in game terms). * Returns a description of what happened. */ def rest() = diff --git a/src/main/scala/Model/Item.scala b/src/main/scala/Model/Item.scala index 3809561..229828d 100644 --- a/src/main/scala/Model/Item.scala +++ b/src/main/scala/Model/Item.scala @@ -1,5 +1,7 @@ package o1game.Model +import scala.annotation.targetName + /** The class `Item` represents items in a text adventure game. Each item has a name * and a longer description. (In later versions of the adventure game, items may * have other features as well.) diff --git a/src/main/scala/Server/Client.scala b/src/main/scala/Server/Client.scala index d9fa684..e557c22 100644 --- a/src/main/scala/Server/Client.scala +++ b/src/main/scala/Server/Client.scala @@ -16,6 +16,7 @@ class Client(val socket: Socket): private var character: Option[Entity] = None private var protocolIsIntact = true private var name: Option[String] = None + private var nextAction: Option[Action] = None /** Calculates the amount of bytes available for future incoming messages */ def spaceAvailable: Int = MAX_MSG_SIZE - incompleteMessageIndex @@ -106,12 +107,22 @@ class Client(val socket: Socket): else None - /** Causes the client to take the actions it has received */ + /** Makes the client play its turn */ + def act(): Unit = + this.nextAction.foreach(this.executeAction(_)) + this.nextAction = None + + /** Checks whether the client has chosen its next action + * + * @return whether the client is ready to act */ + def isReadyToAct: Boolean = this.nextAction.isDefined + + /** Causes the client to interpret the data it has received */ def interpretData(): Unit = LazyList.continually(this.nextLine()) .takeWhile(_.isDefined) .flatten - .foreach(s => takeAction(s)) + .foreach(s => interpretLine(s)) /** Makes the client execute the action specified by `line`. * If there is a protocol error, the function changes @@ -119,7 +130,7 @@ class Client(val socket: Socket): * * @param line the line to interpret */ - private def takeAction(line: String): Unit = + private def interpretLine(line: String): Unit = this.protocolIsIntact = this.protocolState match case WaitingForVersion => if line == GAME_VERSION then @@ -135,15 +146,29 @@ class Client(val socket: Socket): true case WaitingForGameStart => true case InGame => - println(line) - val action = Action(line) - this.character.flatMap(action.execute(_)) match - case Some(s) => this.addDataToSend(s) - case None => this.addDataToSend("You can't do that") - this.character - .map(_.location.fullDescription) - .foreach(this.addDataToSend(_)) + this.bufferAction(Action(line)) true + /** Buffers the action for execution or executes it immediately if it + * doesn't take a turn */ + private def bufferAction(action: Action) = + if ( + this.nextAction.isEmpty && + this.entity.exists(action.takesATurnFor(_)) + ) then + this.nextAction = Some(action) + this.addDataToSend("Waiting for everyone to end their turns...") + else if this.nextAction.isEmpty then + executeAction(action) + + /** Executes the specified action */ + private def executeAction(action: Action) = + this.character.flatMap(action.execute(_)) match + case Some(s) => this.addDataToSend((s)) + case None => this.addDataToSend("You can't do that") + this.character.map(_.location.fullDescription) + .foreach(this.addDataToSend(_)) + + end Client diff --git a/src/main/scala/Server/Clients.scala b/src/main/scala/Server/Clients.scala index 786a09a..6487446 100644 --- a/src/main/scala/Server/Clients.scala +++ b/src/main/scala/Server/Clients.scala @@ -1,6 +1,7 @@ package o1game.Server import scala.util.Try +import scala.util.Random class Clients(maxClients: Int): private val clients: Array[Option[Client]] = Array.fill(maxClients)(None) @@ -28,6 +29,13 @@ class Clients(maxClients: Int): /** Applies the function `f` to all the clients for its side effects. */ def foreach(f: Client => Any): Unit = this.clients.flatten.foreach(f) + /** Executes the function `f` for all clients in a pseudorandom order. */ + def inRandomOrder(f: Client => Any): Unit = + Random.shuffle(this + .clients + .flatten) + .foreach(f) + /** Returns true if the predicate `f` stands for all clients, * false otherwise * diff --git a/src/main/scala/Server/Server.scala b/src/main/scala/Server/Server.scala index 2f78e6b..faf82e1 100644 --- a/src/main/scala/Server/Server.scala +++ b/src/main/scala/Server/Server.scala @@ -3,13 +3,13 @@ package o1game.Server // TODO: TLS/SSL / import javax.net.ssl.SSLServerSocketFactory -import java.lang.Thread.sleep +import java.lang.Thread.{currentThread, sleep} import java.net.{ServerSocket, Socket} import o1game.constants.* import o1game.Model.Adventure import o1game.Model.Entity -import scala.util.Try +import java.lang.System.currentTimeMillis import scala.util.Try /** Converts this string to an array of bytes (probably for transmission). @@ -43,6 +43,7 @@ class Server( private val buffer: Array[Byte] = Array.ofDim(1024) private var bufferIndex = 0 private var adventure: Option[Adventure] = None + private var previousTurn = 0.0 /** Starts the server. Won't terminate under normal circumstances. */ def startServer(): Unit = @@ -57,6 +58,11 @@ class Server( this.writeClientDataToClients() this.clients.removeNonCompliant() this.clients.foreach(_.interpretData()) + if this.canExecuteTurns then + println("taking turns") + this.clients.inRandomOrder(_.act()) + this.writeToAll("next turn!") + this.previousTurn = currentTimeMillis() / 1000 if this.adventure.isDefined && this.joinAfterStart then this.clients.foreach( c => if c.isReadyForGameStart then this.adventure.foreach(a => @@ -67,6 +73,7 @@ class Server( else if this.adventure.isEmpty && !this.clients.isEmpty && this.clients.forall(_.isReadyForGameStart) then this.adventure = Some(Adventure(this.clients.names)) this.clients.foreach(startGameForClient(_)) + this.previousTurn = currentTimeMillis() / 1000 /** Helper function to start the game for the specified client c. * MAY ONLY BE USED IF `this.adventure` is Some! @@ -83,10 +90,22 @@ class Server( case Some(a) => a.getPlayer(n) case None => None case None => None - entity.map(c.giveEntity(_)) + entity.foreach(c.giveEntity(_)) this.writeToClient(s"$timeLimit", c) + /** Helper function to determine if the next turn can be taken */ + private def canExecuteTurns: Boolean = + val requirement1 = this.adventure.isDefined + val requirement2 = !this.clients.isEmpty // nice! you can just return + // to the game after everyone + // left and everything is just + // as before! + val allPlayersReady = this.clients.forall(_.isReadyToAct) + val requirement3 = (allPlayersReady + || currentTimeMillis() / 1000 >= previousTurn + timeLimit) + requirement1 && requirement2 && requirement3 + /** Receives a new client and stores it in `clients`. * |