diff options
author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-26 14:03:23 +0200 |
---|---|---|
committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-26 14:10:45 +0200 |
commit | 38900e0b291d5e0f59afaaa239cd237f733b6588 (patch) | |
tree | ee04f697ab17a75c9563ee87763cbcdcde8d297b | |
parent | 27dd937617cce1e43df1c16e12050f6e88763d54 (diff) | |
download | scalevalapokalypsi-38900e0b291d5e0f59afaaa239cd237f733b6588.tar.gz scalevalapokalypsi-38900e0b291d5e0f59afaaa239cd237f733b6588.zip |
Dying properly
-rw-r--r-- | protocol.txt | 2 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Client/Client.scala | 9 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Client/GameEvent.scala | 3 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Action.scala | 11 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Adventure.scala | 25 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/Entity.scala | 19 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala | 8 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/Player.scala | 17 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/SingEffects.scala | 4 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Server/Server.scala | 13 | ||||
-rw-r--r-- | src/scalevalapokalypsi/UI/main.scala | 5 | ||||
-rw-r--r-- | src/scalevalapokalypsi/constants/constants.scala | 3 |
12 files changed, 90 insertions, 29 deletions
diff --git a/protocol.txt b/protocol.txt index f38e778..b0166ec 100644 --- a/protocol.txt +++ b/protocol.txt @@ -16,3 +16,5 @@ Server: [turn indicator]CRLF [Entities separated with semicolon]CRLF When running turn: [CRLF-separated list of things happening in the players room] + +At end of game: [GAME_END_INDICATOR]
\ No newline at end of file diff --git a/src/scalevalapokalypsi/Client/Client.scala b/src/scalevalapokalypsi/Client/Client.scala index e94fdb0..5364405 100644 --- a/src/scalevalapokalypsi/Client/Client.scala +++ b/src/scalevalapokalypsi/Client/Client.scala @@ -76,6 +76,7 @@ class Client(socket: Socket): "don't initialize with unexecuted turn" ) private val turnInfo = RoomState() + private var gameOver = false /** Takes a client step and optionally returns an in-game event for UI * @@ -108,7 +109,8 @@ class Client(socket: Socket): roomState, this.lineToSing, this.canAct, - Some(timeToTurnEnd).filter(p => this.lastTurnStart != 0) + Some(timeToTurnEnd).filter(p => this.lastTurnStart != 0), + !this.gameOver ) end clientStep @@ -157,6 +159,9 @@ class Client(socket: Socket): if line == TURN_INDICATOR then this.serverLineState = ServerLineState.TurnIndicator + if line == GAME_END_INDICATOR then + this.gameOver = true + serverLineState match case ServerLineState.WaitingForTimeLimit => @@ -185,7 +190,7 @@ class Client(socket: Socket): case ServerLineState.Directions => this.turnInfo.possibleDirections = line.split(LIST_SEPARATOR) - this.serverLineState = ServerLineState.Items // TODO: maybe use a list instead? + this.serverLineState = ServerLineState.Items case ServerLineState.Items => this.turnInfo.visibleItems = line.split(LIST_SEPARATOR) diff --git a/src/scalevalapokalypsi/Client/GameEvent.scala b/src/scalevalapokalypsi/Client/GameEvent.scala index 8aa1e1c..0397b48 100644 --- a/src/scalevalapokalypsi/Client/GameEvent.scala +++ b/src/scalevalapokalypsi/Client/GameEvent.scala @@ -5,7 +5,8 @@ class GameEvent( val roomState: Option[RoomState], val lineToSing: Option[String], val playerCanAct: Boolean, - val timeToNextTurn: Option[Long] + val timeToNextTurn: Option[Long], + val gameIsOn: Boolean ) diff --git a/src/scalevalapokalypsi/Model/Action.scala b/src/scalevalapokalypsi/Model/Action.scala index c7c8a65..21e1286 100644 --- a/src/scalevalapokalypsi/Model/Action.scala +++ b/src/scalevalapokalypsi/Model/Action.scala @@ -40,15 +40,14 @@ class Action(input: String): case "inventory" => Some((false, actor.inventory)) case "sano" => val entityNames = actor.location.getEntityNames.map(_.toLowerCase) - val recipientNamePair = entityNames.map(name => + val recipientNamePair = entityNames.flatMap(name => val possibleNamesWithSuffix = (0 to "ille".length).map(i => - modifiers.takeRight(name.length + i) + modifiers.takeRight(name.length + i) ) possibleNamesWithSuffix.find(s => - s.take(name.length) == name - ) - .map(_.splitAt(name.length)) - ).flatten.headOption + s.take(name.length) == name + ) + .map(_.splitAt(name.length))).headOption val recipient = recipientNamePair.flatMap(p => actor.location.getEntity(p(0)) diff --git a/src/scalevalapokalypsi/Model/Adventure.scala b/src/scalevalapokalypsi/Model/Adventure.scala index ba45abe..b10f7d9 100644 --- a/src/scalevalapokalypsi/Model/Adventure.scala +++ b/src/scalevalapokalypsi/Model/Adventure.scala @@ -46,7 +46,7 @@ class Adventure(val playerNames: Vector[String]): ("Rotten zombie", tangle, 10) ) zombieAttrs.foreach(z => - val zombie = Zombie(z(0), z(1), z(2)) + val zombie = Zombie(this, z(0), z(1), z(2)) npcs += z(0) -> zombie z(1).addEntity(zombie) ) @@ -54,7 +54,7 @@ class Adventure(val playerNames: Vector[String]): def takeNpcTurns(): Unit = npcs.values.foreach(_.act()) - private val gruu = Entity("Gruu", northForest) + private val gruu = Entity(this, "Gruu", northForest) northForest.addEntity(gruu) this.entities += gruu.name -> gruu @@ -67,12 +67,29 @@ class Adventure(val playerNames: Vector[String]): * @return the created player entity */ def addPlayer(name: String): Player = - val newPlayer = Player(name, middle) + val newPlayer = Player(this, name, middle) middle.addEntity(newPlayer) this.entities += name -> newPlayer players += name -> newPlayer newPlayer - + + /** Removes the given entity without further observations. Makes sense in the + * game mostly if the entity's HP is nonpositive. + * + * Removes the entity both from the adventure and the game world + * (i.e. the entitys area). + * + * @param name the name of the entity to remove + * @return whether there was an entity to remove with the given name + */ + def removeEntity(name: String): Boolean = + this.players.remove(name) + this.entities.remove(name).orElse(this.npcs.remove(name)) match + case Some(e) => + e.location.removeEntity(name) + true + case None => false + /** Gets the player entity with the specified name. * * @param name name of the player to find diff --git a/src/scalevalapokalypsi/Model/Entities/Entity.scala b/src/scalevalapokalypsi/Model/Entities/Entity.scala index 336a6b1..aa2a2e2 100644 --- a/src/scalevalapokalypsi/Model/Entities/Entity.scala +++ b/src/scalevalapokalypsi/Model/Entities/Entity.scala @@ -12,6 +12,7 @@ import scala.collection.immutable * @param initialLocation the Area where the entity is instantiated */ class Entity( + val adventure: Adventure, val name: String, initialLocation: Area, initialHP: Int = 100, @@ -31,10 +32,24 @@ class Entity( */ def getVerseAgainst: String = "Esimerkkirivi laulettavaksi" + def isAlive = this.hp > 0 + def takeDamage(amount: Int): Unit = hp -= amount - if hp < 0 then - println("Voi ei, kuolin!") + val event = if this.isAlive then + Event( + Vector(this -> this.condition(0)).toMap, + this.condition(1) + ) + else + println(s"Could remove myself: ${this.adventure.removeEntity(this.name)}") + Event( + Vector(this -> + "Olet täysin menettänyt toimintakykysi. Kaadut elottomana maahan." + ).toMap, + s"${this.name} kaatuu elottomana maahan." + ) + this.location.observeEvent(event) /** Returns a description of the physical condition of this entity, * i.e. the damage it has taken. diff --git a/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala b/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala index 21709ba..944f2e6 100644 --- a/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala +++ b/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala @@ -17,19 +17,21 @@ import scala.util.Random * @param initialLocation the NPC’s initial location */ abstract class NPC( + adventure: Adventure, name: String, initialLocation: Area, initialHP: Int, maxHp: Int -) extends Entity(name, initialLocation, initialHP, maxHp): +) 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(identifier, initialLocation, initialHP, 20): +) extends NPC(adventure, identifier, initialLocation, initialHP, 20): private val damage = 10 private val dialogs = Vector( @@ -60,7 +62,7 @@ class Zombie( .toVector .lift(directionIndex) .flatMap(this.go(_)) - .map(this.location.observeEvent(_)) + .foreach(this.location.observeEvent(_)) else this.location.observeEvent( this.attack(possibleVictims(index)) diff --git a/src/scalevalapokalypsi/Model/Entities/Player.scala b/src/scalevalapokalypsi/Model/Entities/Player.scala index d6b3529..9fc929d 100644 --- a/src/scalevalapokalypsi/Model/Entities/Player.scala +++ b/src/scalevalapokalypsi/Model/Entities/Player.scala @@ -12,7 +12,11 @@ import scalevalapokalypsi.Model.* * @param name the player's name * @param initialLocation the player’s initial location */ -class Player(name: String, initialLocation: Area) extends Entity(name, initialLocation): +class Player( + adventure: Adventure, + name: String, + initialLocation: Area +) extends Entity(adventure, name, initialLocation): private val observations: Buffer[String] = Buffer.empty private val observedEvents: Buffer[Event] = Buffer.empty @@ -49,8 +53,6 @@ class Player(name: String, initialLocation: Area) extends Entity(name, initialLo * @param singQuality the quality of the song */ def applySingEffect(singQuality: Float): Unit = - val res = this.pendingSingEffect.map(ef => ef(singQuality)) - this.pendingSingEffect = None val qualityDescriptions = if singQuality < .10 then ("säälittävää", "epsilonin suuruinen") @@ -63,11 +65,10 @@ class Player(name: String, initialLocation: Area) extends Entity(name, initialLo else ("erinomaista", "merkittävä") val quality = s"Laulu on ${qualityDescriptions(0)} ja sen vaikutus on ${qualityDescriptions(1)}." - val event = res.map(ev => Event( - ev.inFirstPersons.map((k, v) => (k, s"$quality\n$v")), - s"$quality\n${ev.inThirdPerson}" - )) - event.foreach(this.location.observeEvent(_)) + val event = Event(Map.empty, s"$quality") + this.location.observeEvent(event) + this.pendingSingEffect.map(ef => ef(singQuality)) + this.pendingSingEffect = None diff --git a/src/scalevalapokalypsi/Model/SingEffects.scala b/src/scalevalapokalypsi/Model/SingEffects.scala index 42f5188..23b7d37 100644 --- a/src/scalevalapokalypsi/Model/SingEffects.scala +++ b/src/scalevalapokalypsi/Model/SingEffects.scala @@ -10,4 +10,6 @@ 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.from(Vector((target, condition(0)))), condition(1)) + Event(Map.empty, "") // The conditions are automatically shown to + // clients through takeDamage, but other effects + // should explain the changes they have. diff --git a/src/scalevalapokalypsi/Server/Server.scala b/src/scalevalapokalypsi/Server/Server.scala index 2ea8bd4..16a2128 100644 --- a/src/scalevalapokalypsi/Server/Server.scala +++ b/src/scalevalapokalypsi/Server/Server.scala @@ -54,6 +54,7 @@ class Server( this.writeClientDataToClients() this.makeClientsSing() this.writeObservations() + this.endGameForDeadClients() if this.canExecuteTurns then this.clients.foreach(_.giveTurn()) this.adventure.foreach(_.takeNpcTurns()) @@ -183,6 +184,18 @@ class Server( output.flush() ) + private def endGameForDeadClients(): Unit = + this.clients.removeNonSatisfying(c => + c.player.forall((p: Player) => + if !p.isAlive then + this.writeToClient(s"$GAME_END_INDICATOR\r\n", c) + false + else + true + ) + ) + + private def writeToClient(message: String, client: Client): Unit = try { val output = client.socket.getOutputStream diff --git a/src/scalevalapokalypsi/UI/main.scala b/src/scalevalapokalypsi/UI/main.scala index 44ca0e4..7368803 100644 --- a/src/scalevalapokalypsi/UI/main.scala +++ b/src/scalevalapokalypsi/UI/main.scala @@ -106,7 +106,7 @@ import scala.io.StdIn.readLine startClient(client) case None => println( - "Serverille liittyminen epäonnistui. Tarkista internet-yhteytesi. Jos yhteytesi on kunnossa ja liittyminen ei pian onnistu, ota yhteyttä Joel Kronqvistiin <joel.kronqvist@iki.fi> olettaen, ettei vuodesta 2024 ole kulunut kohtuuttomasti aikaa." + "Palvelimelle liittyminen epäonnistui. Tarkista internet-yhteytesi. Jos yhteytesi on kunnossa ja liittyminen ei pian onnistu, ota yhteyttä Joel Kronqvistiin <joel.kronqvist@iki.fi> olettaen, ettei vuodesta 2024 ole kulunut kohtuuttomasti aikaa." ) @@ -128,5 +128,8 @@ def startClient(client: Client): Unit = else val gameEvent = client.clientStep(line) Printer.printGameEvent(gameEvent) + if !gameEvent.gameIsOn then + hasQuit = true + println("Peli on pelattu.") diff --git a/src/scalevalapokalypsi/constants/constants.scala b/src/scalevalapokalypsi/constants/constants.scala index 970d6f7..159a153 100644 --- a/src/scalevalapokalypsi/constants/constants.scala +++ b/src/scalevalapokalypsi/constants/constants.scala @@ -4,9 +4,10 @@ package scalevalapokalypsi.constants val MAX_MSG_SIZE = 1024 // bytes val CRLF: Vector[Byte] = Vector(13.toByte, 10.toByte) val POLL_INTERVAL = 100 // millisec. -val GAME_VERSION = "0.1.0" +val GAME_VERSION = "0.2.0" val TURN_INDICATOR = ">" val SING_INDICATOR = "~" +val GAME_END_INDICATOR = "!" val ACTION_BLOCKING_INDICATOR='.' val ACTION_NONBLOCKING_INDICATOR='+' val INITIAL_CONN_TIMEOUT = 5000 // millisec. |