早就聽說網上有人僅僅用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>