9行のコードでぷよぷよが出来るらしい(前半)

9行のコードでぷよぷよが出来るらしい(前半)

目次

初めに
概要
ソースコード
動かしてみる
コードの内容を理解する
 全体の流れと変数一覧
 初期化とゲームループの開始
 タイマーとキー操作
6行でテトリスも書けるらしい
最後に

初めに

「ぷよぷよ」ってご存じですか、上から色のついたスライムが降ってきて4つ揃うと消えるあれです。
私には思い出深いゲームですが、今の世代の人はぷよぷよってやるんでしょうか。

さて、先日「コード9行でぷよぷよ」という記事を見かけたので、本当に動くのか、どんな内容なのかを検証してみようと思います。

概要

調べてみると、元々は2007年ころにpascalさんという開発者の方が10行ぷよぷよを書いたのが始まりのようです。
javascriptで動くぷよぷよです。本家のぷよは出てきませんのでご了承ください。

始まりのサイトは既に消滅していたので、10行ぷよぷよを紹介していたサイトと、ソースコードを分かりやすく解説してくれたサイトを参考に載せておきます。

ソースコードを公開してくれていたのは下記
 わずか681バイトで動くぷよぷよ

https://zapanet.info/blog/item/1134

ソースコード

サイトに記載されているソースコードは下記
本当に9行なんだとびっくり。
ただ、このままだと何のこっちゃ分かりませんね。

<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>

動かしてみる

先ほどのソースコードをメモ帳等に張り付け、拡張子を”.html”で保存します。何とファイルサイズは687バイト。
後はダブルクリックでファイルを開くだけ。

おやおやおや、真っ白で動かないぞ。

ソースコードをそのまま実行した画面

という事でコードを確認するとforの後に{}が足りないようなので修正。
9行のままだとさすがに分かりにくかったので綺麗にする。
だいぶ全体像が見えてきますね。

for (M = N = [i = 113]; –i; M[i – 1] = i % 8 < 2 | i < 8)の後に {
Y(g = h = e = 9, l = p = K = 0)の前に}
をそれぞれ追加しています。

<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」にして保存して動かしてみます。

9行ぷよぷよの実行画面

こいつ…動くぞ!
本当に動いた、連鎖もする、すごい。
右端だと回転できなかったり、たまにぷよが消えたりするけど、ちゃんとぷよぷよしてます。

コードの内容を理解する

では、9行(成形後は28行)のコード内容を見ていこうと思います。

@yoitomakenouta(ta c)さんが書かれたQiitaの記事が非常に分かりやすかったので
分かりやすい解説を確認しつつ、それでも分からない部分を調べながらゆっくりと理解を深めていきます。

まずは全体の流れと変数の意味を知って進めていくと分かりやすかったので、同じように進めます。

全体の流れと変数一覧

ブログの方でかなり詳しく書かれていたので、追記するところはありません。

<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>
変数意味
M、Nフィールド
K数値
eタイマー
g落下中フラグ
h主ぷよ操作位置
l従ぷよ移動先位置
p従ぷよ移動先位置
E連結しているぷよ位置
C連結しているぷよ位置
B操作ぷよ用乱数
E連結しているぷよの位置
C連結しているぷよの位置
変数一覧

初期化とゲームループの開始

コードの内容で、理解できなかった部分を追記しながら進めて行きます。

 for (M = N = [i = 113]; --i; M[i - 1] = i % 8 < 2 | i < 8) {

M、Nはぷよぷよのフィールドを表してして、一次配列で初期化しています。
初期値でi=113が設定されていて、–iによってiの値がデクリメントされています。

この113は8列×14行の計112をカウントダウンするための数字です。

ぷよぷよフィールド概要

実際のぷよぷよが行われるのは白のセルの部分ですね。

M[i - 1] = i % 8 < 2 | i < 8

iを8で割った余りが2未満か、iが8より小さい場合は真(1)、それ以外は偽(0)とし、結果をM[i-1]へ代入しています。
この条件によってフィールドにブロックを配置しているようです。
i/8の余りは下記のようになり、2未満の左右の端の列ち一番下の列が真となります。

配列の値は、0なら空、1ならブロック、2~5がぷよとなっています。

i/8の余り

タイマーとキー操作

次にタイマーとキー操作についてです。

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;

まずはタイマー部分についてです。

e++;
  //キー操作(ぷよ移動と回転)の受付とぷよ操作
  if (e %= 10)

eがタイマーで、ループの度にe++の部分でタイマーの値が1増加します。
if(e %= 10) の部分でeの値を10で割った余りが0以外であれば操作を受け付け、0の場合は下記の処理に入ります。

次は操作キーコードの判定についてです。

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

keyCodeで検索すると詳しい内容を確認することができました。

今は非推奨みたいですが、各キーに値が割り振られているんですね。今回使うキーだけ抜粋します。
各キーの値から、ー38した値をKに入れています。

keykeyCodeKの値
37-1
391
402
x8850
操作キーに対するK値

上記を踏まえてforの中身を確認すると

 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;

Kが2の場合↓キーが押されているので、e = 0 となりタイマーの経過を待たず、落下処理に移ります。

Kが50の場合xキーが押されているので、ぷよの回転操作に入ります。実際に操作してみれば分かりますが、メインの主ぷよ(h)を中心に周りを従ぷよ(l)が右回りに回転します。
l(従ぷよ配置位置)とp(従ぷよ移動位置)は、後ほど記載する処理で l = 8 ,p = -1で初期化されています。
これは、落下処理が始まるとき、主ぷよの上(+8の位置)に従ぷよがくっついて落ちてくるためです。
xを押すごとに、(8, -1)※上に従ぷよ、(-1,-8)※右に従ぷよ、(-8,1)※下に従ぷよ、(1,8)※左に従ぷよ が切り替わります。
※上で記載したフィールド全ての番号を振ったものを見てもらえると分かりやすいと思います。

なので、 (8, -1)※上に従ぷよ の状態の時に、右端の■ブロック横に主ぷよがあるとxを押しても回転できません。

Kが-1か1の場合←キーか→キーが押されているので、左右に移動します。

h -= M[h + l - K] | M[h - K] ? 0 : K 

M[h + l – K] | M[h – K] の部分で左右端の■ブロックかどうかを判定していて、0であれば移動できます。

最後にN[h](主ぷよの位置)とぷよの色を設定します。

N[h] = B >> 2, N[h + l] = B % 4 - B % 1 + 2;

N[h] = B >> 2 は初めのぷよの表示位置です。
Bはこの後に書かれる処理でランダムな値を持っていて、値によってぷよの色を設定しています。
下記処理で生成される8~24のランダムな整数を4で割った余り+2をする事で2~5でぷよの色を決めている。
色の処理は後半で記述。

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

この処理は落下処理が終わるタイミングで書かれている処理ですが、乱数生成部分が、上記に関わるので記載します。
回転処理の際に l(従ぷよ配置位置)とp(従ぷよ移動位置) の処理の話を書きましたが、この部分で初期化が行われています。

Bにはランダムな値を生成しています。Math.random()メソッドがランダムに0以上1未満の範囲で出乱数を生成しています。
出た値を×16して+8しているので、値は8~24のどれかです。

ここまでで一旦前半部分を終わります。
@yoitomakenouta(ta c) の記事がかなり詳細に説明していただいているので、細部を調べるくらいになってしまいましたが、このコードを1人で解読されたのはかなりしんどかっただろうとお察しします。
後半も頑張って、理解していこうと思います。

6行でテトリスも書けるらしい

紹介した元のブログ内容としてはこちらがもともとのメインだったみたいなんですが
わずか500バイトほどで動く6行テトリスです。
同じサイトに載ってますのでこちらも気になる方は見てみてください。

最後に

今回は、たまたま見かけた「6行テトリス」「9行ぷよぷよ」という言葉に惹かれ見てみましたが、ソースコードは突き詰めればこんなに短くなるのかと感動しました。
コードに触る良いきっかけになりました、特に@yoitomakenouta(ta c)さんには心から感謝です。
後半の落下処理と削除処理の部分も引き続き頑張ろうと思います。

参考

ありがとうございました。

わずか681バイトで動くぷよぷよ

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