HTML5 俄羅斯方塊



沒想到我會突然寫一版HTML5+JS的俄羅斯方塊,只為了練習canvas,雖然最後發現…全部的程式碼三百多行,真正用到canvas才5行左右Orz

/**
 * Created by ercrta on 2015/9/10.
 * 2015-09-10 13:30~19:00
 * 2015-09-11 09:30~12:00
 * 2015-09-11 13:00~14:20
 * 共約9小時
 */

$(function(){
    var GAME_STATUS_INIT=0;
    var GAME_STATUS_RUN=1;
    var GAME_STATUS_STOP=2;
    var GAME_STATUS_END=3;
    var MOVE_DIRECTION_DOWN=0;
    var MOVE_DIRECTION_LEFT=1;
    var MOVE_DIRECTION_RIGHT=2;
    var BOX_STATUS_SPACE=0;         //空白位置
    var BOX_STATUS_MOVE_PUZZLE=1;   //移動中的方塊
    var BOX_STATUS_FIXED_PUZZLE=2;  //固定的方塊
    var MAP_WIDTH=8;
    var MAP_HEIGHT=19;
    var BOX_SIZE=10;
    var map;
    var speed=1000;
    var interval;
    var puzzle_position=[];
    var puzzle_center=[];
    var game_status;
    init();

    function init(){
        set_game_status(GAME_STATUS_INIT);
        $("#puzzle_canvas").attr("width",BOX_SIZE*MAP_WIDTH);
        $("#puzzle_canvas").attr("height",BOX_SIZE*MAP_HEIGHT);
        $("#btn_start").bind('click',function(){
            create_empty_map();
            do_puzzle_create(create_puzzle());
            set_game_status(GAME_STATUS_RUN);
            star_game();
        });
        $("#btn_stop").bind('click',function(){
            set_game_status(GAME_STATUS_STOP);
            clearInterval(interval);
        });
        $("#btn_restart").bind('click',function(){
            create_empty_map();
            do_puzzle_create(create_puzzle());
            set_game_status(GAME_STATUS_RUN);
            star_game();
        });
        $("#btn_continue").bind('click',function(){
            set_game_status(GAME_STATUS_RUN);
            star_game();
        });
        $(document.body).on('keydown', function(e) {
            doKeyDown(e);
        });
    }
    function star_game(){
        clearInterval(interval);
        interval=setInterval(function(){
            var move_success=do_arrow_direction(MOVE_DIRECTION_DOWN);
            if(move_success==false){
                do_set_fixed_puzzle();
                do_clean_fixed_puzzle();
                var create_success=do_puzzle_create(create_puzzle());
                if(create_success==false){
                    set_game_status(GAME_STATUS_END);
                    clearInterval(interval);
                    alert("End Game");
                }
            }
        },speed);
    }
    function doKeyDown(e) {
        if(game_status!=GAME_STATUS_RUN)return;
        var keyID = e.keyCode ? e.keyCode :e.which;
        if(keyID === 38 || keyID === 87)  { // up arrow and W
            do_puzzle_rotation();
        }
        if(keyID === 39 || keyID === 68)  { // right arrow and D
            do_arrow_direction(MOVE_DIRECTION_RIGHT);
        }
        if(keyID === 40 || keyID === 83)  { // down arrow and S
            do_arrow_direction(MOVE_DIRECTION_DOWN);
        }
        if(keyID === 37 || keyID === 65)  { // left arrow and A
            do_arrow_direction(MOVE_DIRECTION_LEFT);
        }
    }
    function set_game_status(_game_status){
        game_status=_game_status;
        view_buttons(_game_status);
    }
    function set_map(_map){
        map=_map;
        view_map();
    }
    function view_buttons(_game_status){
        $("#btn_start").hide();
        $("#btn_stop").hide();
        $("#btn_restart").hide();
        $("#btn_continue").hide();
        switch (_game_status){
            case GAME_STATUS_INIT:
                $("#btn_start").show();
                break;
            case GAME_STATUS_RUN:
                $("#btn_stop").show();
                $("#btn_restart").show();
                break;
            case GAME_STATUS_STOP:
                $("#btn_continue").show();
                break;
            case GAME_STATUS_END:
                $("#btn_restart").show();
                break;
            default :
                break;
        }
    }

    function view_map(){
        var c=document.getElementById("puzzle_canvas");
        var cxt=c.getContext("2d");

        for(var i=0;i<MAP_WIDTH;i++){
            for(var j=0;j<MAP_HEIGHT;j++){
                draw_box(cxt,i,j,map[i][j]);
            }
        }
    }
    function draw_box(cxt,x,y,box){
        switch (box){
            case BOX_STATUS_SPACE:cxt.fillStyle = "#FF0000";break;          //red
            case BOX_STATUS_MOVE_PUZZLE:cxt.fillStyle = "#00FF00";break;    //green
            case BOX_STATUS_FIXED_PUZZLE:cxt.fillStyle = "#0000FF";break;   //blue
            default :
                break;
        }
        cxt.fillRect(x*BOX_SIZE,y*BOX_SIZE,BOX_SIZE,BOX_SIZE);
  cxt.strokeRect(x*BOX_SIZE,y*BOX_SIZE,BOX_SIZE,BOX_SIZE);
    }
    function create_empty_map(){
        var tmp_map=new Array(MAP_WIDTH);
        for(var i=0;i<MAP_WIDTH;i++){
            tmp_map[i]=new Array(MAP_HEIGHT);
            for(var j=0;j<MAP_HEIGHT;j++){
                tmp_map[i][j]=0;
            }
        }
        set_map(tmp_map);
    }
    function create_puzzle(){
        var random_num=Math.floor((Math.random() * 19) + 1);
        switch(random_num){
            case 1:return [[0,0],[1,0],[2,0],[1,1]];break;  //T
            case 2:return [[1,0],[0,1],[1,1],[1,2]];break;  //T
            case 3:return [[1,0],[0,1],[1,1],[2,1]];break;  //T
            case 4:return [[0,0],[0,1],[1,1],[0,2]];break;  //T
            case 5:return [[1,0],[1,1],[1,2],[1,3]];break;  //I
            case 6:return [[0,0],[1,0],[2,0],[3,0]];break;  //I
            case 7:return [[0,0],[1,0],[1,1],[2,1]];break;  //Z
            case 8:return [[1,0],[0,1],[1,1],[0,2]];break;  //Z
            case 9:return [[1,0],[2,0],[0,1],[1,1]];break;  //倒Z
            case 10:return [[0,0],[0,1],[1,1],[1,2]];break;  //倒Z
            case 11:return [[0,0],[0,1],[1,0],[1,1]];break;  //田
            case 12:return [[0,0],[0,1],[0,2],[1,2]];break;  //L
            case 13:return [[0,0],[1,0],[2,0],[0,1]];break;  //L
            case 14:return [[0,0],[1,0],[1,1],[1,2]];break;  //L
            case 15:return [[2,0],[0,1],[1,1],[2,1]];break;  //L
            case 16:return [[1,0],[1,1],[0,2],[1,2]];break;  //倒L
            case 17:return [[0,0],[0,1],[1,1],[2,1]];break;  //倒L
            case 18:return [[0,0],[1,0],[0,1],[0,2]];break;  //倒L
            case 19:return [[0,0],[1,0],[2,0],[2,1]];break;  //倒L
            default :
                return [[1,0],[1,1],[1,2],[1,3]];break;     //I
        }
    }
    function do_arrow_direction(direction){
        switch (direction){
            case MOVE_DIRECTION_DOWN:return do_puzzle_move(0,1);break;
            case MOVE_DIRECTION_LEFT:return do_puzzle_move(-1,0);break;
            case MOVE_DIRECTION_RIGHT:return do_puzzle_move(1,0);break;
            default :
                break;
        }
    }
    function do_puzzle_create(puzzle){
        var new_puzzle_position=[];
        var len=puzzle.length;
        //計算新位置
        for(var i=0;i<len;i++){
            var point_x=puzzle[i][0];
            var point_y=puzzle[i][1];
            new_puzzle_position.push([2+point_x,point_y]);
        }
        return do_set_puzzle_in_map(new_puzzle_position,[3,1]);
    }
    function do_puzzle_move(move_x,move_y){
        var new_puzzle_position=[];
        var len=puzzle_position.length;
        //計算新位置
        for(var i=0;i<len;i++){
            var point_x=puzzle_position[i][0];
            var point_y=puzzle_position[i][1];
            new_puzzle_position.push([point_x+move_x,point_y+move_y]);
        }
        return do_set_puzzle_in_map(new_puzzle_position,[puzzle_center[0]+move_x,puzzle_center[1]+move_y]);
    }
    function do_puzzle_rotation(){
        var new_puzzle_position=[];
        var len=puzzle_position.length;
        //計算新位置
        var center_x=puzzle_center[0];
        var center_y=puzzle_center[1];
        for(var i=0;i<len;i++){
            var point_x=puzzle_position[i][0];
            var point_y=puzzle_position[i][1];

            var new_point_x=-( (point_y-center_y))+center_x;
            var new_point_y=-(-(point_x-center_x))+center_y;

            new_puzzle_position.push([new_point_x,new_point_y]);
        }
        return do_set_puzzle_in_map(new_puzzle_position,puzzle_center);
    }
    function do_set_puzzle_in_map(new_puzzle_position,new_puzzle_center){
        var len=new_puzzle_position.length;
        //檢查位置
        if(chk_puzzle_new_position(new_puzzle_position)==false){
            return false;
        }
        //清除舊位置
        do_clean_old_position();
        //填上新位置
        for(var i=0;i<len;i++){
            var point_x=new_puzzle_position[i][0];
            var point_y=new_puzzle_position[i][1];
            map[point_x][point_y]=BOX_STATUS_MOVE_PUZZLE;
        }
        //��錄puzzle的位置
        puzzle_position=new_puzzle_position;
        puzzle_center=new_puzzle_center;
        set_map(map);
        return true;
    }
    function chk_puzzle_new_position(new_puzzle_position){
        var len=new_puzzle_position.length;
        var check_position=true;
        for(var i=0;i<len;i++){
            var point_x=new_puzzle_position[i][0];
            var point_y=new_puzzle_position[i][1];
            if(point_x>=MAP_WIDTH || point_x<0){
                check_position=false;break;
            }
            if(point_y>=MAP_HEIGHT || point_y<0){
                check_position=false;break;
            }
            try{
                if(map[point_x][point_y]==BOX_STATUS_FIXED_PUZZLE){
                    check_position=false;break;
                }
            }catch (e){
                alert(point_x+','+point_y);
            }
        }
        return check_position;
    }
    function do_clean_old_position(){
        //清除舊的位置
        for(var i=0;i<MAP_WIDTH;i++){
            for(var j=0;j<MAP_HEIGHT;j++){
                if(map[i][j]==BOX_STATUS_MOVE_PUZZLE){
                    map[i][j]=BOX_STATUS_SPACE;
                }
            }
        }
    }
    function do_set_fixed_puzzle(){
        for(var i=0;i<MAP_WIDTH;i++){
            for(var j=0;j<MAP_HEIGHT;j++){
                if(map[i][j]==BOX_STATUS_MOVE_PUZZLE){
                    map[i][j]=BOX_STATUS_FIXED_PUZZLE;
                }
            }
        }
        set_map(map);
    }
    function do_clean_fixed_puzzle(){
        var i,j;
        var fixed_row_number=[];
        //檢查排滿的行數
        for(i=0;i<MAP_HEIGHT;i++){
            var flag=true;
            for(j=0;j<MAP_WIDTH;j++){
                if(map[j][i]!=BOX_STATUS_FIXED_PUZZLE){
                    flag=false;
                    break;
                }
            }
            if(flag){
                fixed_row_number.push(i);
            }
        }
        //清掉
        var len=fixed_row_number.length;
        for(i=0;i<len;i++){
            var row_num=fixed_row_number[i];
            for(j=row_num;j>0;j--){
                for(var m=0;m<MAP_WIDTH;m++){
                    if(j-1>=0) {
                        map[m][j] = map[m][j - 1];
                    }else{
                        map[m][j] = BOX_STATUS_SPACE;
                    }
                }
            }
        }
        set_map(map);
    }
});

2015-09-11建立
完整程式碼下載v0.0
2015-09-20更新,使用localStorage,視窗關閉後,重新打開檔案,會從上次的狀態續繼遊戲
完整程式碼下載v0.1

這次有碰到兩個稍微困擾我的地方:
1.canvas的大小設定,我是用jquery設的,所以本來是用css的方式設定,結果一設,就變形!!
舊寫法:
//$("#puzzle_canvas").css("width",BOX_SIZE*MAP_WIDTH);
//$("#puzzle_canvas").css("height",BOX_SIZE*MAP_HEIGHT);
更新成:
$("#puzzle_canvas").attr("width",BOX_SIZE*MAP_WIDTH);
$("#puzzle_canvas").attr("height",BOX_SIZE*MAP_HEIGHT);
2.旋轉的部份也困擾我了一下,一直鑽牛角尖,想到矩陣相乘去了,事實上只要稍微做一下加減就可以了(因為轉90度比較簡單)
var point_x=puzzle_position[i][0];
var point_y=puzzle_position[i][1];

var new_point_x=-( (point_y-center_y))+center_x;
var new_point_y=-(-(point_x-center_x))+center_y;
Category: 0 意見

nodejs-模組(Module) & 套件(Package)

模組(Module) & 套件(Package)
1.1一個Node.js檔案就是一個模組,這檔案可能是js程式、json或編譯過的c/c++擴充
1.2建立module.js
var name;
exports.setName=function(_name){
name=_name;
};

exports.sayHi=function(){
console.log('Hi! '+name);
}
1.3建立getmodule.js
var my_module=require('./module');
my_module.setName('By');
my_module.sayHi();

1.4執行getmodule.js,出現
Hi! By

1.5關於require & exports
require是用來取得模組的介面
exports是模組公開的介面

1.6require不會重複載入模組
修改getmodule.js如下:
var my_module1=require('./module');
my_module1.setName('my_module1');
my_module1.sayHi();

var my_module2=require('./module');
my_module2.setName('my_module2');
my_module1.sayHi();

執行結果:
Hi! my_module1
Hi! my_module2

我們兩次都是呼叫my_module1,卻發現第二次呼叫的my_module1的值被,my_module2給蓋過,表示my_module1跟my_module2指向同一個實例

1.6簡化exports
有時只想把物件封裝到模組中,建立obj.js內容如下:

function Hi(){
var name;
this.set_name=function(_name){
name=_name;
};

this.say=function(){
console.log("Hi! "+name);
}
}

exports.Hi=Hi;

想取用時,建立getobj.js,內容如下:

var HiClass=require('./obj.js').Hi;
var obj=new HiClass();
obj.set_name('my');
obj.say();

因為用require('./obj.js').Hi;來取用物件有點多餘,所以可以改成下面稍微簡化:

obj.js內容如下:

function Hi(){
var name;
this.set_name=function(_name){
name=_name;
};

this.say=function(){
console.log("Hi! "+name);
}
}

//exports.Hi=Hi;
module.exports=Hi;

getobj.js,內容如下:

//var HiClass=require('./obj.js').Hi;
var HiClass=require('./obj.js');
var obj=new HiClass();
obj.set_name('my');
obj.say();
Category: 0 意見

mysql脫逸字元

今天從DB查東西時,發生一些問題
Table的資料如下:
sn name
1 abc
2 a_b
3 a_c

我想查出a_開頭的name,像a_b跟a_c,所以sql下了:
Select * from name_table where name like LIKE 'a_%'
結果它連abc都出來了,原來_在sql表示的是萬用字元
mysql中的2個萬用字元,與 LIKE 關鍵字一起使用的
參考:http://www.1keydata.com/tw/sql/sql-wildcard.html
%(百分比符號) 代表零個、一個、或數個字母
_(底線) 代表剛好一個字母

解決方式便是使用脫逸字元 \ ,在\後的第一個字元會脫逸,修正語法如下:
Select * from name_table where name like LIKE 'a\_%'
另一個解決方法可使用 ESCAPE ,範例是使用\\,表示在\\後的第一個字元會脫逸:
SELECT * FROM name_table where name like 'a\\_%' ESCAPE '\\'
ESCAPE 範例2,使用z,表示在z後的第一個字元會脫逸
SELECT * FROM name_table where name like 'az_%' ESCAPE 'z'
Category: 0 意見

nodejs-07 製作聊天室02 socket.io的應用

在安裝目錄下的node_modules\socket.io\node_modules\socket.io-client\dist\資料夾下,可以找到socket.io.js,client端(Html)需要include這支js。
找到這支後,讓html include它,也就是在html加入這一行:

接下來我們就可以寫Code了
client端(html):index.html


<html>
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title>nodejs-07 製作聊天室02</title>
  <script src="socket.io.js"></script>
 <script>
  var socket = io.connect('http://localhost:8889');//1.client要求連線
  socket.on('connect',function(){//3.client收到連線通知
   alert('connect');
   socket.emit('ClientCallServer');//4.emit傳送訊息給server,第一個參數是名稱,第二個之後的參數是要傳給server的值,這個範例是不傳值
  });
  socket.on('ServerCallClient',function(){//7.收到server的呼叫
   alert('ServerCallClient');
  });
  socket.on('disconnect', function(){//11.收到Server斷線事件
   alert('server主動斷線');
  });
 </script>
 </head>
 <body>
 測試頁
 </body>
</html>
SERVER端:index.js
var io = require('socket.io').listen(8889);//監聽8889port
io.sockets.on('connection', function (socket) {
 console.log('connect'); //2.收到client的連線
 socket.on('ClientCallServer',function(){
  console.log('5. get client data'); //5.收到client的呼叫
  socket.emit('ServerCallClient'); //6.送資料給client
 });

 socket.on('disconnect', function(){
  console.log('client 斷線'); //9.收到client斷線
 });

 setTimeout(function(){//10.server主動斷線//這是測試用,4秒後server主動斷線
  console.log('server送斷線訊息');
  socket.emit('disconnect');
 },4000);

});

簡單的說,不論cliet端或seerver端,emit就是送出命令;on就是接收命令,socket.io已經把一些常用的東西整合好方便使用了,但我測試下來,對於斷線還是有些bug,尤其是ie,server可能會收不到client傳來的命令,看來這點要再想辦法解決
Category: 0 意見

nodejs-06 製作聊天室01 安裝套件-socket.io

接下來這幾篇要來寫聊天室的製作,首先要先裝socket.io。
socket.io網站: http://socket.io
中文介紹:http://ithelp.ithome.com.tw/question/10102886
開啟command視窗,輸入npm install socket.io 然後就會自動下載並安裝。
這東西我是直接把它當成server的概念來看待,安裝完後,我希望完成client跟server的互動如下圖:

Category: 0 意見

nodejs-05 安裝套件-supervisor

每當一改nodejs的程式後,想要測試時,就一定要把command視窗關掉,然後再開啟重新執行,若是路徑設得比較深入的人…真的光輸入路徑就浪費掉好多時間需按下Ctrl+C,重新啟動,幸好網路上已經有方便的套件可以使用,幫助我們解決這種困擾,安裝supervisor套件就是一個解決方法。

supervisor網站
https://github.com/isaacs/node-supervisor

開啟command視窗,輸入npm install supervisor -g 然後就會自動下載並安裝








安裝完後,要使用supervisor 程式名稱.js來取代之前用node 程式名稱.js
例如前面的範例的程式檔名都是index.js 所以我都是執行 node index.js,現在的話,就是改成執行 supervisor index.js來啟動程式。

之後若是有修改index.js,就不用再重新啟動command視窗,它會自動更新。

 ps.
以後如果想安裝其它套件,只需要輸入下列語法:

npm install 套件名稱 -g 

其中-g指的全域安裝,不論你的程式在那裡,都可以使用這個套件;
若是非全域安裝的話,就要注意你指行install指令的命令下在那個資料夾,那就只有那個資料夾有安裝套件。
Category: 0 意見

nodejs-04 讀取靜態資料(網頁、圖片等)

修改nodejs-03的程式碼:
server端的主程式:index.js
var fs=require('fs');
var http=require('http');
http.createServer(function(req,res){
 console.log(req.url);
 var url=req.url.substr(1);
 console.log(url);
 fs.readFile(url,function(err,data){
  if(err){
   res.writeHead(400);
   res.end();
  }else{
   res.writeHead(200);
   res.end(data);
  }
 });
}).listen(8888);

解說:
console.log(req.url);
//createServer的function帶兩個變數:req、res
//req:跟http request 相關的所有資料
//res:跟http response 相關的所有資料
//req.url可以抓到在瀏覽器開啟時,要求的檔案路徑是什麼,ex:http://localhost:8888/index.html,抓到的就是/index.html

var url=req.url.split('/');
//req.url第一個斜線拿掉,fs.readFile只需要後面的路徑

res.writeHead(400);
res.end();
//找不到擋案時,head回傳錯誤狀態400,讓前端導向找不到網頁
Category: 0 意見