diff options
Diffstat (limited to 'src')
-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 |