Skip to content

Instantly share code, notes, and snippets.

@zlqhem
Created November 14, 2013 00:48
Show Gist options
  • Save zlqhem/7459312 to your computer and use it in GitHub Desktop.
Save zlqhem/7459312 to your computer and use it in GitHub Desktop.
7주차 숙제 @ Functional Programming Principles in Scala https://class.coursera.org/progfun-003/assignment/view?assignment_id=19
package streams
import common._
/**
* This trait represents the layout and building blocks of the game
*
* @TODO: SHOULD RENAME `x` and `y` in class Pos to `row` and `col`. It's
* confusing to have `x` being the vertical axis.
*/
trait GameDef {
/**
* The case class `Pos` encodes positions in the terrain.
*
* IMPORTANT NOTE
* - The `x` coordinate denotes the position on the vertical axis
* - The `y` coordinate is used for the horizontal axis
* - The coordinates increase when moving down and right
*
* Illustration:
*
* 0 1 2 3 <- y 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
*
* ^
* |
*
* x axis
*/
case class Pos(x: Int, y: Int) {
/** The position obtained by changing the `x` coordinate by `d` */
def dx(d: Int) = copy(x = x + d)
/** The position obtained by changing the `y` coordinate by `d` */
def dy(d: Int) = copy(y = y + d)
}
/**
* The position where the block is located initially.
*
* This value is left abstract, it will be defined in concrete
* instances of the game.
*/
val startPos: Pos
/**
* The target position where the block has to go.
* This value is left abstract.
*/
val 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 `x` 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.
*/
val 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 = Block(startPos, startPos)
/**
* 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.x <= b2.x && b1.y <= b2.y, "Invalid block position: b1=" + b1 + ", b2=" + b2)
/**
* Returns a block where the `x` coordinates of `b1` and `b2` are
* changed by `d1` and `d2`, respectively.
*/
def dx(d1: Int, d2: Int) = Block(b1.dx(d1), b2.dx(d2))
/**
* Returns a block where the `y` coordinates of `b1` and `b2` are
* changed by `d1` and `d2`, respectively.
*/
def dy(d1: Int, d2: Int) = Block(b1.dy(d1), b2.dy(d2))
/** The block obtained by moving left */
def left = if (isStanding) dy(-2, -1)
else if (b1.x == b2.x) dy(-1, -2)
else dy(-1, -1)
/** The block obtained by moving right */
def right = if (isStanding) dy(1, 2)
else if (b1.x == b2.x) dy(2, 1)
else dy(1, 1)
/** The block obtained by moving up */
def up = if (isStanding) dx(-2, -1)
else if (b1.x == b2.x) dx(-1, -1)
else dx(-1, -2)
/** The block obtained by moving down */
def down = if (isStanding) dx(1, 2)
else if (b1.x == b2.x) dx(1, 1)
else dx(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)] =
List((left, Left), (right, Right), (up, Up), (down, Down))
/**
* Returns the list of positions reachable from the current block
* which are inside the terrain.
*/
def legalNeighbors: List[(Block, Move)] =
neighbors.filter({ case (block, _) => block.isLegal })
/**
* Returns `true` if the block is standing.
*/
def isStanding: Boolean = b1.x == b2.x && b1.y == b2.y
/**
* Returns `true` if the block is entirely inside the terrain.
*/
def isLegal: Boolean = terrain(b1) && terrain(b2)
}
}
package streams
import common._
/**
* 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 = b.b1 == goal && b.b2 == goal
/**
* 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 stream 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]): Stream[(Block, List[Move])] =
{
b.legalNeighbors.map((x:(Block, Move)) => (x._1, x._2::history))
}.toStream
/**
* 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: Stream[(Block, List[Move])],
explored: Set[Block]): Stream[(Block, List[Move])] =
neighbors.filter{ case (block, _) => !explored.contains(block) }
/**
* The function `from` returns the stream of all possible paths
* that can be followed, starting at the `head` of the `initial`
* stream.
*
* The blocks in the stream `initial` are sorted by ascending path
* length: the block positions with the shortest paths (length of
* move list) are at the head of the stream.
*
* The parameter `explored` is a set of block positions that have
* been visited before, on the path to any of the blocks in the
* stream `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 stream 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 stream.
*
* Note: the solution should not look at or compare the lengths
* of different paths - the implementation should naturally
* construct the correctly sorted stream.
*/
def from(initial: Stream[(Block, List[Move])],
explored: Set[Block]): Stream[(Block, List[Move])] = {
if (initial == Stream.empty) {
Stream.empty
}
else {
val nextBlocks = initial.flatMap {
case (block, history) =>
newNeighborsOnly(neighborsWithHistory(block, history), explored)
}
initial ++ from(nextBlocks, explored.union(nextBlocks.map(_._1).toSet))
}
}
/**
* The stream of all paths that begin at the starting block.
*/
lazy val pathsFromStart: Stream[(Block, List[Move])] =
from((startBlock, Nil) #:: Stream.empty, Set())
/**
* Returns a stream of all possible pairs of the goal block along
* with the history how it was reached.
*/
lazy val pathsToGoal: Stream[(Block, List[Move])] =
pathsFromStart.filter { case (Block(b1, b2),_) => b1 == goal && b2 == goal }
/**
* 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] = if (pathsToGoal == Stream.empty) Nil
else pathsToGoal.head._2
}
package streams
import common._
/**
* 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 = {
def valid(x:Int, y:Int) = x >= 0 && y >= 0 &&
x < levelVector.length &&
y < levelVector(x).length
{
case Pos(x,y) if valid(x,y) => levelVector(x)(y) != '-'
case _ => false
}
}
/**
* 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 = {
val indexVec = levelVector.map(row => row.indexOf(c))
val x = indexVec.indexWhere(ch => ch != -1)
val y = indexVec(x)
Pos(x, y)
}
private lazy val vector: Vector[Vector[Char]] =
Vector(level.split("\n").map(str => Vector(str: _*)): _*)
lazy val terrain: Terrain = terrainFunction(vector)
lazy val startPos: Pos = findChar('S', vector)
lazy val goal: Pos = findChar('T', vector)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment