歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

60行JavaScript代碼俄羅斯方塊游戲全解析

早就聽說網上有人僅僅用60行JavaScript代碼寫出了一個俄羅斯方塊游戲,最近看了看,今天在這篇文章裡面我把我做的分析整理一下(主要是以注釋的形式)。

我用C寫一個功能基本齊全的俄羅斯方塊的話,大約需要1000行代碼的樣子。所以60行乍一看還是很讓人吃驚的。

但是讀懂了代碼之後發現其實整個程序並沒有使用什麼神秘的技術,只不過是利用一些工具或者JavaScript本身的技巧大大簡化了代碼。

總結起來主要是以下三點

1.使用eval來產生JavaScript代碼,減小了代碼體積

2.以字符串作為游戲場景數據,使用正則表達式做查找和匹配,省去了通常應當手動編寫的查找驗證代碼

3.以二進制方式管理俄羅斯方塊數據和場景數據,通過位運算簡化比較和驗證

另外,原作者代碼換行很少,代碼寫的比較緊湊,這也是導致這個程序僅僅只有60行的一個原因。

60行JavaScript代碼俄羅斯方塊游戲 演示地址:http://www.linuxidc.com/files/2016/04/eluosi/els.html

下面給出經過我排版注釋後的代碼。

<!doctype html> 
<html>
    <head>
        <title>俄羅斯方塊-Linux公社 - Linux系統門戶網站www.linuxidc.com</title>
  <meta name="description" content="Linux公社(www.linuxidc.com)是專業的Linux系統門戶網站,實時發布最新Linux資訊,包括Linux、Ubuntu、Fedora、RedHat、紅旗Linux、Linux教程、Linux認證、SUSE Linux、Android、Oracle、Hadoop等技術。"/>
  <meta name="keywords" content="Linux,Ubuntu,Fedora,RedHat,紅旗Linux,Linux教程,Linux系統,Linux安裝,SUSE Linux,Android,Oracle"/>
    </head> 
   
    <body> 
        <div id = "box"
            style = "margin : 20px auto;
                      text-align : center;
                      width : 252px;
                      font : 25px / 25px 宋體;
                      background : #000;
                      color : #9f9;
                      border : #999 20px ridge;
                      text-shadow : 2px 3px 1px #0f0;">
        </div>
       
        <script> 
            //eval的功能是把字符串變成實際運行時的JavaScript代碼
            //這裡代碼變換之後相當於 var map = [0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0x801, 0xfff];
            //其二進制形式如下
            //100000000001 十六進制對照 0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801           
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //100000000001              0x801
            //111111111111              0xfff
            //數據呈U形分布,沒錯,這就是俄羅斯方塊的地圖(或者游戲場地更為合適?)的存儲區
            var map = eval("[" + Array(23).join("0x801,") + "0xfff]");
           
            //這個鋸齒數組存儲的是7種俄羅斯方塊的圖案信息
            //俄羅斯方塊在不同的旋轉角度下會產生不同的圖案,當然可以通過算法實現旋轉圖案生成,這裡為了減少代碼復雜性直接給出了不同旋轉狀態下的圖案數據
            //很明顯,第一個0x6600就是7種俄羅斯方塊之中的正方形方塊
            //0x6600二進制分四行表示如下
            //0110
            //0110
            //0000
            //0000
            //這就是正方形圖案的表示,可以看出,這裡統一采用一個16位數來存儲4 * 4的俄羅斯方塊圖案
            //因為正方形圖案旋轉不會有形狀的改變,所以此行只存儲了一個圖案數據
            var tatris = [[0x6600],
                          [0x2222, 0x0f00],
                          [0xc600, 0x2640],
                          [0x6c00, 0x4620],
                          [0x4460, 0x2e0, 0x6220, 0x740],
                          [0x2260, 0x0e20, 0x6440, 0x4700],
                          [0x2620, 0x720, 0x2320, 0x2700]]; 

            //此對象之中存儲的是按鍵鍵值(上,下,左,右)和函數之間的調用映射關系,之後通過eval可以做好調用映射
            var keycom = {"38" : "rotate(1)",
                          "40" : "down()",
                          "37" : "move(2, 1)",
                          "39" : "move(0.5, -1)"};
           
            //dia存儲選取的俄羅斯方塊類型(一共七種俄羅斯方塊類型)
            //pos是前台正在下落的俄羅斯方塊圖案(每一種俄羅斯方塊類型有屬於自己的圖案,如果不清楚可以查看上文的鋸齒數組)對象
            //bak裡存儲關於pos圖案對象的備份,在需要的時候可以實現對於pos運動的撤銷
            var dia, pos, bak, run;
           
            //在游戲場景上方產生一個新的俄羅斯方塊
            function start(){
           
                //產生0~6的隨機數,~運算符在JavaScript依然是位取反運算,隱式實現了浮點數到整形的轉換,這是一個很丑陋的取整實現方式
                //其作用是在七種基本俄羅斯方塊類型之中隨機選擇一個
                dia = tatris[~~(Math.random() * 7)];
               
                //pos和bak兩個對象分別為前後台,實現俄羅斯方塊運動的備份和撤銷
                bak = pos = {fk : [],                                    //這是一個數組存儲的��圖案轉化之後的二進制數據
                            y : 0,                                        //初生俄羅斯方塊的y坐標
                            x : 4,                                    //初生俄羅斯方塊的x坐標,相對於右側
                            s : ~~(Math.random() * dia.length)};        //在特定的俄羅斯方塊類型之中隨機選擇一個具體的圖案
               
                //新生的俄羅斯方塊不旋轉,所以這裡參數為0
                rotate(0);
            }
           
            //旋轉,實際上這裡做的處理只不過是旋轉旋轉之後的俄羅斯方塊具體圖案,之後進行移位,根據X坐標把位置移動到場景裡對應的地點
            function rotate(r){
           
                //這裡是根據旋轉參數 r 選擇具體的俄羅斯方塊圖案,這裡的 f ,就是上文之中的十六進制數
                //這裡把當前pos.s的值和r(也就是旋轉角度)相加,最後和dia.length求余,實現了旋轉循環
                var f = dia[pos.s = (pos.s + r) % dia.length];
               
                //根據f(也就是上文之中提供的 16 位數據)每4位一行填寫到fk數組之中
                for(var i = 0; i < 4; i++) {
                   
                    //初生的俄羅斯方塊pos.x的值為4,因為是4 * 4的團所以在寬度為12的場景裡左移4位之後就位於中間四列范圍內
                    pos.fk[i] = (f >> (12 - i * 4) & 0x000f) << pos.x;
                }
               
                //更新場景
                update(is()); 
            }     

            //這是什麼意思,這是一個判斷,判斷有沒有重疊
            function is(){ 
           
                //對於當前俄羅斯方塊圖案進行逐行分析
                for(var i = 0; i < 4; i++) {
               
                    //把俄羅斯方塊圖案每一行的二進制位與場景內的二進制位進行位與,如果結果非0的話,那麼這就證明圖案和場景之中的實體(比如牆或者是已經落底的俄羅斯方塊)重合了
                    //既然重合了,那麼之前的運動就是非法的,所以在這個if語句裡面調用之前備份的bak實現對於pos的恢復
                    if((pos.fk[i] & map[pos.y + i]) != 0) {
                       
                        return pos = bak;
                    }                           
                }   

                //如果沒有重合,那麼這裡默認返回空
            }     
           
            //此函數產生用於繪制場景的字符串並且寫入到div之中完成游戲場景的更新
            function update(t){ 
           
                //把pos備份到bak之中,slice(0)意為從0號開始到結束的數組,也就是全數組,這裡不能直接賦值,否則只是建立引用關系,起不到數據備份的效果
                bak = {fk : pos.fk.slice(0), y : pos.y, x : pos.x, s : pos.s}; 
               
                //如果俄羅斯方塊和場景實體重合了的話,就直接return返回,不需要重繪場景
                if (t) {
               
                    return;
                }
               
                //這裡是根據map進行轉換,轉化得到的是01加上換行的原始串
                for(var i = 0, a2 = ""; i < 22; i++) {
               
                    //br就是換行,在這個循環裡,把地圖之中所有數據以二進制數字的形式寫入a2字符串
                    //這裡2是參數,指定基底,2的話就是返回二進制串的形式
                    //slice(1, -1)這裡的參數1,-1作用是取除了牆(收尾位)之外中間場景數據(10位)
                    a2 += map[i].toString(2).slice(1, -1) + "<br/>";
                }
               
                //這裡實現的是對於字符串的替換處理,就是把原始的01字符串轉換成為方塊漢字串
                for(var i = 0, n; i < 4; i++) {
               
                    //這個循環處理的是正在下落的俄羅斯方塊的繪制
                    ////\u25a1是空格方塊,這裡也是隱式使用正則表達式
                    if(/([^0]+)/.test(bak.fk[i].toString(2).replace(/1/g, "\u25a1"))) {
                   
                        a2 = a2.substr(0, n = (bak.y + i + 1) * 15 - RegExp.$_.length - 4) + RegExp.$1 + a2.slice(n + RegExp.$1.length);
                    }
                }
               
                //對於a2字符串進行替換,並且顯示在div之中,這裡是應用
                ////\u25a0是黑色方塊 \u3000是空,這裡實現的是替換div之中的文本,由數字替換成為兩種方塊或者空白
                document.getElementById("box").innerHTML = a2.replace(/1/g, "\u25a0").replace(/0/g, "\u3000");
            } 
       
            //游戲結束
            function over(){ 
           
                //撤銷onkeydown的事件關聯
                document.onkeydown = null;
               
                //清理之前設置的俄羅斯方塊下落定時器
                clearInterval(run);
               
                //彈出游戲結束對話框
                alert("游戲結束"); 
            } 

            //俄羅斯方塊下落
            function down(){
           
                //pos就是當前的(前台)俄羅斯方塊,這裡y坐標++,就相當於下落
                ++pos.y;
               
                //如果俄羅斯方塊和場景實體重合了的話
                if(is()){
               
                    //這裡的作用是消行
                    for(var i = 0; i < 4 && pos.y + i < 22; i++) {
                   
                        //和實體場景進行位或並且賦值,如果最後賦值結果為0xfff,也就說明當前行被完全填充了,可以消行
                        if((map[pos.y + i] |= pos.fk[i]) == 0xfff) {
                       
                            //行刪除
                            map.splice(pos.y + i, 1);
                            //首行添加,unshift的作用是在數組第0號元素之前添加新元素,新的元素作為數組首元素
                            map.unshift(0x801);
                        }
                    }                               
                   
                    //如果最上面一行不是空了,俄羅斯方塊壘滿了,則游戲結束
                    if(map[1] != 0x801) {
                       
                        return over();
                    }
                   
                    //這裡重新產生下一個俄羅斯方塊
                    start(); 
                }
               
                //否則的話更新,因為這裡不是局部更新,是全局更新,所以重新繪制一下map就可以了
                update(); 
            } 

            //左右移動,t參數只能為2或者是0.5
            //這樣實現左移右移(相當於移位運算)這種方法也很丑陋,但是為了簡短只能這樣了
            //這樣做很丑陋,但是可以讓代碼簡短一些
            function move(t, k){ 
           
                pos.x += k; 
               
                for(var i = 0; i < 4; i++) {
                   
                    //*=t在這裡實現了左右移1位賦值的功能
                    pos.fk[i] *= t; 
                }
               
                //左右移之後的更新,這裡同樣進行了重合判斷,如果和左右牆重合的話,那麼一樣會撤銷操作並且不更新場景
                update(is()); 
            } 

            //設置按鍵事件映射,這樣按下鍵的時候就會觸發對應的事件,具體來說就是觸發對應的move,只有2和0.5
            document.onkeydown = function(e) { 
           
                //eval生成的JavaScript代碼,在這裡就被執行了
                eval(keycom[(e ? e : event).keyCode]); 
            };
             
            //這樣看來的話,這幾乎是一個遞歸。。。
            start();

            //設置俄羅斯方塊下落定時器,500毫秒觸發一次,調節這裡的數字可以調整游戲之中俄羅斯方塊下落的快慢
            run = setInterval("down()", 500);
        </script>
    </body>
</html>

下面給出原作者代碼,60行

<!doctype html><html><head></head><body>
<div id="box" ></div>
<script>
var map=eval("["+Array(23).join("0x801,")+"0xfff]");
var tatris=[[0x6600],[0x2222,0xf00],[0xc600,0x2640],[0x6c00,0x4620],[0x4460,0x2e0,0x6220,0x740],[0x2260,0xe20,0x6440,0x4700],[0x2620,0x720,0x2320,0x2700]];
var keycom={"38":"rotate(1)","40":"down()","37":"move(2,1)","39":"move(0.5,-1)"};
var dia, pos, bak, run;
function start(){
    dia=tatris[~~(Math.random()*7)];
    bak=pos={fk:[],y:0,x:4,s:~~(Math.random()*4)};
    rotate(0);
}
function over(){
    document.onkeydown=null;
    clearInterval(run);
    alert("GAME OVER");
}
function update(t){
    bak={fk:pos.fk.slice(0),y:pos.y,x:pos.x,s:pos.s};
    if(t) return;
    for(var i=0,a2=""; i<22; i++)
        a2+=map[i].toString(2).slice(1,-1)+"<br/>";
    for(var i=0,n; i<4; i++)
        if(/([^0]+)/.test(bak.fk[i].toString(2).replace(/1/g,"\u25a1")))
            a2=a2.substr(0,n=(bak.y+i+1)*15-RegExp.$_.length-4)+RegExp.$1+a2.slice(n+RegExp.$1.length);
    document.getElementById("box").innerHTML=a2.replace(/1/g,"\u25a0").replace(/0/g,"\u3000");
}
function is(){
    for(var i=0; i<4; i++)
        if((pos.fk[i]&map[pos.y+i])!=0) return pos=bak;
}
function rotate(r){
    var f=dia[pos.s=(pos.s+r)%dia.length];
    for(var i=0; i<4; i++)
        pos.fk[i]=(f>>(12-i*4)&15)<<pos.x;
    update(is());
}
function down(){
    ++pos.y;
    if(is()){
        for(var i=0; i<4 && pos.y+i<22; i++)
            if((map[pos.y+i]|=pos.fk[i])==0xfff)
                map.splice(pos.y+i,1), map.unshift(0x801);
        if(map[1]!=0x801) return over();
        start();
    }
    update();
}
function move(t,k){
    pos.x+=k;
    for(var i=0; i<4; i++)
        pos.fk[i]*=t;
    update(is());
}
document.onkeydown=function(e){
    eval(keycom[(e?e:event).keyCode]);
};
start();
run=setInterval("down()",400);
</script></body></html>

Copyright © Linux教程網 All Rights Reserved