7行テトリスの解説

03.10.23 版をもとに、解説を行います。

基本事項

  if, for, 三項演算子にて、評価値が 0 のときは false 、0 でないときは true

  A||B のとき A==true ならば B が評価されず、A==false ならば B が評価される
  A&&B のとき A==false ならば B が評価されず、A==true ならば B が評価される

  A||B は、Bに副作用が無ければ A|B あるいは A+B に等価 ?
  A&&B は、Bに副作用が無ければ A*B に等価 ?

  未初期化変数の参照は実行不可だが、配列要素ならば参照可(配列変数自体は不可)
  ×  b = a
  ○  a = 1;  b = a
  ×  y = x[1]
  ○  x = [];  y = x[1]

03.10.23版のソースプログラム

<body onKeyDown=K=event.keyCode-38><script>Z=X=[B=A=12];function Y(){for(C=[q=c
=i=4];i--*K;c-=!Z[h+(K+25?p+K:C[i]=p*A-(p/9|0)*145)])p=B[i];c?0:K+25?h+=K:t?B=C
:0;for(f=K=0;q--;f+=Z[A+p])k=X[p=h+B[q]]=1;if(e=!e)if(h+=A,f|B)for(Z=X,X=[l=228
],B=[[-7,-20,6,h=17,-9,3,3][t=++t%7]-4,0,1,t-6?-A:2];l--;)for(l%A?l-=l%A*!Z[l]:
(P+=k++,c=l+=A);--c>A;)Z[c]=Z[c-A];for(i=S="";i<240;S+=X[i]|(X[i]=Z[i]|=++i%A<2
|i>228)?i%A?"■":"■<br>":"_");document.body.innerHTML=S+P;Z[5]||setTimeout(Y,
99-P)}Y(h=e=K=t=P=0)</script>

書き下したものを次に示します。
一部を展開したり、ブレース(中括弧)なども補っておきます。

 1:  <body onKeyDown=K=event.keyCode-38><script>
 2:  Z=X=[];B=A=12;
 3:  function Y(){
 4:    C=[]; 
 5:    for(q=c=i=4;i--*K;){
 6:      p=B[i];
 7:      C[i]=p*A-(p/9|0)*145;
 8:      c-=!Z[h+(K+25?p+K:C[i])];
 9:    }
10:    c?0:K+25?h+=K:t?B=C:0;
11:    for(f=K=0;q--;){
12:      k=X[p=h+B[q]]=1;
13:      f+=Z[A+p];
14:    }
15:    if(e=!e){
16:      h+=A;
17:      if(f|B){
18:        Z=X,X=[],
19:        B=[[-7,-20,6,h=17,-9,3,3][t=++t%7]-4,0,1,t-6?-A:2];
20:        for(l=228;l--;){
21:          for(l%A?l-=l%A*!Z[l]:(P+=k++,c=l+=A);--c>A;){
22:            Z[c]=Z[c-A];
23:          }
24:        }
25:      }
26:    }
27:    for(i=S="";i<240;){
28:      S+=X[i]|(X[i]=Z[i]|=++i%A<2|i>228)?i%A?"■":"■<br>":"_";
29:    }
30:    document.body.innerHTML=S+P;
31:    Z[5]||setTimeout(Y,99-P)
32:  }
33:  Y(h=e=K=t=P=0)
34:  </script>


1: 34:  HTML部  <body><script> 〜 </script>
    JavaScriptを使う場合に、最低限必要なHTML部分の記述
    onKeyDown=〜 はキーが押されたときに〜を実行する命令、
    event.keyCode は押されたキー番号
    カーソルキーの左右は 37,39 に、リターンキーは 13 、スペースバーは 32
    キー番号は K に代入され、あらかじめ 38 を引いておくことで、
    左右の移動が [-1,+1] になり、文字数削減に役立つ

2: 33:  変数の初期化
A=12 定数、ゲームフィールドの横1列の大きさ
     移動可能部分10個と左右の壁2個を含むので、サイズが12となる

B[]  各パーツのブロックを表す配列
     ブロック4個をそれぞれ、(0,0)を原点とする座標軸で表現する
     ただし、座標(x,y)を1次元に展開し、x+y*12 で位置を表している
     したがってテトリス棒ならば B=[0,1,2,3] あるいは B=[0,1,-12,-24] などとなる
     B=12 で初期化しているのは、17: の if() で技巧的な処理をするため

Z[]  落下中のパーツは含まないゲームフィールド配列
     大きさは 240 = 横12 × 縦20
     確定したブロック(および壁)を表す
     移動可能・回転可能判定、落下判定、1行揃い判定、描画に用いられる

X[]  落下中のパーツを含めたゲームフィールド配列 (概念的には X = Z + B)
     毎回、28: で Z に上書き消去され、12: で B を追加される

h    ブロックの Z[] における現在位置
e    落下が確定するかどうかのフラグ
K    入力キー番号、0なら無入力
t    パーツ番号 0〜6
P    得点


3: 〜 32:  メイン関数

4:  回転先の座標配列の確保

C[]  回転先の座標
     Cが配列であることを宣言しておく

5: 〜 10:  移動先・回転先の座標計算と障害物判定

5:  K=0 すなわちキー入力が無ければ、i--*K == 0 となり、for は処理されない
6:  短くするために一時変数 p に処理ブロック座標を代入
7:  回転先の座標を回転行列を用いて計算
    回転行列は (x',y') = (cos sin, -sin cos)(x,y) というやつです。
    ブロックの座標が p=x+y*12 で表されるため、x=p%12, y=Math.round(p/12) を求める
    ただしビットORを用いることで実数→整数変換可能なので、
    Math.floor(p/12) と p/12|0 が等価になる
    ここでは、Math.round でありフィールドの大きさが小さいため p/9|0 が y に等価

8:  移動先に障害物があるかどうか
    K+25? は keyCode-13 ? に等しい、すなわち入力キーがリターンキーかどうか
    リターンならば回転先の座標を調べ、そうでなければ横移動先を調べる(10: も同じ)
    h+p+K は横移動の移動先座標
    h+C[i] は回転先の座標
    Z[] の壁や床は、番兵の役割を果たす(12: でも同様)
    c-=!Z[] にて、Z[] の該当座標の値が 0 (ブロックがない)ならば c=c-1 となる
    4つすべてで障害物が存在しなければ c==0 となる

10:  移動可能・回転可能判定
    c==0 ならば、移動あるいは回転可能
    8: と同様に K+25 にしたがって、横移動か回転を実行する
    横移動は現在位置を h+K にし、回転は B[] を C[] に置き換えることで実現

    ちなみに t==0 は四角形のパーツであり、回転させない

11: 〜 14:  落下判定と落下ブロックのフィールド配列への描画

f    落下先に床やブロックがあるかどうか f>0 ならば障害物あり
k    1行消去で加算する得点、毎回どこかで 0 か 1 に初期化する必要がある

11:  キー番号 K をクリアしておく
12:  各ブロックの位置 h+B[q] における X[] を 1 とすることでブロックの描画
13:  落下先の座標にある Z[] の値を f に加算
    4つのうちどれか1つでも障害物に接すれば、f>0 となり、接地することになる

15: 〜 26:  パーツの落下処理と1行揃い処理

15:  e=!e により e は 0 と 1 が毎回チェンジする
    したがって、if(e) が2回に1回 true になるので、
    1段落下するまで2回の移動処理(横移動・回転)ができる

16:  落下  h=x+y*12 なので、h+=12 で1段落下(y=y+1)になる
17:  パーツの接地判定(そして初期化判定)
    f>0 ならばパーツが接地したので、if の中に入る
    B は配列 [] であるならば、if(f|B) は false 、
    B=12 のとき(初期化のとき)は、if(f|B) は true となるので、if に入る
    したがって、本来なら落下ごとに再設定するものと初期化を別にする必要のある
    B,h の値を、ここで一括して設定できる

18:  ゲームフィールドの更新
    落下が確定したのでパーツを含む X が新しいフィールド Z になる
    JavaScript の配列 [] は、C でいうポインタに似ているので、
    Z=X としただけでは、Z と X が同じ配列オブジェクトを指すだけ
    X=[] とすることで、X が新しい配列に向けられる

19:  新しいパーツの生成
    テトリス棒を除くすべてのパーツは、0,1,-12 の3つを共有できる
    t-6?-A:2 にて、テトリス棒(t==6)のときのみ、処理を変える
    ついでに h も 17 で初期化

20: 〜 23:  1行揃いの判定と消去

21:  判定
    l%A==0 になると1行がブロックで埋められたことになる
    途中で Z[l] が 0 になる(ブロックが無い)ときは、
    l は l-=l%A*!Z[l] とすることで l%A==0 を飛ばして次の行に行ってしまう
    l%A==0 のとき、まずは得点の加算 P+=k++
    1行消すごとに k が増えるので 1, 1+2=3, 1+2+3=6, 1+2+3+4=10 点入る
    c=l+A は、消し始める座標,  l+=A は対象行からもう一度ループしなおすため

21: 〜 23:  消去
    Z[c]=Z[c-A] で該当行を1行上で上書きしてしまう
    1番上まで繰り返せば(--c>A)、1行揃った行だけがなくなる

27: 〜 30:  画面の描画とゲームフィールドの更新

S[]  実際に画面に描画される内容

27:  i="" を数値とみなすと i==0 になるらしい
     本当は別に i=0 としておくのがよいと思う

28:  描画とフィールド更新
    描画対象は X[i] と Z[i] をORで混合したもの
    ただし、壁や床の番兵を同時に初期化・描画するため、やや、ややこしい

    一つめの三項演算子 ? でブロックの有無を判定
    ブロックが無ければ "_" を描画
    (i+1)%A<2 は左右の壁に相当、画面では離れてみえても、実際はつながっているため
    i>228 は床に相当

    二つめの三項演算子 ? で、右の壁かどうかを判定
    i%A==0 ならば右の壁に相当、改行 <br> を描画する
    i%A>0 ならば、単なるブロックがある

30:  フィールド S[] と得点 P を描画

31:  ゲームオーバー判定
    Z[5] は、パーツの出現地点
    setTimeout(Y,99-P) は、99-P ミリ秒後に関数 Y() を実行するという意味
    P は得点なので、P が99まで大きくなるにつれゲーム速度が速くなる

    Z[5]==0 ならば setTimeout が評価されゲームが継続するが、
    Z[5]==1 すなわち埋まったならば setTimeout は評価されずにゲームが終了する

33:  ゲームのメイン関数 Y() の呼び出し
    h=e=...=P=0 としているのは、単なる文字数稼ぎ

# EOF