aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-15 16:45:09 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-15 16:45:09 +0200
commiteeb83ca379e7f4ab1a86596b80e206df48371454 (patch)
tree0f595308b7ba9077650e8a368b94ba75c5683c71
parentea18a265a22ffc4c3f6ec3ca9d2f542552da9705 (diff)
downloadscalevalapokalypsi-eeb83ca379e7f4ab1a86596b80e206df48371454.tar.gz
scalevalapokalypsi-eeb83ca379e7f4ab1a86596b80e206df48371454.zip
Added observations for Players in model & implemented sending them to other clients
-rw-r--r--src/main/scala/Client/Client.scala4
-rw-r--r--src/main/scala/Model/Action.scala21
-rw-r--r--src/main/scala/Model/Adventure.scala10
-rw-r--r--src/main/scala/Model/Area.scala1
-rw-r--r--src/main/scala/Model/Entity.scala61
-rw-r--r--src/main/scala/Server/Client.scala46
-rw-r--r--src/main/scala/Server/Server.scala21
7 files changed, 107 insertions, 57 deletions
diff --git a/src/main/scala/Client/Client.scala b/src/main/scala/Client/Client.scala
index 7b0f8b2..26b9264 100644
--- a/src/main/scala/Client/Client.scala
+++ b/src/main/scala/Client/Client.scala
@@ -112,7 +112,7 @@ class Client(socket: Socket):
s"\n\n${this.turnInfo}\n${this.actionGetterIndicator}"
private def displayAction(action: String): Unit =
- println(action)
+ println(s"> $action")
if this.canAct then
print(this.actionGetterIndicator)
@@ -149,7 +149,7 @@ class Client(socket: Socket):
this.lastTurnStart = currentTimeMillis / 1000
case ServerLineState.ActionDescription =>
- if line.head == ACTION_BLOCKING_INDICATOR then
+ if !line.isEmpty && line.head == ACTION_BLOCKING_INDICATOR then
this.canAct = false
this.displayAction(line.tail)
diff --git a/src/main/scala/Model/Action.scala b/src/main/scala/Model/Action.scala
index 11b0bc8..9f81256 100644
--- a/src/main/scala/Model/Action.scala
+++ b/src/main/scala/Model/Action.scala
@@ -23,15 +23,30 @@ class Action(input: String):
* of the action (such as “You go west.”). The description is returned in an `Option`
* wrapper; if the command was not recognized, `None` is returned. */
def execute(actor: Entity): Option[String] =
- this.verb match
+ val oldLocation = actor.location
+ val resOption: Option[(String, String)] = this.verb match
case "go" => Some(actor.go(this.modifiers))
case "rest" => Some(actor.rest())
case "get" => Some(actor.pickUp(this.modifiers))
case "drop" => Some(actor.drop(this.modifiers))
- case "xyzzy" => Some("The grue tastes yummy.")
- case "quit" => Some(actor.quit())
+ case "xyzzy" => Some((
+ "The grue tastes yummy.",
+ s"${actor.name} tastes some grue.")
+ )
case other => None
+// println(resOption)
+// println(actor.location.getEntities)
+ resOption.map(_(1)).filter(_.length > 0)
+ .foreach(s =>
+ actor.location.getEntities.filter(_ != actor).foreach(_.observe(s))
+ if oldLocation != actor.location then
+ oldLocation.getEntities.foreach(_.observe(s))
+ )
+
+ resOption.map(_(0))
+
+
/** Returns a textual description of the action object, for debugging purposes. */
override def toString = s"$verb (modifiers: $modifiers)"
diff --git a/src/main/scala/Model/Adventure.scala b/src/main/scala/Model/Adventure.scala
index 4d0a256..7d5a061 100644
--- a/src/main/scala/Model/Adventure.scala
+++ b/src/main/scala/Model/Adventure.scala
@@ -34,16 +34,18 @@ class Adventure(val playerNames: Vector[String]):
"Problem is, there's no battery."
))
- val players: Map[String, Entity] = Map()
+ val players: Map[String, Player] = Map()
playerNames.foreach(this.addPlayer(_))
+ val entities: Map[String, Entity] = Map()
+
/** 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)
+ def addPlayer(name: String): Player =
+ val newPlayer = Player(name, middle)
middle.addEntity(newPlayer)
players += name -> newPlayer
newPlayer
@@ -53,7 +55,7 @@ class Adventure(val playerNames: Vector[String]):
* @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)
+ def getPlayer(name: String): Option[Player] = this.players.get(name)
/** Returns a message that is to be displayed to the player at the beginning of the game. */
def welcomeMessage = "Generic welcome message"
diff --git a/src/main/scala/Model/Area.scala b/src/main/scala/Model/Area.scala
index ae1c98e..5a8de2a 100644
--- a/src/main/scala/Model/Area.scala
+++ b/src/main/scala/Model/Area.scala
@@ -23,6 +23,7 @@ class Area(val name: String, var description: String):
def getNeighborNames: Iterable[String] = this.neighbors.keys
def getItemNames: Iterable[String] = this.items.keys
def getEntityNames: Iterable[String] = this.entities.keys
+ def getEntities: Iterable[Entity] = this.entities.values
/** Tells whether this area has a neighbor in the given direction.
*
diff --git a/src/main/scala/Model/Entity.scala b/src/main/scala/Model/Entity.scala
index c18ffea..37fdbc2 100644
--- a/src/main/scala/Model/Entity.scala
+++ b/src/main/scala/Model/Entity.scala
@@ -1,14 +1,35 @@
package o1game.Model
-import scala.collection.mutable.Map
+import scala.collection.mutable.{Buffer,Map}
-/** A `Player` object represents a player character controlled by the real-life user
+
+
+/** A `Player` object represents a player character controlled by one real-life player
* of the program.
*
* A player object’s state is mutable: the player’s location and possessions can change,
* for instance.
*
* @param startingArea the player’s initial location */
+class Player(name: String, initialLocation: Area) extends Entity(name, initialLocation):
+
+ private val observations: Buffer[String] = Buffer.empty
+
+ override def observe(observation: String): Unit =
+ this.observations.append(observation)
+
+ def readAndClearObservations(): Vector[String] =
+ val res = this.observations.toVector
+ observations.clear()
+ res
+
+end Player
+
+/** An in-game entity.
+ *
+ * @param name the name of the entity
+ * @param initialLocation the Area where the entity is instantiated
+ */
class Entity(val name: String, initialLocation: Area):
private var currentLocation: Area = initialLocation
private var quitCommandGiven = false // one-way flag
@@ -17,35 +38,43 @@ class Entity(val name: String, initialLocation: Area):
/** Determines if the player has indicated a desire to quit the game. */
def hasQuit = this.quitCommandGiven // TODO: This is probably unneccessary?
+ /** Does nothing, except possibly in inherited classes. */
+ def observe(observation: String): Unit =
+ println("no observation made.")
+ ()
+
/** Returns the player’s current location. */
def location = this.currentLocation
/** Attempts to move the player in the given direction. This is successful if there
* is an exit from the player’s current location towards the direction name. Returns
* a description of the result: "You go DIRECTION." or "You can't go DIRECTION." */
- def go(direction: String) =
+ def go(direction: String): (String, String) =
val destination = this.location.neighbor(direction)
if destination.isDefined then
this.currentLocation.removeEntity(this.name)
this.currentLocation = destination.getOrElse(this.currentLocation)
destination.foreach(_.addEntity(this))
- s"You go $direction."
+ (s"You go $direction.", s"$name goes $direction")
else
- "You can't go " + direction + "."
+ (
+ s"You can't go $direction.",
+ s"$name tries to go $direction and stumbles in their feet."
+ )
- def pickUp(itemName: String): String =
+ def pickUp(itemName: String): (String, String) =
this.currentLocation.removeItem(itemName) match
case Some(i) =>
this.inventory += i.name -> i
- s"You pick up the ${i.name}"
- case None => s"There is no $itemName here to pick up."
+ (s"You pick up the ${i.name}", s"$name picks up the ${i.name}")
+ case None => (s"There is no $itemName here to pick up.", "WHAAAT THIS SHOULDN'T HAPPEN???")
- def drop(itemName: String): String =
+ def drop(itemName: String): (String, String) =
this.inventory.remove(itemName) match
case Some(item) =>
this.currentLocation.addItem(item)
- s"You drop the $itemName"
- case None => "You don't have that!"
+ (s"You drop the $itemName", s"$name drops the $itemName")
+ case None => ("You don't have that!", s"$name reaches their backpack to drop $itemName but miserably fails to find it there.")
/** Tells whether this entity can drop the specified item
* (if an action were to specify so).
@@ -57,14 +86,8 @@ class Entity(val name: String, initialLocation: Area):
/** 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() =
- "You rest for a while. Better get a move on, though."
-
- /** Signals that the player wants to quit the game. Returns a description of what happened within
- * the game as a result (which is the empty string, in this case). */
- def quit() =
- this.quitCommandGiven = true
- ""
+ def rest(): (String, String) =
+ ("You rest for a while. Better get a move on, though.", "")
/** Returns a brief description of the player’s state, for debugging purposes. */
override def toString = "Now at: " + this.location.name
diff --git a/src/main/scala/Server/Client.scala b/src/main/scala/Server/Client.scala
index 323b78c..d4f1864 100644
--- a/src/main/scala/Server/Client.scala
+++ b/src/main/scala/Server/Client.scala
@@ -4,8 +4,7 @@ import java.net.Socket
import scala.math.min
import o1game.constants.*
import ServerProtocolState.*
-import o1game.Model.Entity
-import o1game.Model.Action
+import o1game.Model.{Action,Player,Entity}
class Client(val socket: Socket):
private var incompleteMessage: Array[Byte] =
@@ -13,7 +12,7 @@ class Client(val socket: Socket):
private var incompleteMessageIndex = 0
private var protocolState = WaitingForVersion
private var outData: String = ""
- private var character: Option[Entity] = None
+ private var character: Option[Player] = None
private var protocolIsIntact = true
private var name: Option[String] = None
private var nextAction: Option[Action] = None
@@ -42,22 +41,22 @@ class Client(val socket: Socket):
*/
def gameStart(): Unit = this.protocolState = InGame
- /** Returns the entity this client controls in the model.
+ /** Returns the player this client controls in the model.
*
- * @return an option containing the entity
+ * @return an option containing the player
*/
- def entity: Option[Entity] = this.character
+ def player: Option[Player] = this.character
- /** Tells this client object that it controls the specified entity.
+ /** Tells this client object that it controls the specified player.
*
- * @param entity the entity this client is to control
+ * @param player the player this client is to control
*/
- def giveEntity(entity: Entity): Unit =
- this.character = Some(entity)
+ def givePlayer(player: Player): Unit =
+ this.character = Some(player)
- /** Gets the name of this client, which should match the name of the entity
+ /** Gets the name of this client, which should match the name of the player
* that is given to this client. Not very useful if the client hasn't yet
- * received the name or if it already has an entity.
+ * received the name or if it already has an player.
*
* @return the name of this client
*/
@@ -89,7 +88,7 @@ class Client(val socket: Socket):
* @param data data to buffer for sending
*/
private def addDataToSend(data: String): Unit =
- this.outData += s"$data"
+ this.outData += s"$data\r\n"
/** Returns one line of data if there are any line breaks.
@@ -110,7 +109,9 @@ class Client(val socket: Socket):
/** Makes the client play its turn */
def act(): Unit =
this.addDataToSend(ACTION_BLOCKING_INDICATOR.toString)
- this.nextAction.foreach(this.executeAction(_))
+ this.nextAction.foreach(a => this.addDataToSend(
+ s"$ACTION_BLOCKING_INDICATOR${this.executeAction(a)}"
+ ))
this.nextAction = None
/** Checks whether the client has chosen its next action
@@ -135,11 +136,11 @@ class Client(val socket: Socket):
this.protocolIsIntact = this.protocolState match
case WaitingForVersion =>
if line == GAME_VERSION then
- addDataToSend(s"$PROTOCOL_VERSION_GOOD\r\n")
+ addDataToSend(s"$PROTOCOL_VERSION_GOOD")
this.protocolState = WaitingForClientName
true
else
- addDataToSend(s"$PROTOCOL_VERSION_BAD\r\n")
+ addDataToSend(s"$PROTOCOL_VERSION_BAD")
false
case WaitingForClientName =>
this.name = Some(line)
@@ -155,18 +156,17 @@ class Client(val socket: Socket):
private def bufferAction(action: Action) =
if (
this.nextAction.isEmpty &&
- this.entity.exists(action.takesATurnFor(_))
+ this.player.exists(action.takesATurnFor(_))
) then
this.nextAction = Some(action)
else if this.nextAction.isEmpty then
- this.addDataToSend(ACTION_NONBLOCKING_INDICATOR.toString)
- this.executeAction(action)
+ this.addDataToSend(s"$ACTION_NONBLOCKING_INDICATOR${this.executeAction(action)}")
- /** Executes the specified action and buffers its description for sending */
- private def executeAction(action: Action) =
+ /** Executes the specified action and returns its description */
+ private def executeAction(action: Action): String =
this.character.flatMap(action.execute(_)) match
- case Some(s) => this.addDataToSend(s"$s\r\n")
- case None => this.addDataToSend("You can't do that\r\n")
+ case Some(s) => s
+ case None => "You can't do that"
end Client
diff --git a/src/main/scala/Server/Server.scala b/src/main/scala/Server/Server.scala
index 5eb15cb..ac0e010 100644
--- a/src/main/scala/Server/Server.scala
+++ b/src/main/scala/Server/Server.scala
@@ -7,8 +7,7 @@ import java.lang.Thread.{currentThread, sleep}
import java.io.IOException
import java.net.{ServerSocket, Socket}
import o1game.constants.*
-import o1game.Model.Adventure
-import o1game.Model.Entity
+import o1game.Model.{Adventure,Entity,Player}
import o1game.utils.stringToByteArray
import java.lang.System.currentTimeMillis
@@ -53,9 +52,11 @@ class Server(
this.readFromAll()
this.clients.foreach(_.interpretData())
this.writeClientDataToClients()
+ this.writeObservations()
if this.canExecuteTurns then
this.clients.inRandomOrder(_.act())
this.writeClientDataToClients()
+ this.writeObservations()
this.clients.foreach(c =>
this.writeToClient(this.turnStartInfo(c), c)
)
@@ -83,17 +84,25 @@ class Server(
c.gameStart()
val name = c.getName
- val entity: Option[Entity] = name match
+ val playerEntity: Option[Player] = name match
case Some(n) => this.adventure match
case Some(a) => a.getPlayer(n)
case None => None
case None => None
- entity.foreach(c.giveEntity(_))
+ playerEntity.foreach(c.givePlayer(_))
this.writeToClient(
s"$timeLimit\r\n${this.turnStartInfo(c)}", c
)
+
+ private def writeObservations(): Unit =
+ this.clients.foreach(c =>
+ val observations = c.player.map(_.readAndClearObservations())
+// if observations.filter(_.length > 0).isDefined then
+// println(s"Observations of $c: ```$observations```")
+ observations.foreach(_.foreach((s: String) => this.writeToClient(s"$s\r\n", c)))
+ )
/** Helper function to determine if the next turn can be taken */
private def canExecuteTurns: Boolean =
@@ -121,7 +130,7 @@ class Server(
false
private def turnStartInfo(client: Client): String =
- val clientArea = client.entity.map(_.location)
+ val clientArea = client.player.map(_.location)
val areaDesc = clientArea
.map(_.description)
.getOrElse("You are floating in the middle of a soothing void.")
@@ -131,7 +140,7 @@ class Server(
val items = clientArea
.map(_.getItemNames.mkString(LIST_SEPARATOR))
.getOrElse("")
- val entities = client.entity.map(c =>
+ val entities = client.player.map(c =>
c.location
.getEntityNames
.filter(c.name != _)