/
SnakeCube.scala
149 lines (122 loc) · 5.71 KB
/
SnakeCube.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package yuima.funcpearls
import scala.annotation.tailrec
/** Scala implementation of "Solving the Snake Cube Puzzle in Haskel."
* http://web.cecs.pdx.edu/~mpj/snakecube/revised-SnakeCube.pdf
*
* Note: No implementation for a function "advance" in section 9.
*
* @author Yuichiroh Matsubayashi
* Created on 14/11/28.
*/
object SnakeCube {
type Section = List[Position]
type Solution = List[Section]
val snake = List(3, 2, 2, 3, 2, 3, 2, 2, 3, 3, 2, 2, 2, 3, 3, 3, 3)
val standard = SnakeCubePuzzle(
sections = snake,
valid = inCube(3)(_),
initialSolution = List(List(Position(1, 1, 1))),
initialDirection = Direction(0, 0, 1)
)
val meanGreen = standard.copy(sections = List(3, 3, 2, 3, 2, 3, 2, 2, 2, 3, 3, 3, 2, 3, 3, 3))
val king = standard.copy(valid = inCube(4)(_), sections = List(
3, 2, 3, 2, 2, 4, 2, 3, 2, 3, 2, 3, 2, 2, 2,
2, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2, 2, 3, 4, 2,
2, 2, 4, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 2)
)
val king1 = king.copy(initialSolution = List(List(Position(2, 1, 1))))
val king2 = king.copy(initialSolution = List(List(Position(1, 2, 2))))
/** Colors used for visualization.
* Cubes having an odd index are brown and even index are white. */
val colors = Iterator.continually(Seq("brown", "white")).flatten
def main(args: Array[String]) {
solutions(standard).foreach(println)
}
/** gets solutions of a puzzle using a brute force algorithm. */
def solutions(p: SnakeCubePuzzle) = {
def solve(solution: Solution)(prevDir: Direction)(sections: List[Int]): List[Solution] = sections match {
case Nil => List(solution)
case length :: secs =>
newDirections(prevDir).flatMap { newDir =>
extend(p)(solution)(newDir)(length).flatMap { newSolution => solve(newSolution)(newDir)(secs) }
}
}
solve(p.initialSolution)(p.initialDirection)(p.sections)
}
/** gets possible directions for a next move. */
def newDirections(prev: Direction) = List(
Direction(prev.v, prev.w, prev.u),
Direction(-prev.v, -prev.w, -prev.u),
Direction(prev.w, prev.u, prev.v),
Direction(-prev.w, -prev.u, -prev.v)
)
/** extends current move sequences with a move toward a particular direction.
* Note: we must ensure that all of the positions in next section are valid in the given puzzle,
* and we must also check that none of the positions in next section have already been occupied by
* other sections in the starting solution.
* @param p puzzle definition
* @param solution the move sequence so far
* @param dir the direction of a next move
* @param length the cube length of the next section
**/
def extend(p: SnakeCubePuzzle)(solution: Solution)(dir: Direction)(length: Int) = {
val start = solution.head.head
val nextSec = section(start)(dir)(length)
if (nextSec.forall(p.valid) && solution.forall(sec => (nextSec intersect sec).isEmpty)) List(nextSec :: solution)
else Nil
}
/** obtains small-cube positions for a next move.
* @param start the beginning position of the move.
* @param d the direction of the move.
* @param length the small-cube length of the move.
* */
def section(start: Position)(d: Direction)(length: Int) = {
def pieces = Stream.iterate(start) { pos: Position => Position(pos.x + d.u, pos.y + d.v, pos.z + d.w) }
pieces.take(length).tail.reverse.toList
}
/** creates a variant of a puzzle by reversing the order of the sections. */
def reversePuzzle(p: SnakeCubePuzzle) = p.copy(sections = p.sections.reverse)
/** checks whether a given cube location is valid for a solution (it should be inside of the large cube).
* @param size the cube size of the solution (i.e. the cube is size * size * size)
* @param pos the position of a target cube.
**/
def inCube(size: Int)(pos: Position) = {
def inRange(k: Int) = 1 <= k && k <= size
inRange(pos.x) && inRange(pos.y) && inRange(pos.z)
}
/** converts an objective of the puzzle to a different one --that is to find the most-compact,
* flat form where all of the sections in a single level. (i.e. z == 1 for all cubes) */
def flatPuzzle(p: SnakeCubePuzzle) = p.copy(valid = (pos: Position) => pos.z == 1)
/** returns sketch format data representing a one of the solution. */
def showSteps(p: SnakeCubePuzzle) = steps(solutions(p).head).map(showCubes).mkString("\n")
def steps(solution: Solution) = solution.map(_.reverse).reverse.tail
/** returns polygon info of cubes for the sketching tool.
* @param positions the cube positions
**/
def showCubes(positions: List[Position]) = positions.map(p => cube2sketch(colors.next)(p)).mkString("\n")
/** returns polygon info of a single cube for the sketching tool. */
def cube2sketch(color: String)(a: Position) = {
val prefix = s"polygon[fill=$color]"
val Position(x, y, z) = a
val b = (x, y, z - 1)
val c = (x, y - 1, z)
val d = (x - 1, y, z)
val e = (x, y - 1, z - 1)
val f = (x - 1, y, z - 1)
val g = (x - 1, y - 1, z)
val h = (x - 1, y - 1, z - 1)
val faces = List(List(a, d, g, c), List(b, e, h, f), List(a, b, f, d), List(c, g, h, e), List(a, c, e, b), List(d, f, h, g))
faces.map(f => prefix + f.mkString("")).mkString("\n")
}
/** SnakeCube puzzle */
case class SnakeCubePuzzle(sections: List[Int],
valid: Position => Boolean,
initialSolution: Solution,
initialDirection: Direction)
case class Direction(u: Int, v: Int, w: Int) {
require(u.abs + v.abs + w.abs == 1 && u.abs <= 1 && v.abs <= 1 && w.abs <= 1)
}
case class Position(x: Int, y: Int, z: Int) {
override def toString = Seq(x, y, z).mkString("(", ",", ")")
}
}