summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--snake-cube.tl128
1 files changed, 114 insertions, 14 deletions
diff --git a/snake-cube.tl b/snake-cube.tl
index dfaae2e..8a01209 100644
--- a/snake-cube.tl
+++ b/snake-cube.tl
@@ -1,13 +1,47 @@
+;; Snake Cube Solver
+;;
+;; Copyright 2021
+;; Kaz Kylheku <kaz@kylheku.com>
+;; Vancouver, Canada
+;; All rights reserved.
+;;
+;; Redistribution and use in source and binary forms, with or without
+;; modification, are permitted provided that the following conditions are met:
+;;
+;; 1. Redistributions of source code must retain the above copyright notice, this
+;; list of conditions and the following disclaimer.
+;;
+;; 2. Redistributions in binary form must reproduce the above copyright notice,
+;; this list of conditions and the following disclaimer in the documentation
+;; and/or other materials provided with the distribution.
+;;
+;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+;; ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+;; DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+;; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+;; CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+;; OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+;; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;; Context object specifies problem parameters,
+;; and provides a container for gathering solutions
+;; during the recursive search.
+
(defstruct (solve-context l w h) ()
- l w h
- vol
- sz
- sols
+ l w h ;; dimensions of rectangular prism to be filled
+ vol ;; product of dimensions
+ sz ;; "size" -- longest dimension
+ sols ;; list of solutions
(:postinit (me)
(set me.vol (* me.l me.w me.h))
(set me.sz (max me.l me.w me.h)))
+ ;; Solutions are themselves lists of piece objects linked via their parent
+ ;; fields, in reverse. This method converts the list of solutions
+ ;; into a list of ordinary Lisp lists.
(:method sol-lists (me)
(build
(each ((s me.sols))
@@ -15,20 +49,27 @@
(for ((i s)) (i) ((set i i.parent))
(add* i))))))))
+;; Map from piece type to its symbolic nickname, e.g. straight-piece -> s.
(defvarl type-to-sym (hash))
+
+;; Reverse map: e.g. s -> straight-piece.
(defvarl sym-to-type (hash))
+;; base class for piecews.
(defstruct piece ()
- parent
- x y z
- max-x max-y max-z
- min-x min-y min-z
- l w h
- (orientation :z0)
+ parent ;; previous piece in the chain being constructed.
+ x y z ;; coordinates of bottom-left-lower corner of piece
+ max-x max-y max-z ;; bounding box minima and maxima of this piece
+ min-x min-y min-z ;; and all its parent ancestors.
+ l w h ;; bounding box expressed as length-width-height
+ (orientation :z0) ;; orientation for next piece: z0, z1, y0, y1, x0, x1.
+ ;; print piece incondensed notation: just nickname sym and coordinates
(:method print (me stream pretty-p)
(print ^(,me.sym ,me.x ,me.y ,me.z) stream pretty-p))
+ ;; initialize piece: calculate the bounding box and volume
+ ;; of the combination of this piece and its parent.
(:postinit (me)
(set me.max-x (succ me.x)
me.max-y (succ me.y)
@@ -47,19 +88,27 @@
me.l (- me.max-y me.min-y)
me.h (- me.max-z me.min-z)))
+ ;; Check piece for invalid intersections. Returns true if okay,
+ ;; otherwise nils.
(:method intersect-check (me ctx)
(and
- ;; no self intersection
+ ;; No self-intersection of snake chain allowed.
(for ((par me.parent) (ok t)) ((and par ok) ok) ((set par par.parent))
(when (and (eql me.x par.x)
(eql me.y par.y)
(eql me.z par.z))
(zap ok)))
- ;; no out of bounds
+ ;; Bounding box of chain must not exceed box size.
+ ;; This check isn't necessary, but vastly speeds up the search.
(<= me.l ctx.sz)
(<= me.w ctx.sz)
(<= me.h ctx.sz)))
+ ;; Shape check: has the chain produced the required box?
+ ;; This is only tested at the end of the chain when all the pieces
+ ;; are in place. We know that the chain has the right number of
+ ;; pieces, e.g. 27 for 3x3x3 box from the outset. We check for
+ ;; the box only when all pieces are added to the chain
(:method shape-check (me ctx)
(let ((mw me.w) (ml me.l) (mh me.h)
(w ctx.w) (l ctx.l) (h ctx.h))
@@ -70,46 +119,85 @@
(and (eql mw h) (eql ml w) (eql mh l))
(and (eql mw h) (eql ml l) (eql mh w)))))
+ ;; Solution check done at end of chain: if there is
+ ;; a shape match in any orientation, then add the chain
+ ;; to the list of solutions.
(:method solved-check (me ctx)
(if me.(shape-check ctx)
(push me ctx.sols)))
+ ;; Derived hook: add the nickname and type of the piece type
+ ;; to the hashes.
(:function derived (super sub)
(let ((sym (static-slot sub 'sym)))
(set [sym-to-type sym] sub
[type-to-sym sub] sym))))
+;; Straight piece class.
(defstruct straight-piece piece
- (:static sym 's)
+ (:static sym 's) ;; nickname symbol is s.
+
(:method solve (me symbols ctx)
(if symbols
+ ;; If there are pieces left in the chain,
+ ;; continue the solution search by constructing the next
+ ;; piece in the list, and adding it to this straight
+ ;; piece according to the configuration. Then
+ ;; recurse to the new piece's solution method.
(let* ((ori me.orientation)
(x me.x)
(y me.y)
(z me.z)
(nx-loc (caseq ori
+ ;; There are six possible orientations
+ ;; wher the next piece may go, two
+ ;; for each axis. For each orientation,
+ ;; pick the xyz coordinates for the next piece.
(:z0 ^#(,x ,y ,(pred z)))
(:z1 ^#(,x ,y ,(succ z)))
(:x0 ^#(,(pred x) ,y ,z))
(:x1 ^#(,(succ x) ,y ,z))
(:y0 ^#(,x ,(pred y) ,z))
(:y1 ^#(,x ,(succ y) ,z)))))
+ ;; Construct piece from the calculated xyz coordinates.
+ ;; The parent of the piece is this one, and its orientation
+ ;; is inherited from this one, since this one is straight.
(when-match #(@nx @ny @nz) nx-loc
(let ((nx (new* ([sym-to-type (car symbols)])
parent me orientation ori x nx y ny z nz)))
+ ;; The chain can only continue by recursing into
+ ;; the next piece's solve method, if the intersection
+ ;; check passes. If we hit self-intersection or
+ ;; exceed the bounding box, we bail out and backtrack.
(if nx.(intersect-check ctx)
nx.(solve (cdr symbols) ctx)))))
- me.(solved-check ctx))))
+ ;; No pieces left: check if the chain is a solution
+ ;; and add it to the list if so.
+ me.(solved-check ctx))))
+;; Elbow piece class: more tricky than straight piece.
(defstruct elbow-piece piece
(:static sym 'e)
(:method solve (me symbols ctx)
(if symbols
+ ;; If there are pieces left in the chain,
+ ;; continue the solution search by constructing four
+ ;; new pieces, for every possible rotation of the
+ ;; elbow. Try all four ways of continuing by recursing
+ ;; on the solution method of all four ways, or
+ ;; at least those which don't fail the intersection
+ ;; criteria.
(let* ((ori me.orientation)
(x me.x)
(y me.y)
(z me.z)
(nx-ori-loc (caseq ori
+ ;; The elbow piece may be oriented
+ ;; along any of the three axes,
+ ;; and provides four rotations.
+ ;; For each rotation we calculate
+ ;; the next orientation and xyz
+ ;; coordinates.
((:z0 :z1) ^#(#(:x0 ,(pred x) ,y ,z)
#(:x1 ,(succ x) ,y ,z)
#(:y0 ,x ,(pred y) ,z)
@@ -122,20 +210,32 @@
#(:x1 ,(succ x) ,y ,z)
#(:z0 ,x ,y ,(pred z))
#(:z1 ,x ,y ,(succ z)))))))
+ ;; Iterate over the four rotations, and construct
+ ;; the next piece, using the orientation and coordinates
+ ;; from each rotation.
(each-match (#(@nori @nx @ny @nz) nx-ori-loc)
(let ((nx (new* ([sym-to-type (car symbols)])
parent me orientation nori x nx y ny z nz)))
(if nx.(intersect-check ctx)
nx.(solve (cdr symbols) ctx)))))
+ ;; Same as with straight piece: check for solution.
me.(solved-check ctx))))
+;; Main solve function: takes a list of symbol nicknames and length-width-height
+;; parameters.
(defun solve (symbols l w h)
(let ((len (length symbols))
(ctx (new (solve-context l w h))))
(cond
+ ;; empty list of symbols means no solutions
((null symbols) symbols)
+ ;; length of list doesn't correspond to solution volume: no solution.
((neq ctx.vol len) nil)
+ ;; Convert first symbol to a class, then call the solve method
+ ;; for the rest of the symbols.
(t (let ((piece (new* ([sym-to-type (car symbols)])
orientation :z1 x 0 y 0 z 0)))
piece.(solve (rest symbols) ctx)
+ ;; Extract and return list of solutions, in simplified
+ ;; list structure
ctx.(sol-lists))))))