commit 1c0ddbb5b5d95c7546182f5799681a6923424f21 Author: Timothée Floure Date: Wed Oct 30 14:37:57 2019 +0100 Import streams handout diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a35362b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "dotty": { + "trace": { + "remoteTracingUrl": "wss://lamppc36.epfl.ch/dotty-remote-tracer/upload/lsp.log", + "server": { "format": "JSON", "verbosity": "verbose" } + } + } +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..4b4906c --- /dev/null +++ b/build.sbt @@ -0,0 +1,12 @@ +course := "progfun2" +assignment := "streams" +name := course.value + "-" + assignment.value +testSuite := "streams.BloxorzSuite" + +scalaVersion := "0.19.0-RC1" + +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") +libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test +libraryDependencies += ("org.scalacheck" %% "scalacheck" % "1.14.2").withDottyCompat(scalaVersion.value) + +testOptions in Test += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s") diff --git a/grading-tests.jar b/grading-tests.jar new file mode 100644 index 0000000..d7da321 Binary files /dev/null and b/grading-tests.jar differ diff --git a/project/MOOCSettings.scala b/project/MOOCSettings.scala new file mode 100644 index 0000000..ea30b36 --- /dev/null +++ b/project/MOOCSettings.scala @@ -0,0 +1,25 @@ +package ch.epfl.lamp + +import sbt._ +import sbt.Keys._ + +/** + * Settings shared by all assignments, reused in various tasks. + */ +object MOOCSettings extends AutoPlugin { + + object autoImport { + val course = SettingKey[String]("course") + val assignment = SettingKey[String]("assignment") + val testSuite = SettingKey[String]("testSuite") + val options = SettingKey[Map[String, Map[String, String]]]("options") + } + + override def trigger = allRequirements + + override val projectSettings: Seq[Def.Setting[_]] = Seq( + parallelExecution in Test := false, + // Report test result after each test instead of waiting for every test to finish + logBuffered in Test := false + ) +} diff --git a/project/StudentTasks.scala b/project/StudentTasks.scala new file mode 100644 index 0000000..587ba85 --- /dev/null +++ b/project/StudentTasks.scala @@ -0,0 +1,323 @@ +package ch.epfl.lamp + +import sbt._ +import Keys._ + +// import scalaj.http._ +import java.io.{File, FileInputStream, IOException} +import java.nio.file.FileSystems +import org.apache.commons.codec.binary.Base64 +// import play.api.libs.json.{Json, JsObject, JsPath} +import scala.util.{Failure, Success, Try} + +import MOOCSettings.autoImport._ + +case class AssignmentInfo( + key: String, + itemId: String, + premiumItemId: Option[String], + partId: String +) + +/** + * Provides tasks for submitting the assignment + */ +object StudentTasks extends AutoPlugin { + + object autoImport { + val assignmentInfo = SettingKey[AssignmentInfo]("assignmentInfo") + + val packageSourcesOnly = TaskKey[File]("packageSourcesOnly", "Package the sources of the project") + val packageBinWithoutResources = TaskKey[File]("packageBinWithoutResources", "Like packageBin, but without the resources") + val packageSubmissionZip = TaskKey[File]("packageSubmissionZip") + val packageSubmission = inputKey[Unit]("package solution as an archive file") + val runGradingTests = taskKey[Unit]("run black-box tests used for final grading") + } + + + import autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + // submitSetting, // FIXME: restore assignmentInfo setting on assignments + runGradingTestsSettings, + + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput) + ) ++ packageSubmissionZipSettings + + lazy val runGradingTestsSettings = runGradingTests := { + val testSuiteJar = "grading-tests.jar" + if (!new File(testSuiteJar).exists) { + throw new MessageOnlyException(s"Could not find tests JarFile: $testSuiteJar") + } + + val classPath = s"${(Test / dependencyClasspath).value.map(_.data).mkString(File.pathSeparator)}${File.pathSeparator}$testSuiteJar" + val junitProcess = + Fork.java.fork( + ForkOptions(), + "-cp" :: classPath :: + "org.junit.runner.JUnitCore" :: + (Test / testSuite).value :: + Nil + ) + + // Wait for tests to complete. + junitProcess.exitValue() + } + + + /** ********************************************************** + * SUBMITTING A SOLUTION TO COURSERA + */ + + val packageSubmissionZipSettings = Seq( + packageSubmissionZip := { + val submission = crossTarget.value / "submission.zip" + val sources = (packageSourcesOnly in Compile).value + val binaries = (packageBinWithoutResources in Compile).value + IO.zip(Seq(sources -> "sources.zip", binaries -> "binaries.jar"), submission) + submission + }, + artifactClassifier in packageSourcesOnly := Some("sources"), + artifact in (Compile, packageBinWithoutResources) ~= (art => art.withName(art.name + "-without-resources")) + ) ++ + inConfig(Compile)( + Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++ + Defaults.packageTaskSettings(packageBinWithoutResources, Def.task { + val relativePaths = + (unmanagedResources in Compile).value.flatMap(Path.relativeTo((unmanagedResourceDirectories in Compile).value)(_)) + (mappings in (Compile, packageBin)).value.filterNot { case (_, path) => relativePaths.contains(path) } + }) + ) + + val maxSubmitFileSize = { + val mb = 1024 * 1024 + 10 * mb + } + + /** Check that the jar exists, isn't empty, isn't crazy big, and can be read + * If so, encode jar as base64 so we can send it to Coursera + */ + def prepareJar(jar: File, s: TaskStreams): String = { + val errPrefix = "Error submitting assignment jar: " + val fileLength = jar.length() + if (!jar.exists()) { + s.log.error(errPrefix + "jar archive does not exist\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength == 0L) { + s.log.error(errPrefix + "jar archive is empty\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength > maxSubmitFileSize) { + s.log.error(errPrefix + "jar archive is too big. Allowed size: " + + maxSubmitFileSize + " bytes, found " + fileLength + " bytes.\n" + + jar.getAbsolutePath) + failSubmit() + } else { + val bytes = new Array[Byte](fileLength.toInt) + val sizeRead = try { + val is = new FileInputStream(jar) + val read = is.read(bytes) + is.close() + read + } catch { + case ex: IOException => + s.log.error(errPrefix + "failed to read sources jar archive\n" + ex.toString) + failSubmit() + } + if (sizeRead != bytes.length) { + s.log.error(errPrefix + "failed to read the sources jar archive, size read: " + sizeRead) + failSubmit() + } else encodeBase64(bytes) + } + } + + /** Task to package solution to a given file path */ + lazy val packageSubmissionSetting = packageSubmission := { + val args: Seq[String] = Def.spaceDelimited("[path]").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val base64Jar = prepareJar(jar, s) + + val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath) + scala.tools.nsc.io.File(path).writeAll(base64Jar) + } + +/* + /** Task to submit a solution to coursera */ + val submit = inputKey[Unit]("submit solution to Coursera") + lazy val submitSetting = submit := { + val args: Seq[String] = Def.spaceDelimited("").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val assignmentDetails = assignmentInfo.value + val assignmentKey = assignmentDetails.key + val courseName = + course.value match { + case "capstone" => "scala-capstone" + case "bigdata" => "scala-spark-big-data" + case other => other + } + + val partId = assignmentDetails.partId + val itemId = assignmentDetails.itemId + val premiumItemId = assignmentDetails.premiumItemId + + val (email, secret) = args match { + case email :: secret :: Nil => + (email, secret) + case _ => + val inputErr = + s"""|Invalid input to `submit`. The required syntax for `submit` is: + |submit + | + |The submit token is NOT YOUR LOGIN PASSWORD. + |It can be obtained from the assignment page: + |https://www.coursera.org/learn/$courseName/programming/$itemId + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + """.stripMargin + s.log.error(inputErr) + failSubmit() + } + + val base64Jar = prepareJar(jar, s) + val json = + s"""|{ + | "assignmentKey":"$assignmentKey", + | "submitterEmail":"$email", + | "secret":"$secret", + | "parts":{ + | "$partId":{ + | "output":"$base64Jar" + | } + | } + |}""".stripMargin + + def postSubmission[T](data: String): Try[HttpResponse[String]] = { + val http = Http("https://www.coursera.org/api/onDemandProgrammingScriptSubmissions.v1") + val hs = List( + ("Cache-Control", "no-cache"), + ("Content-Type", "application/json") + ) + s.log.info("Connecting to Coursera...") + val response = Try(http.postData(data) + .headers(hs) + .option(HttpOptions.connTimeout(10000)) // scalaj default timeout is only 100ms, changing that to 10s + .asString) // kick off HTTP POST + response + } + + val connectMsg = + s"""|Attempting to submit "${assignment.value}" assignment in "$courseName" course + |Using: + |- email: $email + |- submit token: $secret""".stripMargin + s.log.info(connectMsg) + + def reportCourseraResponse(response: HttpResponse[String]): Unit = { + val code = response.code + val respBody = response.body + + /* Sample JSON response from Coursera + { + "message": "Invalid email or token.", + "details": { + "learnerMessage": "Invalid email or token." + } + } + */ + + // Success, Coursera responds with 2xx HTTP status code + if (response.is2xx) { + val successfulSubmitMsg = + s"""|Successfully connected to Coursera. (Status $code) + | + |Assignment submitted successfully! + | + |You can see how you scored by going to: + |https://www.coursera.org/learn/$courseName/programming/$itemId/ + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + |and clicking on "My Submission".""".stripMargin + s.log.info(successfulSubmitMsg) + } + + // Failure, Coursera responds with 4xx HTTP status code (client-side failure) + else if (response.is4xx) { + val result = Try(Json.parse(respBody)).toOption + val learnerMsg = result match { + case Some(resp: JsObject) => + (JsPath \ "details" \ "learnerMessage").read[String].reads(resp).get + case Some(x) => // shouldn't happen + "Could not parse Coursera's response:\n" + x + case None => + "Could not parse Coursera's response:\n" + respBody + } + val failedSubmitMsg = + s"""|Submission failed. + |There was something wrong while attempting to submit. + |Coursera says: + |$learnerMsg (Status $code)""".stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera responds with 5xx HTTP status code (server-side failure) + else if (response.is5xx) { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera seems to be unavailable at the moment (Status $code) + |Check https://status.coursera.org/ and try again in a few minutes. + """.stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera repsonds with an unexpected status code + else { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera replied with an unexpected code (Status $code) + """.stripMargin + s.log.error(failedSubmitMsg) + } + } + + // kick it all off, actually make request + postSubmission(json) match { + case Success(resp) => reportCourseraResponse(resp) + case Failure(e) => + val failedConnectMsg = + s"""|Connection to Coursera failed. + |There was something wrong while attempting to connect to Coursera. + |Check your internet connection. + |${e.toString}""".stripMargin + s.log.error(failedConnectMsg) + } + + } +*/ + + def failSubmit(): Nothing = { + sys.error("Submission failed") + } + + /** + * ***************** + * DEALING WITH JARS + */ + def encodeBase64(bytes: Array[Byte]): String = + new String(Base64.encodeBase64(bytes)) +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..c0bab04 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.8 diff --git a/project/buildSettings.sbt b/project/buildSettings.sbt new file mode 100644 index 0000000..f7847f8 --- /dev/null +++ b/project/buildSettings.sbt @@ -0,0 +1,7 @@ +// Used for base64 encoding +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" + +// Used for Coursera submussion +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.3.0" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.9" + diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..64a2492 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("io.get-coursier" % "sbt-coursier" % "2.0.0-RC3-5") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.3.4") diff --git a/src/main/scala/streams/Bloxorz.scala b/src/main/scala/streams/Bloxorz.scala new file mode 100644 index 0000000..86e3967 --- /dev/null +++ b/src/main/scala/streams/Bloxorz.scala @@ -0,0 +1,49 @@ +package streams + +/** + * A main object that can be used to execute the Bloxorz solver + */ +object Bloxorz extends App { + + /** + * A level constructed using the `InfiniteTerrain` trait which defines + * the terrain to be valid at every position. + */ + object InfiniteLevel extends Solver with InfiniteTerrain { + val startPos = Pos(1,3) + val goal = Pos(5,8) + } + + println(InfiniteLevel.solution) + + /** + * A simple level constructed using the StringParserTerrain + */ + abstract class Level extends Solver with StringParserTerrain + + object Level0 extends Level { + val level = + """------ + |--ST-- + |--oo-- + |--oo-- + |------""".stripMargin + } + + println(Level0.solution) + + /** + * Level 1 of the official Bloxorz game + */ + object Level1 extends Level { + val level = + """ooo------- + |oSoooo---- + |ooooooooo- + |-ooooooooo + |-----ooToo + |------ooo-""".stripMargin + } + + println(Level1.solution) +} diff --git a/src/main/scala/streams/GameDef.scala b/src/main/scala/streams/GameDef.scala new file mode 100644 index 0000000..4ae4eeb --- /dev/null +++ b/src/main/scala/streams/GameDef.scala @@ -0,0 +1,167 @@ +package streams + +/** + * This trait represents the layout and building blocks of the game + */ +trait GameDef { + + /** + * The case class `Pos` encodes positions in the terrain. + * + * IMPORTANT NOTE + * - The `row` coordinate denotes the position on the vertical axis + * - The `col` coordinate is used for the horizontal axis + * - The coordinates increase when moving down and right + * + * Illustration: + * + * 0 1 2 3 <- col axis + * 0 o o o o + * 1 o o o o + * 2 o # o o # is at position Pos(2, 1) + * 3 o o o o + * + * ^ + * | + * + * row axis + */ + case class Pos(row: Int, col: Int) { + /** The position obtained by changing the `row` coordinate by `d` */ + def deltaRow(d: Int): Pos = copy(row = row + d) + + /** The position obtained by changing the `col` coordinate by `d` */ + def deltaCol(d: Int): Pos = copy(col = col + d) + } + + /** + * The position where the block is located initially. + * + * This value is left abstract, it will be defined in concrete + * instances of the game. + */ + def startPos: Pos + + /** + * The target position where the block has to go. + * This value is left abstract. + */ + def goal: Pos + + /** + * The terrain is represented as a function from positions to + * booleans. The function returns `true` for every position that + * is inside the terrain. + * + * As explained in the documentation of class `Pos`, the `row` axis + * is the vertical one and increases from top to bottom. + */ + type Terrain = Pos => Boolean + + + /** + * The terrain of this game. This value is left abstract. + */ + def terrain: Terrain + + + /** + * In Bloxorz, we can move left, right, Up or down. + * These moves are encoded as case objects. + */ + sealed abstract class Move + case object Left extends Move + case object Right extends Move + case object Up extends Move + case object Down extends Move + + /** + * This function returns the block at the start position of + * the game. + */ + def startBlock: Block = ??? + + + /** + * A block is represented by the position of the two cubes that + * it consists of. We make sure that `b1` is lexicographically + * smaller than `b2`. + */ + case class Block(b1: Pos, b2: Pos) { + + // checks the requirement mentioned above + require(b1.row <= b2.row && b1.col <= b2.col, s"Invalid block position: b1=$b1, b2=$b2") + + /** + * Returns a block where the `row` coordinates of `b1` and `b2` are + * changed by `d1` and `d2`, respectively. + */ + def deltaRow(d1: Int, d2: Int) = Block(b1.deltaRow(d1), b2.deltaRow(d2)) + + /** + * Returns a block where the `col` coordinates of `b1` and `b2` are + * changed by `d1` and `d2`, respectively. + */ + def deltaCol(d1: Int, d2: Int) = Block(b1.deltaCol(d1), b2.deltaCol(d2)) + + + /** The block obtained by moving left */ + def left = + if isStanding then + deltaCol(-2, -1) + else if b1.row == b2.row then + deltaCol(-1, -2) + else + deltaCol(-1, -1) + + /** The block obtained by moving right */ + def right = + if isStanding then + deltaCol(1, 2) + else if b1.row == b2.row then + deltaCol(2, 1) + else + deltaCol(1, 1) + + /** The block obtained by moving up */ + def up = + if isStanding then + deltaRow(-2, -1) + else if b1.row == b2.row then + deltaRow(-1, -1) + else + deltaRow(-1, -2) + + /** The block obtained by moving down */ + def down = + if isStanding then + deltaRow(1, 2) + else if b1.row == b2.row then + deltaRow(1, 1) + else + deltaRow(2, 1) + + + /** + * Returns the list of blocks that can be obtained by moving + * the current block, together with the corresponding move. + */ + def neighbors: List[(Block, Move)] = ??? + + /** + * Returns the list of positions reachable from the current block + * which are inside the terrain. + */ + def legalNeighbors: List[(Block, Move)] = ??? + + /** + * Returns `true` if the block is standing. + */ + def isStanding: Boolean = ??? + + /** + * Returns `true` if the block is entirely inside the terrain. + */ + def isLegal: Boolean = ??? + } +} diff --git a/src/main/scala/streams/InfiniteTerrain.scala b/src/main/scala/streams/InfiniteTerrain.scala new file mode 100644 index 0000000..e43cc16 --- /dev/null +++ b/src/main/scala/streams/InfiniteTerrain.scala @@ -0,0 +1,15 @@ +package streams + +/** + * This trait defines an infinite terrain, where the block can + * go on any position. + * + * It keeps the `startPos` and the `goal` positions abstract. + * + * Using this trait is useful for testing. It can be used to find + * the shortest path between two positions without terrain + * restrictions. + */ +trait InfiniteTerrain extends GameDef { + val terrain: Terrain = (pos: Pos) => true +} diff --git a/src/main/scala/streams/Solver.scala b/src/main/scala/streams/Solver.scala new file mode 100644 index 0000000..0b65d86 --- /dev/null +++ b/src/main/scala/streams/Solver.scala @@ -0,0 +1,85 @@ +package streams + +/** + * This component implements the solver for the Bloxorz game + */ +trait Solver extends GameDef { + + /** + * Returns `true` if the block `b` is at the final position + */ + def done(b: Block): Boolean = ??? + + /** + * This function takes two arguments: the current block `b` and + * a list of moves `history` that was required to reach the + * position of `b`. + * + * The `head` element of the `history` list is the latest move + * that was executed, i.e. the last move that was performed for + * the block to end up at position `b`. + * + * The function returns a lazy list of pairs: the first element of + * the each pair is a neighboring block, and the second element + * is the augmented history of moves required to reach this block. + * + * It should only return valid neighbors, i.e. block positions + * that are inside the terrain. + */ + def neighborsWithHistory(b: Block, history: List[Move]): LazyList[(Block, List[Move])] = ??? + + /** + * This function returns the list of neighbors without the block + * positions that have already been explored. We will use it to + * make sure that we don't explore circular paths. + */ + def newNeighborsOnly(neighbors: LazyList[(Block, List[Move])], + explored: Set[Block]): LazyList[(Block, List[Move])] = ??? + + /** + * The function `from` returns the lazy list of all possible paths + * that can be followed, starting at the `head` of the `initial` + * lazy list. + * + * The blocks in the lazy list `initial` are sorted by ascending path + * length: the block positions with the shortest paths (length of + * move list) are at the head of the lazy list. + * + * The parameter `explored` is a set of block positions that have + * been visited before, on the path to any of the blocks in the + * lazy list `initial`. When search reaches a block that has already + * been explored before, that position should not be included a + * second time to avoid cycles. + * + * The resulting lazy list should be sorted by ascending path length, + * i.e. the block positions that can be reached with the fewest + * amount of moves should appear first in the lazy list. + * + * Note: the solution should not look at or compare the lengths + * of different paths - the implementation should naturally + * construct the correctly sorted lazy list. + */ + def from(initial: LazyList[(Block, List[Move])], + explored: Set[Block]): LazyList[(Block, List[Move])] = ??? + + /** + * The lazy list of all paths that begin at the starting block. + */ + lazy val pathsFromStart: LazyList[(Block, List[Move])] = ??? + + /** + * Returns a lazy list of all possible pairs of the goal block along + * with the history how it was reached. + */ + lazy val pathsToGoal: LazyList[(Block, List[Move])] = ??? + + /** + * The (or one of the) shortest sequence(s) of moves to reach the + * goal. If the goal cannot be reached, the empty list is returned. + * + * Note: the `head` element of the returned list should represent + * the first move that the player should perform from the starting + * position. + */ + lazy val solution: List[Move] = ??? +} diff --git a/src/main/scala/streams/StringParserTerrain.scala b/src/main/scala/streams/StringParserTerrain.scala new file mode 100644 index 0000000..12960c6 --- /dev/null +++ b/src/main/scala/streams/StringParserTerrain.scala @@ -0,0 +1,72 @@ +package streams + +/** + * This component implements a parser to define terrains from a + * graphical ASCII representation. + * + * When mixing in that component, a level can be defined by + * defining the field `level` in the following form: + * + * val level = + * """------ + * |--ST-- + * |--oo-- + * |--oo-- + * |------""".stripMargin + * + * - The `-` character denotes parts which are outside the terrain + * - `o` denotes fields which are part of the terrain + * - `S` denotes the start position of the block (which is also considered + inside the terrain) + * - `T` denotes the final position of the block (which is also considered + inside the terrain) + * + * In this example, the first and last lines could be omitted, and + * also the columns that consist of `-` characters only. + */ +trait StringParserTerrain extends GameDef { + + /** + * A ASCII representation of the terrain. This field should remain + * abstract here. + */ + val level: String + + /** + * This method returns terrain function that represents the terrain + * in `levelVector`. The vector contains parsed version of the `level` + * string. For example, the following level + * + * val level = + * """ST + * |oo + * |oo""".stripMargin + * + * is represented as + * + * Vector(Vector('S', 'T'), Vector('o', 'o'), Vector('o', 'o')) + * + * The resulting function should return `true` if the position `pos` is + * a valid position (not a '-' character) inside the terrain described + * by `levelVector`. + */ + def terrainFunction(levelVector: Vector[Vector[Char]]): Pos => Boolean = ??? + + /** + * This function should return the position of character `c` in the + * terrain described by `levelVector`. You can assume that the `c` + * appears exactly once in the terrain. + * + * Hint: you can use the functions `indexWhere` and / or `indexOf` of the + * `Vector` class + */ + def findChar(c: Char, levelVector: Vector[Vector[Char]]): Pos = ??? + + private lazy val vector: Vector[Vector[Char]] = + Vector(level.split("\r?\n").map(str => Vector(str: _*)).toIndexedSeq: _*) + + lazy val terrain: Terrain = terrainFunction(vector) + lazy val startPos: Pos = findChar('S', vector) + lazy val goal: Pos = findChar('T', vector) + +} diff --git a/src/test/scala/streams/BloxorzSuite.scala b/src/test/scala/streams/BloxorzSuite.scala new file mode 100644 index 0000000..bebf970 --- /dev/null +++ b/src/test/scala/streams/BloxorzSuite.scala @@ -0,0 +1,74 @@ +package streams + +import org.junit._ +import org.junit.Assert.assertEquals + +import Bloxorz._ + +class BloxorzSuite { + trait SolutionChecker extends GameDef with Solver with StringParserTerrain { + /** + * This method applies a list of moves `ls` to the block at position + * `startPos`. This can be used to verify if a certain list of moves + * is a valid solution, i.e. leads to the goal. + */ + def solve(ls: List[Move]): Block = + ls.foldLeft(startBlock) { case (block, move) => + require(block.isLegal) // The solution must always lead to legal blocks + move match + case Left => block.left + case Right => block.right + case Up => block.up + case Down => block.down + } + } + + trait Level1 extends SolutionChecker { + /* terrain for level 1*/ + + val level = + """ooo------- + |oSoooo---- + |ooooooooo- + |-ooooooooo + |-----ooToo + |------ooo-""".stripMargin + + val optsolution = List(Right, Right, Down, Right, Right, Right, Down) + } + + + @Test def `terrain function level 1 (10pts)`: Unit = + new Level1 { + assert(terrain(Pos(0,0)), "0,0") + assert(terrain(Pos(1,1)), "1,1") // start + assert(terrain(Pos(4,7)), "4,7") // goal + assert(terrain(Pos(5,8)), "5,8") + assert(!terrain(Pos(5,9)), "5,9") + assert(terrain(Pos(4,9)), "4,9") + assert(!terrain(Pos(6,8)), "6,8") + assert(!terrain(Pos(4,11)), "4,11") + assert(!terrain(Pos(-1,0)), "-1,0") + assert(!terrain(Pos(0,-1)), "0,-1") + } + + @Test def `find char level 1 (10pts)`: Unit = + new Level1 { + assertEquals(Pos(1, 1), startPos) + } + + + @Test def `optimal solution for level 1 (5pts)`: Unit = + new Level1 { + assertEquals(Block(goal, goal), solve(solution)) + } + + + @Test def `optimal solution length for level 1 (5pts)`: Unit = + new Level1 { + assertEquals(optsolution.length, solution.length) + } + + + @Rule def individualTestTimeout = new org.junit.rules.Timeout(10 * 1000) +} diff --git a/student.sbt b/student.sbt new file mode 100644 index 0000000..855fa0c --- /dev/null +++ b/student.sbt @@ -0,0 +1,9 @@ +// Used for base64 encoding +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" + +// Used for Coursera submussion +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4" + +// Student tasks (i.e. packageSubmission) +enablePlugins(StudentTasks)