以下のURLの7行テトリスのプログラムを、プログラミング初心者(プログラミング言語共通の「教科書的な文法」を理解しようやくアルゴリズムを勉強する重要性がわかってきた程度の)が理解できる初心者向け入門書に載っているような基本的な記述方法で書き直して下さい(javascript)、コードは動作確認が取れたものをご回答下さい。
こちらも理解する努力はするつもりです。変数は増やしても、名称を変えても、コードが長くなっても構いませんのでよろしくお願いします。その際、その処理は何をするのかなどコメントもつけて下さると嬉しさに涙があふれます。
参考:
(公式解説)http://web.archive.org/web/20060111010926/http://www.isl.cs.gunma-u.ac.jp/~shingo/make/7line/teto.html
(紹介ブログ)http://zapanet.info/blog/item/1125
私も興味があったので先ほど読んでみました。
(書き換えたコードはインデントが全角スペースになっています)
>A||B は、Bに副作用が無ければ A|B あるいは A+B に等価 ?
副作用とは、例えば文Bが x=1 という文だとすると、文Bの評価値は(代入演算子'='は代入した値を返すので)1になります。
このとき代入演算子の作用としてxに1が代入されますが、この、文Bが評価されることで"変数xの値が変わること"を文Bの副作用と言います。
>また、等価というのはA=0,B=0の場合A||BはA=B=falseなのでA||B=false。A|B=0+0=0=falseという意味でしょうか?
最終的に真理値(bool)として評価されるのでそのとおりです。
(ただし、A=-2,B=+2などでA+B=0となる場合があるときには成り立たなくなります)
>7: C[i]=p*A-(p/9|0)*145;//A=12,p=x+y*12
A=12,p=x+y*12,(p/9|0)=yを使って代入演算子の右辺を書き換えてみます。
(右辺)=(x+y*12)*12-y*145=x*12+y*144-y*145=-y+x*12。
つまりp=(x,y)をp'=(-y,x)に写していることになります。
>k 1行消去で加算する得点、毎回どこかで 0 か 1 に初期化する必要がある
>という説明ですが、「どこか」というのは12行目だと思うのですが、なぜ初期かは0でも1でもよいのでしょうか?
見たところkを使っているのは12行目(初期化)と21行目(得点Pへの加算)だけのようなので
得点が1→3→6→10と増えるか0→1→3→6と増えるかの違いだけのようです。
>18: Z=X,X=[],
>(略)Zは元Xを向いていて、新Xは新しい配列である空の配列に向けられるという理解で合うっていますか?
合っています。
>19: B=[[-7,-20,6,h=17,-9,3,3][t=++t%7]-4,0,1,t-6?-A:2];
書き換えると
h=17;
t = ++t%7;
TMP=[-7,-20,6,17,-9,3,3];
b0 = TMP[t] -4;
b1 = 0;
b2 = 1;
if(t != 6){
b3 = -12;
}else{
b3 = 2;
}
B = [b0,b1,b2,b3];
のようになります。(わざわざb0で-4しているのは文字数節約のためだと思います)
>20: for(l=228;l--;){
>lというのはブロックの座標を表しているんですよね?下から2段目より上のブロックを全て調べるということで合っていますか?
はい。
>21: for(l%A?l-=l%A*!Z[l]:(P+=k++,c=l+=A);--c>A;){
書き換えると
if(l%12 != 0){
// lが左端のブロックでなければ
if(!Z[l]){
// lにブロックがなければ1つ上の行の右端に移動
l -= l%12;
}
// lにブロックがあれば左のブロックへ
}else{
// lが左端のブロックであれば
P += k++;//得点の加算
l += 12; //lを今の行の右端に戻す
c = l; //次のforのためのカウンターセット
}
//上にあるブロックを1行ずつ下げる処理
for(;--c>12;){
Z[c] = Z[c-12];
}
となります。
28: S+=X[i]|(X[i]=Z[i]|=++i%A<2|i>228)?i%A?"■":"■<br>":"_";
書き換えると
x = X[i]; // 前フレームの固定ブロックと現在の落下中ブロックを退避
side = ++i%A<2; // 左右の端かどうか。左端のとき++i%A=1,右端のとき++i%A=0になる。
// 元のコードではこのインクリメントはあとで実行されるので
// 以下ではi→i-1として補正しています
bottom = i>228; // 底面かどうか
Z[i-1] |= (side|bottom); // Zにフィールドの縁のブロックを合成
X[i-1] = Z[i-1]; // Xにも上書き、落下中ブロックは消去。落下ブロックは10行目で追加される
if(x|X[i-1]){
//ブロックの描画
if(i%A){
S+="■";
}else{
//i%Aが0のとき、すなわち右端のとき
S+="■<br>";
}
}else{
//空白の描画
S+="_";
}
となると思います(コメントにはやや自信がありません)
理解の助けになれば幸いです。
処理される数値の気持ちになって読んでみれば、どの変数がどんな役割をしているのか理解しやすいはず。
裏で数値処理する部分と、実際に表示する部分とで、分けて考えるのがポイントかも。
ところで、なにに引っかかっているんでしょう?
A||B は、Bに副作用が無ければ A|B あるいは A+B に等価 ?
という説明で、副作用とはなんでしょうか?また、等価というのはA=0,B=0の場合A||BはA=B=falseなのでA||B=false。A|B=0+0=0=falseという意味でしょうか?
7: C[i]=p*A-(p/9|0)*145;//A=12,p=x+y*12
→回転行列での-90度回転先x',y'の座標は、-y,xとなるはずで、x=p%12、y=p/9|0なので、p'=-(p/9|0)+(p%12)*Aとなると思うんですが、実際は上記のコードです。返す数値も異なるので私が理解できてないわけですが、これはどういうことでしょうか。
k 1行消去で加算する得点、毎回どこかで 0 か 1 に初期化する必要がある
という説明ですが、「どこか」というのは12行目だと思うのですが、なぜ初期かは0でも1でもよいのでしょうか?
18: Z=X,X=[],
解説のx=[]とすることで、xが新しい配列に向けられるとありますが、Zは元Xを向いていて、新Xは新しい配列である空の配列に向けられるという理解で合うっていますか?
19: B=[[-7,-20,6,h=17,-9,3,3][t=++t%7]-4,0,1,t-6?-A:2];
まず、この文の読み方が分かりません。B=[1,2,3]やB=[[1,2],[3,4]]ならわかりますが、上記の文の[~][~]-4という部分はどうなっているのか見当も付きません。さらに、-7,-20,6・・・-3,3というのは何を表しているのでしょうか?
20: for(l=228;l--;){
lというのはブロックの座標を表しているんですよね?下から2段目より上のブロックを全て調べるということで合っていますか?
21: for(l%A?l-=l%A*!Z[l]:(P+=k++,c=l+=A);--c>A;){
解説のl%A==0になると1行がブロックで埋められたことになるというのがよく分かりません、なぜそう言えるのですか?
また、そのとき、l-=l%A*!Z[l]:フィールドの座標lが空のときlを12で割ったあまりだけlから引く(?)という意味がわかりません。
(P+=k++,c=l+=A);--c>A;)のcが関わる式が表す意味が分かりません。for(l%A?l-=l%A*!Z[l]:(P+=k++,c=l+=A);--c>A;){という文を細かく動作と絡めて翻訳していただけると助かります。
28: S+=X[i]|(X[i]=Z[i]|=++i%A<2|i>228)?i%A?"■":"■<br>":"_";
X[1]または(X[i]=Z[i]|=++i%A<2|i>228)?i%A?"■":"■<br>":"_"をSに加えるというのは分かりますが、(X[i]=Z[i]|=++i%A<2|i>228)の読み方が分かりません。"X[i]=Z[i]"または"=++i%A<2"またはi>228の""の部分が理解できません。一つ目の""はX[i]にZ[i]を代入?、2つ目の""は=がなぜか前についていて読み方の見当もつきません。ブロックの有無を判定しているということですが、これはどういうことでしょうか?それと、壁や床の番兵という説明もよくわかりません。そもそも番兵というものが2種類の意味があるらしいのですが(ダミーのデータと終了専用の値)このプログラムではどういう意味でどこでどのようにして使われているのでしょうか。
細かくてすみません
Roundは四捨五入、floorは切捨ての関数なので、例えば
Round(13/12)=Round(1.0833…)=1,
floor(13/9)=floor(1.44…)=1
という具合に「たまたま」12で割って四捨五入、でも9割って切り捨て、でも変わらないことがある、
ということになります。
今回問題になるのは落下ブロックの位置が、その「たまたま」の範囲に入っているかどうかですが、
落下ブロックは、
□□■□□ 原点(★)付近で左の黒く塗った部分にしか存在しないため、
□■■■□ この範囲で値が一致していればOK、ということになります。
■■★■■ (p=-24, p=-13~-11, p=-2~+2, p=+11~+13, p=+24)
□■■■□ 計算は割愛しますが、見事に必要な範囲で
□□■□□ round(p/12)=floor(p/9)が成り立っています。
(今回は運よく書き換えられただけで、常に成り立つわけではありません。
私的見解ですが、通常のプログラムでは無理に簡潔な書き方を探すより
分かりやすい、しかし無駄がない、書き方にしておくのが良いと思います。)
floor(p/9)=p/9|0が成り立つのはビット和'|'を取る際に値が整数に丸められることに拠ります。
つまり、p/9|0は実行時にfloor(p/9)|0と解釈されるので、成り立ちます。
floor(x)は数学的に定義される数値の整数部分を返します。つまり、
floor(-1.2)=-2 // -1.2 = -2 + 0.8
一方、ビット和を取るときなどに発生する丸めは小数部分を無視します
-1.2|0 = -1|0 = -1
(両者はx≧0の範囲では等しくなります)
という違いがあるため、p<0の部分で
round(p/12)=p/9|0
は成り立ちますが、
round(p/12)=floor(p/9)
は成り立ちません。
※話のスジは先のコメントの通りです
※マイナスの範囲では round(p/12)=p/9|0 のみ成り立ちます
きちんと確認せずにコメントを投稿してしまい
混乱させてしまうことにお詫び申し上げます。
floorとすることでたまたま、必要な範囲でroundと値が一致し、12→9とすることで値がその範囲で一致しつつ一文字削減できる、しかしこれはp<0でround(p/12)と等しくならない。ここでfloor(p/9)をp/9|0と書き換えることでp<0でもround(p/12)と値が一致しかつさらなる文字数削減となると。・・・間違っていたら指摘願います
ほぉ〜、floor()はマイナスの範囲で挙動が異なるのですね。いやはや、大変勉強になりました!たった7行の中にもの凄いアイデア、技術がぎゅうぎゅうに詰め込まれていたことが実感できました。懇切丁寧に理解に導いて下さったSTTS様にはただただ頭が下がる思いです、「プログラミングすげー!!」と心から思いました。また他のプログラムも解体してみようかと思っています。どうもありがとうございました!