aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-07 22:52:25 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-07 22:52:25 +0200
commit239571e3408a3187953bef1dd5d516461bad0e31 (patch)
tree67c062a635affc90c08a92ac61adfed2445985d8
parentdef8975617e5c6da431e41cc889d167b0f2e2bb0 (diff)
downloadscalevalapokalypsi-239571e3408a3187953bef1dd5d516461bad0e31.tar.gz
scalevalapokalypsi-239571e3408a3187953bef1dd5d516461bad0e31.zip
Added turns, time limits and turn order randomization (feature/bug?)
-rw-r--r--src/main/scala/Model/Action.scala8
-rw-r--r--src/main/scala/Model/Area.scala14
-rw-r--r--src/main/scala/Model/Entity.scala8
-rw-r--r--src/main/scala/Model/Item.scala2
-rw-r--r--src/main/scala/Server/Client.scala47
-rw-r--r--src/main/scala/Server/Clients.scala8
-rw-r--r--src/main/scala/Server/Server.scala25
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`.
*