86
59

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-11

はじめに

以前、html+css+jsで作ったマインスイーパーの記事を書いたのだが200行のVue.jsでスネークゲームを作った100行のHaskellでスネークゲームを作ったの記事がトレンドに上がっているのを見つけ、自分もより短いプログラムでマインスイーパーを実装したくなった。
まずは100行を目指してプログラムを書いていたのだが、どうせならもっと短くしようと思いたった7行でテトリスを実装「七行プログラミング」とはJavaScript ショートコードテクニック集(ES6含む)を参考にしてコードを圧縮した。
その結果、当初の目標を大幅に超える23行でマインスイーパーを実装できた。

さあ、マインスイーパーで遊ぶのだ!

See the Pen HTML_Minesweeper_Min by T.D (@td12734) on CodePen.

大きい画面で遊びたい人はこちら
Qiitaだと横が30行ぐらいでゲームの盤面が潰れ始めるので大画面推奨。

参考:Windows版における難易度

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

操作説明

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

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

遊び方

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

プログラム

コード

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

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

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

<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行を見て頂きたい。

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

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

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で小数点以下を切り捨てている。

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

V = () => +f >= f;

上のようにゲームが終了していないかどうかを判断しているが、分かりやすく書くと下と同じことをやっている。

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行だけでこれらの処理をやっている。

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行にしたのだが、分かりやすく書くとこうなる。

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]);
    }
  }
}

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

追記

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

15行のhtmlでマインスイーパーを作った

86
59
6

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