6行テトリスを作ろうと思ったことはありますか。私はありました。そして作ったあとで、5年前に同じことをやった人がいたと知りました。
先に結果を並べます。私の版は468文字。2021年に Qiita で公開されていた版は476文字。数字だけ見ると8文字勝っています。ただ、向こうはゲームオーバー判定も落下速度の加速も残したまま476文字で、私の版はそれを削って468文字です。第1部の末尾にも書いたとおり、負けています。
そのうえで、面白いことも見つかりました。互いに相手が使っていない技法を1つずつ持っています。合わせればもっと縮む。以下、そのあたりを順に書きます。
完成したコード
全6行、各行79文字以内、合計468文字。
| 1 2 3 4 5 6 | <body id=D onKeyDown=K=event.which-38><script>Z=X=[B=A=12];Y=_=>{for(C=[q=c=i =4];f=i--*K;c-=!Z[h+(K+6?p+K:C[i]=p*A-(p/9|0)*145)])p=B[i];for(c?0:K+6?h+=K:t?B =C:0;i=K=q--;f+=Z[A+p])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++,c =l+=A);--c>A;)Z[c]=Z[c-A];for(S="";i<240;S+=X[i]|(X[i]=Z[i]|=++i%A<2|i>228)?i% A?"■":"■<br>":"_");D.innerHTML=S+P;setTimeout(Y,99)};Y(h=e=K=t=P=0)</script> |

.html で保存すればブラウザで動きます。デモページにも置いてあるので、そちらでもすぐ遊べます。左右キーで移動、上キーで回転、下キーで高速落下。スコアは画面右下に出ます。
どこを削ったのか
7行版から6行版へ、削った箇所は5つです。
まず、関数宣言をアロー関数に変えました。function Y(){...} が Y=_=>{...} になります。5文字ぶん。ダミー引数の _ を使うことで、引数なしの関数を最短で書いています。ただしアロー関数は巻き上げが効かないとのことで、定義より前で呼ぶと落ちます。このコードでは定義と初回呼び出しが同じ順で並んでいるので問題ありませんでした。
それから、描画ループを統合しました。7行版は行末の <br> を別のステートメントで足していましたが、三項演算子の分岐の中に組み込みます。ブロックがある場合に行末かどうかでもう一段分けて、行末なら ■<br>、それ以外は ■。空きマスは _。改行のための独立した処理が要らなくなって、10文字ほど減りました。
次がカラー表示です。7行版にはブロックに色を付ける版があって、<b style=color:#…>■ というタグを吐いていました。これを丸ごと落として、■ と _ だけのモノクロにしています。色の出力だけで30文字以上を使うので、6行に収めるなら真っ先に削る場所でした。
そしてスコア計算。7行版は P+=k++ で、同時に消した行数が多いほどボーナスが付く仕組みでした。これを P++ にして、消した行数がそのまま点数になる形にします。変数 k が不要になって3〜4文字。
最後にゲームオーバー判定です。7行版の Z[5]||setTimeout(Y,99-P) は、フィールド上端にブロックが積まれたらタイマーを止める処理でした。setTimeout(Y,99) に変えて、ゲームオーバーなし・一定速度の無限プレイにしています。15文字ほど。
ここで負けている
最後の2つが、後ろめたい削り方でした。
ゲームオーバーを消すと、テトリスとして完結しなくなります。積み上がっても止まらない。落下速度が上がらないので、難易度も変わりません。P+=k++ を P++ にしたのも、スコアの意味を薄くしています。
2021年の476文字版は、そこを残していました。Z[5]||setTimeout(Y,i-P) が最後に付いています。ゲームオーバーも加速もある。そのうえで476文字。
私の468文字は、機能を捨てて得た8文字です。同じ土俵ではありません。

それでも1つ、勝っている箇所があります
アロー関数の書き方です。
2021年の版は Y=()=>{...} と書いています。私の版は Y=_=>{...}。引数がゼロ個の場合、括弧が必須になるので () と書くのが普通です。ただ、引数が1つのときは括弧を省略できる。使わないダミー引数を1つ置けば、() の2文字が _ の1文字になります。

1文字です。ショートコーディングでは1文字が勝敗を分けるので、これは拾い物でした。
逆に、向こうが使っていて私が使っていなかった技法もありました。event.keyCode を event.which に変えると2文字減ります。どちらも非推奨のプロパティだそうですが、キーコードの数値をそのまま取れるという点では同じです。
この記事を書きながら気づいて、あとから取り込みました。上に貼ったコードは which 版です。470文字だったものが468文字になりました。1行目が77文字になりますが、79を超えていなければ問題ありません。改行位置は動かさずに済みました。
つまり、削れる余地は残っていたわけです。自分のコードを何十回も見返して、それでも見落としていた2文字が、他人の記事を読んだら1分で見つかりました。
もう1つ、向こうが検討して見送った手もありました。<br> を <p> に変えると1文字減る。ただし行間が広がって、テトリスの盤面が縦に間延びします。動くけれど見た目が壊れる。見送った判断は正しいと思います。
コードの中身
削った話はここまでで、次は元から入っている技法の話です。7行版から引き継いだ部分がほとんどですが、読んでいて感心した箇所を挙げます。
フィールドは幅12の1次元配列です。
| 1 | Z=X=[B=A=12] |
10列のプレイ領域の左右に壁を1列ずつ足して12。2次元配列にすると添字の計算が2回要りますが、1次元なら1回で済みます。壁との衝突判定も、その位置の値を見るだけ。底は i>228 で、228 は 12×19 です。
テトリミノの生成が、このコードで一番の魔術だと思います。
| 1 | B=[[-7,-20,6,h=17,-9,3,3][t=++t%7]-4,0,1,t-6?-A:2] |
7種類のブロックの形状が、7要素の配列1つに入っています。t=++t%7 で順に取り出して、そこから4つのマスの相対位置を作る。t-6?-A:2 の部分で、7番目のブロックだけ形を変えています。7種類ぶんの座標データを個別に持てば、それだけで100文字を超えます。
回転はこれです。
| 1 | C[i]=p*A-(p/9|0)*145 |
座標に幅を掛けて、9で割った商に145を掛けたものを引く。これで90度回転になります。|0 は小数の切り捨てで、Math.floor の代わりです。9文字を2文字に縮めています。
ライン消去は下から走査します。
| 1 | for(l%A?l-=l%A*!Z[l]:(P++,c=l+=A);--c>A;)Z[c]=Z[c-A]; |
l%A が真、つまり行の途中なら、そのマスが空かどうかで l を進める。空きマスが1つでもあればその行は飛ばします。行末まで来て埋まっていたら、スコアを足して、上の行を1つずつ下へコピーする。ループの初期化式の中で判定も代入も済ませていて、本体は Z[c]=Z[c-A] の1文だけです。
描画に全角文字を使っているのも、いま見ると賢い。
| 1 | S+=X[i]|(X[i]=Z[i]|=++i%A<2|i>228)?i%A?"■":"■<br>":"_" |
■ と _ は全角なので、等幅フォントで正方形に近い見た目になります。CSS を1文字も書かずにマス目が揃う。半角文字だと縦横比が崩れるので、盤面がつぶれて見えます。フォントの都合を利用して、レイアウトの記述を丸ごと省いている。
79文字で切る
意外に手こずったのが、行の分割でした。
1行79文字以内という制限があるので、どこで改行してもいいわけではありません。JavaScript は自動セミコロン挿入があるので、文の途中で改行すると意図しない場所に ; が入ります。文字列リテラルの途中でも切れません。
結果として、改行できるのは演算子の直後や、識別子の途中といった限られた場所になります。上のコードを見ると、i\n=4 のように変数名と代入演算子のあいだで切れていたり、[[\n-7,-20,... のように配列リテラルの中で切れていたりします。読みやすさを完全に捨てて、79という数字だけに合わせた形です。

この作業は機械的にできそうで、できませんでした。1文字削ると、その先の改行位置が全部ずれます。削って、割り直して、動かして、また削る。
468文字の内訳
最後に、何にどれだけ使っているかを並べておきます。
HTML の外枠、つまり <body id=D onKeyDown=...><script> と </script> で約70文字。初期化とフィールド定義で約20文字。ブロックの移動と回転の判定で約120文字。ライン消去で約80文字。描画で約90文字。テトリミノの定義で約50文字。残りがタイマーと初期呼び出しです。
いちばん重いのは移動と回転の判定でした。1つの for 文の中に、7種類のブロックそれぞれの4マスぶんの座標計算と、壁・床・既存ブロックとの衝突判定が同居しています。ここを削る余地は、私には見つけられませんでした。
2021年の記事の筆者も、同じことを書いています。ロジック部分はそのままにした、というより、理解しきれなかった、と。第3部では、そのロジックを組んだ人たちが何をしていたのかを見ます。
参考
- 六行テトリス(Sotobato_Nihu 氏) ── 2021年、476文字・479バイトで6行に収めた記録
- たった7行でテトリスを実装「七行プログラミング」とは ── 498バイト版のコードと解説
- 482バイトテトリス(Ozy 氏) ── 作者の特定と短縮



コメント