3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

7行マインスイーパー

Last updated at Posted at 2023-01-21

7行テトリスの記事を読んだ

7行マインスイーパー(618 byte)を作った。

下記を.htmlなテキストファイルに貼り付けて保存してブラウザで開くと遊べる。
リロードでリセット

ミニファイアに噛ませたらもうちょっとだけ短くなるけど自前の範囲で記載する。

7行マインスイーパー
<body></body><script>g=h=0;(k=(t,m=(v,i)=>i)=>Array(t).fill().map(m))(9,(_,x)=>{k(9,(v,
y)=>{(v=(a=t=>document.body.appendChild((C=t=>document.createElement(t))(t)))('button')
)[T='innerText']="_";v.onclick=e=>{if(h||(o=e.target).f||o.o)return;o[T]=(~(g=g||k(10)[
R='reduce'](p=>(f=()=>~p[I='indexOf'](x=~~(Math.random()*9*9))?f():p.concat([x]))(),[o.
i]).slice(1))[I](o.i))?(h="#"):k(9)[R]((p,r)=>p+!((n=r%3-1+o.x)<0||9<=n||(j=(r-n-4+o.x)
/3+o.y)<0||9<=j||!~g[I](n*9+j)),0);o.o=1};v.oncontextmenu=e=>(o=e.target)[T]=(e.
preventDefault(),h||o.o)?o[T]:(o.f=!o.f)?"p":"_";v.i=(v.x=x)*9+(v.y=y)});a('br')})</script>
  • 盤面は9x9の81セルで、爆弾は10個。
    • 数字を正しく弄くると難易度を変えられるはず
    • 行は3箇所、列は5箇所、爆弾数は1箇所変えればよいはず
  • 各々のボタンは次の通り
    • _:操作していないセル
    • p:フラグ。右クリックでトグルする
    • 0~9:周りにある爆弾の数。オリジナルと異なり0がある。
    • #:爆発したセル。GameOverだ。
    • ※上記の文字セットはChromiumのデフォルトでbutton.innerTextに設定したときその幅が一定になるセレクト
  • オリジナルに寄せた点
    • フラグが設定できる
    • 爆弾セルを開くとゲームオーバーになり操作不可になる
    • 最初に開くセルは安全であることが保証される
  • オリジナルと異なる点
    • フラグの数と数字が一致したら周りを一括openする機能があるが、未実装
    • 開けたセルの周りに爆弾がないと一括openする機能があるが、未実装
    • フラグの数を表示する機能があるが、未実装(これくらいなら実装しても少なめの行数で済みそう)
    • オリジナルはすべての安全なセルを開くとゲームクリアになり操作不可になるが、未実装
    • プレイ時間を記録しない
    • リセットボタンは存在しない。リロードでリセット

利用したワザ

短縮書きの利用

関数

関数は様々に変態できる。
アロー関数式(args)=>{statements}にする場合thisキーワードの仕様が他とチョットダケ違うので注意が必要。

// 普通
function a2s2(arg1,arg2){
  state1;
  state2;
}
// 関数代入
a2s2 = function (arg1,arg2){
  state1;
  state2;
}
// 関数代入 - アロー関数式 ※ this キーワードの仕様の違いに注意 apply を使うなどする
a2s2 = (arg1,arg2)=>{
  state1;
  state2;
}

// 代入しなくても関数という「値」として利用できる
Array(2).map((arg1,arg2)=>{
  state1;
  state2;
})

// 引数が一個の場合
function a1s2(arg1){
  state1;
  state2;
}
// 引数リストの()を省略可能
arg1=>{state1;state2;}

// return する値を1ステートでかける or return した値を使わない場合
function a1s1(arg1){
  return state1;
}
// ブロック{}も省略できる
arg1=>state1;

最後の変換の通り、returnする値を1個の式で表せる or returnした値を呼び出しもとで利用しない場合、コードブロック{}を省略でき、2文字節約になる。
このため、式の数をへらすことが文字数削減につながる。
ただし、一般的な話をすると、このためだけに式をへらす労力を割くと、可読性がメタメタになっていく

かんたんオーバーロードも便利

function k(t,f=0){return k[f]}
k(a) // a[0] がかえる
k(a,2) // a[2] がかえる

関数(メンバ)呼び出し

上記の通り、配列.indexOf配列['indexOf']は同じ関数オブジェクトを返す。
配列['indexOf'](引数)のような呼び出しもできる。
I='indexOf'してからなら配列[I](引数)でも呼び出せる。

暗黙変換の利用

FalsyとTruthy

真偽値の変換。よくあるやつ。
演算子を駆使するといろいろ捗る

// 真偽値定数で使うやつ
0 == false
!0 === true

// 一応紹介するが基本的に不要なやつ。
// どうしても厳密な真偽値に変換しないとダメな場合に利用するショートハンド
!!0 === false
!!1 === true

// 真偽値を変数にするときのやつ
false-0 === 0
true-0 === 1
// *1 とかでも一応可。
// 左辺の true や false は Truthy/falsy ではなく厳密一致

// indexOf の返り値で使うやつ
~-1 === 0 == false
~(/* -1以外の任意の値 */) == true

ロジック改善

爆弾位置配列

0~80の中から10個無作為に選ぶ
10回繰り返しで配列に詰めて既存のときは別の値もってくる処理
whileから再帰関数にして1文にした。

初期
bombsLocation = filedArray(10).reduce(p => {
  while (~p.indexOf(x = ~~(Math.random() * 81)));
  return [x].concat(p)
}, [cell.location]);
bombsLocation.pop();
ほぼ最終
bombsLocation = filedArray(10).reduce(p => (
  f = () =>
    ~p.indexOf(x = ~~(Math.random() * 81))
    ? f()
    : p.concat([x])
  )()
  , [cell.location]).slice(1)

再帰関数にしてすぐに代入できるようにした。
再帰関数にした弊害で、Math.randomの引きが悪いとか、あまりにも高密度な地雷原を設定したりすると、StackOverflowになる想定。

計算順を利用

同じ値の代入

代入演算子=は左オペランドの変数に右オペランドの値を代入して、その式は代入された値を返す。
a=xという値とxは等価という意味((a=x)===xtrue)
また、= は右オペランドの値が確定しないと代入できないので右オペランドの式を評価してから代入する

g=0;h=0;
g=(h=0);
g=h=0;

h=0の=が最初に処理され、hに0が代入される。
式は代入された0として評価され、g=0として処理される

代入即利用

代入が先に行われるようにすればそれより後ろの式で左オペランドにいた変数を代入後の値として利用できる。

a=["ぜろ","いち"];
c=1;
x=a[c]; // x === "いち"

// cの代入を省略
a=["ぜろ","いち"];
x=a[c=1];

// aの代入を省略
x=(a=["ぜろ","いち"])[c=1];
処理順を考慮しないと変な代入になる。
x=a=["ぜろ","いち"][c=1] // a === "いち"

同じ値の代入に記載と同様の順番。a=["ぜろ","いち"][c=1]の右辺["ぜろ","いち"][c=1]が真っ先に評価される。

関数でも可能。
関数値として代入しつつ、代入式の結果に引数リストをわたすことで即実行できる

// 普通
function C(t){return document.createElement(t);}b=C('button');

// アロー関数化して代入式にする
C=t=>document.createElement(t);b=C('button');

// 代入しながら実行する
b=(C=t=>document.createElement(t))('button');

最後の変換は文字数に変化が無いが、式の個数を減らせる。

実際の実装

プロトタイプ
<body></body>
<script>
button = document.createElement('button');
button.innerText = "_";

// map関数呼び出しがすぐに行える配列、を用意する関数を定義する。
filedArray = size => Array(size).fill();
// 無から繰り返しとか配列を生成したい場面が多いときは、こういう関数は便利

// ボタン要素の配列を作成
board = filedArray(9).map(v => filedArray(9).map(v => button.cloneNode(true)))

// 初期化。ガチのundefinedだとダメなのでなんかしら代入しとく必要あり
bombsLocation = 0; // 爆弾位置配列
isGameOver = 0;

clicked = event => {
  cell = event.target; // イベント元呼び出し短縮

  if (cell.flagged || cell.opend || isGameOver) return;
  // フラグたててるか、open済みか、GameOverならなにもしない

  if (!bombsLocation) { // 爆弾位置配列が未定義なら初期化する。
    // reduceで、直前までに配置された爆弾の配列を受け取り、random値を格納していく。
    // 重複していたら登録しない。
    bombsLocation = filedArray(10).reduce(p => {
      while (~p.indexOf(x = ~~(Math.random() * 81))); // 登録済みの間はループする。未登録の位置番号が出たらループを抜けて登録
      return [x].concat(p) // 直前までの爆弾の配列に新しい値を付け加えた配列を返す。 concat関数なら返り値がconcatされた結果の配列になる。
    }, [cell.location]); // 最初にクリックされたセルには爆弾はないことが保証されるため、候補からはずさせる
    bombsLocation.pop(); // 最初にクリックされたセルを除外する。 slice 関数で除外した配列を返すことができる。(のちほどsliceに変更した)
  }

  // 爆発判定する
  if (~bombsLocation.indexOf(cell.location)) {
    // Array.prototype.indexOf は引数と一致する値がない場合は-1、それ以外は0以上の位置index値を返す。
    // ~ ビット否定演算子で -1 は 0(==false) それ以外は -1以下の整数 (==true) になる。
    // 尚、「配列のそれぞれの値に対してテストしていずれかが条件を満たすときtrue」は Array.prototype.some が使える。
    // if(bombsLocation.some(v=>v==cell.location)){ // 多少かしこまるならこっち
    cell.innerText = "#"; // Bomb!
    isGameOver = 1;
    return;
  }

  // 上下左右の 3x3 のセルに存在する爆弾の数を数える。
  count = 0;
  filedArray(3).map((v, i) => filedArray(3).map((v, j) => {
    let row = i - 1; // 行方向の相対位置
    let column = j - 1; // 列方向の相対位置
    if (!row && !column) return; // 自分のときは処理しない。 この処理順なら別にいらない。爆弾ないこと保証されてるので。
    // テスト対象のインデックスを取得する
    row += cell.row;
    column += cell.column;
    if (row < 0 || 9 <= row || column < 0 || 9 <= column) return; // 欄外のインデックスは除外
    if (~bombsLocation.indexOf(row * 9 + column)) count++;
  }));
  cell.innerText = count;
}

// 右クリックイベント
rClicked = event => {
  event.preventDefault(); // 右クリックでメニューでなくする
  cell = event.target;
  if (cell.opend || isGameOver) return;
  cell.flagged = !cell.flagged; // フラグ状態を切り替え
  cell.innerText = cell.flagged ? "p" : "_" // フラグ状態に応じてテキストを変える
}

// 作成した処理をバインドする
board.map((v, row) => {
  v.map((buttonEl, column) => {
    document.body.appendChild(buttonEl);
    buttonEl.onclick = clicked;
    buttonEl.oncontextmenu = rClicked;
    buttonEl.row = row;
    buttonEl.column = column;
    buttonEl.location = row * 9 + column
  });
  document.body.appendChild(document.createElement('br'))
})

setTimeout(x=>Array.from(document.getElementsByTagName('button'),(e,i)=>(isGameOver=e.click(),~bombsLocation['indexOf'](i))?(e.style.background="red"):0),200)

</script>
v3-short
<body></body><script>b=(C=t=>document.createElement(t))('button');b[T='innerText']="_";g=h=0;(k=
(t,m=v=>v)=>Array(t).fill().map(m))(9,v=>k(9,v=>b.cloneNode(!0))).map((_,x)=>{_.map((v,y)=>
{(a=t=>document.body.appendChild(t))(v);v.onclick=e=>{if(h||(o=e.target).f||o.o)return;if(!g){g=k(10).reduce(p=>
{while(~p[I='indexOf'](x=~~(Math.random()*81)));return [x].concat(p)},[o.i]);g.pop()}if(~g[I](o.i)){h=1;return o[T]="#"}
c=0;k(3,(v,i)=>k(3,(v,j)=>{let x=i-1,y=j-1;c+=(((!x&&!y)||(x+=o.x)<0||8<x||(y+=o.y)<0||8<y)?0:(~g[I](x*9+y))?
1:0)}));o.o=1;o[T]=c};v.oncontextmenu=e=>{e.preventDefault();if(h||
(o=e.target).o)return;o.f=!o.f;o[T]=o.f?"p":"_"};v.x=x;v.y=y;v.i=x*9+y});a(C('br'))})</script>
v4-working
<body></body>
<script>
b=(C=t=>document.createElement(t))('button');
b[T='innerText']="_";
g=h=0;
(k=(t,m=v=>v)=>Array(t).fill().map(m))(9,v=>k(9,v=>b.cloneNode(!0))).map((_,x)=>{_.map((v,y)=>{(a=t=>document.body.appendChild(t))(v);
v.onclick=e=>{if(h||(o=e.target).f||o.o)return;
if(!g){g=k(10).reduce(p=>(f=()=>~p[I='indexOf'](x=~~(Math.random()*81))?f():[x].concat(p))(),[o.i]);g.pop()}
if(~g[I](o.i))return h=1,o[T]="#";
c=0;
// k(3,(v,i)=>k(3,(v,j)=>{let x=i-1,y=j-1;c+=(((!x&&!y)||(x+=o.x)<0||8<x||(y+=o.y)<0||8<y)?0:(~g[I](x*9+y))?1:0)}));
// k(3,(v,i)=>k(3,(v,j)=>{n=i-1;c+=((!--j&&!n||(n+=o.x)<0||8<n||(j+=o.y)<0||8<j)?0:(~g[I](n*9+j))?1:0)}));
   k(3,(v,i)=>k(3,(v,j)=>{n=i-1;c+=!(!--j&&!n||(n+=o.x)<0||8<n||(j+=o.y)<0||8<j||!~g[I](n*9+j))-0}));
o.o=1;
o[T]=c};
v.oncontextmenu=e=>(o=e.target)[T]=(e.preventDefault(),h||o.o)?o[T]:(o.f=!o.f)?"p":"_";
v.x=x;
v.y=y;
v.i=x*9+y});
a(C('br'))})
</script>
v5-work
<body></body>
<script>
b=(C=t=>document.createElement(t))('button');b[T='innerText']="_";g=h=0;
(k=(t,m=v=>v)=>Array(t).fill().map(m))(9,(_,x)=>{k(9,(v,y)=>{(a=t=>document.body.appendChild(t))(v=b.cloneNode(!0));
v.onclick=e=>{if(h||(o=e.target).f||o.o)return;
if(!g){g=k(10).reduce(p=>(f=()=>~p[I='indexOf'](x=~~(Math.random()*9*9))?f():[x].concat(p))(),[o.i]);g.pop()}

if(c=~g[I](o.i))return h=1,o[T]="#";k(9,(v,r)=>{n=r%3-1;c+=!(!(j=(r-n-4)/3)&&!n||(n+=o.x)<0||9<=n||(j+=o.y)<0||9<=j||!~g[I](n*9+j))-0});o[T]=c;

o.o=1;
};
v.oncontextmenu=e=>(o=e.target)[T]=(e.preventDefault(),h||o.o)?o[T]:(o.f=!o.f)?"p":"_";
v.x=x;
v.y=y;
v.i=x*9+y});
a(C('br'))})

aa=()=>Array.from(document.getElementsByTagName('button')).filter(e=>~g[I](e.i)).map(e=>e.style.background="gray")

</script>

最終ver

v6-work
<body></body>
<script>
g=h=0;
(k=(t,m=(v,i)=>i)=>Array(t).fill().map(m))(9,(_,x)=>{k(9,(v,y)=>{
(v=(a=t=>document.body.appendChild((C=t=>document.createElement(t))(t)))('button'))[T='innerText']="_";
v.onclick=e=>{
if(h||(o=e.target).f||o.o)return;
o[T]=(~(g=g||k(10)[R='reduce'](p=>(f=()=>~p[I='indexOf'](x=~~(Math.random()*9*9))?f():p.concat([x]))(),
[o.i]).slice(1))[I](o.i))?(h="#"):
k(9)[R]((p,r)=>p+!((n=r%3-1+o.x)<0||9<=n||(j=(r-n-4+o.x)/3+o.y)<0||9<=j||!~g[I](n*9+j)),0);
o.o=1;
};
v.oncontextmenu=e=>(o=e.target)[T]=(e.preventDefault(),h||o.o)?o[T]:(o.f=!o.f)?"p":"_";
v.i=(v.x=x)*9+(v.y=y)});
a('br')})
</script>
v6-mini-620
<body></body><script>g=h=0;(k=(t,m=(v,i)=>i)=>Array(t).fill().map(m))(9,(_,x)=>{k(9,(v,
y)=>{(v=(a=t=>document.body.appendChild((C=t=>document.createElement(t))(t)))('button')
)[T='innerText']="_";v.onclick=e=>{if(h||(o=e.target).f||o.o)return;o[T]=(~(g=g||k(10)[
R='reduce'](p=>(f=()=>~p[I='indexOf'](x=~~(Math.random()*9*9))?f():p.concat([x]))(),[o.
i]).slice(1))[I](o.i))?(h="#"):k(9)[R]((p,r)=>p+!((n=r%3-1+o.x)<0||9<=n||(j=(r-n-4+o.x)
/3+o.y)<0||9<=j||!~g[I](n*9+j)),0);o.o=1};v.oncontextmenu=e=>(o=e.target)[T]=(e.
preventDefault(),h||o.o)?o[T]:(o.f=!o.f)?"p":"_";v.i=(v.x=x)*9+(v.y=y)});a('br')})</script>

テスト用に末尾につけてたコード

アプリ起動後、自動的に全部開けて動作確認をし、
さらには保存したあと再読み込みする手間も省くため周期的にリロードさせた。

JetBrainsのWebStormとか使うとこんなの仕込まなくても保存したらリロードさせる機能とかついている(つよい)
今回はVSCode使ってたので仕組んだ

setInterval(x=>console.warn(1), 1000)
setTimeout(x=>Array.from(document.getElementsByTagName('button'),(e,i)=>(h=e.click(),~g[I](i))?(e.style.background="red"):0),200) // 即実行するとたまにもろもろ未定義扱いにされて動作しない

setTimeout(x=>location.reload(),7000)
3
1
1

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?