aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/scala')
-rw-r--r--src/main/scala/Client/Client.scala178
-rw-r--r--src/main/scala/Client/ReceivedLineParser.scala27
-rw-r--r--src/main/scala/Client/StdinLineReader.scala31
-rw-r--r--src/main/scala/Client/Turn.scala32
-rw-r--r--src/main/scala/Model/Action.scala69
-rw-r--r--src/main/scala/Model/Adventure.scala79
-rw-r--r--src/main/scala/Model/Area.scala117
-rw-r--r--src/main/scala/Model/Entity.scala103
-rw-r--r--src/main/scala/Model/Item.scala20
-rw-r--r--src/main/scala/Server/Client.scala173
-rw-r--r--src/main/scala/Server/Clients.scala82
-rw-r--r--src/main/scala/Server/ConnectionGetter.scala25
-rw-r--r--src/main/scala/Server/Server.scala195
-rw-r--r--src/main/scala/Server/constants.scala19
-rw-r--r--src/main/scala/main.scala26
-rw-r--r--src/main/scala/utils.scala28
16 files changed, 0 insertions, 1204 deletions
diff --git a/src/main/scala/Client/Client.scala b/src/main/scala/Client/Client.scala
deleted file mode 100644
index fc3e6b8..0000000
--- a/src/main/scala/Client/Client.scala
+++ /dev/null
@@ -1,178 +0,0 @@
-package o1game.Client
-
-import java.lang.Thread.sleep
-import java.net.Socket
-import scala.io.Source
-import scala.sys.process.stdout
-import o1game.constants.*
-import o1game.utils.{stringToByteArray,getNCharsFromSocket}
-import o1game.Client.{ReceivedLineParser,StdinLineReader,Turn}
-import scala.concurrent.Future
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.util.{Try, Success, Failure}
-import scala.collection.mutable.Buffer
-import java.lang.System.currentTimeMillis
-
-
-/** A helper enum for `Client` to keep track of communications with the server
- */
-enum ServerLineState:
- case WaitingForTimeLimit,
- ActionDescription,
- TurnIndicator,
- AreaDescription,
- Directions,
- Items,
- Entities
-
-
-/** Creates a new client.
- *
- * @param name the name the client and its player should have
- * @ip the ip of the server to connect to
- * @port the port of the server to connect to
- * @return the client created, if all was successful
- */
-def newClient(name: String, ip: String, port: Int): Option[Client] =
- val socket = Socket(ip, port)
- val output = socket.getOutputStream
- val input = socket.getInputStream
- val initMsg = s"$GAME_VERSION\r\n$name\r\n"
- output.write(stringToByteArray(initMsg))
- val msgLen = (PROTOCOL_VERSION_GOOD + "\r\n").length
- val versionResponse = getNCharsFromSocket(input, msgLen)
- if versionResponse == Some(s"$PROTOCOL_VERSION_GOOD\r\n") then
- Some(Client(socket))
- else
- None
-
-
-
-/** Main class for the client: handles communication with the server
- * and the player. Should be initialized with `newClient`.
- *
- * @param socket the socket the client uses
- */
-class Client(socket: Socket):
-
- /** Essential IO variables */
- private val input = socket.getInputStream
- private val output = socket.getOutputStream
- private val buffer: Array[Byte] = Array.ofDim(MAX_MSG_SIZE)
- private var bufferIndex = 0
- private val serverLineParser = ReceivedLineParser()
- private val stdinReader = StdinLineReader()
-
- private var serverLineState = ServerLineState.WaitingForTimeLimit
-
- /** Variables about the status of the current turn for the client */
- private var canAct = false
- private var timeLimit: Long = 0
- private var lastTurnStart: Long = 0
- private var lastExecutedTurn: Long = 0
- assert(
- lastTurnStart <= lastExecutedTurn,
- "don't initialize with unexecuted turn"
- )
- private val turnInfo = Turn()
-
-
- /** Starts the client. This shouldn't terminate. */
- def startClient(): Unit =
-
- stdinReader.startReading()
-
- while true do
- sleep(POLL_INTERVAL)
-
- this.readAndParseDataFromServer()
-
- if this.lastExecutedTurn < this.lastTurnStart then
- print(this.giveTurn())
-
- stdinReader.newLine().foreach((s: String) =>
- output.write(stringToByteArray(s+"\r\n"))
- )
-
- end startClient
-
-
- private def readAndParseDataFromServer(): Unit =
- var availableBytes = input.available()
- while availableBytes != 0 do
- val bytesRead = input.read(buffer, 0, availableBytes)
- if bytesRead != -1 then
- // TODO: unsafe conversion
- parseDataFromServer(buffer.take(bytesRead))
- availableBytes = input.available()
-
- private def giveTurn(): String =
- this.canAct = true
- this.lastExecutedTurn = currentTimeMillis / 1000
- s"\n\n${this.turnInfo}\n${this.actionGetterIndicator}"
-
- private def displayAction(action: String): Unit =
- println(s"$action")
- if this.canAct then
- print(this.actionGetterIndicator)
-
- private def actionGetterIndicator =
- val timeOfTurnEnd = this.lastTurnStart + this.timeLimit
- val timeToTurnEnd = -currentTimeMillis()/1000 + timeOfTurnEnd
- s"[$timeToTurnEnd]> "
-
-
-
- private def parseDataFromServer(data: Array[Byte]): Unit =
- this.serverLineParser.in(data)
- var nextLine: Option[String] = Some("")
- while nextLine.isDefined do
- nextLine = this.serverLineParser
- .nextLine()
- nextLine
- .foreach(this.parseLineFromServer(_))
-
-
- private def parseLineFromServer(line: String) =
-
- if line == TURN_INDICATOR then
- this.serverLineState = ServerLineState.TurnIndicator
-
- serverLineState match
-
- case ServerLineState.WaitingForTimeLimit =>
- val time = line.toLongOption
- time match
- case Some(t) => this.timeLimit = t
- case None => print("Invalid time limit, oh no!!!")
- this.serverLineState = ServerLineState.TurnIndicator
- this.lastTurnStart = currentTimeMillis / 1000
-
- case ServerLineState.ActionDescription =>
- if line.nonEmpty && line.head == ACTION_BLOCKING_INDICATOR then
- this.canAct = false
- this.displayAction(line.tail)
-
- case ServerLineState.TurnIndicator =>
- this.serverLineState = ServerLineState.AreaDescription
-
- case ServerLineState.AreaDescription =>
- this.turnInfo.areaDescription = line
- this.serverLineState = ServerLineState.Directions
-
- case ServerLineState.Directions =>
- this.turnInfo.possibleDirections = line.split(LIST_SEPARATOR)
- this.serverLineState = ServerLineState.Items // TODO: maybe use a list instead?
-
- case ServerLineState.Items =>
- this.turnInfo.visibleItems = line.split(LIST_SEPARATOR)
- this.serverLineState = ServerLineState.Entities
-
- case ServerLineState.Entities =>
- this.turnInfo.visibleEntities = line.split(LIST_SEPARATOR)
- this.serverLineState = ServerLineState.ActionDescription
- this.lastTurnStart = currentTimeMillis() / 1000
-
- end parseLineFromServer
-
-end Client
diff --git a/src/main/scala/Client/ReceivedLineParser.scala b/src/main/scala/Client/ReceivedLineParser.scala
deleted file mode 100644
index 7cbf935..0000000
--- a/src/main/scala/Client/ReceivedLineParser.scala
+++ /dev/null
@@ -1,27 +0,0 @@
-package o1game.Client
-
-import scala.collection.mutable.Buffer
-import o1game.constants.*
-
-/** A class for checking asynchronously for received lines */
-class ReceivedLineParser:
-
- private var serverLineState = ServerLineState.ActionDescription
-
- private var bufferedData: Buffer[Byte] = Buffer.empty // TODO: suboptimal DS
-
- /** Add received data */
- def in(data: Array[Byte]): Unit =
- this.bufferedData ++= data
-
- /** Read a line from the received data */
- def nextLine(): Option[String] =
- val indexOfCRLF = this.bufferedData.indexOfSlice(CRLF)
- if indexOfCRLF == -1 then
- None
- else
- val splitData = this.bufferedData.splitAt(indexOfCRLF)
- this.bufferedData = Buffer.from(splitData(1).drop(CRLF.length))
- Some(String(splitData(0).toArray))
-
-end ReceivedLineParser
diff --git a/src/main/scala/Client/StdinLineReader.scala b/src/main/scala/Client/StdinLineReader.scala
deleted file mode 100644
index 42a1f40..0000000
--- a/src/main/scala/Client/StdinLineReader.scala
+++ /dev/null
@@ -1,31 +0,0 @@
-package o1game.Client
-
-import scala.concurrent.Future
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.io.StdIn.readLine
-import scala.util.{Try, Success, Failure}
-
-/** This class is for taking new lines from stdin when they are available.
- * reading starts when either newLine or clear or startReading are called.
- */
-class StdinLineReader:
-
- private var nextLine: Future[String] = Future.failed(Exception())
-
- /** Returns a new line of input if there are any. */
- def newLine(): Option[String] =
- this.nextLine.value match
- case Some(Success(s)) =>
- this.startReading()
- Some(s)
- case Some(Failure(e)) =>
- this.startReading()
- None
- case None => None
-
- /** Discards the line that is currently being read and restarts reading */
- def startReading(): Unit =
- this.nextLine = Future(readLine())
-
-
-end StdinLineReader
diff --git a/src/main/scala/Client/Turn.scala b/src/main/scala/Client/Turn.scala
deleted file mode 100644
index 6b78811..0000000
--- a/src/main/scala/Client/Turn.scala
+++ /dev/null
@@ -1,32 +0,0 @@
-package o1game.Client
-
-/** `Turn`s represent information the client has got about a turn.
- * This class exists essentially so that the client has somewhere
- * to store data about turns and something to format that data with.
- */
-class Turn:
-
- /** Description of the area the player controlled by the client is in
- * at the end of the turn. */
- var areaDescription: String = ""
-
- /** Directions the player controlled by the client can go to. */
- var possibleDirections: Array[String] = Array.empty
-
- /** Items the player controlled by the client can see. */
- var visibleItems: Array[String] = Array.empty
-
- /** Entities the player controlled by the client can see. */
- 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")
-
-end Turn
diff --git a/src/main/scala/Model/Action.scala b/src/main/scala/Model/Action.scala
deleted file mode 100644
index 55f7f27..0000000
--- a/src/main/scala/Model/Action.scala
+++ /dev/null
@@ -1,69 +0,0 @@
-package o1game.Model
-
-/** The class `Action` represents actions that a player may take in a text adventure game.
- * `Action` objects are constructed on the basis of textual commands and are, in effect,
- * parsers for such commands. An action object is immutable after creation.
- * @param input a textual in-game command such as “go east” or “rest” */
-class Action(input: String):
-
- private val commandText = input.trim.toLowerCase
- 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 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. */
- def execute(actor: Player): Option[String] =
- 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 to = "to"
- val recipient = modifiers.reverse.takeWhile(_ != ' ').reverse
- val recipientEntity = actor.location.getEntity(recipient)
- val maybeTo = modifiers.slice(
- modifiers.length - recipient.length - s"$to ".length,
- modifiers.length - recipient.length - 1
- )
- val message =
- modifiers.take(modifiers.length - recipient.length - 4)
- if maybeTo == to then
- recipientEntity.map(actor.sayTo(_, message))
- else
- Some(actor.say(modifiers))
- case "drop" => Some(actor.drop(this.modifiers))
- case "xyzzy" => Some((
- "The grue tastes yummy.",
- s"${actor.name} tastes some grue.")
- )
- case other => None
-
-// println(resOption)
-// println(actor.location.getEntities)
- resOption.map(_(1)).filter(_.length > 0)
- .foreach(s =>
- actor.location.getEntities.filter(_ != actor).foreach(_.observe(s))
- if oldLocation != actor.location then
- oldLocation.getEntities.foreach(_.observe(s))
- )
-
- resOption.map(_(0))
-
-
- /** Returns a textual description of the action object, for debugging purposes. */
- override def toString = s"$verb (modifiers: $modifiers)"
-
-end Action
-
diff --git a/src/main/scala/Model/Adventure.scala b/src/main/scala/Model/Adventure.scala
deleted file mode 100644
index dfcb100..0000000
--- a/src/main/scala/Model/Adventure.scala
+++ /dev/null
@@ -1,79 +0,0 @@
-package o1game.Model
-
-import scala.collection.mutable.Map
-
-/** The class `Adventure` represents text adventure games. An adventure consists of a player and
- * a number of areas that make up the game world. It provides methods for playing the game one
- * turn at a time and for checking the state of the game.
- *
- * N.B. This version of the class has a lot of “hard-coded” information that pertains to a very
- * specific adventure game that involves a small trip through a twisted forest. All newly created
- * instances of class `Adventure` are identical to each other. To create other kinds of adventure
- * games, you will need to modify or replace the source code of this class. */
-class Adventure(val playerNames: Vector[String]):
-
- private val middle = Area("Forest", "You are somewhere in the forest. There are a lot of trees here.\nBirds are singing.")
- private val northForest = Area("Forest", "You are somewhere in the forest. A tangle of bushes blocks further passage north.\nBirds are singing.")
- 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.")
- private val destination = home
-
- 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."))
- southForest.addItem(Item(
- "remote",
- "It's the remote control for your TV.\n" +
- "What it was doing in the forest, you have no idea.\n" +
- "Problem is, there's no battery."
- ))
-
- val players: Map[String, Player] = Map()
- playerNames.foreach(this.addPlayer(_))
-
- val entities: Map[String, Entity] = Map()
- private val gruu = Entity("Gruu", northForest)
- northForest.addEntity(gruu)
- this.entities += gruu.name -> gruu
-
- /** Adds a player entity with the specified name to the game.
- *
- * @param name the name of the player entity to add
- * @return the created player entity
- */
- def addPlayer(name: String): Player =
- val newPlayer = Player(name, middle)
- middle.addEntity(newPlayer)
- players += name -> newPlayer
- newPlayer
-
- /** Gets the player entity with the specified name.
- *
- * @param name name of the player to find
- * @return the player, if one with the name was found
- */
- def getPlayer(name: String): Option[Player] = this.players.get(name)
-
- 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/main/scala/Model/Area.scala b/src/main/scala/Model/Area.scala
deleted file mode 100644
index 6721957..0000000
--- a/src/main/scala/Model/Area.scala
+++ /dev/null
@@ -1,117 +0,0 @@
-package o1game.Model
-
-import scala.collection.mutable.Map
-
-/** The class `Area` represents locations in a text adventure game world. A game world
- * consists of areas. In general, an “area” can be pretty much anything: a room, a building,
- * an acre of forest, or something completely different. What different areas have in
- * common is that players can be located in them and that they can have exits leading to
- * other, neighboring areas. An area also has a name and a description.
- * @param name the name of the area
- * @param description a basic description of the area (typically not including information about items) */
-class Area(val name: String, var description: String):
-
- private val neighbors = Map[String, Area]()
- private val items: Map[String, Item] = Map()
- private val entities: Map[String, Entity] = Map()
-
- /** Returns the area that can be reached from this area by moving in the given direction. The result
- * is returned in an `Option`; `None` is returned if there is no exit in the given direction. */
- def neighbor(direction: String): Option[Area] =
- this.neighbors.get(direction)
-
- def getNeighborNames: Iterable[String] = this.neighbors.keys
- def getItemNames: Iterable[String] = this.items.keys
- def getEntityNames: Iterable[String] = this.entities.values.map(_.name)
- def getEntity(name: String): Option[Entity] = this.entities.get(name)
- def getEntities: Iterable[Entity] = this.entities.values
-
- /** Tells whether this area has a neighbor in the given direction.
- *
- * @param direction the direction to check
- * @return whether there is a neighbor in the direction
- */
- def hasNeighbor(direction: String): Boolean =
- this.neighbors.contains(direction)
-
- /** Adds an exit from this area to the given area. The neighboring area is reached by moving in
- * the specified direction from this area. */
- def setNeighbor(direction: String, neighbor: Area) =
- this.neighbors += direction -> neighbor
-
- /** Adds exits from this area to the given areas. Calling this method is equivalent to calling
- * the `setNeighbor` method on each of the given direction–area pairs.
- * @param exits contains pairs consisting of a direction and the neighboring area in that direction
- * @see [[setNeighbor]] */
- def setNeighbors(exits: Vector[(String, Area)]) =
- this.neighbors ++= exits
-
- /** Adds the specified item
- *
- * @param item the item to add
- */
- def addItem(item: Item): Unit = this.items += item.name -> item
-
- /** Adds multiple items
- *
- * @param items a once iterable collection of items to add
- */
- def addItems(items: IterableOnce[Item]) =
- items.iterator.foreach(i => this.items += i.name -> i)
-
- def hasItem(itemName: String) = this.items.contains(itemName)
-
-
- /** Removes the specified item if it exists.
- *
- * @param itemName the name of the item to remove
- * @return an option containing the removed item
- */
- def removeItem(itemName: String): Option[Item] =
- this.items.remove(itemName)
-
- /** Adds the specified entity to the area.
- *
- * @param entity the entity to add.
- */
- def addEntity(entity: Entity): Unit =
- this.entities += entity.name.toLowerCase -> entity
-
- /** Removes the entity with the name `entityName`.
- *
- * @param entityName the name of the entity to remove
- * @return an option containing the removed entity if it was in the area
- */
- def removeEntity(entityName: String): Option[Entity] =
- this.entities.remove(entityName.toLowerCase())
-
- /** Returns a multi-line description of the area as a player sees it. This includes a basic
- * description of the area as well as information about exits and items. If there are no
- * items present, the return value has the form "DESCRIPTION\n\nExits available:
- * DIRECTIONS SEPARATED BY SPACES". If there are one or more items present, the return
- * value has the form "DESCRIPTION\nYou see here: ITEMS SEPARATED BY SPACES\n\nExits available:
- * DIRECTIONS SEPARATED BY SPACES". The items and directions are listed in an arbitrary order. */
- def fullDescription: String =
- val exitList = this.neighbors.keys.mkString(" ")
- val itemList = this.items.keys.mkString(" ")
- val entityList = this.getEntityNames.mkString(" ")
- val itemDescription =
- if this.items.nonEmpty then
- s"\nYou see here: ${itemList}"
- else ""
- val entityDescription =
- if this.entities.nonEmpty then
- s"\nThere are entities: ${entityList}"
- else ""
- (this.description +
- itemDescription +
- entityDescription +
- s"\n\nExits available: $exitList")
-
-
- /** Returns a single-line description of the area for debugging purposes. */
- override def toString =
- this.name + ": " + this.description.replaceAll("\n", " ").take(150)
-
-end Area
-
diff --git a/src/main/scala/Model/Entity.scala b/src/main/scala/Model/Entity.scala
deleted file mode 100644
index d8e8559..0000000
--- a/src/main/scala/Model/Entity.scala
+++ /dev/null
@@ -1,103 +0,0 @@
-package o1game.Model
-
-import scala.collection.mutable.{Buffer,Map}
-
-
-
-/** A `Player` object represents a player character controlled by one real-life player
- * of the program.
- *
- * A player object’s state is mutable: the player’s location and possessions can change,
- * for instance.
- *
- * @param startingArea the player’s initial location */
-class Player(name: String, initialLocation: Area) extends Entity(name, initialLocation):
-
- private val observations: Buffer[String] = Buffer.empty
-
- override def observe(observation: String): Unit =
- this.observations.append(observation)
-
- def readAndClearObservations(): Vector[String] =
- val res = this.observations.toVector
- observations.clear()
- res
-
-end Player
-
-/** An in-game entity.
- *
- * @param name the name of the entity
- * @param initialLocation the Area where the entity is instantiated
- */
-class Entity(val name: String, initialLocation: Area):
- private var currentLocation: Area = initialLocation
- private var quitCommandGiven = false // one-way flag
- private val inventory: Map[String, Item] = Map()
-
- /** Determines if the player has indicated a desire to quit the game. */
- def hasQuit = this.quitCommandGiven // TODO: This is probably unneccessary?
-
- /** Does nothing, except possibly in inherited classes. */
- def observe(observation: String): Unit =
- println("no observation made.")
- ()
-
- /** Returns the player’s current location. */
- def location = this.currentLocation
-
- /** Attempts to move the player in the given direction. This is successful if there
- * is an exit from the player’s current location towards the direction name. Returns
- * a description of the result: "You go DIRECTION." or "You can't go DIRECTION." */
- def go(direction: String): (String, String) =
- val destination = this.location.neighbor(direction)
- 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) =
- 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.", "WHAAAT THIS SHOULDN'T HAPPEN???")
-
- def drop(itemName: String): (String, String) =
- 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!", s"$name reaches their backpack to drop $itemName but miserably fails to find it there.")
-
- def sayTo(entity: Entity, message: String): (String, String) =
- entity.observe(s"Alice: \"$message\"")
- (s"You say so to ${entity.name}.", "")
-
- def say(message: String): (String, String) =
- ("You say that aloud.", s"$name: \"$message\"")
-
- /** Tells whether this entity can drop the specified item
- * (if an action were to specify so).
- *
- * @param itemName the name to check
- * @return whether this entity has this item and can drop it
- */
- def canDrop(itemName: String): Boolean = this.inventory.contains(itemName)
-
- /** Causes the player to rest for a short while (this has no substantial effect in game terms).
- * 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 = "Now at: " + this.location.name
-
-end Entity
diff --git a/src/main/scala/Model/Item.scala b/src/main/scala/Model/Item.scala
deleted file mode 100644
index 229828d..0000000
--- a/src/main/scala/Model/Item.scala
+++ /dev/null
@@ -1,20 +0,0 @@
-package o1game.Model
-
-import scala.annotation.targetName
-
-/** The class `Item` represents items in a text adventure game. Each item has a name
- * and a longer description. (In later versions of the adventure game, items may
- * have other features as well.)
- *
- * N.B. It is assumed, but not enforced by this class, that items have unique names.
- * That is, no two items in a game world have the same name.
- *
- * @param name the item’s name
- * @param description the item’s description */
-class Item(val name: String, val description: String):
-
- /** Returns a short textual representation of the item (its name, that is). */
- override def toString = this.name
-
-end Item
-
diff --git a/src/main/scala/Server/Client.scala b/src/main/scala/Server/Client.scala
deleted file mode 100644
index 3cd2b36..0000000
--- a/src/main/scala/Server/Client.scala
+++ /dev/null
@@ -1,173 +0,0 @@
-package o1game.Server
-
-import java.net.Socket
-import scala.math.min
-import o1game.constants.*
-import ServerProtocolState.*
-import o1game.Model.{Action,Player,Entity}
-
-class Client(val socket: Socket):
- private var incompleteMessage: Array[Byte] =
- Array.fill(MAX_MSG_SIZE)(0.toByte)
- private var incompleteMessageIndex = 0
- private var protocolState = WaitingForVersion
- private var outData: String = ""
- private var character: Option[Player] = None
- private var protocolIsIntact = true
- private var name: Option[String] = None
- private var nextAction: Option[Action] = None
-
- /** Calculates the amount of bytes available for future incoming messages */
- def spaceAvailable: Int = MAX_MSG_SIZE - incompleteMessageIndex
-
- /** Tests whether the client has behaved according to protocol.
- *
- * @return false if there has been a protocol violation, true otherwise
- */
- def isIntactProtocolWise: Boolean = this.protocolIsIntact
-
- /** Marks that this client misbehaved in eyes of the protocol */
- def failedProtocol(): Unit = this.protocolIsIntact = false
-
- /** Tests whether this client is initialized and ready to start the game
- *
- * @return true if the client is ready to join the game
- */
- def isReadyForGameStart: Boolean =
- this.protocolState == WaitingForGameStart
-
- /** Signals this client that it's joining the game. This is important so
- * that this object knows to update its protocol state.
- */
- def gameStart(): Unit = this.protocolState = InGame
-
- /** Returns the player this client controls in the model.
- *
- * @return an option containing the player
- */
- def player: Option[Player] = this.character
-
- /** Tells this client object that it controls the specified player.
- *
- * @param player the player this client is to control
- */
- def givePlayer(player: Player): Unit =
- this.character = Some(player)
-
- /** Gets the name of this client, which should match the name of the player
- * that is given to this client. Not very useful if the client hasn't yet
- * received the name or if it already has an player.
- *
- * @return the name of this client
- */
- def getName: Option[String] = this.name
-
- /** Sets `data` as received for the client.
- *
- * @return false means there was not enough space to receive the message
- */
- def receiveData(data: Vector[Byte]): Boolean =
- for i <- 0 until min(data.length, spaceAvailable) do
- this.incompleteMessage(this.incompleteMessageIndex + i) = data(i)
- this.incompleteMessageIndex += data.length
- this.incompleteMessageIndex =
- min(this.incompleteMessageIndex, MAX_MSG_SIZE)
- data.length < spaceAvailable
-
- /** Returns data that should be sent to this client.
- * The data is cleared when calling.
- */
- def dataToThisClient(): String =
- val a = this.outData
- this.outData = ""
- a
-
- /** Specifies that the data should be buffered for
- * sending to this client
- *
- * @param data data to buffer for sending
- */
- private def addDataToSend(data: String): Unit =
- this.outData += s"$data\r\n"
-
-
- /** Returns one line of data if there are any line breaks.
- * Removes the parsed data from the message buffering area.
- */
- private def nextLine(): Option[String] =
- var nextCRLF = this.incompleteMessage.indexOf(CRLF(0))
- if this.incompleteMessage(nextCRLF + 1) != CRLF(1) then nextCRLF = -1
- if nextCRLF != -1 then
- val message = this.incompleteMessage.take(nextCRLF)
- val rest = this.incompleteMessage.drop(nextCRLF + 2)
- this.incompleteMessage = rest ++ Array.fill(nextCRLF + 1)(0.toByte)
- // TODO: the conversion may probably be exploited to crash the server
- Some(String(message))
- else
- None
-
- /** Makes the client play its turn */
- def act(): Unit =
- this.addDataToSend(ACTION_BLOCKING_INDICATOR.toString)
- this.nextAction.foreach(a => this.addDataToSend(
- s"$ACTION_BLOCKING_INDICATOR${this.executeAction(a)}"
- ))
- this.nextAction = None
-
- /** Checks whether the client has chosen its next action
- *
- * @return whether the client is ready to act */
- def isReadyToAct: Boolean = this.nextAction.isDefined
-
- /** Causes the client to interpret the data it has received */
- def interpretData(): Unit =
- LazyList.continually(this.nextLine())
- .takeWhile(_.isDefined)
- .flatten
- .foreach(s => interpretLine(s))
-
- /** Makes the client execute the action specified by `line`.
- * If there is a protocol error, the function changes
- * the variable `protocolIsIntact` to false.
- *
- * @param line the line to interpret
- */
- private def interpretLine(line: String): Unit =
- this.protocolIsIntact = this.protocolState match
- case WaitingForVersion =>
- if line == GAME_VERSION then
- addDataToSend(s"$PROTOCOL_VERSION_GOOD")
- this.protocolState = WaitingForClientName
- true
- else
- addDataToSend(s"$PROTOCOL_VERSION_BAD")
- false
- case WaitingForClientName =>
- this.name = Some(line)
- this.protocolState = WaitingForGameStart
- true
- case WaitingForGameStart => true
- case InGame =>
- this.bufferAction(Action(line))
- true
-
- /** Buffers the action for execution or executes it immediately if it
- * doesn't take a turn */
- private def bufferAction(action: Action) =
- if (
- this.nextAction.isEmpty &&
- this.player.exists(action.takesATurnFor(_))
- ) then
- this.nextAction = Some(action)
- else if this.nextAction.isEmpty then
- 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/main/scala/Server/Clients.scala b/src/main/scala/Server/Clients.scala
deleted file mode 100644
index 6487446..0000000
--- a/src/main/scala/Server/Clients.scala
+++ /dev/null
@@ -1,82 +0,0 @@
-package o1game.Server
-
-import scala.util.Try
-import scala.util.Random
-
-class Clients(maxClients: Int):
- private val clients: Array[Option[Client]] = Array.fill(maxClients)(None)
-
- /** Adds `client` to this collection of clients.
- *
- * @param client the Client to add
- * @return true if there was room for the client
- * i.e. fewer clients than `maxClients`, false otherwise
- */
- def addClient(client: Client): Boolean =
- val i = this.clients.indexOf(None)
- if i == -1 then
- false
- else
- this.clients(i) = Some(client)
- true
-
- /** Returns all the clients.
- *
- * @return an iterable of all the clients
- */
- def allClients: Iterable[Client] = clients.toVector.flatten
-
- /** Applies the function `f` to all the clients for its side effects. */
- def foreach(f: Client => Any): Unit = this.clients.flatten.foreach(f)
-
- /** Executes the function `f` for all clients in a pseudorandom order. */
- def inRandomOrder(f: Client => Any): Unit =
- Random.shuffle(this
- .clients
- .flatten)
- .foreach(f)
-
- /** Returns true if the predicate `f` stands for all clients,
- * false otherwise
- *
- * @param f the predicate to check for all clients
- * @return whether `f` stands for all clients
- */
- def forall(f: Client => Boolean): Boolean = this.clients.flatten.forall(f)
-
- /** Gets the names of all the clients stored by this object.
- *
- * @return the names of the clients
- */
- def names: Vector[String] = this.clients.flatten.flatMap(_.getName).toVector
-
- def isEmpty: Boolean = this.clients.flatten.isEmpty
-
- /** Applies the function `f` to all the clients for its side effects
- * and removes all the clients for which `f([client])` returns false.
- * This is useful for doing IO with the client and removing clients
- * with stale sockets.
- *
- * @param f the function to apply to all the clients and filter them with
- */
- def removeNonSatisfying(f: Client => Boolean): Unit =
- for i <- this.clients.indices do
- this.clients(i) match
- case Some(c) =>
- if !f(c) then
- this.clients(i) = None
- case None =>
-
- /** Removes clients that have not behaved according to protocol */
- def removeNonCompliant(): Unit =
- this.removeNonSatisfying(_.isIntactProtocolWise)
-
- /** Applies the function f to all clients for its side effects.
- * If the function throws an exception, the client is removed.
- * Probably a more concise alternative to `removeNonSatisfying`,
- * but might catch exceptions unintentionally.
- *
- * @param f the function to apply for its side effects to each client
- */
- def mapAndRemove(f: Client => Unit): Unit =
- this.removeNonSatisfying(c => Try(f(c)).isSuccess)
diff --git a/src/main/scala/Server/ConnectionGetter.scala b/src/main/scala/Server/ConnectionGetter.scala
deleted file mode 100644
index b3246a7..0000000
--- a/src/main/scala/Server/ConnectionGetter.scala
+++ /dev/null
@@ -1,25 +0,0 @@
-package o1game.Server
-
-import java.io.IOException
-import java.net.{ServerSocket, Socket}
-import scala.concurrent.Future
-import scala.util.{Failure, Success}
-import scala.concurrent.ExecutionContext.Implicits.global
-
-/** Small helper class for getting new connections using futures */
-class ConnectionGetter(val socket: ServerSocket):
-
- private var nextClient: Future[Socket] = Future.failed(IOException())
-
- /** Returns a new socket to a client if there is any new connections. */
- def newClient(): Option[Socket] =
- this.nextClient.value match
- case Some(Success(s)) =>
- nextClient = Future(socket.accept())
- Some(s)
- case Some(Failure(e)) =>
- nextClient = Future(socket.accept())
- None
- case None => None
-
-end ConnectionGetter
diff --git a/src/main/scala/Server/Server.scala b/src/main/scala/Server/Server.scala
deleted file mode 100644
index 7864c49..0000000
--- a/src/main/scala/Server/Server.scala
+++ /dev/null
@@ -1,195 +0,0 @@
-package o1game.Server
-
-
-// TODO: TLS/SSL / import javax.net.ssl.SSLServerSocketFactory
-
-import java.lang.Thread.{currentThread, sleep}
-import java.io.IOException
-import java.net.{ServerSocket, Socket}
-import o1game.constants.*
-import o1game.Model.{Adventure,Entity,Player}
-import o1game.utils.stringToByteArray
-
-import java.lang.System.currentTimeMillis
-import scala.util.Try
-
-
-/** `Server` exists to initialize a server for the game
- * and run it with its method `startServer`.
- *
- * @param port the TCP port the server should listen on
- * @param maxClients the maximum number of clients that may be in the game
- * simultaneously.
- * @param timeLimit the time limit clients should have to execute their turns.
- * @param joinAfterStart whether new clients are accepted after the game has
- * been started
- */
-class Server(
- port: Int,
- maxClients: Int,
- val timeLimit: Int,
- val joinAfterStart: Boolean
-):
-
- private val socket = ServerSocket(port)
- private val clientGetter = ConnectionGetter(socket)
- private val clients: Clients = Clients(maxClients)
- private val buffer: Array[Byte] = Array.ofDim(1024)
- private var bufferIndex = 0
- private var adventure: Option[Adventure] = None
- private var previousTurn = 0.0
-
- /** Starts the server. Won't terminate under normal circumstances. */
- def startServer(): Unit =
- while true do
- this.serverStep()
- sleep(POLL_INTERVAL)
-
- private def serverStep(): Unit =
- this.clients.removeNonCompliant()
- if this.adventure.isEmpty || this.joinAfterStart then
- this.receiveNewClient()
- this.readFromAll()
- this.clients.foreach(_.interpretData())
- this.writeClientDataToClients()
- this.writeObservations()
- if this.canExecuteTurns then
- this.clients.inRandomOrder(_.act())
- this.writeClientDataToClients()
- this.writeObservations()
- this.clients.foreach(c =>
- this.writeToClient(this.turnStartInfo(c), c)
- )
- this.previousTurn = currentTimeMillis() / 1000
- if this.adventure.isDefined && this.joinAfterStart then
- this.clients.foreach( c => if c.isReadyForGameStart then
- this.adventure.foreach(a =>
- c.getName.foreach(n => a.addPlayer(n))
- )
- startGameForClient(c)
- )
- else if this.adventure.isEmpty && !this.clients.isEmpty && this.clients.forall(_.isReadyForGameStart) then
- this.adventure = Some(Adventure(this.clients.names))
- this.clients.foreach(startGameForClient(_))
- this.previousTurn = currentTimeMillis() / 1000
-
- /** Helper function to start the game for the specified client c.
- * MAY ONLY BE USED IF `this.adventure` is Some!
- * Apparently guard clauses are bad because they use return or something,
- * but assertions should be fine, as long as they enforce the function
- * contract?
- */
- private def startGameForClient(c: Client): Unit =
- assert(this.adventure.isDefined)
- c.gameStart()
- val name = c.getName
-
- val playerEntity: Option[Player] = name match
- case Some(n) => this.adventure match
- case Some(a) => a.getPlayer(n)
- case None => None
- case None => None
- playerEntity.foreach(c.givePlayer(_))
-
- this.writeToClient(
- s"$timeLimit\r\n${this.turnStartInfo(c)}", c
- )
-
- this.clients.foreach(c =>
- if c.player != playerEntity then
- c.player.foreach(_.observe(s"${name.getOrElse("Unknown player")} joins the game."))
- )
-
-
- private def writeObservations(): Unit =
- this.clients.foreach(c =>
- val observations = c.player.map(_.readAndClearObservations())
- observations.foreach(_.foreach((s: String) =>
- this.writeToClient(s"$ACTION_NONBLOCKING_INDICATOR$s\r\n", c))
- )
- )
-
- /** Helper function to determine if the next turn can be taken */
- private def canExecuteTurns: Boolean =
- val requirement1 = this.adventure.isDefined
- val requirement2 = !this.clients.isEmpty // nice! you can just return
- // to the game after everyone
- // left and everything is just
- // as before!
- val allPlayersReady = this.clients.forall(_.isReadyToAct)
- val requirement3 = (allPlayersReady
- || currentTimeMillis() / 1000 >= previousTurn + timeLimit)
- requirement1 && requirement2 && requirement3
-
-
- /** Receives a new client and stores it in `clients`.
- *
- * @return describes if a client was added
- */
- private def receiveNewClient(): Boolean =
- this.clientGetter.newClient() match
- case Some(c) =>
- clients.addClient(Client(c))
- true
- case None =>
- false
-
- private def turnStartInfo(client: Client): String =
- val clientArea = client.player.map(_.location)
- val areaDesc = clientArea
- .map(_.description)
- .getOrElse("You are floating in the middle of a soothing void.")
- val directions = clientArea
- .map(_.getNeighborNames.mkString(LIST_SEPARATOR))
- .getOrElse("")
- val items = clientArea
- .map(_.getItemNames.mkString(LIST_SEPARATOR))
- .getOrElse("")
- val entities = client.player.map(c =>
- c.location
- .getEntityNames
- .filter(c.name != _)
- .mkString(LIST_SEPARATOR)
- ).getOrElse("")
- s"$TURN_INDICATOR\r\n$areaDesc\r\n$directions\r\n$items\r\n$entities\r\n"
-
- /** Sends `message` to all clients
- *
- * @param message the message to send
- */
- private def writeToAll(message: String): Unit =
- this.clients.mapAndRemove(c =>
- val output = c.socket.getOutputStream
- output.write(stringToByteArray(message))
- output.flush()
- )
-
- private def writeToClient(message: String, client: Client): Unit =
- try {
- val output = client.socket.getOutputStream
- output.write(stringToByteArray(message))
- output.flush()
- } catch {
- case e: IOException => client.failedProtocol()
- }
-
- /** Sends every client's `dataToThisClient` to the client */
- private def writeClientDataToClients(): Unit =
- this.clients.mapAndRemove(c =>
- val output = c.socket.getOutputStream
- val data = c.dataToThisClient()
- output.write(stringToByteArray(data))
- output.flush()
- )
-
- /** Reads data sent by clients and stores it in the `Client`s of `clients` */
- private def readFromAll(): Unit =
- clients.mapAndRemove(c =>
- val input = c.socket.getInputStream
- while input.available() != 0 do
- val bytesRead = input.read(buffer)
- if bytesRead != -1 then
- c.receiveData(buffer.take(bytesRead).toVector)
- )
-
-end Server
diff --git a/src/main/scala/Server/constants.scala b/src/main/scala/Server/constants.scala
deleted file mode 100644
index 083db4e..0000000
--- a/src/main/scala/Server/constants.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-
-package o1game.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 TURN_INDICATOR = ">"
-val ACTION_BLOCKING_INDICATOR='.'
-val ACTION_NONBLOCKING_INDICATOR='+'
-
-val LIST_SEPARATOR=";"
-
-val PROTOCOL_VERSION_GOOD = "1"
-val PROTOCOL_VERSION_BAD = "0"
-//assert(PROTOCOL_VERSION_BAD.length <= PROTOCOL_VERSION_GOOD.length)
-
-enum ServerProtocolState:
- case WaitingForVersion, WaitingForClientName, WaitingForGameStart, InGame
diff --git a/src/main/scala/main.scala b/src/main/scala/main.scala
deleted file mode 100644
index d68664a..0000000
--- a/src/main/scala/main.scala
+++ /dev/null
@@ -1,26 +0,0 @@
-
-import o1game.Client.newClient
-import o1game.Server.Server
-import java.lang.Thread
-import scala.concurrent.Future
-import scala.concurrent.ExecutionContext.Implicits.global
-
-import scala.io.StdIn.readLine
-
-// TODO: add proper logic for starting the game
-@main def main(): Unit =
- print("How do you want to play?\n1) Host and join local game\n2) Join local game\n> ")
- readLine().toIntOption match
- case Some(1) =>
- Thread(() => Server(2267, 5, 30, true).startServer()).start()
- println("Server started in background.")
- print("Choose a name:\n> ")
- val name = readLine()
- newClient(name, "127.0.0.1", 2267).map(_.startClient())
- case Some(2) =>
- print("Choose a name:\n> ")
- val name = readLine()
- newClient(name, "127.0.0.1", 2267).map(_.startClient())
- case _ => println("Invalid input")
-
-
diff --git a/src/main/scala/utils.scala b/src/main/scala/utils.scala
deleted file mode 100644
index cfca568..0000000
--- a/src/main/scala/utils.scala
+++ /dev/null
@@ -1,28 +0,0 @@
-package o1game.utils
-
-import java.io.InputStream
-
-/** Converts this string to an array of bytes (probably for transmission).
- *
- * @param str the string to convert
- * @return an array of bytes representing the string in UTF8.
- */
-def stringToByteArray(str: String): Array[Byte] =
- str.toVector.map(_.toByte).toArray
-
-/** Reads n characters from the given InputStream blockingly.
- *
- * @param input the InputStream to read from
- * @param n the number of bytes to read
- * @return The read result, or None in case of failure
- */
-def getNCharsFromSocket(input: InputStream, n: Int): Option[String] =
- val buffer: Array[Byte] = Array.ofDim(n)
- var i = 0
- var failed = false
- while i < n && !failed do
- val res = input.read(buffer, i, n - i)
- if res < 0 then failed = true
- i += res
- // TODO: better error handling
- if failed then None else Some(String(buffer))