Solve Hanoi recursively

// This library exposes 3 functions:
//   hanoi.moveDisk(fromPeg, toPeg);  This moves the top disk from the fromPeg to the toPeg
//   hanoi.getSparePeg(fromPeg, toPeg); This returns the remaining peg that isn't the fromPeg or the toPeg
//   hanoi.isSolved(toPeg); This returns true if all disks are on toPeg and no invalid moves have been used
var hanoi = (function() {
var sprites = function() {
"use strict";

    // A minimalist sprite library for KA visualizations.  
    //   Devin Balkcom, June 2014.

    // Uses prototypical object pattern like that described at 
    // http://davidwalsh.name/javascript-objects-deconstruction
    var extend = function(proto, properties) {
        var obj = Object.create(proto);
        for (var p in properties) {
            if ( properties.hasOwnProperty(p) ) {
                obj[p] = properties[p];
            }
        }
        return obj;
    };

    // global constants used to indicate time periods for timelines
    var FOREVER = -1;
    var NONE = -2;

    // optionally pass in a function to draw background, etc in drawInit
    var Scene = {
        init: function(drawInit) {
            this.sprites = [];
            this.drawInit = drawInit;
            this.draggingSprite = NONE;
            this.lastClickedSprite = null;

            this.mousePressed = null;            

        },
        draw: function() {
            if (this.drawInit) { 
                this.drawInit(); 
            }
            for (var i = 0; i < this.sprites.length; i++) {

                var sprite = this.sprites[i];
                sprite.draw();

                // update mouseOver property for sprites and 
                //  selected sprite for the scene
                sprite.mouseOver = false;
                if (sprite.containsPoint(mouseX, mouseY) ) {
                    sprite.mouseOver = true;
                }
            }
            if (this.draggingSprite !== NONE) {
                this.sprites[this.draggingSprite].draw();
            }
        },

        add:  function(sprite) {
            this.sprites.push(sprite);
        },

        removeAll: function(spriteName) {
            for (var i = this.sprites.length - 1; i >= 0 ; i--) {
                if (this.sprites[i].name === spriteName) {
                    this.sprites.splice(i, 1);
                }
            }
        }
    };

    // definition of a grid that can be used to convert between coordinates of
    //  larger cells and pixel coordinates.  Useful for moving sprites between
    //  locations on a on-screen grid. 

    var Grid = {
        // uses object-creation pattern from 
        //  http://davidwalsh.name/javascript-objects-deconstruction

        init: function(leftX, topY, width, height) {
            this.leftX = leftX;
            this.topY = topY;
            this.width = width;
            this.height = height;
        },
        pixelX: function(gridX) {
            return this.leftX + gridX * (this.width);
        },
        pixelY: function(gridY) {
            return this.topY+ gridY * (this.height);
        }
    };

    // Definition of sprite object
    /////////////////////////////

    // name is a string describing the sprite name.  Used to find sprites in the scene
    //  (for example, to remove them).

    // optionally pass in parameters for initial location and height.
    //  However, getX(), getH(), getW(), and getH() are the proper way to get location, width
    //  and height of a sprite.  Typically, these functions are bound to a model
    //  in a model-view design.

    var Sprite = {

        init:  function(name, x, y, w, h) {

            this.name = name;

            this.x = x;
            this.y = y;
            this.w = w;
            this.h = h;

            this.mouseOver = false;
            this.mousePressed = false;

            this.draggable = false;

            this.visible = true;
            this.selected = false;
            this.release = undefined;
        },

        // set up initial functions to return location of the sprite.  For a sprite
        //  that is not bound to a model, these are fine default functions to return location
        getX: function() {
            return this.x;
        },
        getY: function() {
            return this.y;
        },

        getW: function() {
            return this.w;
        },

        getH: function() {
            return this.h;
        },

        show: function() {
            this.visible = true;
        },

        hide: function() {
            this.visible = false;
        },
        select: function() {
            this.selected = true;
        },
        unselect:function() {
            this.selected = false; 
        },

        draw: function() {},

        translate: function(tx, ty) {
            this.x += tx;
            this.y += ty;
        },

        // animate the motion of a sprite from one location to another on a grid.        
        gridMove: function(timeline, workingFrame, grid, fromGridX, fromGridY, toGridX, toGridY) {


            var dx = ( grid.pixelX(toGridX) - grid.pixelX(fromGridX) );
            var dy = ( grid.pixelY(toGridY) - grid.pixelY(fromGridY) );

            // normalize speed
            var length = sqrt(dx * dx + dy * dy);

            var duration = length;

            dx /= length;
            dy /= length;

            timeline.addEvent(workingFrame, duration, this.translate.bind(this, dx, dy) );

            return duration;

        },


        containsPoint: function(x, y) {
            return x >= this.getX() && y >= this.getY()  &&
                x < (this.getX() + this.getW()) && y < (this.getY() + this.getH());
        }
    };

    var TextSprite = Object.create(Sprite);

    TextSprite.init = function(string, x, y, alignment, label) {
        sprites.Sprite.init.call(this, label, x, y, 0, 0);
        this.string = string;
        this.alignment = alignment;
    };

    TextSprite.draw = function() {
        textAlign(this.alignment);
        fill(0, 0, 0);
        text(this.string, this.x, this.y);
        textAlign(LEFT);

    };

    TextSprite.changeText = function(timeline, workingFrame, string) {
        var textSprite = this;
        timeline.addEvent(workingFrame, 1, function() {
            textSprite.string = string;
         } );
    };

    var Timeline = {

        // a simple hashtable with keys that are times (a frame number) 
        //  and values that are functions to run at those times:

        events: [],
        currentFrame: 0, // an int keeping track of current frame to draw
        speed: 1,
        time: 0,  // a float value keeping track of current time on the timeline
        paused: true,

        lastFrame: 0,

        // a list of times that can be advanced to with "step"
        bookmarks: [],
        nextPause: NONE,

        init: function() {
        },

        setSpeed: function(speed) {
            this.speed = speed;
        },

        // add an event function to the list of events.  Optional parameter
        //   fnAtEnd gives a second function to execute after repeats are 
        //   completed 
        addEvent: function(start, repeats, fn, fnAtEnd) {
            var eventObject = {start: start, repeats: repeats, fn: fn};
            this.events.push(eventObject);

            if(fnAtEnd) {
                this.events.push({start: start + repeats - 1, repeats: 1, fn: fnAtEnd});
            }
            this.lastFrame = start + repeats - 1;
        },

        bookmark: function(t) {
            this.bookmarks.push(t);
        },

        update: function() {
            // catch the drawing up to the time by incrementing currentFrame until it
            //  reaches the floor of the time value.
            while(this.currentFrame < floor(this.time) ) {
                for (var i = 0; i < this.events.length; i++) {
                    var event = this.events[i];
                    if(this.currentFrame >= event.start) {
                        if(event.repeats === FOREVER ||
                            this.currentFrame < (event.start + event.repeats)) {
                                event.fn();
                        }
                    }
                }
            
                this.currentFrame++;
            }

            if (this.nextPause !== NONE && this.time >= this.nextPause) {
                this.pause();
            }

            if (!this.paused) {
                this.time += this.speed;
            }

        },

        step: function() {
            // find the least bookmark greater than the current time, and 
            //  enable playing until that time is reached
            for (var i = 0; i < this.bookmarks.length; i++ ) {
                if (this.bookmarks[i] > this.time) {
                    //this.time = this.bookmarks[i];
                    this.nextPause = this.bookmarks[i];
                    this.play();
                    break;
                }
            }
        },

        play: function() {
            this.paused = false;
        },

        pause: function() {
            this.paused = true;
        }
        
    };

    var startAnimation = function(scene, timeline) {

        draw = function() {
            if(typeof timeline !== 'undefined'){
                timeline.update();
            }
            if(typeof scene !== 'undefined'){
                scene.draw();
            }
        };

        mousePressed = function() {

            if(scene.mousePressed) {
                scene.mousePressed();
            }

            for (var i = 0; i < scene.sprites.length; i++) {
                var sprite = scene.sprites[i];
                if(sprite.mouseOver) {
                    sprite.mousePressed = true;
                    scene.lastClickedSprite = sprite;

                    if (sprite.onAction) {
                        sprite.onAction();
                    }

                    if(sprite.draggable) {
                        scene.draggingSprite = i;
                        sprite.lastDragX = mouseX;
                        sprite.lastDragY = mouseY;
                    }
                }
            }

        };

        mouseReleased = function() {
            
            if ((scene.draggingSprite !== NONE) &&
                (scene.sprites[scene.draggingSprite].release !== undefined)) {
                scene.sprites[scene.draggingSprite].release();
            }
            scene.draggingSprite = NONE;

            // clear all mouseDown variables for sprites
            for (var i = 0; i < scene.sprites.length; i++) {
                var sprite = scene.sprites[i];
                sprite.mousePressed = false;
            }
            
        };

        mouseDragged = function() {
            if(scene.draggingSprite !== NONE) {
                var sprite = scene.sprites[scene.draggingSprite];

                if (sprite.draggable) {
                    sprite.translate(mouseX - sprite.lastDragX,
                            mouseY - sprite.lastDragY);
                    sprite.lastDragX = mouseX;
                    sprite.lastDragY = mouseY;
                }
            }
        };

        scene.draw();
    };

    return {extend: extend,
            Scene: Scene,
            Grid: Grid,
            Sprite: Sprite,
            TextSprite: TextSprite,
            Timeline: Timeline,
            startAnimation: startAnimation,
            FOREVER: FOREVER,
            NONE: NONE
        };
}();

var frame = 1;
var WINDOW_SIZE = width || 400;
var objects = (function() {
    var PEG_WIDTH = 20;
    var DISK_HEIGHT = 20;
    var solving = false;
    var draggingEnabled = true;

    // Uses inheritance pattern from 
    //   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
    // ==============================================================================================

    // The disk sprite.
    var Disk = sprites.extend(sprites.Sprite, {
        init: function(diskObject) {
            var w = diskObject.w;
            var h = diskObject.h;
            var number = diskObject.number;
            var peg = diskObject.peg;
            var color = diskObject.color;

            var x = peg.x - w / 2 + peg.w / 2;
            var disksUnder = peg.disks.length;
            var y = peg.y + peg.h- (h+0.1) * (disksUnder + 1);
            
            sprites.Sprite.init.call(this, "Disk " + str(number), x, y, w, h); // calls superclass constructor
            
            this.x=x;
            this.y=y;
            
            this.number = number;
            this.color = color;

            this.draggable = false;
        
            this.release = function() {
                this.dragging = false;
                if (draggingEnabled){
                    //make sure the moved disk came from the top of it's stack
                    if(this.peg.getTopDisk() === this){
                        for (var i = 0; i < pegs.length; i++) {
                            var peg = pegs[i];
                            if (posIsInRect(mouseX, mouseY, peg.x - WINDOW_SIZE / 8, peg.y, WINDOW_SIZE / 4, peg.h)){
                                //check that the destination peg is empty or
                                //that this disk is smaller than the top disk on the destination peg 
                                if ( (peg.countDisks() ===0) ||(this.number < peg.getTopDisk().number ) ) {
                                    this.moveToPeg(peg);
                                }
                                break;
                            }
                        }
                    }
                    //snaps disk back to position when released
                    timeline.addEvent(frame - 3, 1, this.moveToPegPosition.bind(this));
                }
            };

            this.peg = peg;
            this.peg.addDisk(this);
            scene.add(this);
            
        },

        moveToPeg: function(destPeg) {
            // Moves the disk around in the data structures to seemingly move it from one disk to another
            this.peg.removeDisk();
            this.peg = destPeg;
            this.peg.addDisk(this);
            this.dragging = false;
        },
        
        moveToPegPosition: function(){
            //move the position of the disk
            this.x = this.peg.x + this.peg.w / 2 - this.w / 2;
            var disksUnder = this.peg.getNumberOfDisksUnder(this);
            this.y = this.peg.y + this.peg.h - (this.h+0.1) * (disksUnder + 1);
        },

        draw: function() {
            
            textAlign(CENTER, CENTER);
            stroke(lerpColor(0, 0, 0));
 
            fill(this.color[0], this.color[1], this.color[2]);
            rect(this.x, this.y, this.w, this.h, 3);

            // draw disk number
            fill(0, 0, 0);
            text(this.number, this.x + this.w / 2, this.y + this.h / 2);
        },

        onAction: function() {
            this.dragging = draggingEnabled;
            this.draggable = (this === this.peg.getTopDisk());
        }
    });

    var Peg = sprites.extend(sprites.Sprite, {
        init: function(pegObject) {
            var x = pegObject.x;
            var y = pegObject.y;
            var w = pegObject.w;
            var h = pegObject.h;
            var letter = pegObject.letter;
            var number = pegObject.number;

            sprites.Sprite.init.call(this, "Peg " + letter, x, y, w, h);
            this.letter = letter;
            this.number = number;
            this.disks = [];
            scene.add(this);
        },

        removeDisk: function() {
            if(this.disks.length === 0){ return;}
            this.disks.pop();
        },

        addDisk: function(disk) {
            this.disks.push(disk);
        },

        countDisks: function() {
            return this.disks.length;
        },

        getTopDisk: function() {
            return this.disks[this.disks.length-1];
        },

        getNumberOfDisksUnder: function(disk) {
            return this.disks.indexOf(disk);
        },

        draw: function() {
            textAlign(CENTER, CENTER);
            stroke(lerpColor(0, 0, 0));
            fill(149, 149, 149);    // dark gray, peg color from Tom Cormen's images
            rect(this.x, this.y, this.w, this.h, 3);

            // draw peg letter
            fill(0, 0, 0);
            text(this.letter, this.x + this.w / 2, this.y + 15);
        }
    });

    var Table = sprites.extend(sprites.Sprite, {
        init: function(tableObject){
            var x = tableObject.x;
            var y = tableObject.y;
            var w = tableObject.w;
            var h = tableObject.h;

            sprites.Sprite.init.call(this, "Table", x, y, w, h);
            scene.add(this);
        },

        draw: function() {
            textAlign(CENTER, CENTER);
            stroke(lerpColor(0, 0, 0));
            fill(139, 59, 4);
            rect(this.x, this.y, this.w, this.h, 3);
            frame++;
        }
    });

    // End classes
    // ===============================================================================================
    // Start functions

    // From devin's code:
    var posIsInRect = function(x, y, rectX, rectY, width, height) {
        // Returns a boolean for whether the given position is 
        //  within the specified rectangle

        return (x >= rectX && x < rectX + width && y >= rectY && y < rectY + height);
    };

    var workingFrame = 0;

    var updateWorkingFrame = function(value) {
        workingFrame = value;
    };

    var getWorkingFrame = function() {
        return workingFrame;
    };

    // End functions
    //=================================================================================================

    var scene = Object.create(sprites.Scene);
    scene.init(function() {
        background(255, 255, 255);
        textAlign(CENTER, CENTER);
    });
    var timeline = Object.create(sprites.Timeline);

    var pegs = [];
    var disks = [];

    var numberOfDisks = 5;

    var calcDiskHight = DISK_HEIGHT;

    if ((numberOfDisks + 1) * DISK_HEIGHT > (WINDOW_SIZE / 6) * 3 - DISK_HEIGHT  * 1.3) {
        calcDiskHight = ((WINDOW_SIZE / 6) * 3 - DISK_HEIGHT  * 1.3) / numberOfDisks;
    }

    var diskColors = [[252, 51, 61], [252, 106, 7], [255, 255, 11],
            [69, 255, 60], [64, 146, 255]];

    // Create pegs and add them to the list
    var peg = Object.create(Peg);
    peg.init({x: (WINDOW_SIZE / 4) * 1 - PEG_WIDTH / 2,
        y: WINDOW_SIZE / 5,
        w: PEG_WIDTH,
        h: (WINDOW_SIZE / 6) * 3,
        letter: "A",
        number: 1,
        disks: []
    });

    pegs.push(peg);

    peg = Object.create(Peg);
    peg.init({x: (WINDOW_SIZE / 4) * 2 - PEG_WIDTH / 2,
        y: WINDOW_SIZE / 5,
        w: PEG_WIDTH,
        h: (WINDOW_SIZE / 6) * 3,
        letter: "B",
        number: 2,
        disks: []
    });

    pegs.push(peg);

    peg = Object.create(Peg);
    peg.init({x: (WINDOW_SIZE / 4) * 3 - PEG_WIDTH / 2,
        y: WINDOW_SIZE / 5,
        w: PEG_WIDTH,
        h: (WINDOW_SIZE / 6) * 3,
        letter: "C",
        number: 3,
        disks: []
    });

    pegs.push(peg);

    // Create disks and add them to the full list
    // disk1-0
    for (var i = numberOfDisks - 1; i >= 0; i--) {
        var disk = Object.create(Disk);
        var diskNum = i + 1;

        // Optionally allow the peg locations to be configured via settings,
        // like {disk1:0, disk2:1}, where pegs are 0-indexed but disks aren't
        var pegNum = parseInt((Program.settings()["disk" + diskNum] || 0), 10);
        
        disk.init({
            w: (((WINDOW_SIZE * 2 / 4 - WINDOW_SIZE / 4) - PEG_WIDTH * 1.3) / (1.3 * numberOfDisks) * (i + 1) + PEG_WIDTH * 1.3),
            h: calcDiskHight,
            number: diskNum,
            peg: pegs[pegNum],
            color: diskColors[i%(diskColors.length)]
        });
        //disks add themselves automatically to their peg in the init function

        disks.push(disk);
    }

    // Create table
    var table = Object.create(Table);
    table.init({x: 40,
        y: WINDOW_SIZE * 7 / 10,
        w: WINDOW_SIZE - 80,
        h: DISK_HEIGHT
    });
    
    sprites.startAnimation(scene, timeline);
    timeline.play();
    
    return {pegs: pegs, disks: disks, timeline: timeline, scene: scene, workingFrame: workingFrame, updateWorkingFrame: updateWorkingFrame, getWorkingFrame: getWorkingFrame};
})();

var convertLetterToPeg = function(pegLetter) {
    if(pegLetter === "A" || pegLetter === "B" || pegLetter === "C"){
        return objects.pegs[pegLetter.charCodeAt(0)-65];
    }
    return null;   
};

var badmove=false;

var moveDisk = function(fromPeg, toPeg) {
    //no moves allowed after a bad move
    if(badmove){
        return;
    }
    
    var triedFromPeg = fromPeg;
    var triedToPeg = toPeg;
    fromPeg = convertLetterToPeg(fromPeg);
    toPeg = convertLetterToPeg(toPeg);
    
    var validmove=true;
    var badMoveMessage="";
    if(fromPeg === null){
        validmove=false;
        badMoveMessage = "'"+triedFromPeg+"' is not a valid peg";    
        badMoveMessage += "\nvalid peg names are 'A','B' or 'C'";
        badmove=true;
    }
    else if(toPeg === null){
        validmove=false;
        badMoveMessage = "'"+triedToPeg+"' is not a valid peg";
        badMoveMessage += "\nvalid peg names are 'A','B' or 'C'";
        badmove=true;    
    }
    else if(fromPeg.countDisks() === 0){ 
        validmove=false;
        badMoveMessage = "You attempted to move a disk from peg "+ fromPeg.letter;
        badMoveMessage += " but that peg\nwas empty";
        badmove=true;
    }
    else if(toPeg.countDisks() > 0 && fromPeg.getTopDisk().number > toPeg.getTopDisk().number ){
        validmove=false;
        badMoveMessage = "You attempted to move disk "+fromPeg.getTopDisk().number +" from peg ";
        badMoveMessage += fromPeg.letter + " onto disk " + toPeg.getTopDisk().number + " on peg "; 
        badMoveMessage += toPeg.letter +"\nIt is against the rules to move larger disks onto smaller disks";
        badmove=true;
    }
    if(!validmove){
        badMoveMessage += "\nFurther calls to the moveDisk function will be blocked.";
        badMoveMessage += "\nPress 'Restart' to be able to call the function again.";
    }
    
    var nowFrame = objects.getWorkingFrame();
    var duration = WINDOW_SIZE * 3 / (16 * 4);
    
    var workingDisk= null;
    if(fromPeg !== null){ workingDisk = fromPeg.getTopDisk();}
    var numOnToPeg= null;
    if(toPeg !== null){ numOnToPeg = toPeg.disks.length; }

    objects.timeline.addEvent(nowFrame, duration, function() {
        
        //reject invalid moves
        if(!validmove){
            return;
        }
        
        objects.scene.draggingSprite = objects.scene.sprites.indexOf(workingDisk);
        
        var destY = toPeg.y + toPeg.h - (workingDisk.h+0.1) * (numOnToPeg + 1);
        var changeInY = destY - workingDisk.y;
        var yIncrament = changeInY / (duration - (frame - nowFrame - 4));

        var destX = toPeg.x + toPeg.w / 2 - workingDisk.w / 2;
        var changeInX = destX - workingDisk.x;
        var xIncrament = changeInX / (duration - (frame - nowFrame - 4));

        workingDisk.translate(xIncrament, yIncrament);
        
    }, function() {
        
        //reject invalid moves, and announce them
        if(!validmove){
            println(badMoveMessage);
            return;
        }
        
        println(fromPeg.letter + " -> "+toPeg.letter);
        
        //move disk to final position
        workingDisk.x = toPeg.x + toPeg.w / 2 - workingDisk.w / 2;
        var disksUnder = numOnToPeg;
        workingDisk.y = toPeg.y + toPeg.h - (workingDisk.h+0.1) * (disksUnder + 1);
        
    });
    objects.workingFrame = nowFrame + duration + 1;
    objects.updateWorkingFrame(objects.workingFrame);
    
    if(validmove){
        var workingDisk = fromPeg.getTopDisk();
        workingDisk.moveToPeg(toPeg);
    }
    
};

var getSparePeg = function(peg1, peg2) {
    peg1 = convertLetterToPeg(peg1);
    peg2 = convertLetterToPeg(peg2);
    if(peg1 === null || peg2 === null){ return null; }
    var sparePegIndex = 3 - (peg1.number - 1) - (peg2.number - 1);
    return objects.pegs[sparePegIndex].letter;
};

var isSolved=function(toPeg){
    if(badmove){ return false; }
    var pegA= convertLetterToPeg("A");
    var pegB= convertLetterToPeg("B");
    var pegC= convertLetterToPeg("C");
    var goalPeg = convertLetterToPeg(toPeg);
    if(goalPeg === null){ return false; }
    if(goalPeg.disks.length !== 5 ){return false;}
    if(goalPeg === pegA){ return pegB.disks.length ===0 &&  pegC.disks.length ===0;   }
    if(goalPeg === pegB){ return pegA.disks.length ===0 &&  pegC.disks.length ===0;   }
    if(goalPeg === pegC){ return pegA.disks.length ===0 &&  pegB.disks.length ===0;   }
};

return {
    getSparePeg: getSparePeg,
    moveDisk: moveDisk,
    isSolved: isSolved
};
})();

var solveHanoi = function(numDisks, fromPeg, toPeg) {
    // base case:  no disks to move
    if (numDisks === 0) { 
        return;
    }
    // recursive case:
    var sparePeg = hanoi.getSparePeg(fromPeg, toPeg);
    solveHanoi(numDisks-1, fromPeg, sparePeg);
    hanoi.moveDisk(fromPeg, toPeg);
    solveHanoi(numDisks-1, sparePeg, toPeg);
};

solveHanoi(5, "A", "B");
Program.assertEqual(hanoi.isSolved("B"),true);

Last updated