はじめに
以前、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]);
}
}
}
ここの処理を読む上で重要になって来るのはtrue
は1
、false
は0
と判断される認識である。
例えば(a[y][x - 1] > 0)
がtrueなら1、falseなら0が入る。
追記
この記事を書いたわずか一週間後に無慈悲されました(RTA並感)
onloadに全て突っ込めばscriptタグを消せるテクニックに感心せざるを得ません。