diff options
author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-18 02:01:12 +0200 |
---|---|---|
committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-18 02:01:12 +0200 |
commit | fe2543627bcec1ea0f7a429bede20ca293458ba9 (patch) | |
tree | 42c9630d65292c90bee37444fde14fcf99cc3ffe | |
parent | a98f089035dbcc94c14c9cd6246c3150bee84241 (diff) | |
download | scalevalapokalypsi-fe2543627bcec1ea0f7a429bede20ca293458ba9.tar.gz scalevalapokalypsi-fe2543627bcec1ea0f7a429bede20ca293458ba9.zip |
Major change! Changed Events to have multiple first persons & changed sending action results to clients using this. This improves code and made it easy to finally make arrival/leaving messages when going from one area to another!
This commit is too big indeed, and there are probably bugs. I'm annoyed we didn't set up unit testing during the course. I should've taken my time to set up IDEA myself for that. Now the bugs will just have to be fixed in future commits.
-rw-r--r-- | src/scalevalapokalypsi/Model/Action.scala | 75 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Adventure.scala | 11 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/Entity.scala | 94 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/Player.scala | 5 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Event.scala | 36 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/SingEffects.scala | 8 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Server/Client.scala | 43 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Server/Server.scala | 24 |
8 files changed, 166 insertions, 130 deletions
diff --git a/src/scalevalapokalypsi/Model/Action.scala b/src/scalevalapokalypsi/Model/Action.scala index 30fbf46..a781ee8 100644 --- a/src/scalevalapokalypsi/Model/Action.scala +++ b/src/scalevalapokalypsi/Model/Action.scala @@ -15,33 +15,25 @@ class Action(input: String): private val verb = commandText.takeWhile( _ != ' ' ) private val modifiers = commandText.drop(verb.length).trim - def takesATurnFor(actor: Player): 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 "say" => false - case "laula" => false - 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` wrapper; if the command was not - * recognized, `None` is returned. + * assuming that the command was understood. Informs the player and the + * entities surrounding it about the result. Returns true if the command + * was understood and possible, false otherwise. * * @param actor the acting player - * @return A textual description of the action, or `None` if the action - * was not recognized. + * @return Boolean indicating whether the action possibly taken takes a + * turn or not. */ - def execute(actor: Player): Option[String] = + def execute(actor: Player): Boolean = 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 "say" => + val resOption: Option[(Boolean, Event)] = this.verb match + case "go" => + val result = actor.go(this.modifiers) + result.foreach(r => oldLocation.observeEvent(r)) + result.map((true, _)) + case "rest" => Some((true, actor.rest())) + case "get" => Some((false, actor.pickUp(this.modifiers))) + case "say" => val to = "to" val recipient = modifiers.reverse.takeWhile(_ != ' ').reverse val recipientEntity = actor.location.getEntity(recipient) @@ -52,10 +44,10 @@ class Action(input: String): val message = modifiers.take(modifiers.length - recipient.length - 4) if maybeTo == to then - recipientEntity.map(actor.sayTo(_, message)) + recipientEntity.map(e => (false, actor.sayTo(e, message))) else - Some(actor.say(modifiers)) - case "drop" => Some(actor.drop(this.modifiers)) + Some((false, actor.say(modifiers))) + case "drop" => Some((false, actor.drop(this.modifiers))) case "laula" => val end = modifiers.takeRight("suohon".length) val start = @@ -64,28 +56,27 @@ class Action(input: String): val targetEntity = actor.location.getEntity(start) targetEntity .foreach(e => actor.setSingEffect(DefaultSingAttack(e))) - targetEntity.foreach(_.observeString(s"${actor.name} laulaa sinua suohon!")) - targetEntity.map(e => ( - "Aloitat suohonlaulun.", - s"${actor.name} aloittaa suohonlaulun." - )) + targetEntity.map(t => + (false, Event( + Map.from(Vector((t, s"${actor.name} laulaa sinua suohon!"))), + s"${actor.name} laulaa henkilöä ${t.name} suohon." + )) + ) else None - case "xyzzy" => Some(( - "The grue tastes yummy.", + case "xyzzy" => Some((false, Event( + Map.from(Vector((actor, "The grue tastes yummy."))), s"${actor.name} tastes some grue.") - ) + )) case other => None - resOption.map(_(1)).filter(_.length > 0) - .foreach(s => - actor.location.getEntities.filter(_ != actor).foreach(_.observeString(s)) - if oldLocation != actor.location then - oldLocation.getEntities.foreach(_.observeString(s)) - ) - - resOption.map(_(0)) - + val res: (Boolean, Event) = resOption + .getOrElse((false, Event( + Map.from(Vector((actor, "Tuo ei ole asia, jonka voit tehdä."))), + "" + ))) + actor.location.observeEvent(res(1)) + res(0) /** Returns a textual description of the action object, for debugging purposes. */ override def toString = s"$verb (modifiers: $modifiers)" diff --git a/src/scalevalapokalypsi/Model/Adventure.scala b/src/scalevalapokalypsi/Model/Adventure.scala index 0fbf6cd..9347bfa 100644 --- a/src/scalevalapokalypsi/Model/Adventure.scala +++ b/src/scalevalapokalypsi/Model/Adventure.scala @@ -66,16 +66,5 @@ class Adventure(val playerNames: Vector[String]): def getEntity[A >: Entity](name: String) = this.players.getOrElse(name, this.entities.get(name)) - /** Returns a message that is to be displayed to the player at the beginning of the game. */ - def welcomeMessage = "Generic welcome message" - - /** Plays a turn by executing the given in-game command, such as “go west”. Returns a textual - * report of what happened, or an error message if the command was unknown. In the latter - * case, no turns elapse. */ - def playTurnOfPlayer(playerName: String, command: String): Option[String] = - val action = Action(command) - val actor = this.players.get(playerName) - actor.flatMap(action.execute(_)) - end Adventure diff --git a/src/scalevalapokalypsi/Model/Entities/Entity.scala b/src/scalevalapokalypsi/Model/Entities/Entity.scala index 26dd7dc..e7cd45c 100644 --- a/src/scalevalapokalypsi/Model/Entities/Entity.scala +++ b/src/scalevalapokalypsi/Model/Entities/Entity.scala @@ -1,8 +1,10 @@ package scalevalapokalypsi.Model.Entities -import scala.collection.mutable.{Buffer,Map} +import scala.collection.mutable.{Buffer, Map} import scalevalapokalypsi.Model.* +import scala.collection.immutable + /** An in-game entity. * @@ -69,46 +71,91 @@ class Entity( * direction name. Returns a description of the result: "You go DIRECTION." * or "You can't go DIRECTION." */ - def go(direction: String): (String, String) = + def go(direction: String): Option[Event] = val destination = this.location.neighbor(direction) + val oldEntities = this.location.getEntities.filter(_ != this) + val newEntities = destination.map(_.getEntities) if destination.isDefined then val removeSuccess = this.currentLocation.removeEntity(this.name) assert(removeSuccess.isDefined) // Production - assertions off this.currentLocation = destination.getOrElse(this.currentLocation) destination.foreach(_.addEntity(this)) - (s"You go $direction.", s"$name goes $direction") - else - ( - s"You can't go $direction.", - s"$name tries to go $direction and stumbles in their feet." - ) - def pickUp(itemName: String): (String, String) = + val leaving = oldEntities.zip( + Vector.fill + (oldEntities.size) + (s"${this.name} leaves this location.") + ) + //val arriving = newEntities.map(n => n.zip( + // Vector.fill + // (n.size) + // (s"${this.name} arrives here.") + //)).getOrElse(Vector()) + val self = Vector((this, s"You go $direction.")) + Some(Event( + (leaving ++ self).toMap, + s"$name arrives here." + )) + else None + + def pickUp(itemName: String): Event = this.currentLocation.removeItem(itemName) match case Some(i) => this.inventory += i.name -> i - (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.", + Event( + immutable.Map.from(Vector((this, s"You pick up the ${i.name}"))), + s"$name picks up the ${i.name}" + ) + case None => Event( + immutable.Map.from(Vector((this, s"There is no $itemName here to pick up."))), s"${this.name} tries to pick up something but gets just dirt in their hands." ) - def drop(itemName: String): (String, String) = + def drop(itemName: String): Event = this.inventory.remove(itemName) match case Some(item) => this.currentLocation.addItem(item) - (s"You drop the $itemName", s"$name drops the $itemName") - case None => ( - "You don't have that!", + Event( + immutable.Map.from(Vector((this, s"You drop the $itemName"))), + s"$name drops the $itemName" + ) + case None => Event( + immutable.Map.from(Vector((this, "You don't have that!"))), s"$name reaches their backpack to drop $itemName but miserably fails to find it there." ) - def sayTo(entity: Entity, message: String): (String, String) = - entity.observeString(s"${this.name}: \"$message\"") - (s"You say so to ${entity.name}.", "") + def sayTo(entity: Entity, message: String): Event = + if entity == this then this.ponder(message: String) + else + Event( + immutable.Map.from(Vector( + (this, s"Sanot niin henkilölle ${entity.name}."), + (entity, s"${this.name}: “${message}”") + )), + s"Kuulet henkilön ${this.name} sanovan jotain henkilölle ${entity.name}" + ) - def say(message: String): (String, String) = - ("You say that aloud.", s"$name: \"$message\"") + def ponder(message: String): Event = + Event( + immutable.Map.from(Vector( + (this, s"Mietit itseksesi: “$message”") + )), + s"${this.name} näyttää pohtivan jotain itsekseen." + ) + + def say(message: String): Event = + Event( + immutable.Map.from(Vector((this, "Sanot niin ääneen."))), + s"$name: “$message”" + ) + + /** Causes the player to rest for a turn. + * Returns a description of what happened. */ + def rest(): Event = + Event( + immutable.Map.from(Vector((this, "Lepäät hetken."))), + s"${this.name} levähtää." + ) /** Tells whether this entity can drop the specified item * (if an action were to specify so). @@ -118,11 +165,6 @@ class Entity( */ def canDrop(itemName: String): Boolean = this.inventory.contains(itemName) - /** Causes the player to rest for a turn. - * Returns a description of what happened. */ - 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 = s"${this.name} at ${this.location.name}" diff --git a/src/scalevalapokalypsi/Model/Entities/Player.scala b/src/scalevalapokalypsi/Model/Entities/Player.scala index f231c28..cac5bf1 100644 --- a/src/scalevalapokalypsi/Model/Entities/Player.scala +++ b/src/scalevalapokalypsi/Model/Entities/Player.scala @@ -41,7 +41,7 @@ class Player(name: String, initialLocation: Area) extends Entity(name, initialLo */ def setSingEffect(effect: SingEffect): Unit = this.pendingSingEffect = Some(effect) - + def getSingEffectTarget: Option[Entity] = this.pendingSingEffect.map(_.target) @@ -66,8 +66,7 @@ class Player(name: String, initialLocation: Area) extends Entity(name, initialLo val quality = s"Laulu on ${qualityDescriptions(0)} ja sen vaikutus on ${qualityDescriptions(1)}." val event = res.map(ev => Event( - ev.target, - s"$quality\n${ev.inFirstPerson}", + ev.inFirstPersons.map((k, v) => (k, s"$quality\n$v")), s"$quality\n${ev.inThirdPerson}" )) event.foreach(this.location.observeEvent(_)) diff --git a/src/scalevalapokalypsi/Model/Event.scala b/src/scalevalapokalypsi/Model/Event.scala index cba611d..9055fd8 100644 --- a/src/scalevalapokalypsi/Model/Event.scala +++ b/src/scalevalapokalypsi/Model/Event.scala @@ -1,28 +1,34 @@ package scalevalapokalypsi.Model import scalevalapokalypsi.Model.Entities.Entity +import scala.collection.immutable.Map /** A description of an action. - * - * @param target the entity that was as a target in this event - * @param inFirstPerson textual description of the event in first person - * @param inThirdPerson textual description of the event in third person - */ + * + * @param inFirstPersons a Map of descriptions in first person for entities + * given as keys + * @param inThirdPerson textual description of the event in third person + */ class Event( - val target: Entity, - val inFirstPerson: String, + val inFirstPersons: Map[Entity, String], val inThirdPerson: String ): + // And why are we not just using a map with a default value? + // Wrapping this in an Event creates a more specific abstraction. + // It indicates, that instances of this class are precisely descriptions + // of events, and it allows changing the private implementation without + // touching the public interface. + private val values = inFirstPersons.withDefaultValue(inThirdPerson) + /** Gets the description of this event as seen by the given - * entity. Note that this method does no checks whether the given entity - * could even see the event, only what it would have looked like to them. - * - * @param entity the entity whose perspective to use - * @return a textual description of the event - */ + * entity. Note that this method does no checks whether the given entity + * could even see the event, only what it would have looked like to them. + * + * @param entity the entity whose perspective to use + * @return a textual description of the event + */ def descriptionFor(entity: Entity): String = - if entity == target then inFirstPerson - else inThirdPerson + this.values.apply(entity) end Event
\ No newline at end of file diff --git a/src/scalevalapokalypsi/Model/SingEffects.scala b/src/scalevalapokalypsi/Model/SingEffects.scala index 6702df5..42f5188 100644 --- a/src/scalevalapokalypsi/Model/SingEffects.scala +++ b/src/scalevalapokalypsi/Model/SingEffects.scala @@ -1,11 +1,7 @@ package scalevalapokalypsi.Model import scalevalapokalypsi.Model.Entities.Entity - -def defaultSingAttack(targetEntity: Entity)(singQuality: Float): Event = - targetEntity.takeDamage((singQuality * 30).toInt) - val condition = targetEntity.condition - Event(targetEntity, condition(0), condition(1)) +import scala.collection.immutable.Map trait SingEffect(val target: Entity): def apply(singQuality: Float): Event @@ -14,4 +10,4 @@ class DefaultSingAttack(target: Entity) extends SingEffect(target): def apply(singQuality: Float): Event = this.target.takeDamage((singQuality * 50).toInt) // TODO: remove magic value val condition = this.target.condition - Event(target, condition(0), condition(1)) + Event(Map.from(Vector((target, condition(0)))), condition(1)) diff --git a/src/scalevalapokalypsi/Server/Client.scala b/src/scalevalapokalypsi/Server/Client.scala index 1af83bf..17c3777 100644 --- a/src/scalevalapokalypsi/Server/Client.scala +++ b/src/scalevalapokalypsi/Server/Client.scala @@ -18,6 +18,7 @@ class Client(val socket: Socket): private var protocolIsIntact = true private var name: Option[String] = None private var nextAction: Option[Action] = None + private var turnUsed = false private var singStartTime: Option[Long] = None def clientHasSong = this.singStartTime.isDefined @@ -114,16 +115,13 @@ class Client(val socket: Socket): None /** Makes the client play its turn */ - def act(): Unit = - this.nextAction.foreach(a => this.addDataToSend( - s"$ACTION_BLOCKING_INDICATOR${this.executeAction(a)}" - )) - this.nextAction = None + def giveTurn(): Unit = + this.turnUsed = false /** Checks whether the client has chosen its next action * * @return whether the client is ready to act */ - def isReadyToAct: Boolean = this.nextAction.isDefined + def hasActed: Boolean = this.turnUsed /** Causes the client to interpret the data it has received */ def interpretData(): Unit = @@ -154,12 +152,31 @@ class Client(val socket: Socket): true case WaitingForGameStart => true case InGame => - this.bufferAction(Action(line)) + this.executeLine(line) true /** Buffers the action for execution or executes it immediately if it * doesn't take a turn */ - private def bufferAction(action: Action) = + private def executeLine(line: String) = + if !this.turnUsed then + this.singStartTime match + case Some(t) => + val timePassed = currentTimeMillis()/1000 - t + this.player.foreach(_.applySingEffect( + 1 / max(5, timePassed) * 5 + )) + this.singStartTime = None + case None => + val action = Action(line) + val takesATurn = this.character.exists(p => action.execute(p)) + if takesATurn then + this.addDataToSend(s"$ACTION_BLOCKING_INDICATOR") + this.turnUsed = true + + /* + val takesATurn = this.character.exists(action.execute(_)) + if takesATurn then + this.addDataToSend(s"$ACTION_BLOCKING_INDICATOR") if ( this.nextAction.isEmpty && this.player.exists(action.takesATurnFor(_)) @@ -170,19 +187,13 @@ class Client(val socket: Socket): case Some(t) => val timePassed = currentTimeMillis()/1000 - t this.player.foreach(_.applySingEffect( - 5 / max(5, timePassed) + 1 / max(5, timePassed) * 5 )) this.singStartTime = None case None => this.addDataToSend( s"$ACTION_NONBLOCKING_INDICATOR${this.executeAction(action)}" - ) - - /** Executes the specified action and returns its description */ - private def executeAction(action: Action): String = - this.character.flatMap(action.execute(_)) match - case Some(s) => s - case None => "You can't do that" + )*/ end Client diff --git a/src/scalevalapokalypsi/Server/Server.scala b/src/scalevalapokalypsi/Server/Server.scala index 609b581..bfb0893 100644 --- a/src/scalevalapokalypsi/Server/Server.scala +++ b/src/scalevalapokalypsi/Server/Server.scala @@ -3,7 +3,7 @@ package scalevalapokalypsi.Server // TODO: TLS/SSL / import javax.net.ssl.SSLServerSocketFactory -import scalevalapokalypsi.Model.Adventure +import scalevalapokalypsi.Model.{Adventure, Event} import scalevalapokalypsi.Model.Entities.Player import scalevalapokalypsi.constants.* import scalevalapokalypsi.utils.stringToByteArray @@ -55,9 +55,9 @@ class Server( this.makeClientsSing() this.writeObservations() if this.canExecuteTurns then - this.clients.inRandomOrder(_.act()) - this.writeClientDataToClients() - this.writeObservations() + this.clients.foreach(_.giveTurn()) + //this.writeClientDataToClients() + //this.writeObservations() this.clients.foreach(c => this.writeToClient(this.turnStartInfo(c), c) ) @@ -100,12 +100,14 @@ class Server( s"$timeLimit\r\n${this.turnStartInfo(c)}", c ) - this.clients.foreach(c => - if c.player != playerEntity then - c.player.foreach(_.observeString( - s"${name.getOrElse("Unknown player")} joins the game.") - ) - ) + val joinEvent = c.player.map(p => Event( + Map.from(Vector((p, ""))), + s"${p.name} joins the game." + )) + joinEvent.foreach(ev => this.clients.foreach(cl => + if cl != c then + cl.player.foreach(_.observe(ev)) + )) private def writeObservations(): Unit = @@ -137,7 +139,7 @@ class Server( // to the game after everyone // left and everything is just // as before! - val allPlayersReady = this.clients.forall(_.isReadyToAct) + val allPlayersReady = this.clients.forall(_.hasActed) val requirement3 = (allPlayersReady || currentTimeMillis() / 1000 >= previousTurn + timeLimit) requirement1 && requirement2 && requirement3 |