aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.txt95
-rw-r--r--scalevalapokalypsi.iml1
-rw-r--r--src/scalevalapokalypsi/Client/Client.scala3
-rw-r--r--src/scalevalapokalypsi/Client/ReceivedLineParser.scala7
-rw-r--r--src/scalevalapokalypsi/Client/RoomState.scala (renamed from src/scalevalapokalypsi/Client/Turn.scala)0
-rw-r--r--src/scalevalapokalypsi/Model/Action.scala5
-rw-r--r--src/scalevalapokalypsi/Model/Area.scala27
-rw-r--r--src/scalevalapokalypsi/Server/Client.scala10
-rw-r--r--src/scalevalapokalypsi/Server/Server.scala7
-rw-r--r--src/scalevalapokalypsi/UI/.bsp/scala.json45
-rw-r--r--src/scalevalapokalypsi/UI/Printer.scala78
-rw-r--r--src/scalevalapokalypsi/UI/StdinLineReader.scala (renamed from src/scalevalapokalypsi/Client/StdinLineReader.scala)6
-rw-r--r--src/scalevalapokalypsi/UI/main.scala132
-rw-r--r--src/scalevalapokalypsi/constants/constants.scala8
-rw-r--r--src/scalevalapokalypsi/main.scala115
-rw-r--r--src/scalevalapokalypsi/utils/utils.scala45
16 files changed, 416 insertions, 168 deletions
diff --git a/README.txt b/README.txt
index 5f4f277..4ada829 100644
--- a/README.txt
+++ b/README.txt
@@ -23,18 +23,99 @@ ne olennaisesti tehdä mitä tahansa, sen sijaan että Z-kone millään lailla
rajoittaisi niitä.
+Järjestelmävaatimukset (TÄRKEÄÄ)
+--------------------------------
+
+INTELLIJ IDEA:n OLETUSTERMINAALI EI OLE TUETTU!!! Sinun on pelattava tätä
+jossain tuetussa terminaalissa. Muuten tuloste tulee näyttämään oudolta
+(todennäköisesti rivin alku tulostuu toistuvasti siihen, mihin yrität
+kirjoittaa tai kursorisi ei pääse pois rivin alusta). Onneksi tuetut
+terminaalit ovat varsin yleisiä: nopealla testillä kaikki keksityt
+terminaalit olivat tuettuja IDEAa lukuun ottamatta.
+
+Tuettuja olivat ainakin:
+* cmd.exe (dokumentaation perusteella - ei ollut saatavilla testiin)
+* alacritty (toimii myös Windowsilla)
+* xterm
+* gnome-terminal
+* konsole
+* kitty
+* xfce4-terminal
+* jopa st (miten sekin on OOB parempi kuin IDEA?)
+
+Tarkemmin määriteltynä, jos uuden terminaalin lataamista ennen tahdot varmistua
+kyseisen terminaalin sopivuudesta: terminaalisi on tuettava ANSI-koodeja `ESC7`,
+`ESC8` ja `ESC[0E` (jossa 0 voisi muissa sovelluksissa olla jokin muukin luku).
+Näiden koodien bitit ovat samassa järjestyksessä 0x001b 0x0037, 0x001b 0x0038 ja
+0x001b 0x005b 0x0045, mikäli edelliset merkinnät olivat epäselvät.
+
+
Pelin käynnistäminen
--------------------
-[yksittäisen käyttäjän käynnistäminen]
+Saat käynnistettyä pelin seuraavasti:
+```
+1) Liity viralliselle palvelimelle (cron4.fi)
+2) Käynnistä palvelin laitteellasi ja liity sille
+3) Liity mielivaltaiselle palvelimelle
+> 1
+Valitse itsellesi pelin sisäinen alter ego.
+> [nimi]
+[liityt peliin]
+```
+Huomaa, että olet liittynyt julkiselle palvelimelle, jolla saattaa olla muita
+pelaajia. Jos olet assari arvioimassa, kovin moni tuskin vielä tietää pelistä
+taikka palvelimesta, joten pelin tila lienee jotakuinkin ennallaan.
Moninpeliominaisuudesta saat eniten iloa irti, joten jaa [pelin nimi]
-ystävillesi ja ala pelaamaan! Jos sinulla ei ole ystäviä, voit myös kokea
-moninpeliominaisuuden mahtavuuden hieman vaisumpana käynnistämällä toisen
-käyttäjän ylläolevien ohjeiden mukaisesti ja liittymällä samaan peliin
-(eli samalle serverille), johon edellisellä käyttäjällä liityit. Toista
-tämä niin monta kertaa kuin tahdot, kunnes serveri valittaa liian suuresta
-pelaajamäärästä.
+ystävillesi ja ala pelaamaan! Jos sinulla ei ole ystäviä, onnistuu moninpeli-
+ominaisuuden testaaminen joko liittymällä useamman kerran julkiselle palveli-
+melle tai pystyttämällä palvelin omalle koneelle seuraavasti `n` pelaajaa
+varten.
+
+Prosessi 1:
+```
+Miten tahdot pelata?
+1) Liity viralliselle palvelimelle (cron4.fi)
+2) Käynnistä palvelin laitteellasi ja liity sille
+3) Liity mielivaltaiselle palvelimelle
+> 2
+Valitse portti palvelimelle. (Jätä tyhjäksi oletusta 2267 varten)
+>
+Kuinka monta pelaajaa pelissä saa olla samanaikaisesti?
+> [n]
+Syötä vuorojen aikaraja yksiköttömänä sekunneissa odottelun vähentämiseksi. Syötä 0 aikarajan poistamiseksi. (Jätä tyhjäksi oletusta 30 varten)
+>
+Palvelin käynnistetty taustalla.
+Valitse itsellesi pelin sisäinen alter ego.
+> [nimi]
+[liityt peliin]
+```
+
+n x muu prosessi: (huom. en ole prosessiteekkari)
+```
+Miten tahdot pelata?
+1) Liity viralliselle palvelimelle (cron4.fi)
+2) Käynnistä palvelin laitteellasi ja liity sille
+3) Liity mielivaltaiselle palvelimelle
+> 3
+Syötä palvelimen verkkotunnus.
+> 127.0.0.1
+Valitse portti palvelimelle. (Jätä tyhjäksi oletusta 2267 varten)
+>
+Valitse itsellesi pelin sisäinen alter ego.
+> Bob
+```
+
+Huomaa, että pelin ajamista varten samalla koneella useampaan kertaan, on
+IntelliJ IDEA:n ajokonfiguraatiota muokattava. Olettaen, että ohjelma on kerran
+ajettu main-funktion play-napista, valitse play-napin vierestä valikko, jossa
+lukee `main` tai `[pelin nimi]` -jotain. Sieltä valitse "Edit Configurations".
+Valitse oikea kohde avautuvan ikkunan vasemmasta palkista, klikkaa kohtaa
+"Modify options" ja valitse "Allow multiple instances". Sitten OK/Apply.
+Jos tästä muodostuu ongelma, kannattaa ensin vähän googlata tai lukea seuraava
+StackOverflow:
+=> https://stackoverflow.com/questions/41226555/how-do-i-run-the-same-application-twice-in-intellij
Pelin tavoite
diff --git a/scalevalapokalypsi.iml b/scalevalapokalypsi.iml
index a76f8e7..c38826d 100644
--- a/scalevalapokalypsi.iml
+++ b/scalevalapokalypsi.iml
@@ -8,5 +8,6 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="scala-sdk-3.3.0" level="application" />
+ <orderEntry type="library" name="scala-sdk-3.3.0" level="application" />
</component>
</module> \ No newline at end of file
diff --git a/src/scalevalapokalypsi/Client/Client.scala b/src/scalevalapokalypsi/Client/Client.scala
index 0532038..e94fdb0 100644
--- a/src/scalevalapokalypsi/Client/Client.scala
+++ b/src/scalevalapokalypsi/Client/Client.scala
@@ -5,7 +5,7 @@ import scala.io.Source
import scala.sys.process.stdout
import scalevalapokalypsi.constants.*
import scalevalapokalypsi.utils.{stringToByteArray,getNCharsFromSocket}
-import scalevalapokalypsi.Client.{ReceivedLineParser,StdinLineReader,RoomState}
+import scalevalapokalypsi.Client.{ReceivedLineParser,RoomState}
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Try, Success, Failure}
@@ -119,7 +119,6 @@ class Client(socket: Socket):
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()
diff --git a/src/scalevalapokalypsi/Client/ReceivedLineParser.scala b/src/scalevalapokalypsi/Client/ReceivedLineParser.scala
index 9337ce1..bccba59 100644
--- a/src/scalevalapokalypsi/Client/ReceivedLineParser.scala
+++ b/src/scalevalapokalypsi/Client/ReceivedLineParser.scala
@@ -2,6 +2,7 @@ package scalevalapokalypsi.Client
import scala.collection.mutable.Buffer
import scalevalapokalypsi.constants.*
+import scalevalapokalypsi.utils.*
/** A class for checking asynchronously for received lines */
class ReceivedLineParser:
@@ -10,11 +11,11 @@ class ReceivedLineParser:
private var bufferedData: Buffer[Byte] = Buffer.empty // TODO: suboptimal DS
- /** Add received data */
+ /** Add received data */
def in(data: Array[Byte]): Unit =
this.bufferedData ++= data
- /** Read a line from the received data */
+ /** Read a line from the received data */
def nextLine(): Option[String] =
val indexOfCRLF = this.bufferedData.indexOfSlice(CRLF)
if indexOfCRLF == -1 then
@@ -22,6 +23,6 @@ class ReceivedLineParser:
else
val splitData = this.bufferedData.splitAt(indexOfCRLF)
this.bufferedData = Buffer.from(splitData(1).drop(CRLF.length))
- Some(String(splitData(0).toArray))
+ byteArrayToString(splitData(0).toArray)
end ReceivedLineParser
diff --git a/src/scalevalapokalypsi/Client/Turn.scala b/src/scalevalapokalypsi/Client/RoomState.scala
index 02fe11b..02fe11b 100644
--- a/src/scalevalapokalypsi/Client/Turn.scala
+++ b/src/scalevalapokalypsi/Client/RoomState.scala
diff --git a/src/scalevalapokalypsi/Model/Action.scala b/src/scalevalapokalypsi/Model/Action.scala
index a781ee8..fdfbf75 100644
--- a/src/scalevalapokalypsi/Model/Action.scala
+++ b/src/scalevalapokalypsi/Model/Action.scala
@@ -29,7 +29,10 @@ class Action(input: String):
val resOption: Option[(Boolean, Event)] = this.verb match
case "go" =>
val result = actor.go(this.modifiers)
- result.foreach(r => oldLocation.observeEvent(r))
+ 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)))
diff --git a/src/scalevalapokalypsi/Model/Area.scala b/src/scalevalapokalypsi/Model/Area.scala
index 96392ba..c07f2f9 100644
--- a/src/scalevalapokalypsi/Model/Area.scala
+++ b/src/scalevalapokalypsi/Model/Area.scala
@@ -103,33 +103,6 @@ class Area(val name: String, var description: String):
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)
diff --git a/src/scalevalapokalypsi/Server/Client.scala b/src/scalevalapokalypsi/Server/Client.scala
index d689356..5727fc6 100644
--- a/src/scalevalapokalypsi/Server/Client.scala
+++ b/src/scalevalapokalypsi/Server/Client.scala
@@ -3,6 +3,7 @@ package scalevalapokalypsi.Server
import java.net.Socket
import scala.math.{min,max}
import scalevalapokalypsi.constants.*
+import scalevalapokalypsi.utils.*
import ServerProtocolState.*
import scalevalapokalypsi.Model.Action
import scalevalapokalypsi.Model.Entities.Player
@@ -113,8 +114,7 @@ class Client(val socket: Socket):
val rest = this.incompleteMessage.drop(nextCRLF + 2)
this.incompleteMessage = rest ++ Array.fill(nextCRLF + 1)(0.toByte)
this.incompleteMessageIndex = 0
- // TODO: the conversion may probably be exploited to crash the server
- Some(String(message))
+ byteArrayToString(message)
else
None
@@ -178,7 +178,7 @@ class Client(val socket: Socket):
this.singStartTime = None
- case None =>
+ case None if isPrintable(line) =>
val action = Action(line)
val takesATurn = this.character.exists(p => action.execute(p))
@@ -186,6 +186,10 @@ class Client(val socket: Socket):
this.addDataToSend(s"$ACTION_BLOCKING_INDICATOR")
this.turnUsed = true
+ case None =>
+
+ () // There were some illegal chars but whatever
+
end executeLine
end Client
diff --git a/src/scalevalapokalypsi/Server/Server.scala b/src/scalevalapokalypsi/Server/Server.scala
index 8debdba..89db302 100644
--- a/src/scalevalapokalypsi/Server/Server.scala
+++ b/src/scalevalapokalypsi/Server/Server.scala
@@ -124,12 +124,7 @@ class Server(
target.foreach(t =>
if c.player.exists(_.isSinging) && !c.clientHasSong then
val verse = t.getVerseAgainst
- println(s"got verse against: “$verse”")
- this.writeToClient(
- s"${SING_INDICATOR}$verse\r\n",
- // TODO: store the verse and check how close client input is when determining sing quality
- c
- )
+ this.writeToClient(s"${SING_INDICATOR}$verse\r\n", c)
c.startSong(verse)
)
)
diff --git a/src/scalevalapokalypsi/UI/.bsp/scala.json b/src/scalevalapokalypsi/UI/.bsp/scala.json
new file mode 100644
index 0000000..fe879ce
--- /dev/null
+++ b/src/scalevalapokalypsi/UI/.bsp/scala.json
@@ -0,0 +1,45 @@
+{
+ "name": "scala",
+ "argv": [
+ "/home/cron4/.cache/coursier/arc/https/github.com/scala/scala3/releases/download/3.5.0/scala3-3.5.0-x86_64-pc-linux.tar.gz/scala3-3.5.0-x86_64-pc-linux/bin/scala-cli",
+ "--cli-default-scala-version",
+ "3.5.0",
+ "--repository",
+ "file:///home/cron4/.cache/coursier/arc/https/github.com/scala/scala3/releases/download/3.5.0/scala3-3.5.0-x86_64-pc-linux.tar.gz/scala3-3.5.0-x86_64-pc-linux/maven2",
+ "--prog-name",
+ "scala",
+ "bsp",
+ "--json-options",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/UI/.scala-build/ide-options-v2.json",
+ "--json-launcher-options",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/UI/.scala-build/ide-launcher-options.json",
+ "--envs-file",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/UI/.scala-build/ide-envs.json",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/UI/main.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/UI/Printer.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Client/Client.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Client/GameEvent.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Client/ReceivedLineParser.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Client/RoomState.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Client/StdinLineReader.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Model/Action.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Model/Adventure.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Model/Area.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Model/Entities",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Model/Event.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Model/Item.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Model/SingEffects.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Server/Client.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Server/Clients.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Server/ConnectionGetter.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/Server/Server.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/constants/constants.scala",
+ "/home/cron4/Dev/scalevalapokalypsi/src/scalevalapokalypsi/utils/utils.scala"
+ ],
+ "version": "1.4.0",
+ "bspVersion": "2.1.1",
+ "languages": [
+ "scala",
+ "java"
+ ]
+} \ No newline at end of file
diff --git a/src/scalevalapokalypsi/UI/Printer.scala b/src/scalevalapokalypsi/UI/Printer.scala
new file mode 100644
index 0000000..a33864f
--- /dev/null
+++ b/src/scalevalapokalypsi/UI/Printer.scala
@@ -0,0 +1,78 @@
+package scalevalapokalypsi.UI
+import scalevalapokalypsi.Client.GameEvent
+import java.lang.System.currentTimeMillis
+import scalevalapokalypsi.utils.isPrintable
+
+/** A singleton for printing. Keeps track about the "action query" at the start
+ * of lines and has a helper function "pryntGameEvent".
+ */
+object Printer:
+ var inputIndicatorAtStartOfLine = false
+ var queriedLineToSing = false
+ var singStartTime: Option[Long] = None
+
+ /** Prints the given game event.
+ *
+ * @param gameEvent the event to print
+ */
+ def printGameEvent(gameEvent: GameEvent): Unit =
+
+ val actions = gameEvent.actions.map(_.mkString("\n"))
+ val roomState = gameEvent.roomState.map(_.toString)
+ val lineToSing = gameEvent.lineToSing
+
+ if
+ inputIndicatorAtStartOfLine &&
+ (actions.isDefined ||
+ roomState.isDefined ||
+ lineToSing.isDefined)
+ then
+ this.printLn("")
+
+ actions.foreach(this.printLn(_))
+
+ roomState.foreach(this.printLn(_))
+
+ lineToSing match
+ case Some(l) =>
+ if this.singStartTime.isEmpty then
+ this.singStartTime = Some(currentTimeMillis() / 1000)
+ print(s"Laula: “$l”\n ")
+ val timeSpent = this.singStartTime.map((t: Long) =>
+ (currentTimeMillis / 1000 - t).toString
+ ).getOrElse("?")
+ print(this.timeIndicatorUpdater(timeSpent))
+ case None =>
+ this.singStartTime = None
+
+ val timeLeft = s"${gameEvent.timeToNextTurn.getOrElse("∞")}"
+
+ if
+ gameEvent.playerCanAct &&
+ lineToSing.isEmpty &&
+ !inputIndicatorAtStartOfLine
+ then
+ this.inputIndicatorAtStartOfLine = true
+ print(s"[$timeLeft s]> ")
+
+ if gameEvent.playerCanAct && lineToSing.isEmpty then
+ print(this.timeIndicatorUpdater(timeLeft))
+
+ end printGameEvent
+
+ /** Prints the given string with a trailing newline added. Should be used
+ * instead of ordinary println because printing outside of the Printer
+ * might cause weird-looking output.
+ *
+ * @param s the line to print
+ */
+ def printLn(s: String): Unit =
+ if isPrintable(s) then
+ println(s)
+ else
+ println("Virhe: epätavallinen merkki havaittu tulosteessa.")
+ this.inputIndicatorAtStartOfLine = false
+
+ private def timeIndicatorUpdater(t: String): String =
+ s"\u001b7\u001b[0E[$t s]> \u001b8"
+
diff --git a/src/scalevalapokalypsi/Client/StdinLineReader.scala b/src/scalevalapokalypsi/UI/StdinLineReader.scala
index 6ba8761..4d0f778 100644
--- a/src/scalevalapokalypsi/Client/StdinLineReader.scala
+++ b/src/scalevalapokalypsi/UI/StdinLineReader.scala
@@ -1,4 +1,4 @@
-package scalevalapokalypsi.Client
+package scalevalapokalypsi.UI
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
@@ -16,13 +16,15 @@ class StdinLineReader:
def newLine(): Option[String] =
this.nextLine.value match
case Some(Success(s)) =>
+ if s.contains("\u0000") then
+ println("End of stream!")
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())
diff --git a/src/scalevalapokalypsi/UI/main.scala b/src/scalevalapokalypsi/UI/main.scala
new file mode 100644
index 0000000..44ca0e4
--- /dev/null
+++ b/src/scalevalapokalypsi/UI/main.scala
@@ -0,0 +1,132 @@
+package scalevalapokalypsi.UI
+
+import scalevalapokalypsi.Client.{newClient, Client, GameEvent}
+import scalevalapokalypsi.Server.Server
+import scalevalapokalypsi.constants.*
+import scalevalapokalypsi.utils.*
+import java.lang.Thread.sleep
+import scala.util.{Try,Success,Failure}
+import scala.collection.immutable.LazyList
+
+import java.lang.Thread
+import scala.io.StdIn.readLine
+
+
+@main def main(): Unit =
+
+ val option = getValidInput[Int](
+ "Miten tahdot pelata?\n" +
+ s"1) Liity viralliselle palvelimelle ($DEFAULT_SERVER)\n" +
+ "2) Käynnistä palvelin laitteellasi ja liity sille\n" +
+ "3) Liity mielivaltaiselle palvelimelle",
+ s => s.toIntOption match
+ case None =>
+ Left("Syötä kokonaisluku.")
+ case Some(i) =>
+ if i < 1 || i > 3 then
+ Left("Syötä kokonaisluku väliltä [1,3].")
+ else
+ Right(i)
+ )
+
+
+ val serverName =
+ if option == 3 then
+ readLine("Syötä palvelimen verkkotunnus.\n> ")
+ else if option == 2 then
+ "127.0.0.1"
+ else
+ DEFAULT_SERVER
+
+ val serverPort =
+ if option == 1 then
+ DEFAULT_PORT
+ else
+ getValidInput[Int](
+ s"Valitse portti palvelimelle. (Jätä tyhjäksi oletusta $DEFAULT_PORT varten)",
+ s =>
+ if s == "" then
+ Right(DEFAULT_PORT)
+ else
+ s.toIntOption match
+ case None =>
+ Left("Syötä kokonaisluku.")
+ case Some(i) =>
+ if i > 0 && i <= 65535 then
+ Right(i)
+ else
+ Left("Syötä portti lailliselta väliltä.")
+ )
+
+ val maxClients = if option == 2 then
+ getValidInput[Int](
+ "Kuinka monta pelaajaa pelissä saa olla samanaikaisesti?",
+ s => s.toIntOption match
+ case None =>
+ Left("Syötä kokonaisluku.")
+ case Some(i) =>
+ if i > 1000 then
+ println(
+ "Aika ison määrän valitsit, mutta olkoon menneeksi."
+ )
+ if i > 0 then
+ Right(i)
+ else
+ Left("Syötä positiivinen kokonaisluku.")
+ )
+ else
+ 0
+
+ val timeLimit = if option == 2 then
+ getValidInput[Int](
+ s"Syötä vuorojen aikaraja yksiköttömänä sekunneissa odottelun vähentämiseksi. Syötä 0 aikarajan poistamiseksi. (Jätä tyhjäksi oletusta $DEFAULT_TURN_TIME_LIMIT varten)",
+ s => if s == "" then
+ Right(DEFAULT_TURN_TIME_LIMIT)
+ else
+ s.toIntOption
+ .toRight("Syötä kokonaisluku")
+ .filterOrElse(
+ _ >= 0,
+ "Syötä epänegatiivinen kokonaisluku."
+ )
+ )
+ else
+ 0
+
+ if option == 2 then
+ Thread(() => new Server(serverPort, maxClients, timeLimit, true).startServer()).start()
+ println("Palvelin käynnistetty taustalla.")
+
+ val name = readLine("Valitse itsellesi pelin sisäinen alter ego.\n> ")
+
+ Try(newClient(name, serverName, serverPort))
+ .toOption
+ .flatten match
+ case Some(client) =>
+ 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."
+ )
+
+
+
+/** Client game loop. Handles output to and from the client in the eyes of the
+ * terminal.
+ */
+def startClient(client: Client): Unit =
+ var hasQuit = false
+ val stdinReader = StdinLineReader()
+ stdinReader.startReading()
+ while !hasQuit do
+ sleep(POLL_INTERVAL)
+ val line = stdinReader.newLine()
+ if line.map(_.length).getOrElse(0) > 1024 then
+ Printer.printLn("Virhe: Syötteesi oli liian pitkä.")
+ else if line == Some("quit") then
+ hasQuit = true
+ else
+ val gameEvent = client.clientStep(line)
+ Printer.printGameEvent(gameEvent)
+
+
diff --git a/src/scalevalapokalypsi/constants/constants.scala b/src/scalevalapokalypsi/constants/constants.scala
index 7d4e1a6..970d6f7 100644
--- a/src/scalevalapokalypsi/constants/constants.scala
+++ b/src/scalevalapokalypsi/constants/constants.scala
@@ -10,9 +10,17 @@ val SING_INDICATOR = "~"
val ACTION_BLOCKING_INDICATOR='.'
val ACTION_NONBLOCKING_INDICATOR='+'
val INITIAL_CONN_TIMEOUT = 5000 // millisec.
+val DEFAULT_PORT: Int = 2267
+val DEFAULT_TURN_TIME_LIMIT = 30
+val DEFAULT_SERVER = "cron4.fi"
val LIST_SEPARATOR=";"
+val BYTE_ALLOWED =
+ (n: Byte) => !(n <= 31 || (n >= 127 && n <= 159))
+
+val FORBIDDEN_CHARACTERS = "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u007F\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F"
+
val PROTOCOL_VERSION_GOOD = "1"
val PROTOCOL_VERSION_BAD = "0"
// assert(PROTOCOL_VERSION_BAD.length <= PROTOCOL_VERSION_GOOD.length)
diff --git a/src/scalevalapokalypsi/main.scala b/src/scalevalapokalypsi/main.scala
deleted file mode 100644
index e357845..0000000
--- a/src/scalevalapokalypsi/main.scala
+++ /dev/null
@@ -1,115 +0,0 @@
-package scalevalapokalypsi
-
-import scalevalapokalypsi.Client.{newClient, Client, StdinLineReader, GameEvent}
-import scalevalapokalypsi.Server.Server
-import scalevalapokalypsi.constants.*
-import java.lang.Thread.sleep
-import java.lang.System.currentTimeMillis
-import scala.util.Try
-
-import java.lang.Thread
-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(() => new Server(2267, 5, 30, true).startServer()).start()
- println("Server started in background.")
- print("Choose a name:\n> ")
- val name = readLine()
- Try(newClient(name, "127.0.0.1", 2267))
- .toOption
- .flatten match
- case Some(client) =>
- startClient(client)
- case None =>
- println("Starting the client failed.")
-
- case Some(2) =>
- print("Choose a name:\n> ")
- val name = readLine()
- Try(newClient(name, "127.0.0.1", 2267))
- .toOption
- .flatten match
- case Some(client) =>
- startClient(client)
- case None =>
- println("Starting the client failed.")
- case _ => println("Invalid input")
-
-def startClient(client: Client): Unit =
- var hasQuit = false
- val stdinReader = StdinLineReader()
- stdinReader.startReading()
- val printer = Printer()
- while !hasQuit do
- sleep(POLL_INTERVAL)
- val line = stdinReader.newLine()
- if line.map(_.length).getOrElse(0) > 1024 then
- printer.printLn("Virhe: Syötteesi oli liian pitkä.")
- else if line == Some("quit") then
- hasQuit = true
- else
- val gameEvent = client.clientStep(line)
- printer.printGameEvent(gameEvent)
-
-
-class Printer:
- var inputIndicatorAtStartOfLine = false
- var queriedLineToSing = false
- var singStartTime: Option[Long] = None
-
- def printGameEvent(gameEvent: GameEvent): Unit =
-
- val actions = gameEvent.actions.map(_.mkString("\n"))
- val roomState = gameEvent.roomState.map(_.toString)
- val lineToSing = gameEvent.lineToSing
-
- if
- inputIndicatorAtStartOfLine &&
- (actions.isDefined ||
- roomState.isDefined ||
- lineToSing.isDefined)
- then
- this.printLn("")
-
- actions.foreach(this.printLn(_))
-
- roomState.foreach(this.printLn(_))
-
- lineToSing match
- case Some(l) =>
- if this.singStartTime.isEmpty then
- this.singStartTime = Some(currentTimeMillis() / 1000)
- print(s"Laula: “$l”\n ")
- val timeSpent = this.singStartTime.map((t: Long) =>
- (currentTimeMillis / 1000 - t).toString
- ).getOrElse("?")
- print(this.timeIndicatorUpdater(timeSpent))
- case None =>
- this.singStartTime = None
-
- val timeLeft = s"${gameEvent.timeToNextTurn.getOrElse("∞")}"
-
- if
- gameEvent.playerCanAct &&
- lineToSing.isEmpty &&
- !inputIndicatorAtStartOfLine
- then
- this.inputIndicatorAtStartOfLine = true
- print(s"[$timeLeft s]> ")
-
- if gameEvent.playerCanAct && lineToSing.isEmpty then
- print(this.timeIndicatorUpdater(timeLeft))
-
- end printGameEvent
-
- def printLn(s: String): Unit =
- println(s)
- this.inputIndicatorAtStartOfLine = false
-
- private def timeIndicatorUpdater(t: String): String =
- s"\u001b[s\u001b[0E[$t s]> \u001b[u"
-
diff --git a/src/scalevalapokalypsi/utils/utils.scala b/src/scalevalapokalypsi/utils/utils.scala
index ab262ad..54d407b 100644
--- a/src/scalevalapokalypsi/utils/utils.scala
+++ b/src/scalevalapokalypsi/utils/utils.scala
@@ -2,6 +2,9 @@ package scalevalapokalypsi.utils
import java.io.InputStream
import java.nio.charset.StandardCharsets
+import scala.util.Try
+import scalevalapokalypsi.constants.*
+import scala.io.StdIn.readLine
/** Converts this string to an array of bytes (probably for transmission).
*
@@ -11,6 +14,16 @@ import java.nio.charset.StandardCharsets
def stringToByteArray(str: String): Array[Byte] =
str.getBytes(StandardCharsets.UTF_8)
+/** Converts the given byte array to a string if possible*.
+ * (* Doesn't convert strings with control sequences in them)
+ *
+ * @param bytes the byte array to convert
+ * @return the matching string, if possible*.
+ */
+def byteArrayToString(bytes: Array[Byte]): Option[String] =
+ Try(String(bytes, StandardCharsets.UTF_8))
+ .toOption
+
/** Reads n characters from the given InputStream blockingly.
*
* @param input the InputStream to read from
@@ -25,5 +38,33 @@ def getNCharsFromSocket(input: InputStream, n: Int): Option[String] =
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, StandardCharsets.UTF_8))
+ if failed then None else byteArrayToString(buffer)
+
+def isPrintable(s: String): Boolean =
+ FORBIDDEN_CHARACTERS.forall((c: Char) => !(s contains c))
+
+
+/** Gets input from STDIN until the line entered is appoved
+ * by a validator function.
+ *
+ * @param message A query to represent the user with when requesting input
+ * @param validator A validator function. Should return Right[A] when the
+ * input is valid and Left[String] when the input is invalid,
+ * where the string will be printed to the user to notify
+ * their input was invalid.
+ * @return The first encountered valid string, see `validator`.
+ */
+def getValidInput[A](
+ message: String,
+ validator: String => Either[String, A]
+): A =
+ LazyList.continually(readLine(s"$message\n> "))
+ .flatMap(input =>
+ validator(input) match
+ case Left(s) =>
+ println(s)
+ None
+ case Right(a) =>
+ Some(a)
+ ).head
+