aboutsummaryrefslogtreecommitdiff
path: root/src/scalevalapokalypsi
diff options
context:
space:
mode:
Diffstat (limited to 'src/scalevalapokalypsi')
-rw-r--r--src/scalevalapokalypsi/Model/Action.scala75
-rw-r--r--src/scalevalapokalypsi/Model/Adventure.scala11
-rw-r--r--src/scalevalapokalypsi/Model/Entities/Entity.scala94
-rw-r--r--src/scalevalapokalypsi/Model/Entities/Player.scala5
-rw-r--r--src/scalevalapokalypsi/Model/Event.scala36
-rw-r--r--src/scalevalapokalypsi/Model/SingEffects.scala8
-rw-r--r--src/scalevalapokalypsi/Server/Client.scala43
-rw-r--r--src/scalevalapokalypsi/Server/Server.scala24
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