23行のhtmlでマインスイーパーを作った(Qiitaで遊べるよ!!)

# はじめに
以前、[html+css+jsで作ったマインスイーパーの記事](https://qiita.com/TD12734/items/8872da559c7fb707f14c)を書いたのだが[200行のVue.jsでスネークゲームを作った](https://qiita.com/miyauchoi/items/f753e5fa0209ab2034dd)や[100行のHaskellでスネークゲームを作った](https://qiita.com/lotz/items/9c62a5a0eb0e34a86ac7)の記事がトレンドに上がっているのを見つけ、自分もより短いプログラムでマインスイーパーを実装したくなった。
まずは100行を目指してプログラムを書いていたのだが、どうせならもっと短くしようと思い[たった7行でテトリスを実装「七行プログラミング」とは](https://qiita.com/ryuichi1208/items/f9e6ac2b99bbe4fc82d3)、[JavaScript ショートコードテクニック集（ES6含む）](https://qiita.com/ichimonji_haji/items/f602f6c437be9e7daa88)を参考にしてコードを圧縮した。
その結果、当初の目標を大幅に超える**23行**でマインスイーパーを実装できた。

# さあ、マインスイーパーで遊ぶのだ!
<p class="codepen" data-height="450" data-theme-id="0" data-default-tab="result" data-user="td12734" data-slug-hash="WNNvRBK" style="height: 450px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="HTML_Minesweeper_Min">
  <span>See the Pen <a href="https://codepen.io/td12734/pen/WNNvRBK">
  HTML_Minesweeper_Min</a> by T.D (<a href="https://codepen.io/td12734">@td12734</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

[大きい画面で遊びたい人はこちら](https://codepen.io/td12734/full/WNNvRBK)
Qiitaだと横が30行ぐらいでゲームの盤面が潰れ始めるので大画面推奨。

**参考：Windows版における難易度**

- 難易度：横×縦(地雷数)
- 初級：9×9(10)
- 中級：16×16(40)
- 上級：30×16(99)

## 操作説明
- 左クリック：マスを開ける
- 右クリック：空いていないマスの上に旗を立てる
- キーボード：盤面の縦横サイズ、地雷の数を入力する

※スマホで遊ぶ場合、長タップする事で右クリック可能です。

## 遊び方
マスをクリックして、地雷が無いマスを全て開けるゲームです。地雷のマスをクリックしたら負け。
開けたマスに書いてある数字はそのマス周囲8マスのうち、地雷があるマスの個数です。
やり直しは「Start」ボタンを押してください。
上の入力フィールドに数値を入力し、ゲームの難易度を変更することも可能です。

# プログラム
## コード

ゲームは以下のコードで実装した。七行プログラミングのルールに従い、1行は79文字以下としている。

```html
<body>w<input id=W type=number required value=9 min=8 max=70>h<input id=H type=
number required value=9 min=8 max=25>*<input id=M type=number required value=9
min=9><input type=button value=Start onclick=S()><p id=p><table id=t style=
"border:solid;border-collapse:collapse"><script>window.onload=S=function(){if(R
(W))W.value=9;if(R(H))H.value=9;M.max=((w=W.value)-1)*((h=H.value)-1);if(R(M))M
.value=10;m=f=z=M.value;s=performance.now();T();for(i=t.rows.length;i-->0;t.
deleteRow(0));a=new Array(h);for(i=h;i-->0;){a[i]=new Array(w);r=t.insertRow(0)
;for(j=w;j-->0;){d=r.insertCell(o=a[i][j]=0);[d.w,d.h,d.style]=[j,i,"width:24"+
"px;height:24px;border:solid;text-align:center;cursor:default"];d.onclick=
function(){if(!V()||this[v].match("[#-8]"))return;if(!o)do if(a[y=(Math.random(
)*h)|0][x=(Math.random()*w)|0]<1&&(Math.abs(this.w-x)>1||Math.abs(this.h-y)>1)
&&z--)a[y][x]=1;while(z);if(!a[this.h][this.w])N(this);else if(f="Lose")for(i=h
;i-->0;)for(j=w;j-->0;)if(a[i][j])t.rows[i].cells[j][v]="※"};d.oncontextmenu=
function(){if(V()&&!this[v].match("[*-8]"))if(!this[v]&&f-->0)this[v]="#";else
if(++f)this[v]="";return !1}}}};T=()=>((p[v="innerHTML"]=((k=((e=((performance.
now()-s)/1e3)|0)/60)|0)<10?"0":"")+k+((l=e%60)<10?":0":":")+l+(+f>=f?" #":" ")+
f)&&V())?setTimeout(T):0;V=()=>+f>=f;R=r=>!r.reportValidity();function N(c){if(
!c||c[v].match("[*-8]"))return;c.style.background="gray";if(~c[v].indexOf("#")
&&f++)T();if(!(c[v]=(a[y=c.h][(x=c.w)-1]>0)+(a[y][x+1]>0)+(y>0&&a[y-1][x-1]>0)+
(y>0&&a[y-1][x]>0)+(y>0&&a[y-1][x+1]>0)+(y<h-1&&a[y+1][x-1]>0)+(y<h-1&&a[y+1][x
]>0)+(y<h-1&&a[y+1][x+1]>0))&&(c[v]="-"))for(c.i=9;c.i-->0;)if(c.i!=4)N(((X=c.w
+1-(c.i/3|0))>=0)*((Y=c.h+1-c.i%3)>=0)*(X<w)*(Y<h)>0?t.rows[Y].cells[X]:0);if(
++o>=w*h-m)f="Win"}</script>
```

何書いてあるか分からないと思うので書き下したコードを以下に貼る。

```html
<body>
  w<input id=W type=number required value=9 min=8 max=70>
  h<input id=H type=number required value=9 min=8 max=25>
  *<input id=M type=number required value=9 min=9>
  <input type=button value=Start onclick=S()>
  <p id=p>
  <table id=t style="border:solid;border-collapse:collapse">
  <script>
    window.onload = S = function () {
      if (R(W)) W.value = 9;
      if (R(H)) H.value = 9;
      M.max = ((w = W.value) - 1) * ((h = H.value) - 1);
      if (R(M)) M.value = 10;
      m = f = z = M.value;
      s = performance.now();
      T();
      for (i = t.rows.length; i-- > 0; t.deleteRow(0));
      a = new Array(h);
      for (i = h; i-- > 0;) {
        a[i] = new Array(w);
        r = t.insertRow(0);
        for (j = w; j-- > 0;) {
          d = r.insertCell(o = a[i][j] = 0);
          [d.w, d.h, d.style] = [j, i, "width:24px;height:24px;border:solid;text-align:center;cursor:default"];
          d.onclick = function () {
            if (!V() || this[v].match("[#-8]")) return;
            if (!o) do if (a[y = (Math.random() * h) | 0][x = (Math.random() * w) | 0] < 1 && (Math.abs(this.w - x) > 1 || Math.abs(this.h - y) > 1) && z--) a[y][x] = 1; while (z);
            if (!a[this.h][this.w]) N(this);
            else if (f = "Lose") for (i = h; i-- > 0;)for (j = w; j-- > 0;)if (a[i][j]) t.rows[i].cells[j][v] = "※"
          };
          d.oncontextmenu = function () {
            if (V() && !this[v].match("[*-8]")) if (!this[v] && f-- > 0) this[v] = "#";
            else if (++f) this[v] = "";
            return !1
          }
        }
      }
    };
    T = () => ((p[v = "innerHTML"] = ((k = ((e = ((performance.now() - s) / 1e3) | 0) / 60) | 0) < 10 ? "0" : "") + k + ((l = e % 60) < 10 ? ":0" : ":") + l + (+f >= f ? " #" : " ") + f) && V()) ? setTimeout(T) : 0;
    V = () => +f >= f;
    R = r => !r.reportValidity();
    function N(c) {
      if (!c || c[v].match("[*-8]")) return;
      c.style.background = "gray";
      if (~c[v].indexOf("#") && f++) T();
      if (!(c[v] = (a[y = c.h][(x = c.w) - 1] > 0) + (a[y][x + 1] > 0) + (y > 0 && a[y - 1][x - 1] > 0) + (y > 0 && a[y - 1][x] > 0) + (y > 0 && a[y - 1][x + 1] > 0) + (y < h - 1 && a[y + 1][x - 1] > 0) + (y < h - 1 && a[y + 1][x] > 0) + (y < h - 1 && a[y + 1][x + 1] > 0)) && (c[v] = "-")) for (c.i = 9; c.i-- > 0;)if (c.i != 4) N(((X = c.w + 1 - (c.i / 3 | 0)) >= 0) * ((Y = c.h + 1 - c.i % 3) >= 0) * (X < w) * (Y < h) > 0 ? t.rows[Y].cells[X] : 0);
      if (++o >= w * h - m) f = "Win"
    }
  </script>
```

結構長くなったように見えるがこれでも49行しかない。
ただ、コードの解読はまだ困難だと思うので以下で説明を行う。

## 変数、関数の説明
コード圧縮の都合上、全ての変数名と関数名は1文字になっている。
その結果、可読性が大幅に失われたのでここでは変数名、関数名について説明する。

- `変数、関数名`
 - 1文字に圧縮する前に付けたであろう名前
 - この行以降は動作説明など

### グローバル変数
- `a`
 - mineArray
 - 地雷があるか否かを格納した配列
 - 0なら空きマス、1なら地雷
- `d`
 - td
 - テーブルのセル
- `e`
 - elapsedTime
 - ゲーム開始から経過秒
- `f`
 - flags,finishString
 - 持っている旗(残りの地雷)の数
 - 地雷を踏んだ時、地雷以外のマスを全て開けた時はゲーム結果の文字列を格納する
- `h`
 - height
 - 盤面の縦サイズ
- `i`,`j`
 - お馴染みのループ調整変数
- `k`
 - 特に無し
 - 経過分数
- `l`
 - 特に無し
 - 経過秒数
- `m`
 - mines
 - 地雷の数
- `o`
 - opendCells
 - 既にクリックして開けたマスの数
- `p`
 - pElement
 - 経過時間、残りの旗の数、勝敗を表示するpタグの要素
- `r`
 - tr
 - reportValidity
 - 初期化時は`R`の引数の役割を果たす
 - その後、テーブルの行を格納する
- `s`
 - startTime
 - ゲーム開始時間
- `t`
 - tableElement
 - ゲームの盤面
- `v`
 - innerHTMLValue
 - 文字列の"innerHTML"
- `w`
 - width
 - 盤面の横サイズ
- `x`
 - xRandom
 - 地雷敷設時に使用する、ランダムな横の座標
 - 周囲の地雷の数を数える時、横の座標を格納する
- `y`
 - yRandom
 - 地雷敷設時に使用する、ランダムな縦の座標
 - 周囲の地雷の数を数える時、縦の座標を格納する
- `z`
 - 特に無し
 - 地雷敷設時に使用する、敷設すべき残りの地雷数
- `H`
 - HeightInput
 - 縦サイズの入力フィールド
- `M`
 - MineInput
 - 地雷の入力フィールド
- `W`
 - WidthInput
 - 横サイズの入力フィールド
- `X`
 - 特に無し
 - 周囲のマスを開ける時、横の座標を格納する
- `Y`
 - 特に無し
 - 周囲のマスを開ける時、縦の座標を格納する

### ローカル変数
- `c`
 - cell
 - セル
- `h`
 - height
 - セルの縦座標
- `i`
 - お馴染みのループ調整変数
- `w`
 - width
 - セルの横座標

### 関数
- `N`
 - NoMineCellClick
 - 地雷が無いマスをクリックしたときの処理を行う
- `R`
 - ReportInValidity
 - 入力フィールドの数値が`不正`か
 - 不正ならtrue
- `S`
 - Start
 - Startボタンを押した時やページを開いた時に初期化処理を行う
- `T`
 - TextChange
 - タイマー,旗の数のテキストを変更する
- `V`
 - isValidGame
 - ゲームが有効か(終了していないか)
 - fが数値(旗の数)ならtrue

## 動作説明、工夫点
全部解説が必要そうだが、面倒なので一部だけ解説を行う。
気が向けば全部解説するかも。

### html部分
`<script>`タグ以外は閉じタグが無くても動作するので、閉じタグを消して文字を削減している。
属性値の`"`もstyle以外は無くても動くので省略している。
また、idを設定しているが、idと同じ名前の変数を宣言していなければ`getElementById`をしていなくてもそのidを持つ要素にアクセス可能。

### 変数への数値の代入方法

地雷の個数の設定を行う以下の3行を見て頂きたい。

```js
M.max = ((w = W.value) - 1) * ((h = H.value) - 1);
if (R(M)) M.value = 10;
m = f = z = M.value;
```

これを分かりやすく書くとこうなる。

```js
w = W.value;
h = H.value;
M.max = (w - 1) * (h - 1);
if (R(M)) {
  M.value = 10;
}
m = M.value;
f = M.value;
z = M.value;
```

変数は文中でも値を設定可能なので、w,hは初めて使われるM.maxの設定時に代入する。
この行では変数-1をする都合上カッコで括っているので文字数は変化しないが、括る必要がなければ変数名とセミコロンの2文字を省略できる。
このような代入を**if文中を含め**あらゆる所で行い、文字数をちまちま消している。
また、値が同じ変数は一度に代入可能である。
今回はm,f,zはどれもM.valueの値を取るので一度に代入している。

### 小数点以下を切り捨てる
切り捨ては通常`Math.floor()`を使うがこれは長すぎる。
なのでビット演算を利用して`{数値}|0`で小数点以下を切り捨てている。

### ゲームが終了していないかどうか

```js
V = () => +f >= f;
```
上のようにゲームが終了していないかどうかを判断しているが、分かりやすく書くと下と同じことをやっている。

```js
function V () {
  return isNumber(f);
}
```

`f`には周囲の地雷の個数、つまり1から8までの数字と勝敗、つまりWinとLoseの文字列が入り得るがfが数字ならゲームが終わっていない(true)を返す。
ただ、isNumberは長いのでfの前に+を付けて数字に変換し、変換前後で値が変わっていないかで数字かどうかを判断している。
仮にfが1なら`1 >= 1 → true`、fがLoseなら`NaN >= Lose → false`のように判断される。

### 開けたマスの周囲のマスの地雷の個数を調べ、0個なら周囲のマスを全部開ける

実は最後の方のこの1行だけでこれらの処理をやっている。

```js
if (!(c[v] = (a[y = c.h][(x = c.w) - 1] > 0) + (a[y][x + 1] > 0) + (y > 0 && a[y - 1][x - 1] > 0) + (y > 0 && a[y - 1][x] > 0) + (y > 0 && a[y - 1][x + 1] > 0) + (y < h - 1 && a[y + 1][x - 1] > 0) + (y < h - 1 && a[y + 1][x] > 0) + (y < h - 1 && a[y + 1][x + 1] > 0)) && (c[v] = "-")) for (c.i = 9; c.i-- > 0;)if (c.i != 4) N(((X = c.w + 1 - (c.i / 3 | 0)) >= 0) * ((Y = c.h + 1 - c.i % 3) >= 0) * (X < w) * (Y < h) > 0 ? t.rows[Y].cells[X] : 0);
```

かなり強引に1行にしたのだが、分かりやすく書くとこうなる。

```js
y = c.h;
x = c.w;
//c[v](c.innerHTML)に周りの地雷の個数を入れる
c[v] = (a[y][x - 1] > 0) + (a[y][x + 1] > 0) + (y > 0 && a[y - 1][x - 1] > 0) + (y > 0 && a[y - 1][x] > 0) + (y > 0 && a[y - 1][x + 1] > 0) + (y < h - 1 && a[y + 1][x - 1] > 0) + (y < h - 1 && a[y + 1][x] > 0) + (y < h - 1 && a[y + 1][x + 1] > 0);
if (c[v] === 0) {
  c[v] = "-";
  for (c.i = (9 - 1); c.i > 0; c.i--) {
    X = c.w + 1 - (c.i / 3 | 0);
    Y = c.h + 1 - c.i % 3;
    //自分と同じセルでは無く、範囲外のセルでもない時
    if (c.i != 4 && X >= 0 && Y >= 0 && X < w && Y < h) {
      //周りのマスを開ける
      N(t.rows[Y].cells[X]);
    }
  }
}
```

ここの処理を読む上で重要になって来るのは`true`は`1`、`false`は`0`と判断される認識である。
例えば`(a[y][x - 1] > 0)`がtrueなら1、falseなら0が入る。

# 追記
この記事を書いたわずか一週間後に無慈悲されました(RTA並感)
onloadに全て突っ込めばscriptタグを消せるテクニックに感心せざるを得ません。

**[15行のhtmlでマインスイーパーを作った](https://qiita.com/nagtkk/items/554cc5e24e23b954d8a0)**