aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-18 02:01:12 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-18 02:01:12 +0200
commitfe2543627bcec1ea0f7a429bede20ca293458ba9 (patch)
tree42c9630d65292c90bee37444fde14fcf99cc3ffe
parenta98f089035dbcc94c14c9cd6246c3150bee84241 (diff)
downloadscalevalapokalypsi-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.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