LoginSignup
543
497

More than 1 year has passed since last update.

10行ぷよぷよのソースコードを読む

Last updated at Posted at 2020-05-31

プログラマをする上では10行程度のプログラムを読めるといいとのこと。上記記事のブックマークコメントから飛んで10行ぷよぷよなるものを目にしたので、ソースコードを解読してみました。
解説記事を探したのですが、見当たらなかったので書いてみてます。

10行ぷよぷよについて

javascriptで動くぷよぷよです。
開発者のPascalさんのサイトはもう残っていないとのことで、下記からソースを拝借しました。

実行すると、一人用のとことんぷよぷよが遊べます。
そのまま実行しても怒られるので、後述しますがちょっとだけ手を加えてます。

GIF 2020-05-31 16-09-40.gif

ソースコード

ソースコードは以下です。

puyo.html
<body onKeyDown=K=event.keyCode-38 id=D><script>for(M=N=[i=113];--i;M[i-1]=i%8<
2|i<8)function Y(){e++;if(e%=10)for(N=[K-2?K-50?h-=M[h+l-K]|M[h-K]?0:K:M[h+p]||
(x=p,p=-l,l=x):e=0],K=0;++i<113;N[i]=M[i])N[h]=B>>2,N[h+l]=B%4-B%1+2;if(!e&&(h
-=8,!g||M[h]+M[h+l])){C=[M=N];for(i=g=1;++i<103;!M[i]*n&&(M[i]=n,g=M[i+8]=0))n=
M[i+8];for(;--i;){n=c=0;for(E=[i];g&M[i]>1&n>=c>>2;t>102|C[t]|M[i]-M[t]||(E[++n
]=C[t]=t))t=p,p=-l,l=t,t+=E[c++>>2];for(;c>16&&n;)g=M[E[n--]]=0}B=g?Math.random
(h=100,l=8,p=-1)*16+8:--e}for(i=104,S="";i--;S+=n--?n?"<a style=color:#"+(248*n
)+">●"+"</a>":i%8?"":"■<br>":"_")n=N[i];D.innerHTML=S;M[100]*g||setTimeout
(Y,50)}Y(g=h=e=9,l=p=K=0)</script>

実は現在のブラウザだとこのままじゃ動かないので、少しだけ手を加えるとこんな感じ。(一箇所 {...} を書き足してます)

puyo.html
<body onKeyDown=K=event.keyCode-38 id=D><script>for(M=N=[i=113];--i;M[i-1]=i%8<
2|i<8){function Y(){e++;if(e%=10)for(N=[K-2?K-50?h-=M[h+l-K]|M[h-K]?0:K:M[h+p]||
(x=p,p=-l,l=x):e=0],K=0;++i<113;N[i]=M[i])N[h]=B>>2,N[h+l]=B%4-B%1+2;if(!e&&(h
-=8,!g||M[h]+M[h+l])){C=[M=N];for(i=g=1;++i<103;!M[i]*n&&(M[i]=n,g=M[i+8]=0))n=
M[i+8];for(;--i;){n=c=0;for(E=[i];g&M[i]>1&n>=c>>2;t>102|C[t]|M[i]-M[t]||(E[++n
]=C[t]=t))t=p,p=-l,l=t,t+=E[c++>>2];for(;c>16&&n;)g=M[E[n--]]=0}B=g?Math.random
(h=100,l=8,p=-1)*16+8:--e}for(i=104,S="";i--;S+=n--?n?"<a style=color:#"+(248*n
)+">●"+"</a>":i%8?"":"■<br>":"_")n=N[i];D.innerHTML=S;M[100]*g||setTimeout
(Y,50)}}Y(g=h=e=9,l=p=K=0)</script>

これで動くんだ、すごい。
これじゃ何もわからないので、とりあえずbeautifyします。

puyo.html
<body onKeyDown=K=event.keyCode-38 id=D>
<script>

for (M = N = [i = 113]; --i; M[i - 1] = i % 8 <
    2 | i < 8) {
    function Y() {
        e++;
        if (e %= 10)
            for (N = [K - 2 ? K - 50 ? h -= M[h + l - K] | M[h - K] ? 0 : K : M[h + p] ||
                    (x = p, p = -l, l = x) : e = 0
                ], K = 0; ++i < 113; N[i] = M[i]) N[h] = B >> 2, N[h + l] = B % 4 - B % 1 + 2;
        if (!e && (h -= 8, !g || M[h] + M[h + l])) {
            C = [M = N];
            for (i = g = 1; ++i < 103; !M[i] * n && (M[i] = n, g = M[i + 8] = 0)) n =
                M[i + 8];
            for (; --i;) {
                n = c = 0;
                for (E = [i]; g & M[i] > 1 & n >= c >> 2; t > 102 | C[t] | M[i] - M[t] || (E[++n] = C[t] = t)) t = p, p = -l, l = t, t += E[c++ >> 2];
                for (; c > 16 && n;) g = M[E[n--]] = 0
            }
            B = g ? Math.random(h = 100, l = 8, p = -1) * 16 + 8 : --e
        }
        for (i = 104, S = ""; i--; S += n-- ? n ? "<a style=color:#" + (248 * n) + ">●" + "</a>" : i % 8 ? "" : "■<br>" : "_") n = N[i];
        D.innerHTML = S;
        M[100] * g || setTimeout(Y, 50)
    }
}
Y(g = h = e = 9, l = p = K = 0)

</script>
</body>

あっなんかこねくり回して最後にhtml書いてるなーくらいはわかるようになりました。
この状態でゴリゴリ読んでみたので、以降は解説に入ります。

解説

おおまかな流れ

コードにコメントを入れて、大まかな流れを説明します。

puyo.html
<body onKeyDown=K=event.keyCode-38 id=D>
<script>

// フィールドを初期化して枠のブロックを配置
for (M = N = [i = 113]; --i; M[i - 1] = i % 8 <
    2 | i < 8) {
    // コード削減のためループ内部でそのまま関数宣言
    function Y() {
        // タイマーをインクリメント
        e++;
        // キー操作受付(xで右回転/→で移動) + 操作ぷよ配置
        if (e %= 10)
            for (N = [K - 2 ? K - 50 ? h -= M[h + l - K] | M[h - K] ? 0 : K : M[h + p] ||
                    (x = p, p = -l, l = x) : e = 0
                ], K = 0; ++i < 113; N[i] = M[i]) N[h] = B >> 2, N[h + l] = B % 4 - B % 1 + 2;
        // 落下処理
        if (!e && (h -= 8, !g || M[h] + M[h + l])) {
            C = [M = N];
            // 下にぷよが存在しない場合に落下させる
            for (i = g = 1; ++i < 103; !M[i] * n && (M[i] = n, g = M[i + 8] = 0)) n =
                M[i + 8];
            // 同色で連結しているぷよをカウント、4連結以上で消滅させる
            for (; --i;) {
                n = c = 0;
                for (E = [i]; g & M[i] > 1 & n >= c >> 2; t > 102 | C[t] | M[i] - M[t] || (E[++n] = C[t] = t)) t = p, p = -l, l = t, t += E[c++ >> 2];
                for (; c > 16 && n;) g = M[E[n--]] = 0
            }
            // 新しく落ちてくるぷよを生成
            B = g ? Math.random(h = 100, l = 8, p = -1) * 16 + 8 : --e
        }
        // フィールドを描画
        for (i = 104, S = ""; i--; S += n-- ? n ? "<a style=color:#" + (248 * n) + ">●" + "</a>" : i % 8 ? "" : "■<br>" : "_") n = N[i];
        D.innerHTML = S;
        // ゲーム継続/終了判定
        M[100] * g || setTimeout(Y, 50)
    }
}
// ゲーム開始
Y(g = h = e = 9, l = p = K = 0)

</script>
</body>

以降は個々の処理を見ていきます。

変数

変数がわかると見通しが良さそうなので、先に一覧します。

変数 役割
M, N フィールド
K 数値
e タイマー
g 落下中フラグ
h 主ぷよ操作位置
l 従ぷよ現在位置
p 従ぷよ移動先位置
E 連結しているぷよ位置
C 連結しているぷよ位置
B 操作ぷよ用乱数

初期化処理

for (M = N = [i = 113]; --i; M[i - 1] = i % 8 <
    2 | i < 8) {
    function Y() {
...
}
Y(g = h = e = 9, l = p = K = 0)
変数 役割
M, N フィールド

ぷよぷよのフィールドに使うM, Nを1次元配列で初期化しています。左上から右下にデクリメントされています。
113はぷよぷよのフィールドである 6 * 12 に、左右下のブロックと上1列を足した 8 * 14 = 112--i でカウントダウンして使うための数字です。

配列の値が持つ意味を以下に書きます。

意味
0
1 ブロック
2-5 ぷよ(4色)

--iiを返すことを利用し、カウンタをcondition部に書いています。
final-expression部ではフィールドにブロック(M[i] = 1)を配置しています。

for loop の中は関数の宣言に使われています。
もちろん無駄に回数分呼ばれますが、コード量の削減を優先しています。

最後に、loop内で宣言した関数Yを実行しています。
初期化している変数については都度解説します。

キー操作受付

        e++;
        // キー操作受付(xで右回転/→で移動) + 操作ぷよ配置
        if (e %= 10)
            for (N = [K - 2 ? K - 50 ? h -= M[h + l - K] | M[h - K] ? 0 : K : M[h + p] ||
                    (x = p, p = -l, l = x) : e = 0
                ], K = 0; ++i < 113; N[i] = M[i]) N[h] = B >> 2, N[h + l] = B % 4 - B % 1 + 2;

if文の代わりに、論理演算子、三項演算子を多用しています。
見やすく改行していきます。

        e++;
        if (e %= 10)
            for (N = [
                K - 2 ?
                    K - 50 ?
                        h -= M[h + l - K] | M[h - K] ?
                            0
                            : K
                        : M[h + p] || (x = p, p = -l, l = x)
                    : e = 0
                ], K = 0;
                ++i < 113;
                N[i] = M[i])            
                N[h] = B >> 2, N[h + l] = B % 4 - B % 1 + 2;
変数 役割
M, N フィールド
K 数値
e タイマー
g 落下中フラグ
h 主ぷよ操作位置
l 従ぷよ現在位置
p 従ぷよ移動先位置
B 操作ぷよ用乱数

1つ1つ見ていきます。

        e++;
        if (e %= 10)
...

eはタイマーです。代入と判定を同時にしています。0の場合のみ操作を受け付けず、後述の落下処理に入ります。

次はキーコードの判定です。
キーコードについてはHTML側に書いてあるので、先に貼っておきます。

<body onKeyDown=K=event.keyCode-38 id=D>

拾ったキーコードを-38することで、Kには下記の値が入ります。

K キー
-1
1
2
50 x

これを踏まえると、こうなります。

            for (N = [
                K - 2 ?
                    K - 50 ?
                        // ←キー/→キーの場合
                        h -= M[h + l - K] | M[h - K] ?
                            0
                            : K
                        // xキーの場合
                        : M[h + p] || (x = p, p = -l, l = x)
                    // ↓キーの場合
                    : e = 0
                ], K = 0;

Nに代入する意味は特になく、コード削減のためにこう書いています。
ここではh, e, p, l をキー操作に合わせて更新しています。

まず↓キーの場合、e = 0 が実行されます。インクリメントを待たず、落下処理に移動します。
←キー/→キーの場合、 h -= -1 or 1 が実行されます。hは操作しているぷよの位置を表すので、左右に移動できます。
M[h + l - K] | M[h - K] は壁判定です。移動先が空(0)の場合だけ移動します。

xキーは回転操作です。ぷよが右に回転します。主ぷよの操作位置hは変わりませんが、従ぷよが回転してlが変わります。
lpは、このあとそれぞれl = 8, p = -1で初期化されています。
なので、xを押すごとに (8,-1), (-1,-8), (-8,1), (1,8)と遷移します。

                    // ↓キーの場合
                    : e = 0
                ], K = 0;
                ++i < 113;
                N[i] = M[i])            

今回もカウンタがcondition部に書かれています。初期化を避けるため、同じiをインクリメントしたりデクリメントしたりしていきます。
final-expression部では雑に初期化したNをもとに戻しています。
ここのfor loopはなんとか削減出来るような気もちょっとします。

                ++i < 113;
                N[i] = M[i])            
                N[h] = B >> 2, N[h + l] = B % 4 - B % 1 + 2;

最後にN[h]を設定します。ここで出てくるBはのちのち出てきますが、下記で乱数がセットされています。

            B = g ? Math.random(h = 100, l = 8, p = -1) * 16 + 8 : --e

h,l,pの初期化の処理を混ぜていますが、乱数に影響はありません。
ここでは8~24の乱数を生成しています。それぞれ主ぷよN[h]に4で割った商を、従ぷよn[h+l]に4で割った剰余を使って2-5であらわされる色ぷよを設定しています。

ここまでが操作処理です。

落下処理

        // 落下処理
        if (!e && (h -= 8, !g || M[h] + M[h + l])) {
            C = [M = N];
            // 下にぷよが存在しない場合に落下させる
            for (i = g = 1; ++i < 103; !M[i] * n && (M[i] = n, g = M[i + 8] = 0)) n =
                M[i + 8];
            // 同色で連結しているぷよをカウント、4連結以上で消滅させる
            for (; --i;) {
                n = c = 0;
                for (E = [i]; g & M[i] > 1 & n >= c >> 2; t > 102 | C[t] | M[i] - M[t] || (E[++n] = C[t] = t)) t = p, p = -l, l = t, t += E[c++ >> 2];
                for (; c > 16 && n;) g = M[E[n--]] = 0
            }
            // 新しく落ちてくるぷよを生成
            B = g ? Math.random(h = 100, l = 8, p = -1) * 16 + 8 : --e
        }

これも改行します。

        if (!e && (h -= 8, !g || M[h] + M[h + l])) {
            C = [M = N];
            for (i = g = 1; 
                ++i < 103;
                !M[i] * n && (M[i] = n, g = M[i + 8] = 0)) n = M[i + 8];

            for (; --i;) {
                n = c = 0;
                for (E = [i];
                    g & M[i] > 1 & n >= c >> 2;
                    t > 102 | C[t] | M[i] - M[t] || (E[++n] = C[t] = t))
                    t = p, p = -l, l = t, t += E[c++ >> 2];

                for (; c > 16 && n;) g = M[E[n--]] = 0
            }
            B = g ? Math.random(h = 100, l = 8, p = -1) * 16 + 8 : --e
変数 役割
M, N フィールド
e タイマー
g 落下中フラグ
l ぷよ隣接位置
p ぷよ隣接位置(next)
E 連結しているぷよ位置(リスト)
C 連結しているぷよ位置(フィールド)
B 操作ぷよ用乱数

1つ1つ見ていきます。

        if (!e && (h -= 8, !g || M[h] + M[h + l])) {

まず判定です。eはタイマーです。e %= 10 されているので、10回毎に !etrueになります。
タイマーの次は、操作位置hを1段下に移動(-8)します。これはほとんどtrueになります。
続いてgですが、これは落下中フラグです。0なら操作中となります。
M[h], M[h+l]は移動先の操作ぷよの位置です。移動先が共に空(0)なら0になります。

なのでこれは、なんらかのぷよが落下中または操作ぷよの落下先が空じゃなければ(接地していれば)処理開始、という判定になります。

        if (!e && (h -= 8, !g || M[h] + M[h + l])) {
            C = [M = N];
            for (i = g = 1; 
                ++i < 103;
                !M[i] * n && (M[i] = n, g = M[i + 8] = 0)) n = M[i + 8];

C を初期化していますが、まだ使いません。
for の処理は落下処理です。
全フィールドを下の段から走査し、M[i+8]に値が存在していてM[i]が空の場合に、M[i]=M[i+8]を実行します。
その際に、落下中フラグg0にします。
これで落下しうる全てのぷよが1段落下します。

            for (; --i;) {
                n = c = 0;
                for (E = [i];
                    g & M[i] > 1 & n >= c >> 2;
                    t > 102 | C[t] | M[i] - M[t] || (E[++n] = C[t] = t))
                    t = p, p = -l, l = t, t += E[c++ >> 2];

                for (; c > 16 && n;) g = M[E[n--]] = 0

ここがぷよを消す処理です。

先に流れを説明します。
* 上から全フィールドを走査(i)
* M[i]が色ぷよの場合に周囲4マスを走査し
* 周囲に同色の未捜査のぷよがあったら、そのぷよを起点に周囲4マスを走査
* 未捜査のぷよが無くなったら終了し、4つ以上のぷよを走査していたらそれらを削除

中身を見ていきます。

まずiを例のごとくデクリメントして上からloopします。

珍しくcondition部がに純粋にconditionが書かれている気がします。
まずgの判定が入っているので、落下途中ではぷよを消さないようにしています。
M[i] > 1 なので、消すのは2-5の色ぷよだけです。
n >= c >> 2 は、後述しますが走査の完了を判定します。

condition部の次は中身である t = p, p = -l, l = t, t += E[c++ >> 2]; が実行されます。
t = p, p = -l, l = ttの位置を回転させています。
また、c++ >> 20-30になります。E[0]にはiが格納されていますので、ti周りに回転します。

final-expression部の t > 102 | C[t] | M[i] - M[t] || (E[++n] = C[t] = t) が実行されます。
C は走査済みのフィールドを格納します。
A | B || C は AとB両方がfalseの場合だけCが実行されるということなので、
tが102以下(画面外の13段目を含まない)で、未走査(C[t]false)で、iと同色(M[i] - M[t])の場合に、tを次の走査対象としてEに格納してC[t]を走査済みに、nをインクリメントします。

新しい走査対象を中心にまたtを回転させ、同様の処理を続けます。
新しい走査対象が見つからないときに、インクリメントのタイミングからcnの4倍になります。
これが走査完了条件となって、戦術のn >= c >> 2trueとなってループを抜けます。

最後に、for (; c > 16 && n;) g = M[E[n--]] = 0で、c > 16 すなわち4つ以上のぷよを走査した場合に、Eをループしてぷよを消します。

            B = g ? Math.random(h = 100, l = 8, p = -1) * 16 + 8 : --e

ぷよを消す処理まで終わったら、次の操作ぷよを生成します。
落下が完了(g)していれば操作ぷよに使う乱数を生成し、していなければ落下処理を続けるためにタイマーを戻します。
h=100は操作ぷよの出現位置です。

描画処理

        // フィールドを描画
        for (i = 104, S = ""; i--; S += n-- ? n ? "<a style=color:#" + (248 * n) + ">●" + "</a>" : i % 8 ? "" : "■<br>" : "_") n = N[i];
        D.innerHTML = S;

HTMLを描画します。
フィールドであるNをループして、上からタグを生成しています。端まで要った場合(i%8==0)に改行するくらいです。

終了処理

        // ゲーム継続/終了判定
        M[100] * g || setTimeout(Y, 50)

落下が完了していて、かつ操作ぷよの出現位置が空でない場合はゲームオーバーです。
そうでない場合は、再び関数を実行して次のフレームに移動します。

ということで、ぷよぷよがなんと動きました。すごい。

終わりに

ちょうどぷよぷよをネタにコードを書こうと思っていたところにこれを見つけて、実装の参考にもなるし見てみるか...と思ったのがきっかけでした。だいぶ参考になった。

ノーヒントで読むのはしんどかったですが、わかりやすい描画処理あたりから遡って追えば読めました。ちょっと論理演算子に強くなった気がします。楽しかったです。

現在CodeGolfとかやってる人たちならもっともっと短くできるんだろうか。
気が向けば挑戦してみたいです。

543
497
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
543
497