In case Flash no longer exists; a copy of this site is included in the Flashpoint archive's "ultimate" collection.

Dead Code Preservation :: Archived AS3 works from wonderfl.net

Rubik's Cube (v1.3)

/**
 * Copyright Patrik-Malik ( http://wonderfl.net/user/Patrik-Malik )
 * MIT License ( http://www.opensource.org/licenses/mit-license.php )
 * Downloaded from: http://wonderfl.net/c/630M
 */

// forked from PESakaTFM's [Papervision3D] Rubik's Cube (v1.3)
// forked from PESakaTFM's [Papervision3D] Rubik's Cube (v1.2) w/ comments
// forked from PESakaTFM's [Papervision3D] Rubik's Cube (v1.1)
// forked from PESakaTFM's [Papervision3D] Rubik's Cube (v1.0) [WORKING]
// forked from PESakaTFM's [Alternativa3D] Rubik's Cube (v0.2)
// forked from PESakaTFM's [Alternativa3D] Rubik's Cube (v0.1)
/**
Fixed the WIN condition as well as the timer which was off.
Added comments.
Final Step: Auto Solve.
**/

package {
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.KeyboardEvent;
    import flash.events.MouseEvent;
    import flash.geom.Point;
    import flash.text.TextField;
    import flash.ui.Keyboard;
    
    import net.hires.debug.Stats;
    
    import org.papervision3d.core.math.Matrix3D;
    import org.papervision3d.core.math.Number3D;
    import org.papervision3d.render.QuadrantRenderEngine;
    import org.papervision3d.view.BasicView;

    [SWF(backgroundColor=0x0, width=465, height=456, frameRate=30)]
    public class Main extends Sprite
    {
    //This is the number of random moves it makes to scramble.  22 should suffice
        public const SCRAMBLE_DEPTH:int = 22;
        
    //I'm using this to handle ENTER_FRAME events within the MiniCube class
        public static var tween:Sprite = new Sprite();
        
        public var background:Sprite;
        public var bgClicked:Boolean = false;
        public var dragPoint:Point = new Point();
        public var mdp:Point = new Point(); // mouse down point
        public var movesDisp:TextField;
        public var rubik:RubiksCube;
        public var scrambled:Boolean = false;
        public var scrambleCount:int = 0;
        public var scrambleBtn:LabelButton;
        public var shift:Boolean;
        public var timer:TimerDisplay = new TimerDisplay();
        public var view:BasicView;
        
        private var axes:Array = ['x','y','z'];
        private var _moves:int = 0;
        private var scrambling:Boolean = false;
        
        public function Main()
        {
    //Add Background to catch MouseEvents.  Maybe add image here later
            background = new Sprite();
            background.graphics.beginFill(0x0,1);
            background.graphics.drawRect(0,0,465,465);
            background.graphics.endFill();
            addChild(background);
            
    //Simple textfield to display move count
            movesDisp = new TextField();
            movesDisp.x = 360;
            movesDisp.textColor = 0xFFFFFF;
            movesDisp.width = 100;
            addChild(movesDisp);
            
    //Very simple button to scramble the cube and start the puzzle timer
            scrambleBtn = new LabelButton("Scramble");
            scrambleBtn.x = 10;
            scrambleBtn.y = 350;
            scrambleBtn.addEventListener(MouseEvent.CLICK, onScrambleClick);
            addChild(scrambleBtn);
            
            addChild(new Stats());
            
            init3DEngine();
            
            timer.x = 200;
            addChild(timer);
            
            rubik = new RubiksCube();
    //The "win" event is dispatched when all the MiniCubes are in the correct orientation
            rubik.addEventListener("win", onWin);
            view.scene.addChild(rubik);
        }
        
    /**
    * I just copied and pasted this from another project.
    */
        private function init3DEngine():void
        {
            view = new BasicView(0, 0, true, true, "Target");                        
            view.camera.z = -100;
            view.buttonMode = true;
            view.renderer = new QuadrantRenderEngine(QuadrantRenderEngine.CORRECT_Z_FILTER);
            
            this.addChild(view);
            this.addEventListener(Event.ENTER_FRAME, onEventRender3D);
            
            stage.addEventListener(MouseEvent.MOUSE_DOWN, startRotation);
            stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
            stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
        }
        
    /**
    * Handles the MOUSE_DOWN event anywhere on the stage
    */
        private function startRotation(event:MouseEvent):void
        {
    //If it's still scrambling we don't want to do anything
            if(scrambleCount > 0) return;
            
    //Check if the target is the background
            bgClicked = event.target == background;
            
            stage.addEventListener(MouseEvent.MOUSE_UP, endDrag);
            stage.addEventListener(Event.MOUSE_LEAVE, endDrag);
            stage.addEventListener(MouseEvent.MOUSE_MOVE, onMove);
            
    //Store the mouse coordinates
            mdp.x = mouseX;
            mdp.y = mouseY;
            onMove();
        }
        
        public function endDrag(event:Event = null):void
        {
            stage.removeEventListener(MouseEvent.MOUSE_UP, endDrag);
            stage.removeEventListener(Event.MOUSE_LEAVE, endDrag);
            stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMove);
        }
        
    /**
    * This function is a little complicated since I'm handling several cases within
    */
        public function onMove(event:Event=null):void
        {
    //If the click target was the background we want to rotate the whole cube
            if(bgClicked)
            {
                var m:Matrix3D;
    //If the SHIFT key is down we want to rotate on the Z-axis, otherwise we rotate on the X&Y axes
                if(!shift)
                {
                    m = Matrix3D.rotationY((mouseX - mdp.x)/150);
                    m = Matrix3D.multiply(m, Matrix3D.rotationX(-(mouseY - mdp.y)/150));
                }
                else
                {
                    var rot:Number = (mouseX < 233)? (mouseY - mdp.y)/150 : (mdp.y - mouseY)/150;
                    rot += (mouseY > 233)? (mouseX - mdp.x)/150 : (mdp.x - mouseX)/150;
                    m = Matrix3D.rotationZ(rot);
                }
                
    //By multiplying the cube's transform by the rotation matrix we will rotate it on the global axes
    //Also, by doing it in small steps we get a consistent rotation that the user will expect
                rubik.transform = Matrix3D.multiply(m, rubik.transform);
                
                mdp.x = mouseX;
                mdp.y = mouseY;
            }
            else //If the target is anything but the background
            {
                dragPoint.x = mouseX;
                dragPoint.y = mouseY;
                
    //We only want to do something if the user has dragged far enough
                if(Point.distance(mdp, dragPoint) > 10)
                {
    //This will convert the 2D line into a 3D line using the cube's rotated axes
                    var n:Number3D = MyUtils.transformNumber(new Number3D(mdp.x - dragPoint.x, dragPoint.y - mdp.y, 0), 
                                                            Matrix3D.inverse(rubik.transform));
    //The cross product of the new 3D drag line and the normal of the side selected will give us which axis to rotate on
                    n = Number3D.cross(n, rubik.selSide);
                    
    //What follows is that check to see which axis best fits.
                    var axis:String = 'x';
                    var largest:Number = Math.abs(n.x);
                    
                    if(Math.abs(n.y) > largest)
                    {
                        largest = Math.abs(n.y);
                        axis = 'y';
                    }
                    if(Math.abs(n.z) > largest)
                    {
                        largest = Math.abs(n.z);
                        axis = 'z';
                    }
                    
    //We only want a positive or negative 1 (one) to tell us which direction to turn (CW or CCW)
                    rubik.move(axis, Math.round(n[axis]/largest));
                    if(scrambled) moveCount++; //Incriment number of moves if scrambled
                    
                    endDrag();
                }
            }
        }
        
        private function onEventRender3D(e:Event):void
        {
            
    //If we haven't scrambled enough yet, make another random move.
            if(scrambleCount > 0)
            {
                scrambleCount--;
                var rand1:int = Math.random()*2.99 >> 0;
                var rand2:int = (Math.round(Math.random()) == 0)? -1 : 1;
                var rand3:int = Math.random()*2.99 >> 0;
                rubik.moveAtOnce(axes[rand1], rand2, rand3);
                
                scrambling = true;
            }
    //I'm thinking of only calling this if there has been a change made to the cube...
    //But I haven't bothered yet.
            view.singleRender();
            
    //We have to update the MiniCubes after PV3D renders because the rotation properties need
    //to be translated onto the matrix transforms internally first.
            if(scrambling)
            {
                scrambling = false;
                rubik.fixRotation();
                if(scrambleCount == 0)
                {
                    //Start only after scramble is complete.
                    scrambled = true;
                    timer.start();
                    moveCount = 0;
                }
            }
        }
        
        public function get moveCount():int { return _moves; }
        public function set moveCount(value:int):void
        {
            _moves = value;
            movesDisp.text = "Moves: "+_moves;
        }
        
        private function onKeyDown(event:KeyboardEvent):void
        {
            shift = event.shiftKey;
        }
        
        private function onKeyUp(event:KeyboardEvent):void
        {
            shift = event.shiftKey;
        }
        
        private function onScrambleClick(event:MouseEvent):void
        {
            if(scrambleCount == 0)
            {
    //Since we are checking this every frame we only need to set it to something greater then 1
                scrambleCount = SCRAMBLE_DEPTH;
            }
        }
        
        private function onWin(event:Event):void
        {
    //For now, I'm just going to stop the timer on WIN.
            if(scrambled)
            {
                scrambled = false;
                timer.stop();
            }
        }
    }
}
    import flash.display.Sprite;
    import flash.events.MouseEvent;
    import flash.events.Event;
    import flash.events.TimerEvent;
    import flash.text.TextField;
    import flash.text.TextFieldAutoSize;
    import flash.utils.Timer;
    
    import org.papervision3d.core.math.Matrix3D;
    import org.papervision3d.core.math.Number3D;
    import org.papervision3d.materials.ColorMaterial;
    import org.papervision3d.materials.MovieMaterial;
    import org.papervision3d.materials.utils.MaterialsList;
    import org.papervision3d.objects.DisplayObject3D;
    import org.papervision3d.objects.primitives.Cube;

class RubiksCube extends DisplayObject3D
{
    public var selected:Number3D;
    public var selSide:Number3D;
    
    private var miniCubes:Vector.<MiniCube> = new Vector.<MiniCube>();
    private var temp:Vector.<MiniCube>;
    private var endParams:Object;
    
    private var inMotion:Boolean = false;
    private var opperation:String;
    
    public function RubiksCube()
    {
        createCube();
    }
    
    private function createCube():void
    {
        var cube:MiniCube;
        for(var i:int = 0; i<3; i++)
        {
            for(var j:int = 0; j<3; j++)
            {
                for(var k:int = 0; k<3; k++)
                {
                    cube = new MiniCube(k,j,i);
                    cube.addEventListener(MouseEvent.MOUSE_DOWN, onCubeSelected);
                    
                    miniCubes.push(cube);
                    addChild(cube);
                }
            }
        }
        
        name = "rubik";
    }
    
    private function onCubeSelected(event:MouseEvent):void
    {
    //Can't select anything if it's still moving
        if(!inMotion)
        {
            selected = MiniCube(event.currentTarget).location;
            selSide = event.target.selectedSide;
        }
    }
    
    public function move(axis:String, dir:int):void
    {
    //Check and set it inMotion
        if(inMotion || selected == null) return;
        inMotion = true;
    
    //Store the target rotation
        endParams = {rotationX:0, rotationY:0, rotationZ:0};
        endParams['rotation'+axis.toUpperCase()] = 90*dir;
        
    //Find the MiniCubes on the same layer and axis.
        temp = new Vector.<MiniCube>();
        for(var i:int=0; i<27; i++)
        {
            if(miniCubes[i].location[axis] == selected[axis])
            {
                temp.push(miniCubes[i]);
            }
        }
    //DisplayObject3D does not dispatch the ENTER_FRAME event, so I'm using this static Sprite
        Main.tween.addEventListener(Event.ENTER_FRAME, tweenSection);
    }
    
    //This is basically the same thing as above, but it doesn't tween the rotation
    public function moveAtOnce(axis:String, dir:int, layer:int):void
    {
        if(layer > 2 || layer < 0) return;
        inMotion = true;
        
        var rot:String = 'rotation'+axis.toUpperCase();
        for(var i:int=0; i<27; i++)
        {
            if(miniCubes[i].location[axis] == layer)
            {
                miniCubes[i][rot] = 90*dir;
            }
        }
    }
    
    /**
    * Tweens the rotation over 10 frames
    */
    private function tweenSection(event:Event):void
    {
        var finished:Boolean = true;
        for(var i:int=0; i<temp.length; i++)
        {
            for(var param:String in endParams)
            {
                if(temp[i][param] < endParams[param]-9)
                {
                    temp[i][param] += 9;
                    finished = false;
                }
                else if(temp[i][param] > endParams[param]+9)
                {
                    temp[i][param] -= 9;
                    finished = false;
                }
                else
                {
                    temp[i][param] = endParams[param];
                }
            }
        }
        if(finished)
        {
            Main.tween.removeEventListener(Event.ENTER_FRAME, tweenSection);
            fixRotation();
        }
    }
    
    /**
    * This is an important function as it straightens out all the axes for each object down to the bottom child
    */
    public function fixRotation():void
    {
        if(!inMotion) return;
        
        var win:Boolean = true;
        var str:String = '';
        var str2:String = '';
            
        var place:int=0;
        temp = new Vector.<MiniCube>(27);
        for(var i:int=0; i<27; i++)
        {
    //The Update function returns a String that reprisents
    //the minicube's rotation.
            str = miniCubes[i].update();
            place = miniCubes[i].location.z*9 + miniCubes[i].location.y*3 + miniCubes[i].location.x;
            temp[place] = miniCubes[i];
            
    //If the rotation on any 2 minicubes are different then
    //the cube is still unsolved.
            if(str2 != '' && str2 != str) win = false;
            str2 = str;
        }
    
    //If all cubes have the same rotation then we WIN.
        if(win)
        {
            this.dispatchEvent(new Event("win"));
        }
        
        miniCubes = temp;
        
        inMotion = false;
    }
}

class MiniCube extends DisplayObject3D
{
    public var cube:Cube;
    private var _loc:Number3D;
    private var _selFace:String;
    
    //Store the normals for each side of the MiniCubes
    private var faceNums:Object = {
        'back': new Number3D(0,0,1),
        'front': new Number3D(0,0,-1),
        'top': new Number3D(0,-1,0),
        'bottom': new Number3D(0,1,0),
        'right': new Number3D(-1,0,0),
        'left': new Number3D(1,0,0)
    }
    
    public function MiniCube(k:int, j:int, i:int)
    {
    //Set the colors for each side with Black as the default
        var matList:Object = {all:     new ColorMaterial(0x000000, 1, true)};
        if(i == 0) matList.back =     createColorMC(0xD80505, 'back');
        if(i == 2) matList.front =     createColorMC(0xFF9900, 'front');
        if(j == 2) matList.top =     createColorMC(0xFFFFFF, 'top');
        if(j == 0) matList.bottom = createColorMC(0xFFFF00, 'bottom');
        if(k == 2) matList.right =     createColorMC(0x0018EE, 'right');
        if(k == 0) matList.left =     createColorMC(0x1CA91B, 'left');
        
        cube = new Cube(new MaterialsList(matList), 10, 10, 10);
        
        addChild(cube);
        
    //Use the location for a unique name
        name = "MC"+i+j+k;
        cube.x = 11*k - 11;
        cube.y = 11*j - 11;
        cube.z = 11*i - 11;
        
        _loc = new Number3D(k,j,i);
    }

    public function update():String
    {
    //Transfers the rotation from this to the cube child object
        cube.transform = Matrix3D.multiply(this.transform, cube.transform);
        rotationX = rotationY = rotationZ = 0;
        
    //Update the cube's location
        _loc.x = Math.round((cube.x + 11)/11);
        _loc.y = Math.round((cube.y + 11)/11);
        _loc.z = Math.round((cube.z + 11)/11);
        
    //Round off position
        cube.x = 11*_loc.x - 11;
        cube.y = 11*_loc.y - 11;
        cube.z = 11*_loc.z - 11;
        
    //Round off rotation
        var m:Matrix3D = cube.transform;
        for(var i:int=1; i<4; i++)
        {
            for(var j:int=1; j<4; j++)
            {
                m['n'+i+j] = Math.round(m['n'+i+j]);
            }
        }
        cube.transform = m;
        
    //Returns a String reprisentation of the rotation matrix
    //If all the cubes have the same rotation then they MUST
    //be correctly aligned, thus WIN.
        return m.n11.toString()+m.n12.toString()+m.n13.toString()+
            m.n21.toString()+m.n22.toString()+m.n23.toString()+
            m.n31.toString()+m.n32.toString()+m.n33.toString();
    }
    
    public function get location():Number3D { return _loc; }
    public function get selectedFace():String { return _selFace; }
    public function get selectedSide():Number3D 
    {
    //Get's the current selected face's normal vector and transforms it to find the actual normal
        var n:Number3D = faceNums[_selFace];
        n = MyUtils.transformNumber(n, cube.transform);
        
        return n;
    }
    
    /**
    * Just creates a color material that can recieve MouseEvents
    */
    private function createColorMC(color:uint, name:String):MovieMaterial
    {
        var colorBox:Sprite = new Sprite();
        colorBox.graphics.beginFill(color);
        colorBox.graphics.drawRect(0, 0, 100, 100);
        colorBox.graphics.endFill();
        colorBox.name = name;
        colorBox.mouseChildren = false;
        colorBox.addEventListener(MouseEvent.MOUSE_DOWN, onMovieMatClicked);
        
        var movieMat:MovieMaterial = new MovieMaterial(colorBox, true, true);
        movieMat.interactive = true;
        movieMat.smooth = true;
        
        return movieMat;
    }
    
    private function onMovieMatClicked(event:MouseEvent):void
    {
        _selFace = event.target.name;
        this.dispatchEvent(event);
    }
}

class MyUtils
{
    /**
    * If there is a function native in PV3D somewhere to do this, I couldn't find it.
    */
    public static function transformNumber(n:Number3D, m:Matrix3D):Number3D
    {
        var v:Number3D = new Number3D(0,0,0);
        
        v.x = m.n11 * n.x + m.n12 * n.y + m.n13 * n.z;
        v.y = m.n21 * n.x + m.n22 * n.y + m.n23 * n.z;
        v.z = m.n31 * n.x + m.n32 * n.y + m.n33 * n.z;
        
        return v;
    }
}

/**
* This is just a simple and ugly button which I'm using for now until I get around to creating some nicer assets
*/
class LabelButton extends Sprite
{
    protected var _label:String;
    protected var tf:TextField;
    
    public function LabelButton(label:String = "Label")
    {
        tf = new TextField();
        tf.textColor = 0xFFFFFF;
        tf.x = tf.y = 5;
        tf.mouseEnabled = false;
        addChild(tf);
        
        this.buttonMode = true;
        
        this.label = label;
    }
    
    public function get label():String { return _label; }
    public function set label(value:String):void
    {
        tf.text = value;
        tf.autoSize = TextFieldAutoSize.LEFT;
        
        graphics.clear();
        graphics.beginFill(0x808080, 1);
        graphics.drawRect(0,0, tf.width + 10, tf.height + 10);
        graphics.endFill();
    }
}

/**
* Simple and ugly timer which I'm using until I create some nicer assets.
*/
class TimerDisplay extends Sprite
{
    private var clock:TextField;
    private var timer:Timer;
    
    private var startBtn:LabelButton;
    private var resetBtn:LabelButton;
    private var pauseBtn:LabelButton;
    
    public function TimerDisplay()
    {
        timer = new Timer(100);
        timer.addEventListener(TimerEvent.TIMER, onTick);
        
        clock = new TextField();
        clock.textColor = 0xFFFFFF;
        clock.x = 5;
        clock.y = 5;
        clock.width = 150;
        clock.selectable = false;
        clock.mouseEnabled = false;
        
        this.mouseEnabled = false;
                
        addChild(clock);
    }
    
    private function onTick(event:Event):void
    {
        var ticks:int = timer.currentCount;
        var h:int = int(ticks/36000);
        var m:int = (ticks%36000)/600;
        var s:int = (ticks%600)/10;
        clock.text = "Time "+ h.toString() +":"+ LeadingZeros(m) +":"+ LeadingZeros(s);
    }
    
    public function start():void
    {
        timer.reset();
        timer.start();
    }
    
    public function togglePause():Boolean
    {
        if(timer.running)
        {
            timer.stop();
        }
        else
        {
            timer.start();
        }
        return timer.running;
    }
    
    public function stop():void
    {
        timer.stop();
    }
    
    public function get currentTime():String 
    {
        var ticks:int = timer.currentCount;
        var h:int = int(ticks/3600);
        var m:int = (ticks%3600)/600;
        var s:Number = (ticks%600)/10;
        return h.toString() +":"+ LeadingZeros(m) +":"+ s.toString();
    }
    
    private function LeadingZeros(value:int, length:int=2):String
    {
        var str:String = new String(value);
        
        while(str.length < length)
        {
            str = "0"+str;
        }
        
        return str;
    }
}