diff options
author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-15 16:45:09 +0200 |
---|---|---|
committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-15 16:45:09 +0200 |
commit | eeb83ca379e7f4ab1a86596b80e206df48371454 (patch) | |
tree | 0f595308b7ba9077650e8a368b94ba75c5683c71 | |
parent | ea18a265a22ffc4c3f6ec3ca9d2f542552da9705 (diff) | |
download | scalevalapokalypsi-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.scala | 4 | ||||
-rw-r--r-- | src/main/scala/Model/Action.scala | 21 | ||||
-rw-r--r-- | src/main/scala/Model/Adventure.scala | 10 | ||||
-rw-r--r-- | src/main/scala/Model/Area.scala | 1 | ||||
-rw-r--r-- | src/main/scala/Model/Entity.scala | 61 | ||||
-rw-r--r-- | src/main/scala/Server/Client.scala | 46 | ||||
-rw-r--r-- | src/main/scala/Server/Server.scala | 21 |
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 != _) |