LoginSignup
66
27

More than 3 years have passed since last update.

14行(元15行)のhtmlでマインスイーパーを作った

Last updated at Posted at 2019-10-17

はじめに

以下の記事に影響を受けて作り始めました。

類似のものを作る許可だけでなく、レギュレーションまで文面化していただき、重ね重ね感謝申し上げます。

というわけで、記録更新に挑戦だぁ!

1行減って14行になりました (最後に追記)

あそべる!

See the Pen Minesweeper15 by nagtkk (@nagtkk) on CodePen.

Chrome / Firefox / Edge で動作確認しています。

初期値や見た目・操作方法は、できるだけオリジナルと同じになるように。

  • 左クリック:マスを開ける
  • 右クリック:旗の on/off

なお Windows 版の難易度は次のようになっているらしいです。

  • 初級:9×9(地雷10)
  • 中級:16×16(地雷40)
  • 上級:30×16(地雷99)

コード!

<!doctype html><body onload="K='<input type=';E=(m,n=8)=>K+`number required ${V
='value'}=9 max=${m} min=${n} id=`;F='#';L=(f,n=h)=>{while(n)f(--n)};Z=x=>(x|=0
)>9?x:'0'+x;B[I='innerHTML']=`w${E(70)}W>h${E(25)}H>*${E(0,9)}M>${K}button ${V}
=Start onclick=S()><p id=P><table style=border-collapse:collapse id=T>`;S=k=>{a
=k=[];w=G(W);h=G(H);M.max=~-w*~-h;f=m=G(M,10);R=x=>0|x*Math.random();C=(x,y)=>c
=T.rows[y].cells[x];p=m-w*h;Q=(x,y)=>a[y]&&a[y][x];A=(x,y,f)=>L(u=>s+=0|f(x-1+u
%3,y+~-(u/3)),9,s=0)|s;O=(x,y)=>Q(x,y)==0&&(C(x,y)[I]==F?++f:!c[I])&&((c[I]=A(x
,y,Q)||'-',c.style.background='gray',s)||A(x,y,O),++p);T[I]='';U=t=>P[I]=Z(t=(t
-(b=b||t))/6e4)+`:${Z(t%1*60,p&&k==a&&requestAnimationFrame(U))} ${g||F+f}`;U(L
(y=>L((x,c=r.insertCell(s[x]=g=b=0))=>{c.onclick=_=>{while(m)Q(i=R(w),j=R(h))|i
==x&j==y?0:m-=a[j][i]=1;p&&!c[I]&&(g=Q(x,y)?L(j=>L(i=>Q(p=i,j)&&(C(i,j)[I]=
'&#8251'),w))||'Lose':O(x,y)?0:'Win')};c.style=`width:24px;height:24px;border:
solid;text-align:center;cursor:default`;c.oncontextmenu=_=>!!p&&((s=c[I])==F||!
s)&&!~(c[I]=!s&&f--?F:(++f,''))},w,r=T.insertRow(0),s=a[y]=[])))};S(G=(e,v=9)=>
e[V]=e.reportValidity()?e[V]:v)"id=B>

script は全部 onload に突っ込んでいます。
中身を抜き出して、展開してみると以下のような感じに。

onload
K = '<input type=';
E = (m, n = 8) => K + `number required ${V = 'value'}=9 max=${m} min=${n} id=`;
F = '#';
L = (f, n = h) => { while (n) f(--n) };
Z = x => (x |= 0) > 9 ? x : '0' + x;
B[I = 'innerHTML'] = `
    w${E(70)}W>h${E(25)}H>*${E(0, 9)}M>${K}button ${V}=Start onclick=S()>
    <p id=P>
    <table style=border-collapse:collapse id=T>
`;
S = k => {
    a = k = [];
    w = G(W);
    h = G(H);
    M.max = ~-w * ~-h;
    f = m = G(M, 10);
    R = x => 0 | x * Math.random();
    C = (x, y) => c = T.rows[y].cells[x];
    p = m - w * h;
    Q = (x, y) => a[y] && a[y][x];
    A = (x, y, f) => L(u => s += 0 | f(x - 1 + u % 3, y + ~-(u / 3)), 9, s = 0) | s;
    O = (x, y) => Q(x, y) == 0 && (C(x, y)[I] == F ? ++f : !c[I]) && (
        (
            c[I] = A(x, y, Q) || '-',
            c.style.background = 'gray',
            s
        ) || A(x, y, O),
        ++p
    );
    T[I] = '';
    U = t =>
        P[I] = Z(t = (t - (b = b || t)) / 6e4) +
        `:${Z(t % 1 * 60, p && k == a && requestAnimationFrame(U))} ${g || F + f}`;
    U(L(y => L((x, c = r.insertCell(s[x] = g = b = 0)) => {
        c.onclick = _ => {
            while (m) Q(i = R(w), j = R(h)) | i == x & j == y ? 0 :
                m -= a[j][i] = 1;
            p && !c[I] && (
                g = Q(x, y) ?
                    L(j => L(i => Q(p = i, j) && (C(i, j)[I] = '&#8251'), w))
                    || 'Lose' :
                    O(x, y) ? 0 : 'Win')
        };
        c.style = `width:24px;height:24px;border:solid;text-align:center;cursor:default`;
        c.oncontextmenu = _ => !!p && ((s = c[I]) == F || !s) &&
            !~(c[I] = !s && f-- ? F : (++f, ''))
    }, w, r = T.insertRow(0), s = a[y] = [])))
};
S(G = (e, v = 9) => e[V] = e.reportValidity() ? e[V] : v)

展開してもわけわかりませんね。
各変数・関数等の一覧は最後に。

変なところで初期化してたり、定義順がバラバラだったりするのは、
文字数削減と、改行のタイミングをいじるためです。

細々とした小技とか、正直書ききれない部分が多いので、
個人的にポイントだと思ったところを記事に抜き出してみます。

他に、ここどうなってるねん、という部分があれば、コメント辺りで聞いていただければ。

短くするポイント

onload に直接書く

html の属性値は引用符さえつければ改行可能。
<script></script> を削れるのは結構大きいので、全部突っ込む。
他の要素も中で作る。

Template literal を積極的に使っていく

先頭末尾以外への文字列の連結が短く済むことが多々ある。

'a'+x+'b'+y+'c'
`a${x}b${y}c`

ついでに文字列内で改行も可能なので、調整がやりやすい。

requestAnimationFrame の活用

関数名が長いので、単にアップデートするだけなら setTimeout のほうが良い。
ただし、経過時間が必要な場合、
new Dateperformance.now() と組み合わせる必要が出てくるので
コードは全体が長くなりがちになる。

requestAnimationFrameは、第一引数で時刻(DOMHighResTimeStamp)を受け取れるので、
経過時間が必要ならば一考の余地アリ。
基準時刻の設定に一工夫要るけれど。

Arrow function と細かな工夫

functionreturn をガンガンそぎ落としていく。

A=function(x){return y};
A=x=>y;

カンマ演算子等を使用して、
可能な限り return を削っていく。

F=x=>{A(x);return B(x)};
F=x=>(A(x),B(x));
F=x=>A(x)||B(x); // A(x) が falsy 確定の時
F=x=>A(x)&&B(x); // A(x) が truthy 確定の時
F=x=>A(x)|B(x); // A(x) が整数評価時に 0, B(x) が整数値の時

無引数の関数はダミーの引数を用いたほうが短く書ける。

X=()=>{};
X=_=>{};

2回以上同じ引数で呼び出されるコードは、デフォルト引数を利用すると短くできる。

F=(x,y)=>{};F(10,2);F(20,3);F(30,2);
F=(x,y=2)=>{};F(10);F(20,3);F(30);

余分な実引数は、ブロック外しなどに活用できる。

F=(x,y)=>{};
G=(x,y)=>{R();F(x,y)};
G=(x,y)=>F(x,y,R());

余分な仮引数は、局所変数として利用できる。

F=(x,y)=>{let c=/*略*/;/*略*/};
F=(x,y,c=/*略*/)=>{/*略*/};

短く書くために主に大域変数を多用しているけれど、
イベント周りでは thisevent から要素にアクセスするよりも、
局所変数に入れてクロージャに持ち込んだほうが有利。

F=_=>{e=makeElement();e.onclick=function(){this.hoge()};/*略*/};
F=_=>{let e=makeElement();e.onclick=_=>e.hoge();/*略*/};
F=(e=makeElement())=>{e.onclick=_=>e.hoge();/*略*/};

その他、ループ処理も多くなるようなら関数化した方が短くなることがある。

F=(n,f)=>{while(n)f(--n)};

for(y=h;y--;)for(x=w;x--;){}
F(h,y=>F(w,x=>{}));

もちろん、関数定義の分文字数を消費するので、1,2回のループ程度だと割に合わない。

今回は、

  • 初期化(2ループ)
  • マスのオープン(1ループ)
  • 敗北時の処理(2ループ)

と頻繁に出てくるので関数化している(引数順序は例と異なるが)。
ループカウンタを局所変数にできるという利点もある。

その他もろもろ

m=(w-1)*(h-1);
m=w*h-w-h+1; // 展開してみるとか
m=~-w*~-h; // ビット演算にしてみるとか

// 分岐は削りどころ
if(c)B(x);
c&&B(x);

if(c)x=v;
c&&(x=v); // これだと変わらないので
c?x=v:0; // こう
x=c?v:x; // あるいはこう

// 前後の式と組み合わせてみたり
if(c)x=v;y=x;
y=x=c?v:x;

// 短絡不要なとき、論理演算の代わりにビット演算も検討してみる
if(x<0||y<0){}
if(x<0|y<0){}  // boolean => 0 or 1 に変換されるので同じ結果を生む

後は何を書いたらいいやら。
頑張って短くするほど、説明困難になってゆく。

とりあえず、どんな値が入ってくるかと、
演算によって何に(暗黙に)変換されるかを意識しながら、
手を変え品を変え、コードパスをバッサバッサと切り落としていくのが良いかと思われます。

理由があって元より長くなっている部分

<!doctype html> をちゃんと書く

オリジナルで使われていて、このコードでも使用している <p><table> という要素の並びは、
実は html5 としてドキュメントが解釈されない場合にうまく動かないことがある。

<!doctype html> を消して chrome あたりで、
ローカルファイルから実行するとエラーになることを
確認できると思う。

CodePen 経由だと動いてしまうが、
これは CodePen が勝手に <!doctype html> を付け加えているため。
CodePen 以外でも実行できるように今回はちゃんと書いた。

何故うまくいかないかと言うと、
古い仕様では、直後に<table> が続く <p> の終了タグが省略できるとは、
明確には書かれていないため。
ブラウザによっては <table><p> の子になってしまう。
そうなると、<p> の内容更新時に <table> が消えてしまい、エラーとなる。

html5 にせずに </p> を足せばよいかと言うとそうもいかない。

古い仕様ではテーブルセルの height の扱いが違うので、今度はマス目がつぶれて横長になってしまう。
潰れることを想定して高めに height を設定すると、今度は勝手に html5 化される CodePen で細長いマスになってしまう。

両対応な style を書こうとするとコード自体が長くなるので、
結局 <!doctype html> が一番短いというオチ。

文字実体参照(or \uXXXX)を使う

地雷の表現に文字 を使用しているが、
文字コードを一切指定していないので、
環境次第で文字化けする可能性がある。

今回は直接ソースコードに記述せずに &#8251 とした。
meta タグを書くよりは短くて済む。

これまた CodePen だと、問題なく行けてしまうが、
これも CodePen が utf-8 で保存して、<meta charset='utf-8'> を付け加えているため。

各変数の説明

省略前の名称と TypeScript 風の型定義も付記しておきます。

各変数等の説明
  • S = Start: () => void;
    • ゲーム開始
  • B = Body: HTMLBodyElement;
    • document.body
  • W = WidthInput: HTMLInputElement;
    • 幅の入力要素
  • H = HeightInput: HTMLInputElement;
    • 高さの入力要素
  • M = MineCountInput: HTMLInputElement;
    • 地雷数の入力要素
  • P = Paragraph: HTMLParagraphElement;
    • メッセージ表示要素
  • T = Table: HTMLTableElement;
    • 盤面表示要素
  • F = '#';
    • フラグを表す文字。複数回登場するので変数に。
  • V = 'value';
  • I = 'innerHTML';
  • K = '<input type=';
    • 文字列圧縮のために利用
  • E = EmbedInputElement: (max: number, min?: number) => string;
    • 入力要素生成のヘルパー
  • L = Loop: (f: (x: number) => void, n?: number) => void;
    • n-1 から 0 まで繰り返し f を呼び出す。
  • G = GetInput: (e: HTMLInputElement, v?: number) => number;
    • 入力取得。
    • 不正な値であった場合、v で上書きする。
  • Z = ZeroPadding: (x: number) => number | string;
    • x を整数に変換し、2 桁未満ならば 0 で埋める。
  • C = Cell: (x: number, y: number) => HTMLTableCellElement;
    • セルを取得する
  • Q = QueryMine: (x: number, y: number) => 0 | 1 | undefined;
    • 地雷数取得。範囲外のアクセスは undefined を返す。
  • A = LoopAround: (x: number, y: number, f: (x: number, y: number) => any) => number;
    • 自分も含めた周辺 9 マスの座標に対し f を呼び出し、戻り値を整数に変換した合計を返す。
  • R = Random: (x: number) => number;
    • 0 以上 x 未満の整数乱数を生成
  • O = Open: (x:number, y: number) => false | number;
    • マスを開く
    • 開くことができなければ false (もう開いている or 地雷)
    • 開くことができた場合、現在の勝ち点(後述)を返す。
    • 周辺に地雷が無ければ、再帰的にマスを開いていく。
  • U = Update: (t?: number) => void
    • メッセージの更新
  • a = answer: number[][];
    • 各マスにおける地雷の有無を 0, 1 で保持する。
  • w = width: number;
    • 盤面の幅
  • h = height: number;
    • 盤面の高さ
  • f = flags: number;
    • フラグの数
  • m = mines: number;
    • 未敷設の地雷数。
    • 初回クリック時に 0 になるまで地雷を埋める。
  • p = point: number;
    • 勝ち点。
    • マスを開くごとに加算。
    • 初期値は 地雷数 - 全マス で負から始まり、0 で終了。
    • フラグを兼ねているので、敗北時にも 0 にセットされる。
  • s = something: any;
    • 一時変数。色々入れる。
  • b = baseTime: number;
    • 開始時刻
  • g = gameState: 0 | 'Win' | 'Lose';
    • ゲーム状態
  • k = keepAlive: typeof a;
    • S 内の局所変数。
    • a (=answer) の参照を保持し、U(=Update)で変化がないかチェックする。
    • 新規ゲーム開始時に、前のゲームのタイマー処理を確実に停止するため。
  • r = currentRow: HTMLTableRowElement;
    • 初期化時に使用
  • c = currentCell: HTMLTableCellElement;
    • 最後に取得されたテーブルセル
  • c = currentCell: HTMLTableCellElement;
    • 初期化ループ内の局所変数。
    • グローバル変数の c と同名だが、こちらは onclick/oncontextmenu 内で使う個々のセル。

おわりに

レギュレーションがリッチなこともあって、色々考えることが多くとても楽しかったです。

あと数文字なら削れそうな気もしつつ、14行は遠そうなので今回のチャレンジはここまで。

みんなもショートコーディングしよう! (※実務以外で)

おわらなかった (追記)

結局14行に出来てしまいました。

<!doctype html><body onload="K='<input type=';E=(m,n=8)=>K+`number required ${V
='value'}=9 max=${m} min=${n} id=`;F='#';L=(f,n=N)=>{while(n)f(--n)};Z=x=>(x|=0
)>9?x:'0'+x;B[I='innerHTML']=`w${E(70)}W>h${E(25)}H>*${E(0,9)}M>${K}button ${V}
=Start onclick=S()><p id=P><table style=border-collapse:collapse id=T>`;S=k=>{a
=k=[];w=G(W);h=G(H);N=w*h;u=w+2;M.max=~-w*~-h;f=m=G(M,10);C=[p=m-N];T[I]='';U=
t=>P[I]=Z(t=(t-(b=b||t))/6e4)+`:${Z(t%1*60,p&&k==a&&requestAnimationFrame(U))}
${g||F+f}`;U(L((i,j=X(i=N+~i),c=C[j]=(r=i%w?r:T.insertRow()).insertCell())=>{a[
j]=b=g=0;c.style=`width:24px;height:24px;border:solid;text-align:center;cursor:
default`;c.oncontextmenu=_=>!!p&&((s=c[I])==F||!s)&&!~(c[I]=!s&&f--?F:(++f,''))
c.onclick=_=>{while(m)a[i=X(N*Math.random())]|i==j?0:m-=a[i]=1;p&&!c[I]&&(g=a[j
]?L(i=>a[i=X(p=i)]&&(C[i][I]='&#8251'))||'Lose':O(j)?0:'Win')}}))};X=i=>u*-~(i/
w)-~(i%w);O=i=>a[i]==0&&((c=C[i])[I]==F?++f:!c[I])&&((c[I]=A(i,i=>0|a[i])||'-',
c.style.background='gray',s)||A(i,O),++p);A=(i,f)=>L(j=>s+=f(i-1+j%3+~-(j/3)*u)
,9,s=0)|s;S(G=(e,v=9)=>e[V]=e.reportValidity()?e[V]:v)"id=B>

変更点

大枠では変わっていないのですが、今まで x,y 座標で扱っていたものを、
内部的に周囲にダミーのマスを置いた上で、インデックス i=(w+2)*(y+1)+(x+1) で扱うよう変更。

これに伴い、

  • ダミーのないインデックスからダミー付きインデックスに変換する関数 X 追加
  • N = w * h, u = w + 2 追加
  • 関数 C がなくなり配列 C
  • 関数 Q がなくなり、a を直接参照
  • 乱数の生成個所が一か所になったので R を削除
  • 2引数の関数が1引数になり括弧を削除(これが結構大きい)
    • (x,y)=>...i=>...
  • テーブルセルの初期化を逆順から正順に
  • その他もろもろ順序や計算式等の変更

を経て、14行に。

実のところインデックスで持つのは15行の時に検討していたのですが、

  • インデックスにすると周辺マスの探索が長くなる
  • ⇒ 周辺に 1 マス番兵を用意すれば、短く書けそう?
  • ⇒ 今度はセルの初期化が長くなる
  • ⇒ やっぱだめだな

となって一度棄却したはずだったのですが、日を置いてから眺めてみると、
セルの初期化を正順にすれば短く書けてしまうという。

思い込みは怖い...。


スネークゲーム

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