Sunday 26 September 2010

Tetris - JavaScript - PART 3 - Glueing the parts together

Here is the game, glue together.

TODO:
  • Add the final two L shape
  • End Game Condition
  • Scoring
  • Put in some pretty graphics
I'm going to stop here, I figure out how to make the game lighting faster in any browser.

Ways to make it faster
  • Instead of creating new object and moving div around, we could initialize the game with all the object it need and use class to activate the object.
  • Some browser are hyperactive and don't delegate their display process and it will slow down the game when there are many object manipulation. This can be over come with grouping of object. Instead of moving the block one by one, we could group them into a row and move the rows.

hope this little game inspire or give some one a cool idea.

Cheers,

Below are the actual game, and it should be running on your browser right now.








/**
 * A little game of falling block, the goal are to score point by
 * controlling the falling block in a way to place them neatly on the bottom
 * of the screen. When the block are filled in contiguously from left to right
 * on either row, score will be earn.
 *
 * @author Do Quoc Cong
 */
Tetris = function(p_holder, p_blockSize, p_xSize, p_ySize) {
    //We don't need multiple instance
    if (window._Tetris != undefined) {
        return window._Tetris;
    }

    window._Tetris = this;
    this.m_keyCode = {
        40 : 'down',
        38 : 'up',
        37 : 'left',
        39 : 'right'
    };
    this.m_holder = p_holder;
    this.m_action = null;
    this.m_blockSize = p_blockSize ? p_blockSize: 32;
    this.m_xSize = p_xSize ? p_xSize: 20;
    this.m_ySize = p_ySize ? p_ySize: 10;
    this.m_grid = Array(p_xSize);
    this.m_falling = Array();
    this.m_timeOut = null;
    this.m_fallingStatue = 0;
    this.m_fallingShape = 0,
    this.m_shape = [
            [[0,0],[0,1],[1,0],[1,1]], // Square
            [[0,0],[1,0],[2,0],[3,0]], // Long Stick
            [[0,0],[1,0],[1,1],[2,1]], // \
            [[0,1],[1,1],[1,0],[2,0]], // /
            [[0,1],[1,0],[1,1],[1,2]]  // Spike shape
        ];
    
    this.m_shapeTranspormation = [
        [],
        [
            [[1,-1],[0,0],[-1,1],[-2,2]],
            [[-1,1],[0,0],[1,-1],[2,-2]]
        ],
        [
            [[2,0],[0,2],[0,0],[0,0]],
            [[-2,0],[0,-2],[0,0],[0,0]]
        ],
        [
            [[2,0],[0,-2],[0,0],[0,0]],
            [[-2,0],[0,2],[0,0],[0,0]]
        ],
        [
            [[1,-1],[1,1],[0,0],[-1,-1]],
            [[1,1],[-1,1],[0,0],[1,-1]],
            [[-1,1],[-1,-1],[0,0],[1,1]],
            [[-1,-1],[1,-1],[0,0],[-1,1]]
        ]
    ];
    $(p_holder).css('width', this.m_blockSize * this.m_ySize + 1 );
    $(p_holder).css('height', this.m_blockSize * this.m_xSize + 1 );

    for (var i = 0; i < p_xSize; i++) {
        this.m_grid[i] = Array(p_ySize);
    }

    // Bind window event to tetris
    $(window).keydown(function(event) {
        return Tetris().keyDown(event);
    });

    // Bind window event to tetris
    $(window).keyup(function(event) {
        return Tetris().keyUp(event);
    });

    //Event handler for KeyDown
    this.keyDown = function(event) {
        if (this.m_keyCode[event.keyCode] != undefined) {
//            console.log(this.m_keyCode[event.keyCode]);
            //Converting keycode into, up, down, left,
            //right action and set tetris action.
            this.m_action = this.m_keyCode[event.keyCode];
            return false;
        } else if(event.keyCode == 27){
            clearTimeout(this.m_timeOut);
        }
        return true;
    };

    //Event handler for KeyUp
    this.keyUp = function(event) {
        this.m_action = null;
        return true;
    }
    /**
     * Add a block to the tetris grid
     *
     * @param p_x the x cordinate to create the block
     * @param p_y the y cordinate to create the block
     * @param p_falling indicated wether this block is falling
     */
    this.addBlock = function(p_x, p_y, p_falling) {
        /**
         * This code is expensive, there are better ways
         * of implementing this section of the code
         * but for simplicity we leaving it
         */
        if (this.validCordinate(p_x, p_y) && !this.blockExist(p_x, p_y)) {
            var block = document.createElement('div');
            this.m_holder.append(block);
            $(block).addClass('block');
            if(this.moveBlock(block,p_x, p_y)){
                block.m_falling = p_falling?p_falling:false;
                if(block.m_falling){
                    $(block).addClass('falling');
                    this.m_falling.push(block);
                }
                return block;
            }
        }
        // There are some error
        return false;
    };
    
    /**
     * Check if the block existed at cordinate x, y
     *
     * @param p_x x cordinate to be check
     * @param p_x y cordinate to be check
     */
    this.blockExist = function(p_x, p_y) {
        if(this.m_grid[p_x]){
            return this.m_grid[p_x][p_y] != undefined && !this.m_grid[p_x][p_y].m_falling;
        } else {
            return false;
        }
    };
    
    /**
     * Check if the cordinate is with in the tetris region.
     * 
     * @param p_x x cordinate to be check
     * @param p_x y cordinate to be check
     */
    this.validCordinate = function(p_x, p_y) {
        return this.m_xSize > p_x && p_x >= 0 && this.m_ySize > p_y && p_y >= 0;
    }

    /**
     * Attemp to move the block to cordinate x and y, if the cordinate are
     * not occupied.
     *
     * @param p_block the block to be move
     * @param p_x the x cordinate to be move too
     * @param p_y the y cordinate to be move too
     */
    this.moveBlock = function(p_block, p_x, p_y) {
        if(!this.blockExist(p_x, p_y) && this.validCordinate(p_x, p_y)){
            if(p_block.m_x != undefined || p_block.m_y != undefined){
                if(this.m_grid[p_block.m_x][p_block.m_y] == p_block){
                    this.m_grid[p_block.m_x][p_block.m_y] = undefined;
                }
            }
            $(p_block).css("top", this.m_blockSize * p_x + 1);
            $(p_block).css("left", this.m_blockSize * p_y + 1);
            p_block.m_x = p_x;
            p_block.m_y = p_y;
            this.m_grid[p_x][p_y] = p_block;
            return true;
        }
        return false;
    };
    
    /**
     * Remove the block pass via parameter
     *
     * @param p_block the block to be remove.
     */
    this.removeBlock = function(p_block) {
        return this.removeCordinate(p_block.m_x, p_block.m_y);
    };
    
    /**
     * Remove the block from x, y cordinate if it existed.
     * 
     * @param p_x x cordinate of block
     * @param p_y y cordinate of block
     */
    this.removeCordinate = function( p_x, p_y){
        if(this.blockExist(p_x, p_y)){
            var block = this.m_grid[p_block.m_x][p_block.m_y];
            this.m_grid[p_block.m_x][p_block.m_y] = undefined;
            block.remove();
            return true;
        } else {
            return false;
        }
    };
    
    /**
     * Make the block that are mark as falling fall.
     */
    this.fall = function() {
        return this.moveFalling(1,0);
    };
    
    /**
     * Convert the falling block to normal blocks
     * And clear out and block when the rows are filled
     */
    this.fallingLanded = function() {
        for(var index in this.m_falling){
            block = this.m_falling[index];
            block.m_falling = false;
            $(block).removeClass('falling');
            
            var emptySlot = false;
            for(var i = 0; i < this.m_ySize ; i++){
                if(this.m_grid[block.m_x][i] == undefined){
                    emptySlot = true;
                }
            }
            
            if(!emptySlot){
                for(var i = 0; i < this.m_ySize ; i++){
                    $(this.m_grid[block.m_x][i]).remove();
                    this.m_grid[block.m_x][i] = undefined;
                }
                
                for(var x = block.m_x; x > 0; x--){
                    for(var y = 0; y < this.m_ySize; y++){
                        if(this.m_grid[x][y] != undefined){
                            this.moveBlock(this.m_grid[x][y],this.m_grid[x][y].m_x+1,this.m_grid[x][y].m_y);
                        };
                    }
                }
            }
        };
        
        this.m_falling = Array();
    };
    
    /**
     * Interperted the user, input and regulate how fast the games response
     * to the user input.
     */
    this.userControl = function(){
        switch(this.m_action){
            case 'left':
                this.moveFalling(0,-1);
                break;
            case 'right':
                this.moveFalling(0,1);
                break;
            case 'down':
                this.fall();
                break;
            case 'up':
                this.rotateFalling();
                break;
            default:
                break;
        }
        this.m_timeOutUserControl = setTimeout(function (){Tetris().userControl()}, 100);
    };
    
    /**
     * Rotate the shape using pre define transformations.
     */
    this.rotateFalling = function(){
        var shapeTransformation = this.m_shapeTranspormation[this.m_fallingShape];
        if(shapeTransformation.length > 1){
            var transformation = shapeTransformation[this.m_fallingStatue];
            
            for(var index in transformation){
                var x = transformation[index][0];
                var y = transformation[index][1];
                var block = this.m_falling[index];
                if(this.blockExist(block.m_x + x, block.m_y + y) || !this.validCordinate(block.m_x + x, block.m_y + y)){
                    return false;
                }
            }
            
            for(var index in transformation){
                var x = transformation[index][0];
                var y = transformation[index][1];
                var block = this.m_falling[index];
                this.moveBlock(block,block.m_x + x, block.m_y + y);
            }
            
            this.m_fallingStatue = (this.m_fallingStatue+1) % shapeTransformation.length;
        } else {
            console.log('NO TRANSFORMATION EXISTED');
        }
    }
    
    /**
     * Add a pre define block to the game.
     *
     * @param p_index the shape to add
     */
    this.addFallingShape = function(p_index) {
        if(p_index == undefined){
            p_index = Math.floor(Math.random()*5);
        }
        
        this.m_fallingShape = p_index;
        this.m_fallingStatue = 0;
        var shape = this.m_shape[p_index];
        
        for(var index in shape){
            this.addBlock(shape[index][0],shape[index][1], true);
        }
    }
    
    /**
     * Move all the falling block by p_x and p_y
     *
     * @param p_x cordinate for the falling blocks to move to
     * @param p_y cordinate for the falling blocks to move to 
     */
    this.moveFalling = function(p_x, p_y){
        for(var index in this.m_falling){
            block = this.m_falling[index];
            if(this.blockExist(block.m_x+p_x,block.m_y+p_y)
                    || !this.validCordinate(block.m_x+p_x, block.m_y+p_y)){
                return false;
            }
        };
        
        for(var index in this.m_falling){
            block = this.m_falling[index];
            this.moveBlock(block,block.m_x+p_x,block.m_y+p_y);
        };
        return true;
    }
    
    /**
     * Game Timer
     */
    this.run = function() {
        if(!this.fall()){
            this.fallingLanded();
            this.addFallingShape();
        };
        this.m_timeOut = setTimeout(function (){Tetris().run()}, 500);
    };
    
    /**
     * Initalize the game.
     */
    this.start = function() {
        this.addFallingShape();
        this.userControl();
        this.run();
    }
};


Saturday 25 September 2010

Tetris - JavaScript - PART 2 - Block Manipulation

Today where are going to give the program ability to add, remove and move blocks around.
 I add in a bit more comment and made the code abit neater.

The new functions are addBlock, removeBlock, moveBlock, blockExist, and validateCordinate.


It don't really do anything at this stage, but you can try it out with the following code

The html require the following
div.block {
            width:32px;
            height:32px;
            border:1px solid;
            position:absolute;
            top:10px;
            left:10px;
            background:green;
        };

And you need the div element,

< div id="tetrisHolder" style="background: none repeat scroll 0% 0% teal; 
display: block; height: 701px; position: relative; width: 351px;">

Finally the actually script

new Tetris($("#tetrisHolder"), 35, 20, 10);
  Tetris().addBlock(0,1);
  Tetris().addBlock(0,2);

The above code will add two block next to each other.

Cheers.

/**
 * A little game of falling block, the goal are to score point by
 * controlling the falling block in a way to place them neatly on the bottom
 * of the screen. When the block are filled in contiguously from left to right
 * on either row, score will be earn.
 *
 * @author Do Quoc Cong
 */
Tetris = function(p_holder, p_blockSize, p_xSize, p_ySize) {
    //We don't need multiple instance
    if (window._Tetris != undefined) {
        return window._Tetris;
    }

    window._Tetris = this;
    this.m_keyCode = {
        40 : 'down',
        38 : 'up',
        37 : 'left',
        39 : 'right'
    };
    this.m_holder = p_holder;
    this.m_action = null;
    this.m_blockSize = p_blockSize ? p_blockSize: 32;
    this.m_xSize = p_xSize ? p_xSize: 20;
    this.m_ySize = p_ySize ? p_ySize: 10;
    this.m_grid = Array(p_xSize);
    this.m_falling = Array();

    for (var i = 0; i < p_xSize; i++) {
        this.m_grid[i] = Array(p_ySize);
    }

    // Bind window event to tetris
    $(window).keydown(function(event) {
        Tetris().keyDown(event);
    });

    // Bind window event to tetris
    $(window).keyup(function(event) {
        Tetris().keyUp(event);
    });

    //Event handler for KeyDown
    this.keyDown = function(event) {
        if (this.m_keyCode[event.keyCode] != undefined) {
            console.log(this.m_keyCode[event.keyCode]);
            //Converting keycode into, up, down, left,
            //right action and set tetris action.
            this.m_action = this.m_keyCode[event.keyCode];
        };
    };

    //Event handler for KeyUp
    this.keyUp = function(event) {
        //Clearing up the action so tetris will be idle
        this.m_action = null;
    }
    /**
     * Add a block to the tetris grid
     *
     * @param p_x the x cordinate to create the block
     * @param p_y the y cordinate to create the block
     * @param p_falling indicated wether this block is falling
     */
    this.addBlock = function(p_x, p_y, p_falling) {
        /**
         * This code is expensive, there are better ways
         * of implementing this section of the code
         * but for simplicity we leaving it
         */
        if (this.validCordinate(p_x, p_y) && !this.blockExist(p_x, p_y)) {
            var block = document.createElement('div');
            this.m_holder.append(block);
            block = $(block);
            block.addClass('block');
            if(this.moveBlock(block,p_x, p_y)){
                block.m_falling = p_falling?p_falling:false;
                if(block.m_falling){
                    this.m_falling.push(block);
                }
                return block;
            }
        }
        // There are some error
        return false;
    };
    
    /**
     * Check if the block existed at cordinate x, y
     *
     * @param p_x x cordinate to be check
     * @param p_x y cordinate to be check
     */
    this.blockExist = function(p_x, p_y) {
        return this.m_grid[p_x][p_y] != undefined && !this.m_grid[p_x][p_y].m_falling;
    };
    
    /**
     * Check if the cordinate is with in the tetris region.
     * 
     * @param p_x x cordinate to be check
     * @param p_x y cordinate to be check
     */
    this.validCordinate = function(p_x, p_y) {
        return this.m_xSize > p_x && p_x >= 0 && this.m_ySize > p_y && p_y >= 0;
    }

    /**
     * Attemp to move the block to cordinate x and y, if the cordinate are
     * not occupied.
     *
     * @param p_block the block to be move
     * @param p_x the x cordinate to be move too
     * @param p_y the y cordinate to be move too
     */
    this.moveBlock = function(p_block, p_x, p_y) {
        if(!this.blockExist(p_x, p_y)){
            if(p_block.m_x != undefined || p_block.m_y != undefined){
                this.m_grid[p_block.m_x][p_block.m_y] = undefined;
            }
            p_block.css("top", this.m_blockSize * p_x + 1);
            p_block.css("left", this.m_blockSize * p_y + 1);
            p_block.m_x = p_x;
            p_block.m_y = p_y;
            this.m_grid[p_x][p_y] = p_block;
            return true;
        }
        return false;
    };
    
    /**
     * Remove the block pass via paramitar
     *
     * @param p_block the block to be remove.
     */
    this.removeBlock = function(p_block) {
        return this.removeCordinate(p_block.m_x, p_block.m_y);
    };
    
    /**
     * Remove the block from x, y cordinate if it existed.
     * 
     * @param p_x x cordinate of block
     * @param p_y y cordinate of block
     */
    this.removeCordinate = function( p_x, p_y){
        if(this.blockExist(p_x, p_y)){
            var block = this.m_grid[p_block.m_x][p_block.m_y];
            this.m_grid[p_block.m_x][p_block.m_y] = undefined;
            block.remove();
            return true;
        } else {
            return false;
        }
    }
};