aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-27 12:29:43 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-27 12:29:43 +0200
commit98407b35ff477f372baa92bf582b90a961d4ad16 (patch)
treebb58925090075d1e9e30dd1593547db2cbe03bb6
parent38900e0b291d5e0f59afaaa239cd237f733b6588 (diff)
downloadscalevalapokalypsi-98407b35ff477f372baa92bf582b90a961d4ad16.tar.gz
scalevalapokalypsi-98407b35ff477f372baa92bf582b90a961d4ad16.zip
Added part of story & improved singing with multiple verses & hemingway distance
-rw-r--r--src/scalevalapokalypsi/Client/Client.scala6
-rw-r--r--src/scalevalapokalypsi/Client/RoomState.scala25
-rw-r--r--src/scalevalapokalypsi/Model/Action.scala16
-rw-r--r--src/scalevalapokalypsi/Model/Adventure.scala310
-rw-r--r--src/scalevalapokalypsi/Model/Entities/Entity.scala70
-rw-r--r--src/scalevalapokalypsi/Model/Entities/NPCs/Bartender.scala52
-rw-r--r--src/scalevalapokalypsi/Model/Entities/NPCs/Cultist.scala53
-rw-r--r--src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala66
-rw-r--r--src/scalevalapokalypsi/Model/Entities/NPCs/Robber.scala72
-rw-r--r--src/scalevalapokalypsi/Model/Entities/NPCs/Zombie.scala70
-rw-r--r--src/scalevalapokalypsi/Model/Entities/Player.scala26
-rw-r--r--src/scalevalapokalypsi/Model/Item.scala8
-rw-r--r--src/scalevalapokalypsi/Model/SingEffects.scala3
-rw-r--r--src/scalevalapokalypsi/Server/Client.scala73
-rw-r--r--src/scalevalapokalypsi/Server/Server.scala14
-rw-r--r--src/scalevalapokalypsi/UI/StdinLineReader.scala20
-rw-r--r--src/scalevalapokalypsi/UI/main.scala7
-rw-r--r--src/scalevalapokalypsi/utils/utils.scala7
18 files changed, 720 insertions, 178 deletions
diff --git a/src/scalevalapokalypsi/Client/Client.scala b/src/scalevalapokalypsi/Client/Client.scala
index 5364405..1ca4e7e 100644
--- a/src/scalevalapokalypsi/Client/Client.scala
+++ b/src/scalevalapokalypsi/Client/Client.scala
@@ -189,7 +189,11 @@ class Client(socket: Socket):
this.serverLineState = ServerLineState.Directions
case ServerLineState.Directions =>
- this.turnInfo.possibleDirections = line.split(LIST_SEPARATOR)
+ val dirs = line.split(LIST_SEPARATOR)
+ if dirs(0) == "" && dirs.length == 1 then
+ this.turnInfo.possibleDirections = Array.empty
+ else
+ this.turnInfo.possibleDirections = dirs
this.serverLineState = ServerLineState.Items
case ServerLineState.Items =>
diff --git a/src/scalevalapokalypsi/Client/RoomState.scala b/src/scalevalapokalypsi/Client/RoomState.scala
index 02fe11b..7eb3d5b 100644
--- a/src/scalevalapokalypsi/Client/RoomState.scala
+++ b/src/scalevalapokalypsi/Client/RoomState.scala
@@ -20,13 +20,22 @@ class RoomState:
var visibleEntities: Array[String] = Array.empty
override def toString: String =
- val itemDesc = "You can see the following items: " +
- this.visibleItems.mkString(", ")
- val entityDesc = "The following entities reside in the room: " +
- this.visibleEntities.mkString(", ")
- val directionDesc = "There are exits to " +
- this.possibleDirections.mkString(", ")
- (s"$areaDescription\n$directionDesc\n" +
- s"\n$itemDesc\n$entityDesc")
+ val directionDesc = iterableToString(this.possibleDirections)
+ .map(s => s"Täältä pääsee $s.\n")
+ .getOrElse("")
+ val itemDesc = iterableToString(this.visibleItems)
+ .map(s => s"Täällä on $s.\n")
+ .getOrElse("")
+ val entityDesc = iterableToString(this.visibleEntities)
+ .map(s => s"Seuraavat olennot ovat täällä: $s.\n")
+ .getOrElse("")
+ s"$areaDescription\n$directionDesc$itemDesc$entityDesc".dropRight(1)
+
+ private def iterableToString(iterable: Iterable[String]): Option[String] =
+ Some("")
+ .map(_ + iterable.dropRight(1).mkString(", "))
+ .map(_ + (if iterable.size >= 2 then " ja " else ""))
+ .map(_ + iterable.takeRight(1).headOption.getOrElse(""))
+ .filter(_ != "")
end RoomState
diff --git a/src/scalevalapokalypsi/Model/Action.scala b/src/scalevalapokalypsi/Model/Action.scala
index 21e1286..75ce68c 100644
--- a/src/scalevalapokalypsi/Model/Action.scala
+++ b/src/scalevalapokalypsi/Model/Action.scala
@@ -28,16 +28,16 @@ class Action(input: String):
def execute(actor: Player): Boolean =
val oldLocation = actor.location
val resOption: Option[(Boolean, Event)] = this.verb match
- case "go" =>
+ case "mene" =>
val result = actor.go(this.modifiers)
result.foreach(r =>
if actor.location != oldLocation then
oldLocation.observeEvent(r)
)
result.map((true, _))
- case "rest" => Some((true, actor.rest()))
- case "get" => Some((false, actor.pickUp(this.modifiers)))
- case "inventory" => Some((false, actor.inventory))
+ case "lepää" => Some((true, actor.rest()))
+ case "poimi" | "ota" => Some((false, actor.pickUp(this.modifiers)))
+ case "tavaraluettelo" => Some((false, actor.inventory))
case "sano" =>
val entityNames = actor.location.getEntityNames.map(_.toLowerCase)
val recipientNamePair = entityNames.flatMap(name =>
@@ -91,7 +91,9 @@ class Action(input: String):
)).toMap, fromThirdPerson.getOrElse(""))
)
)
- case "drop" => Some((false, actor.drop(this.modifiers)))
+ case "tiputa" | "pudota" => Some((false, actor.drop(this.modifiers)))
+ case "käytä" =>
+ Some(true -> actor.useItem(this.modifiers))
case "laula" =>
val end = modifiers.takeRight("suohon".length)
val start =
@@ -109,8 +111,8 @@ class Action(input: String):
else
None
case "xyzzy" => Some((false, Event(
- Map.from(Vector((actor, "The grue tastes yummy."))),
- s"${actor.name} tastes some grue.")
+ Map.from(Vector((actor, "Grue maistuu."))),
+ s"${actor.name} maistaa.")
))
case other => None
diff --git a/src/scalevalapokalypsi/Model/Adventure.scala b/src/scalevalapokalypsi/Model/Adventure.scala
index b10f7d9..dd55727 100644
--- a/src/scalevalapokalypsi/Model/Adventure.scala
+++ b/src/scalevalapokalypsi/Model/Adventure.scala
@@ -1,8 +1,17 @@
package scalevalapokalypsi.Model
import scala.collection.mutable.Map
+import scala.collection.immutable
import scalevalapokalypsi.Model.Entities.*
import scalevalapokalypsi.Model.Entities.NPCs.*
+import scala.math.min
+
+
+// This file is different from other files in that this file needs to have lots
+// of long lines of text. Thus I've chosen to take the liberty to write
+// carelessly lines longer than 80 characters, which is not allowed in other
+// files (obviously excluding long strings that will be printed, because
+// splitting them apart in the file would make it a pain to grep for them.)
/** The class `Adventure` holds data of the game world and provides methods
* for implementing a user interface for it.
@@ -15,35 +24,147 @@ import scalevalapokalypsi.Model.Entities.NPCs.*
*/
class Adventure(val playerNames: Vector[String]):
- private val middle = Area("Forest", "Olet keskellä metsää. Metsä on täynnä puita.\nLintua laulaa.")
- private val northForest = Area("Forest", "Olet keskellä metsää. Tiheä pensaikko estää sinua kulkemasta pohjoiseen. \nLintua laulaa.")
- private val southForest = Area("Forest", "The forest just goes on and on.")
- private val clearing = Area("Forest Clearing", "You are at a small clearing in the middle of forest.\nNearly invisible, twisted paths lead in many directions.")
- private val tangle = Area("Tangle of Bushes", "You are in a dense tangle of bushes. It's hard to see exactly where you're going.")
- private val home = Area("Home", "Home sweet home! Now the only thing you need is a working remote control.")
-
- middle.setNeighbors(Vector("north" -> northForest, "east" -> tangle, "south" -> southForest, "west" -> clearing))
- northForest.setNeighbors(Vector("east" -> tangle, "south" -> middle, "west" -> clearing))
- southForest.setNeighbors(Vector("north" -> middle, "east" -> tangle, "south" -> southForest, "west" -> clearing))
- clearing.setNeighbors(Vector("north" -> northForest, "east" -> middle, "south" -> southForest, "west" -> northForest))
- tangle.setNeighbors(Vector("north" -> northForest, "east" -> home, "south" -> southForest, "west" -> northForest))
- home.setNeighbors(Vector("west" -> tangle))
-
- clearing.addItem(Item("battery", "It's a small battery cell. Looks new.", 1))
- southForest.addItem(Item(
- "laulukäärö",
- "Jukranpujut, löysit laulukäärön!\n" +
- "Et vielä voi tehdä sillä mitään, koska et edes osaa laula.", 1
- ))
+ private val chieftainsGates = Area(
+ "Kyläpäällikön portti",
+ "Olet kyläpäällikön talon porttien edessä. Kylänraitti jatkuu pohjoiseen."
+ )
+ private val villageMain = Area(
+ "Kylänraitti",
+ "Olet kylänraitilla. Monet kylän asukkaista ovat liikkeellä."
+ )
+ private val bar = Area(
+ "Baari",
+ "[TODO] Olet baarissa. Tavallisesti täällä soisi musiikki ja ihmiset tanssivat ilosta, mutta nyt täällä nuokkuu vain muutama ihmistä baaritiskillä."
+ )
+ private val villageNorth = Area(
+ "Kylän pohjoinen portti",
+ "Olet kylän pohjoisen sisäänkäynnin ulkopuolella. Luukallio siintää idässä. Kylästänne vie tie naapurikylää kohti pohjoiseen. Lännessä virtaa Kattojoki, jonka anneista kylänne kalastajat saavat elantonsa. Heillä onkin ollut töitä viime aikoina, kun metsästäjät ovat olleet vastahakoisia lähtemään Luumetsään."
+ )
+ private val robberRoad = Area(
+ "Tie naapurikylään",
+ "Kuljet tietä naapurikylän suuntaan. Yhtäkkiä rosvojoukko yllättää sinut."
+ )
+ private val villageEast = Area(
+ "Itäinen pelto",
+ "Olet pellolla. Näet idässä Luukalliota ympäröivän, synkän näköisen Luumetsän. Voit vannoa näkeväsi siellä epämääräistä liikettä."
+ )
+ private val westForest = Area(
+ "Luumetsä",
+ "Olet metsässä Luukallion länsipuolella. Kuulet ympäriltäsi epämääräisiä ääniä, eivätkä ne kaikki ole eläinten aiheuttamia."
+ )
+ private val northForest = Area(
+ "Luumetsä",
+ "[TODO] Olet metsässä Luukallion pohjoispuolella. Kuulet ympäriltäsi epämääräisiä ääniä."
+ )
+ private val northOfNorthForest = Area(
+ "Luumetsä",
+ "Luumetsä jatkuu jatkumistaan..."
+ )
+ private val eastOfEastForest = Area(
+ "Luumetsä",
+ "Luumetsä jatkuu jatkumistaan..."
+ )
+ private val southOfSouthForest = Area(
+ "Luumetsä",
+ "Luumetsä jatkuu jatkumistaan..."
+ )
+ private val eastForest = Area(
+ "Luumetsä",
+ "[TODO] idk mitä täällä on?"
+ )
+ private val southForest = Area(
+ "Luumetsä",
+ "Olet Luukallion eteläpuolella. Näet kallion seinällä sileän kohdan, johon on kirjoitettu riimuja, joita et tunnista. Seinää katsomassa seisoo yksi kyläläisistä, jotka koottiin etsimään teitä piinaavan ilmiön alkulähdettä."
+ )
+ private val villageSouth = Area(
+ "Eteläinen pelto",
+ "Olet pellolla kylän eteläpuolella. Näet pellon laidalla pienen metsäaukion. Joki virtaa länsipuolellasi."
+ )
+ private val forestInSouthField = Area(
+ "Metsäaukio",
+ "Olet pienellä metsäaukiolla. Näet puiden lomassa pienen majan, josta nousee savua."
+ )
+ private val hutInFieldForest = Area(
+ "Erakon tupa",
+ "Olet pimeässä majassa, jonka ilmassa hiki ja yrtinkatku sekoittuvat."
+ )
+ private val caveDescent = Area(
+ "Käytävä kallion uumeniin",
+ "Olet hämärässä, soihduin valaistussa käytävässä. Se näyttää vievän syvälle kallion uumeniin. Katosta tippuu hiljalleen jotakin punaista nestettä."
+ )
+ private val secondCaveDescent = Area(
+ "Käytävä kallion uumeniin",
+ "Olet käytävässä, joka vie maan pinnalta syvälle kallion uumeniin. Käytävä on tässä kohtaa hieman laajempi."
+ )
+ private val bossDoor = Area(
+ "Vuoren aula",
+ "Tunnet kallion painostavan kivimassan päälläsi. Olet puolikaaren muotoisessa huoneessa. Edessäsi on avainreiällinen ovi."
+ )
+ private val storageRoom = Area(
+ "Varastohuone",
+ "Olet varastohuoneessa. Näet täällä tynnyreitä, joiden sisältö ei näytä siltä, että tahdot koskea siihen. Tavarat huoneessa näyttävät siltä, että niitä saatettaisiin käyttää joihinkin kulttimenoihin. Luulet, että yksi tynnyreistä on täynnä verta."
+ )
+ private val otherCaveRoom = Area(
+ "Idk",
+ "[TODO] mitä tänne?"
+ )
+ private val bossFightRoom = Area(
+ "Suuri huone kallion sisässä",
+ "Olet suuressa, ympyränmuotoisessa huoneessa. Lattialle on piirretty verinen kuvio, ja soihdut lepattavat punaista tulta."
+ )
+ private val mountaintop = Area(
+ "Kalliolla kukkulalla",
+ "Olet kiivennyt Luukallion huipulle. Näet täältä kotikyläsi peukun kokoisena. Takanasi levittyy Luumetsä pitkänä ja pimeänä, mutta taivaalla valkoiset pilvet halkovat horisonttia. Rinteet ovat kaikkiin suuntiin jyrkkiä paitsi länteen."
+ )
+ private val chieftainsGarden = Area(
+ "Kyläpäällikön piha",
+ "Olet kyläpäällikön pihalla. Sinun ei todellakaan pitäisi olla täällä ilman lupaa."
+ )
+ private val chieftainsHouse = Area(
+ "Kylänpäällikön talo",
+ "Olet suuressa aulassa kyläpäällikön talossa."
+ )
+
+
+ chieftainsGates .setNeighbors(Vector("pohjoiseen" -> villageMain))
+ villageMain .setNeighbors(Vector("etelään" -> chieftainsGates, "baariin" -> bar, "pohjoiseen" -> villageNorth))
+ bar .setNeighbors(Vector("ulos" -> villageMain))
+ villageNorth .setNeighbors(Vector("etelään" -> villageMain, "pohjoiseen" -> robberRoad, "itään" -> villageEast))
+ robberRoad .setNeighbors(Vector("etelään" -> villageNorth, "pohjoiseen" -> robberRoad))
+ villageEast .setNeighbors(Vector("pohjoiseen" -> villageNorth, "etelään" -> villageSouth, "itään" -> westForest))
+ villageSouth .setNeighbors(Vector("pohjoiseen" -> villageEast, "itään" -> villageEast, "metsikköön" -> forestInSouthField))
+ forestInSouthField.setNeighbors(Vector("mökkiin" -> hutInFieldForest, "pois metsiköstä" -> villageSouth))
+ hutInFieldForest .setNeighbors(Vector("ulos" -> forestInSouthField))
+ mountaintop .setNeighbors(Vector("länteen" -> westForest))
+ northForest .setNeighbors(Vector("pohjoiseen" -> northOfNorthForest, "itään" -> eastForest, "länteen" -> westForest))
+ eastForest .setNeighbors(Vector("pohjoiseen" -> northForest, "etelään" -> southForest, "itään" -> eastOfEastForest))
+ southForest .setNeighbors(Vector("itään" -> eastForest, "länteen" -> westForest, "etelään" -> southOfSouthForest))
+ westForest .setNeighbors(Vector("pohjoiseen" -> northForest, "etelään" -> southForest, "länteen" -> villageEast, "itään" -> mountaintop))
+ northOfNorthForest.setNeighbors(Vector("pohjoiseen" -> northOfNorthForest, "itään" -> northOfNorthForest, "länteen" -> northOfNorthForest, "etelään" -> northForest))
+ eastOfEastForest .setNeighbors(Vector("pohjoiseen" -> eastOfEastForest, "itään" -> eastOfEastForest, "länteen" -> eastForest, "etelään" -> eastOfEastForest))
+ southOfSouthForest.setNeighbors(Vector("pohjoiseen" -> southForest, "itään" -> southOfSouthForest, "länteen" -> southOfSouthForest, "etelään" -> southOfSouthForest))
+ caveDescent .setNeighbors(Vector("ulos" -> southForest, "syvemmälle" -> secondCaveDescent))
+ secondCaveDescent .setNeighbors(Vector("ylemmäs" -> caveDescent, "syvemmälle" -> bossDoor))
+ bossDoor .setNeighbors(Vector("länteen" -> storageRoom, "itään" -> otherCaveRoom, "ulos" -> secondCaveDescent))
+ storageRoom .setNeighbors(Vector("ulos" -> bossDoor))
+ otherCaveRoom .setNeighbors(Vector("ulos" -> bossDoor))
+ chieftainsGarden .setNeighbors(Vector("pohjoiseen" -> chieftainsGates, "kyläpäällikön taloon" -> chieftainsHouse))
+ chieftainsHouse .setNeighbors(Vector("ulos" -> chieftainsGarden))
+
+ chieftainsHouse.addItem(Item("siivilä", "mystinen, musta esine, jonka pohjassa on reikiä ja jota koristaa yliviivattua silmää kuvastava logo", 1))
+
val entities: Map[String, Entity] = Map()
+ val players: Map[String, Player] = Map()
+ playerNames.foreach(this.addPlayer(_))
+
val npcs: Map[String, NPC] = Map()
private val zombieAttrs = Vector(
- ("Weary zombie", clearing, 20),
- ("Smelly zombie", home, 20),
- ("Rotten zombie", tangle, 10)
+ ("mädäntyvä kyläläinen rääsyissä", westForest, 20),
+ ("mädäntyvä kyläläinen mekossa", westForest, 20),
+ ("räsyinen olento Joukon vaatteissa", northOfNorthForest, 10)
)
zombieAttrs.foreach(z =>
val zombie = Zombie(this, z(0), z(1), z(2))
@@ -51,15 +172,140 @@ class Adventure(val playerNames: Vector[String]):
z(1).addEntity(zombie)
)
- def takeNpcTurns(): Unit =
- npcs.values.foreach(_.act())
+ object JoukosDad extends NPC(this, "Joukon isä", bar, 100, 100):
+ def act(): Unit = ()
+ private var hasBeenTalkedTo: Boolean = false
+ def getDialog: String =
+ if northForest.getEntity(
+ "räsyinen olento Joukon vaatteissa"
+ ).isDefined || !hasBeenTalkedTo then
+ hasBeenTalkedTo = true
+ "Voi minun poikaani! Mikä karmea kohtalo! Löytäisitkö hänet vallanneen olennon vuorilta? Päästäisitkö poikani kärsimyksestä?"
+ else
+ "Ai se on tehty? Voi sentään. No, nyt hän ainakin on saavuttanut levon. Kunpa näin ei olisi tarvinnut käydä."
+ end JoukosDad
- private val gruu = Entity(this, "Gruu", northForest)
- northForest.addEntity(gruu)
- this.entities += gruu.name -> gruu
+ object VillageChieftainGuard extends NPC(this, "Vartija", chieftainsGates, 100, 100):
+ private var isAsleep = false
+ def act(): Unit =
+ if this.location.getItemNames.exists(_ == "oluttuoppi") then
+ this.location.observeEvent(this.pickUp("oluttuoppi"))
+ this.location.observeEvent(Event(immutable.Map.empty, s"${this.name} juo oluen ja sammuu."))
+ this.isAsleep = true
+ this.location.setNeighbor("etelään", chieftainsGarden)
+ def getDialog: String =
+ if !this.isAsleep then
+ "Et voi mennä kyläpäällikön taloon ilman kutsua."
+ else
+ "ZzzzZzZZZz..."
+ end VillageChieftainGuard
- val players: Map[String, Player] = Map()
- playerNames.foreach(this.addPlayer(_))
+
+ object Bartender extends NPC(this, "baarin pitäjä", bar, 100, 100):
+
+ private var dialogIndex = 0
+ private val dialogs = Vector(
+ "Onnea matkaan. Tarjoan sinulle tuopin olutta rohkaisuksi.",
+ "Onnea matkaan."
+ )
+
+ def act(): Unit = ()
+
+ def getDialog: String =
+ if dialogIndex == 0 then
+ this.location.addItem(Item(
+ "oluttuoppi",
+ "Tuopillinen kuohuvaa ja raikasta olutta. Se tuoksuu aika vahvalta.",
+ 1
+ ))
+ this.location.observeEvent(
+ Event(
+ immutable.Map.empty,
+ "Baarimikko kaataa tuoppiin olutta ja asettaa oluttuopin pöydälle."
+ )
+ )
+ dialogIndex = min(dialogIndex + 1, this.dialogs.length)
+ dialogs(dialogIndex - 1)
+ end getDialog
+
+ end Bartender
+
+ //object Hermit extends NPC(this, "Tietäjäerakko", hutInFieldForest, 100, 100):
+ //
+ // private var scrollRecipients =
+
+// object ShoutingCultist extends Cultist(this, "Kaapupäinen henkilö", secondCaveDescent, 100, 100):
+// private var hasShouted = false
+// override def act(): Unit =
+// val playerLocations = players.values.map(_.location)
+// if !hasShouted && playerLocations contains caveDescent then
+// caveDescent.observe(Event(immutable.Map.empty, "Kuulet huudon: “Tunkeilija! Valmistautukaa!”"))
+// hasShouted = true
+// super.act()
+// end ShoutingCultist
+// val cultist2 = Cultist(this, "Silmätön kaljupää", secondCaveDescent)
+
+ this.npcs += Bartender.name -> Bartender
+ this.bar.addEntity(Bartender)
+ this.npcs += JoukosDad.name -> JoukosDad
+ this.bar.addEntity(JoukosDad)
+ this.npcs += VillageChieftainGuard.name -> VillageChieftainGuard
+ this.chieftainsGates.addEntity(VillageChieftainGuard)
+// this.npcs += ShoutingCultist.name -> ShoutingCultist
+// this.secondCaveDescent.addEntity(ShoutingCultist)
+// this.npcs += cultist2
+// this.secondCaveDescent.addEntity(cultist2)
+
+
+ object SeparatorScroll extends Item(
+ "separoitumisen laulukäärö",
+ "Laulukäärö, joka antaa sanat separoitumisen laululle, joka saa aikaan raon jopa harmaaseen kiveen.",
+ 1
+ ):
+
+ class SeparatorEffect(actor: Entity) extends SingEffect(actor):
+ def getVerses: Vector[String] =
+ Vector(
+ "Separoidu kova kivi",
+ "jakaudu ja ratkeapi",
+ "laske minut syvyyksiisi",
+ "kalmankylmiin lähteisiisi"
+ )
+
+ def apply(quality: Float): Event =
+ if actor.location == southForest && quality > .1 && !southForest.hasNeighbor("kallion sisään") then
+ southForest.setNeighbor("kallion sisään", caveDescent)
+ Event(
+ Vector(actor -> "Kivi halkeaa rytinällä. Nyt näet sisään soihduilla valaistuun käytävään, joka johtaa kallion uumeniin.").toMap,
+ s"Näet kun kivi halkeaa kahtia henkilön ${actor.name} laulun voimasta. Kallion sisästä paljastuu käytävä."
+ )
+ else if actor.location == southForest && quality <= .1 then
+ Event(
+ Vector(actor -> "Kivi natisee liitoksissaan, mutta jää odottamaan voimallisempaa laulua.").toMap,
+ s"Kivi natisee hieman kun ${actor.name} laulaa, mutta mitään merkittävää ei tapahdu."
+ )
+ else
+ Event(Vector(actor -> "Laulat laulun, mutta mitään ei tapahdu.").toMap, "")
+ end SeparatorEffect
+
+ override def use(user: Entity): Option[Event] =
+ user match
+ case p: Player =>
+ p.setSingEffect(SeparatorEffect(p))
+ Some(Event(
+ Vector(user -> "Avaat käärön varovasti ja aloitat separoitumisen laulun.").toMap,
+ s"${user.name} alkaa laulamaan kääröstä."
+ ))
+ case other =>
+ None
+ end use
+
+ end SeparatorScroll
+
+ bar.addItem(SeparatorScroll)
+
+ def takeNpcTurns(): Unit =
+ npcs.values.foreach(_.act())
/** Adds a player entity with the specified name to the game.
*
@@ -67,8 +313,8 @@ class Adventure(val playerNames: Vector[String]):
* @return the created player entity
*/
def addPlayer(name: String): Player =
- val newPlayer = Player(this, name, middle)
- middle.addEntity(newPlayer)
+ val newPlayer = Player(this, name, chieftainsGates)
+ chieftainsGates.addEntity(newPlayer)
this.entities += name -> newPlayer
players += name -> newPlayer
newPlayer
diff --git a/src/scalevalapokalypsi/Model/Entities/Entity.scala b/src/scalevalapokalypsi/Model/Entities/Entity.scala
index aa2a2e2..6a2072d 100644
--- a/src/scalevalapokalypsi/Model/Entities/Entity.scala
+++ b/src/scalevalapokalypsi/Model/Entities/Entity.scala
@@ -30,7 +30,8 @@ class Entity(
*
* @return the verse to sing against this entity
*/
- def getVerseAgainst: String = "Esimerkkirivi laulettavaksi"
+ def getVerseAgainst: Vector[String] =
+ Vector("Esimerkkirivi laulettavaksi")
def isAlive = this.hp > 0
@@ -42,7 +43,7 @@ class Entity(
this.condition(1)
)
else
- println(s"Could remove myself: ${this.adventure.removeEntity(this.name)}")
+ this.adventure.removeEntity(this.name)
Event(
Vector(this ->
"Olet täysin menettänyt toimintakykysi. Kaadut elottomana maahan."
@@ -97,9 +98,9 @@ class Entity(
val leaving = oldEntities.zip(
Vector.fill
(oldEntities.size)
- (s"${this.name} leaves this location.")
+ (s"${this.name} lähtee $direction")
)
- val self = Vector((this, s"You go $direction."))
+ val self = Vector((this, s"Menet $direction."))
Some(Event(
(leaving ++ self).toMap,
s"$name saapuu tänne."
@@ -112,8 +113,12 @@ class Entity(
val inventoryWeight = items.values.map(p => p(1).weight).sum
if inventoryWeight + i.weight > maxInventoryWeight then
Event(
- immutable.Map.from(Vector((this, s"Voimasi eivät riitä kannattelemaan esinettä ${i.name}, koska kannat liikaa"))),
- s"")
+ Vector((
+ this,
+ s"Voimasi eivät riitä kannattelemaan esinettä ${i.name}, koska kannat liikaa"
+ )).toMap,
+ s""
+ )
else
if items.contains(i.name) then
val (current, _) = items(i.name)
@@ -121,13 +126,27 @@ class Entity(
else
this.items += i.name -> (1, i)
Event(
- immutable.Map.from(Vector((this, s"Poimit esineen ${i.name}"))),
+ Vector((this, s"Poimit esineen ${i.name}")).toMap,
s"$name poimi esineen ${i.name}"
)
- case None => Event(
- immutable.Map.from(Vector((this, s"Täällä ei ole esinettä $itemName noukittavaksi."))),
- s"${this.name} yritti ottaa jotakin, mutta sai vain likaa käsilleen."
- )
+ case None =>
+ Event(
+ immutable.Map.from(Vector((
+ this,
+ s"Täällä ei ole esinettä $itemName noukittavaksi."
+ ))),
+ s"${this.name} yritti ottaa jotakin, mutta sai vain likaa käsilleen."
+ )
+
+ def removeItem(itemName: String): Boolean =
+ this.items.get(itemName).map((count, item) =>
+ if count > 1 then
+ this.items.remove(itemName)
+ else
+ this.items(itemName) = (count - 1, item)
+ assert(this.items(itemName)(0) == count - 1)
+ Some(true)
+ ).isDefined
def drop(itemName: String): Event =
this.items.remove(itemName) match
@@ -138,12 +157,21 @@ class Entity(
this.items += itemName -> (current - 1, item)
this.currentLocation.addItem(item)
Event(
- immutable.Map.from(Vector((this, s"Pudotit esineen $itemName"))),
+ immutable.Map.from(
+ Vector((this, s"Pudotit esineen $itemName"))
+ ),
s"$name Pudotti esineen $itemName"
)
+ case Some((current, item)) =>
+ this.items.remove(item.name)
+ println(" [virhe] esineitä ei koskaan pitäisi olla nollaa")
+ Event(
+ Vector((this, "Sinulla ei ole tuota esinettä.")).toMap,
+ ""
+ )
case None => Event(
- immutable.Map.from(Vector((this, "Sinulla ei ole tätä esinettä!"))),
- s"$name yritti tonkia rpustaan esineen $itemName mutta ei löytänyt sitä."
+ Vector((this, "Sinulla ei ole tätä esinettä!")).toMap,
+ s"$name yritti tonkia repustaan esineen $itemName mutta ei löytänyt sitä."
)
def sayTo(entity: Entity, message: String): Event =
@@ -154,7 +182,7 @@ class Entity(
(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}"
+ s"Kuulet henkilön ${this.name} sanovan jotain henkilölle ${entity.name}."
)
def ponder(message: String): Event =
@@ -185,7 +213,14 @@ class Entity(
* @param itemName the name to check
* @return whether this entity has this item and can drop it
*/
- def canDrop(itemName: String): Boolean = this.items.contains(itemName)
+ //def canDrop(itemName: String): Boolean = this.items.contains(itemName)
+
+ def useItem(itemName: String): Event =
+ val item: Option[Item] = this.items.get(itemName).map(_(1))
+ val event: Option[Event] = item.flatMap(_.use(this))
+ event.getOrElse(
+ Event(Vector(this -> "Sinulla ei ole tuota esinettä.").toMap, "")
+ )
/** Returns a brief description of the player’s state, for debugging purposes. */
override def toString = s"${this.name} at ${this.location.name}"
@@ -204,7 +239,4 @@ class Entity(
s"")
-
-
-
end Entity
diff --git a/src/scalevalapokalypsi/Model/Entities/NPCs/Bartender.scala b/src/scalevalapokalypsi/Model/Entities/NPCs/Bartender.scala
new file mode 100644
index 0000000..1743380
--- /dev/null
+++ b/src/scalevalapokalypsi/Model/Entities/NPCs/Bartender.scala
@@ -0,0 +1,52 @@
+/*
+package scalevalapokalypsi.Model.Entities.NPCs
+
+import scalevalapokalypsi.Model.{Area,Event,Item,Adventure}
+import scala.math.min
+
+class Bartender(
+ adventure: Adventure,
+ initialLocation: Area
+) extends NPC(
+ adventure,
+ "baarimikko",
+ initialLocation,
+ 100,
+ 100
+):
+
+
+ private var dialogIndex = 0
+
+ private val dialogs = Vector(
+ "Onnea matkaan. Tarjoan sinulle tuopin olutta rohkaisuksi.",
+ "Onnea matkaan."
+ )
+
+ def getDialog: String =
+
+ if dialogIndex == 0 then
+ this.location.addItem(Item(
+ "oluttuoppi",
+ "Tuopillinen kuohuvaa ja raikasta olutta. Se tuoksuu aika vahvalta.",
+ 1
+ ))
+ this.location.observeEvent(
+ Event(
+ Map.empty,
+ "Baarimikko kaataa tuoppiin olutta ja asettaa oluttuopin pöydälle."
+ )
+ )
+
+ dialogIndex = min(dialogIndex + 1, this.dialogs.length)
+
+ dialogs(dialogIndex - 1)
+
+ end getDialog
+
+
+ def act(): Unit = ()
+
+
+end Bartender
+*/
diff --git a/src/scalevalapokalypsi/Model/Entities/NPCs/Cultist.scala b/src/scalevalapokalypsi/Model/Entities/NPCs/Cultist.scala
new file mode 100644
index 0000000..fa5602e
--- /dev/null
+++ b/src/scalevalapokalypsi/Model/Entities/NPCs/Cultist.scala
@@ -0,0 +1,53 @@
+
+package scalevalapokalypsi.Model.Entities.NPCs
+
+import scala.collection.mutable.Buffer
+import scalevalapokalypsi.Model.*
+import scalevalapokalypsi.Model.Entities.*
+import scala.util.Random
+
+class Cultist(
+ adventure: Adventure,
+ identifier: String,
+ initialLocation: Area,
+ initialHP: Int = 100,
+ maxHP: Int = 100
+) extends NPC(adventure, identifier, initialLocation, initialHP, maxHP):
+
+ private val damage = 20
+
+ override def getDialog: String =
+ "Verta! Lisää verta!"
+
+ override def act(): Unit =
+ val possibleVictims = this.location
+ .getEntities
+ .filter(_ != this)
+ .filter(_ match
+ case c: Cultist => false
+ case other => true
+ )
+ .toVector
+ val index: Int =
+ if possibleVictims.isEmpty then 0
+ else Random.between(0, possibleVictims.length)
+ if !possibleVictims.isEmpty then
+ this.location.observeEvent(
+ this.curse(possibleVictims(index))
+ )
+
+
+ private def curse(entity: Entity): Event =
+ entity.takeDamage(this.damage)
+ Event(
+ Map.from(Vector((
+ entity,
+ s"${this.name} lausuu pimeän loitsun. Näet varjon pyyhältävän sinua kohti ja sinut valtaa kylmyys.\n" +
+ s"${entity.condition(0)}"
+ ))),
+ s"${this.name} käyttää kirousta henkilöön ${entity.name}\n" +
+ s"${entity.condition(1)}"
+ )
+
+end Cultist
+
diff --git a/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala b/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala
index 944f2e6..7d9996c 100644
--- a/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala
+++ b/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala
@@ -1,10 +1,8 @@
package scalevalapokalypsi.Model.Entities.NPCs
-import scala.collection.mutable.Buffer
import scalevalapokalypsi.Model.*
import scalevalapokalypsi.Model.Entities.*
-import scala.util.Random
/** A `NPC` object represents a non-playable in-game character controlled by
* the server using this objects `act` method. It can also be "talked to": it
@@ -25,67 +23,3 @@ abstract class NPC(
) extends Entity(adventure, name, initialLocation, initialHP, maxHp):
def getDialog: String
def act(): Unit
-
-class Zombie(
- adventure: Adventure,
- identifier: String,
- initialLocation: Area,
- initialHP: Int = 20
-) extends NPC(adventure, identifier, initialLocation, initialHP, 20):
-
- private val damage = 10
- private val dialogs = Vector(
- "örvlg",
- "grr",
- "äyyrrrgrlgb ww",
- "aaak brzzzwff ååö",
- "äkb glan abglum",
- "öub gpa"
- )
-
- override def getDialog: String =
- val dialogIndex = Random.between(0, this.dialogs.length)
- this.dialogs(dialogIndex)
-
- override def act(): Unit =
- val possibleVictims = this.location
- .getEntities
- .filter(_ != this)
- .toVector
- val index: Int =
- if possibleVictims.isEmpty then 0
- else Random.between(0, possibleVictims.length)
- if possibleVictims.isEmpty then
- val possibleDirections = this.location.getNeighborNames.toVector
- val directionIndex = Random.between(0, possibleDirections.length*2)
- possibleDirections
- .toVector
- .lift(directionIndex)
- .flatMap(this.go(_))
- .foreach(this.location.observeEvent(_))
- else
- this.location.observeEvent(
- this.attack(possibleVictims(index))
- )
-
-
- private def attack(entity: Entity): Event =
- if Random.nextBoolean() then
- entity.takeDamage(this.damage)
- Event(
- Map.from(Vector((
- entity,
- s"${this.name} puree sinua, hyi yäk!\n" +
- s"${entity.condition(0)}"
- ))),
- s"${this.name} puree henkilöä ${entity.name}.\n" +
- s"${entity.condition(1)}"
- )
- else
- Event(
- Map.from(Vector((
- entity,
- s"${this.name} yrittää purra sinua mutta kaatuu ohitsesi."
- ))),
- s"${this.name} yrittää purra henkilöä ${entity.name}, mutta epäonnistuu surkeasti."
- )
diff --git a/src/scalevalapokalypsi/Model/Entities/NPCs/Robber.scala b/src/scalevalapokalypsi/Model/Entities/NPCs/Robber.scala
new file mode 100644
index 0000000..fc009a6
--- /dev/null
+++ b/src/scalevalapokalypsi/Model/Entities/NPCs/Robber.scala
@@ -0,0 +1,72 @@
+
+package scalevalapokalypsi.Model.Entities.NPCs
+
+import scala.collection.mutable.Buffer
+import scalevalapokalypsi.Model.*
+import scalevalapokalypsi.Model.Entities.*
+import scala.util.Random
+
+class Robber(
+ adventure: Adventure,
+ name: String,
+ val weaponName: String,
+ val weaponDamage: Int,
+ val hitChance: Float,
+ initialLocation: Area,
+ initialHP: Int = 20
+) extends NPC(adventure, name, initialLocation, initialHP, 20):
+
+ private val dialogs = Vector(
+ "Rahat tai henki!",
+ "Anna tänne!",
+ "Syödään se ryöstön jälkeen!",
+ "Vesihiisi sihisi hississä ja muumit laaksosta poissaolollaan."
+ )
+
+ override def getDialog: String =
+ val dialogIndex = Random.between(0, this.dialogs.length)
+ this.dialogs(dialogIndex)
+
+ override def act(): Unit =
+ val possibleVictims = this.location
+ .getEntities
+ .filter(_ != this)
+ .filter(_ match
+ case z: Robber => false
+ case other => true
+ )
+ .toVector
+ val index: Int =
+ if possibleVictims.isEmpty then 0
+ else Random.between(0, possibleVictims.length)
+ if !possibleVictims.isEmpty then
+ this.location.observeEvent(
+ this.attack(possibleVictims(index))
+ )
+ //else
+ // this.location.getNeighborNames.filter(this.location.neighbor(_).map(_.getEntities.size > 0).getOrElse(false))
+
+
+ private def attack(entity: Entity): Event =
+ if Random.nextFloat() < this.hitChance then
+ entity.takeDamage(this.weaponDamage)
+ Event(
+ Map.from(Vector((
+ entity,
+ s"${this.name} lyö sinua ${this.weaponName}\n" +
+ s"${entity.condition(0)}"
+ ))),
+ s"${this.name} lyö henkilöä ${entity.name} ${this.weaponName}.\n" +
+ s"${entity.condition(1)}"
+ )
+ else
+ Event(
+ Map.from(Vector((
+ entity,
+ s"${this.name} yrittää lyödä sinua mutta väistät."
+ ))),
+ s"${this.name} yrittää lyödä henkilöä ${entity.name}, mutta tämä väistää."
+ )
+
+end Robber
+
diff --git a/src/scalevalapokalypsi/Model/Entities/NPCs/Zombie.scala b/src/scalevalapokalypsi/Model/Entities/NPCs/Zombie.scala
new file mode 100644
index 0000000..56cb160
--- /dev/null
+++ b/src/scalevalapokalypsi/Model/Entities/NPCs/Zombie.scala
@@ -0,0 +1,70 @@
+
+package scalevalapokalypsi.Model.Entities.NPCs
+
+import scala.collection.mutable.Buffer
+import scalevalapokalypsi.Model.*
+import scalevalapokalypsi.Model.Entities.*
+import scala.util.Random
+
+class Zombie(
+ adventure: Adventure,
+ identifier: String,
+ initialLocation: Area,
+ initialHP: Int = 20
+) extends NPC(adventure, identifier, initialLocation, initialHP, 20):
+
+ private val damage = 10
+ private val dialogs = Vector(
+ "örvlg",
+ "grr",
+ "äyyrrrgrlgb ww",
+ "aaak brzzzwff ååö",
+ "äkb glan abglum",
+ "öub gpa"
+ )
+
+ override def getDialog: String =
+ val dialogIndex = Random.between(0, this.dialogs.length)
+ this.dialogs(dialogIndex)
+
+ override def act(): Unit =
+ val possibleVictims = this.location
+ .getEntities
+ .filter(_ != this)
+ .filter(_ match
+ case z: Zombie => false
+ case other => true
+ )
+ .toVector
+ val index: Int =
+ if possibleVictims.isEmpty then 0
+ else Random.between(0, possibleVictims.length)
+ if !possibleVictims.isEmpty then
+ this.location.observeEvent(
+ this.attack(possibleVictims(index))
+ )
+
+
+ private def attack(entity: Entity): Event =
+ if Random.nextBoolean() then
+ entity.takeDamage(this.damage)
+ Event(
+ Map.from(Vector((
+ entity,
+ s"${this.name} puree sinua, hyi yäk!\n" +
+ s"${entity.condition(0)}"
+ ))),
+ s"${this.name} puree henkilöä ${entity.name}.\n" +
+ s"${entity.condition(1)}"
+ )
+ else
+ Event(
+ Map.from(Vector((
+ entity,
+ s"${this.name} yrittää purra sinua mutta kaatuu ohitsesi."
+ ))),
+ s"${this.name} yrittää purra henkilöä ${entity.name}, mutta epäonnistuu surkeasti."
+ )
+
+end Zombie
+
diff --git a/src/scalevalapokalypsi/Model/Entities/Player.scala b/src/scalevalapokalypsi/Model/Entities/Player.scala
index 9fc929d..62d4180 100644
--- a/src/scalevalapokalypsi/Model/Entities/Player.scala
+++ b/src/scalevalapokalypsi/Model/Entities/Player.scala
@@ -1,6 +1,7 @@
package scalevalapokalypsi.Model.Entities
import scala.collection.mutable.{Buffer, Map}
+import scala.collection.immutable
import scalevalapokalypsi.Model.*
/** A `Player` object represents a player character controlled by one real-life player
@@ -21,6 +22,15 @@ class Player(
private val observations: Buffer[String] = Buffer.empty
private val observedEvents: Buffer[Event] = Buffer.empty
private var pendingSingEffect: Option[SingEffect] = None
+// private var verseToSing: Option[String] = None
+
+ override def getVerseAgainst: Vector[String] =
+ Vector(
+ "Pian pimeässä suossa",
+ "lepäät levä ympärilläs",
+ "lauluasi lausumahan",
+ "et sikaa paremmin pysty"
+ )
override def observe(event: Event): Unit =
this.observedEvents.append(event)
@@ -44,8 +54,11 @@ class Player(
def setSingEffect(effect: SingEffect): Unit =
this.pendingSingEffect = Some(effect)
- def getSingEffectTarget: Option[Entity] =
- this.pendingSingEffect.map(_.target)
+ def getVerses: Option[Vector[String]] =
+ this.pendingSingEffect.map(_.getVerses)
+
+// def getSingEffectTarget: Option[Entity] =
+// this.pendingSingEffect.map(_.target)
/** Applies the pending sing effect and informs the surronding Entities
* about the effects of the song.
@@ -65,12 +78,11 @@ class Player(
else ("erinomaista", "merkittävä")
val quality =
s"Laulu on ${qualityDescriptions(0)} ja sen vaikutus on ${qualityDescriptions(1)}."
- val event = Event(Map.empty, s"$quality")
+ val event = Event(immutable.Map.empty, s"$quality")
this.location.observeEvent(event)
- this.pendingSingEffect.map(ef => ef(singQuality))
+ this.pendingSingEffect
+ .map(ef => ef(singQuality))
+ .map(this.location.observeEvent(_))
this.pendingSingEffect = None
-
-
-
end Player
diff --git a/src/scalevalapokalypsi/Model/Item.scala b/src/scalevalapokalypsi/Model/Item.scala
index cccb489..eea7044 100644
--- a/src/scalevalapokalypsi/Model/Item.scala
+++ b/src/scalevalapokalypsi/Model/Item.scala
@@ -1,5 +1,6 @@
package scalevalapokalypsi.Model
+import scalevalapokalypsi.Model.Entities.Entity
import scala.annotation.targetName
/** The class `Item` represents items in a text adventure game. Each item has a
@@ -13,8 +14,11 @@ class Item(val name: String, val description: String, val weight: Int):
/** Returns a short textual representation of the item (its name, that is). */
override def toString = this.name
-
-
+
+ /** Makes the caller entity use this Item. Default implementation
+ * does nothing, this can be implemented by extending.
+ */
+ def use(caller: Entity): Option[Event] = None
end Item
diff --git a/src/scalevalapokalypsi/Model/SingEffects.scala b/src/scalevalapokalypsi/Model/SingEffects.scala
index 23b7d37..e0f9135 100644
--- a/src/scalevalapokalypsi/Model/SingEffects.scala
+++ b/src/scalevalapokalypsi/Model/SingEffects.scala
@@ -5,11 +5,12 @@ import scala.collection.immutable.Map
trait SingEffect(val target: Entity):
def apply(singQuality: Float): Event
+ def getVerses: Vector[String]
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(Map.empty, "") // The conditions are automatically shown to
// clients through takeDamage, but other effects
// should explain the changes they have.
+ def getVerses: Vector[String] = this.target.getVerseAgainst
diff --git a/src/scalevalapokalypsi/Server/Client.scala b/src/scalevalapokalypsi/Server/Client.scala
index 5727fc6..940888b 100644
--- a/src/scalevalapokalypsi/Server/Client.scala
+++ b/src/scalevalapokalypsi/Server/Client.scala
@@ -8,6 +8,22 @@ import ServerProtocolState.*
import scalevalapokalypsi.Model.Action
import scalevalapokalypsi.Model.Entities.Player
import java.lang.System.currentTimeMillis
+import scala.collection.mutable.Buffer
+
+
+// Note to graders etc
+// This class has an interesting design choice:
+// It does not write data directly to the client.
+// This is because with some multithreaded implementation of
+// Server.scala it could lead to race conditions etc where
+// several writes happened at once.
+//
+// Thus, this class never writes to the client anything, and
+// all TCP communication is the responsibility of the server.
+// Because of lack of time though, I had to rely on a very dirty
+// trick - the client has a workaround way to actually write data
+// to the client by queuing it to the server via the method
+// `dataToThisClient`.
class Client(val socket: Socket):
private var incompleteMessage: Array[Byte] =
@@ -21,12 +37,31 @@ class Client(val socket: Socket):
private var nextAction: Option[Action] = None
private var turnUsed = false
private var singStartTime: Option[Long] = None
- private var verseToSing: String = ""
+ private var versesToSing: Vector[String] = Vector.empty
+ private val versesSung: Buffer[String] = Buffer.empty
+ private var verseIndex = 0
def clientHasSong = this.singStartTime.isDefined
- def startSong(verse: String): Unit =
- this.verseToSing = verse
- this.singStartTime = Some(currentTimeMillis() / 1000)
+ def startSongIfNeeded(): Unit =
+ if this.player.exists(_.isSinging) && !this.clientHasSong then
+ val verses = this.player.flatMap(_.getVerses)
+ verses.foreach(v =>
+ this.versesToSing = v
+ this.verseIndex = 0
+ this.singStartTime = Some(currentTimeMillis() / 1000)
+ this.startVerse()
+ )
+
+ /** Starts the next verse for the remote client,
+ * use only when you have checked that there are verses left to sing!
+ */
+ def startVerse(): Unit =
+ val verse = this.versesToSing.lift(this.verseIndex)
+ verse.foreach(v =>
+ this.addDataToSend(s"${SING_INDICATOR}$v")
+ )
+ this.verseIndex += 1
+
/** Calculates the amount of bytes available for future incoming messages */
def spaceAvailable: Int = this.incompleteMessage.size - incompleteMessageIndex - 1
@@ -166,17 +201,27 @@ class Client(val socket: Socket):
if !this.turnUsed then
this.singStartTime match
case Some(t) =>
-
- val timePassed = currentTimeMillis()/1000 - t
-
- val quality = if line == this.verseToSing then
- 5.0 / max(5.0, timePassed.toDouble)
+ if this.verseIndex == this.versesToSing.length then
+ val timeQuality =
+ 5.0*this.versesToSing.length /
+ max(5.0, currentTimeMillis()/1000 - t)
+
+ val songToSing = this.versesToSing.mkString("")
+ .toLowerCase
+ val songSung = this.versesSung.mkString("").toLowerCase
+ val quality =
+ timeQuality * (
+ 1.0 - hammingDistance(songToSing, songSung)
+ .toFloat
+ / songToSing.length.toFloat
+ )
+
+ this.player.foreach(_.applySingEffect(quality.toFloat))
+
+ this.singStartTime = None
else
- 0.0
-
- this.player.foreach(_.applySingEffect(quality.toFloat))
-
- this.singStartTime = None
+ this.versesSung += line
+ this.startVerse()
case None if isPrintable(line) =>
diff --git a/src/scalevalapokalypsi/Server/Server.scala b/src/scalevalapokalypsi/Server/Server.scala
index 16a2128..6bf21d4 100644
--- a/src/scalevalapokalypsi/Server/Server.scala
+++ b/src/scalevalapokalypsi/Server/Server.scala
@@ -51,8 +51,8 @@ class Server(
this.receiveNewClient()
this.readFromAll()
this.clients.foreach(_.interpretData())
- this.writeClientDataToClients()
this.makeClientsSing()
+ this.writeClientDataToClients()
this.writeObservations()
this.endGameForDeadClients()
if this.canExecuteTurns then
@@ -61,7 +61,7 @@ class Server(
this.clients.foreach(c =>
this.writeToClient(this.turnStartInfo(c), c)
)
- this.previousTurn = currentTimeMillis() / 1000
+ this.previousTurn = currentTimeMillis().toDouble / 1000.0
if this.adventure.isDefined && this.joinAfterStart then
this.clients.foreach( c => if c.isReadyForGameStart then
this.adventure.foreach(a =>
@@ -76,7 +76,7 @@ class Server(
then
this.adventure = Some(Adventure(this.clients.names))
this.clients.foreach(startGameForClient(_))
- this.previousTurn = currentTimeMillis() / 1000
+ this.previousTurn = currentTimeMillis().toDouble / 1000.0
/** Helper function to start the game for the specified client c.
* MAY ONLY BE USED IF `this.adventure` is Some!
@@ -120,13 +120,7 @@ class Server(
private def makeClientsSing(): Unit =
this.clients.foreach(c =>
- val target = c.player.flatMap(_.getSingEffectTarget)
- target.foreach(t =>
- if c.player.exists(_.isSinging) && !c.clientHasSong then
- val verse = t.getVerseAgainst
- this.writeToClient(s"${SING_INDICATOR}$verse\r\n", c)
- c.startSong(verse)
- )
+ c.startSongIfNeeded()
)
/** Helper function to determine if the next turn can be taken */
diff --git a/src/scalevalapokalypsi/UI/StdinLineReader.scala b/src/scalevalapokalypsi/UI/StdinLineReader.scala
index 4d0f778..1509f75 100644
--- a/src/scalevalapokalypsi/UI/StdinLineReader.scala
+++ b/src/scalevalapokalypsi/UI/StdinLineReader.scala
@@ -12,18 +12,22 @@ class StdinLineReader:
private var nextLine: Future[String] = Future.failed(Exception())
- /** Returns a new line of input if there are any. */
- def newLine(): Option[String] =
+ /** Returns a new line of input as a Right when there are any.
+ * If there is no new line due to EOF, returns Left(true),
+ * if there is no new line due to some other error, returns Left(false)
+ */
+ def newLine(): Either[Boolean, String] =
this.nextLine.value match
case Some(Success(s)) =>
- if s.contains("\u0000") then
- println("End of stream!")
- this.startReading()
- Some(s)
+ if s == null then
+ Left(true)
+ else
+ this.startReading()
+ Right(s)
case Some(Failure(e)) =>
this.startReading()
- None
- case None => None
+ Left(false)
+ case None => Left(false)
/** Discards the line that is currently being read and restarts reading */
def startReading(): Unit =
diff --git a/src/scalevalapokalypsi/UI/main.scala b/src/scalevalapokalypsi/UI/main.scala
index 7368803..e172f24 100644
--- a/src/scalevalapokalypsi/UI/main.scala
+++ b/src/scalevalapokalypsi/UI/main.scala
@@ -121,12 +121,13 @@ def startClient(client: Client): Unit =
while !hasQuit do
sleep(POLL_INTERVAL)
val line = stdinReader.newLine()
- if line.map(_.length).getOrElse(0) > 1024 then
+ if line.toOption.map(_.length).getOrElse(0) > 1024 then
Printer.printLn("Virhe: Syötteesi oli liian pitkä.")
- else if line == Some("quit") then
+ else if line == Right("quit") || line == Left(true) then
hasQuit = true
+ println("Poistut pelistä.")
else
- val gameEvent = client.clientStep(line)
+ val gameEvent = client.clientStep(line.toOption)
Printer.printGameEvent(gameEvent)
if !gameEvent.gameIsOn then
hasQuit = true
diff --git a/src/scalevalapokalypsi/utils/utils.scala b/src/scalevalapokalypsi/utils/utils.scala
index 54d407b..4be0687 100644
--- a/src/scalevalapokalypsi/utils/utils.scala
+++ b/src/scalevalapokalypsi/utils/utils.scala
@@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets
import scala.util.Try
import scalevalapokalypsi.constants.*
import scala.io.StdIn.readLine
+import scala.math.{min,abs}
/** Converts this string to an array of bytes (probably for transmission).
*
@@ -24,6 +25,12 @@ def byteArrayToString(bytes: Array[Byte]): Option[String] =
Try(String(bytes, StandardCharsets.UTF_8))
.toOption
+/** Incredibly LÖRS string metric algorithm.
+ * LÖRSiness explained by there being 6 hours left to the DL
+ */
+def hammingDistance(s1: String, s2: String): Float =
+ s1.zip(s2).map(p => p(0) != p(1)).filter((b: Boolean) => b).length
+
/** Reads n characters from the given InputStream blockingly.
*
* @param input the InputStream to read from