Edited at

自己再生型・魔インスイーパを創る


そうだマインスイーパを作ろう。

Qiitaの記事で誰かがマインスイーパを作っていて、自分も作りたいなと思ったので作ります。

でもどうせ作るなら、ただのマインスイーパじゃ面白くないので

「いくらマスを開けても自己再生して元に戻ってしまう」という魔インスイーパを作ります。


STEP1:触媒であるマインスイーパを作る。


一旦普通のマインスイーパを作ります。

バッサリとした要件としては


  • マス目状のボード

  • 開くとゲームオーバーになるマインが複数個埋め込まれてる

  • 最初のクリック時には必ずマインでは無いマスが開く

  • マインではないマスには周りのマインマスの数が書かれてある。

という感じでしょうか。

一度ゲームを作ったことがある人なら割と簡単に組めそうな気がします。


ボードを書く関数

今回javascriptでマインスイーパを作りますが

canvasは使わずdomを利用してレンダリングしようと思います。

理由はいくつかあって


  • クリックする処理が簡単に書ける

  • レンダリングする処理が簡単に書ける

  • 高度なレンダリングを要求するゲームではない

  • 慣れてるcssを使える

というわけなので

function ボードを書く関数(){

let html='';
cells=new Array(X_LEN);
for(let cnt_x=0;cnt_x<X_LEN;cnt_x++){
cells[cnt_x]=new Array(Y_LEN);
for(let cnt_y=0;cnt_y<Y_LEN;cnt_y++){
html+='<div style="--x:'+cnt_x+';--y:'+cnt_y+'" id="cell_'+cnt_x+'-'+cnt_y+'" class="cell_style"></div>';
}
}
document.getElementById('screen').innerHTML=html;
}

という感じに書いてレンダリングします。

コツはcssの変数を利用していることですね。

IE?そんなもん放っておけ。

むしろ駆逐しろ。


開けたマスの周りの座標を配列で返す関数

マインスイーパーでは頻繁に「あるマスの周りのマス」を参照します。

単純に

[x-1,y-1][x ,y-1][x+1,y-1]

[x-1,y ][x ,y ][x+1,y ]

[x-1,y+1][x ,y+1][x+1,y+1]

というように参照すれば良いわけですが

x=0,y=0である場合、左上である[x-1,y-1]を参照しようとすると

Cannot read property '-1' of undefined

というエラーでjsが倒れてしまいます。

「そんな配列定義はないで候…」と呻いているみたいですね。

なのでjsを張り倒さないために

function マスの周りの座標を配列で返す関数(x,y){

let cells=[];
if(x!==0){ //←
cells.push({x:x-1,y:y});
}
if(y!==0){ //↑
cells.push({x:x,y:y-1});
}
if(x!==X_LEN-1){ //→
cells.push({x:x+1,y:y});
}
if(y!==Y_LEN-1){ //↓
cells.push({x:x,y:y+1});
}
if(x!==0 && y!==0){ //↖
cells.push({x:x-1,y:y-1});
}
if(x!==X_LEN-1 && y!==Y_LEN-1){ //↘
cells.push({x:x+1,y:y+1});
}
if(x!==0 && y!==Y_LEN-1){ //↙
cells.push({x:x-1,y:y+1});
}
if(x!==X_LEN-1 && y!==0){ //↗
cells.push({x:x+1,y:y-1});
}

return cells;
}

という感じに書きまs…

…じゃなくて無駄のないように

function マスの周りの座標を配列で返す関数(x,y){

let cells=[];
if(x!==0){ //←
cells.push({x:x-1,y:y});
if(y!==0){ //↖
cells.push({x:x-1,y:y-1});
}
}
if(y!==0){ //↑
cells.push({x:x,y:y-1});
if(x!==X_LEN-1){ //↗
cells.push({x:x+1,y:y-1});
}
}
if(x!==X_LEN-1){ //→
cells.push({x:x+1,y:y});
if(y!==Y_LEN-1){ //↘
cells.push({x:x+1,y:y+1});
}
}
if(y!==Y_LEN-1){ //↓
cells.push({x:x,y:y+1});
if(x!==0){ //↙
cells.push({x:x-1,y:y+1});
}
}

return cells;
}

という感じに書きましょう。


マインを設置する関数

マインスイーパは最初のクリックでは必ずマイン以外のマスが開くように配慮されています。

なんとも優しい配慮がされたゲームですね。

今回はさらに"""優しくするために"""クリックされた周囲のマスもマインが無いようにしたいと思います。

function マインを設置する関数(x,y){

let クリックしたマスの周囲のマスの配列=開けたマスの周りの座標を配列で返す関数(x,y);
let cnt=0;
while(cnt<MINE){
let マイン設置予定X位置=Math.floor(Math.random()*X_LEN);
let マイン設置予定Y位置=Math.floor(Math.random()*Y_LEN);
if(既にマインが設置されているか? || クリックしたマスと同じか?){
continue;
}

let check_flag=false;
for(let cnt_a=0;len_a=クリックしたマスの周囲のマスの配列.length;cnt_a<len_a;cnt_a++){
if(クリックしたマスの周囲のマスと同じか?){
check_flag=true;
break;
}
}
if(check_flag){
continue;
}

マインの設置処理();
cnt++;
}
}

こんな感じで書きました。

実際はもう少し追記して、マインの周りの数を数える処理も書いていますが

まぁ僕が教えなくても、皆つよつよエンジニアなので不要でしょう!


マス開ける関数

予想どおりに


  • マインの設置

までは上手くいきましたが、どうしてもマスを開ける処理が上手く組めません。


function マス開ける関数(x,y){
開けたマスの地雷チェック関数(x,y);
let cells=開けたマスの周りの座標を配列で返す関数(x,y);
for(let cnt=0,len=cells.length;cnt<len;cnt++){
マスを開ける関数(x,y);
}

return;
}

なーんて感じに再帰関数的に書いていたのですが

Uncaught RangeError: Maximum call stack size exceeded

というエラーでjsが死んでしまいます。

どうやら「関数呼びすぎで候…」と遺言を残しているみたいです。

確かに

1マス→8マス→64マス→512マス…

という風に関数がすごい勢いでコールされてしまいます。

なので再帰関数ではない方法で処理を書く必要があります。

function マス開ける関数(cells){

let 次に開けるマスの配列=[];
for(let cnt=0,len=cells.length;cnt<len;cnt++){
開けたマスの地雷チェック関数(cells[cnt].x,cells[cnt].y);
if(マイン数が0であるか? && 既に開けられていないか? && 周囲チェックしたマスであるか?){
次に開けるマスの配列.push(開けたマスの周りの座標を配列で返す関数(cells[cnt].x,cells[cnt].y));
}
}
マス開ける関数(次に開けるマスの配列);

return;
}

こんな感じに書いて解決しました。

実際は再帰関数なのですが、1つの関数が1つの関数を呼んでるだけなので

これでjs君もニッコリです?

ん?ニッコリだよなぁ??

なぁ、おい。そうだろjs。

はい、じゃぁニッコリということで。


STEP2:呪文を詠唱する


マインスイーパの完成

一旦普通のマインスイーパが作れました。

これから自己再生する魔インスイーパにするために、呪文を唱えていきましょう!


「自己再生する」とは

アニメとかゲームとかでありがちですよね

いくら燃やし尽くされても、塵から再生するとか

もがれた腕がニュルニュルと復活するとか

ここでポイントなのは、ダメージを受けた所から再生するということです。

なかったことにはならないんですね。

次の瞬間、何もなかったかのように腕が元に戻っていたなら

それはもはや時空操作系の能力ですよね。

なので魔インスイーパもそれっぽく再生するようにします。

ポイントとしては


  • 再生は生えてくるような感じで

  • ニュル感

です。


マスが再生する関数

再生させるためには


  • 開けられる直前だった状態

の記録が必要になります。

なので、マス開ける関数に追記しながら書いていきます。

function マス開ける関数(cells){

let 次に開けるマスの配列=[];
再生配列.push({
時間:recovery_time,
配列:cells
});
recovery_time=Math.min(recovery_time*1.2,5000);

for(let cnt=0,len=cells.length;cnt<len;cnt++){
開けたマスの地雷チェック関数(cells[cnt].x,cells[cnt].y);
if(マイン数が0であるか? && 既に開けられていないか? && 周囲チェックしたマスであるか?){
次に開けるマスの配列.push(開けたマスの周りの座標を配列で返す関数(cells[cnt].x,cells[cnt].y));
}
}
if(次に開けるマスの配列.length!==0){
マス開ける関数(次に開けるマスの配列);
}else{
setTimeout( //マスの再生処理
再生する関数,
6000,
再生配列
);
}

return;
}

function 再生する関数(再生配列){
let record=再生配列[再生配列.length-1];
for(let cnt=0,len=record.cells.length;cnt<len;cnt++){
let cell_r=record.cells[cnt];
マスを閉じる関数(cell_r.x,cell_r.y);
}

if(再生配列.length===1){
return;
}
再生配列.pop();

timer.push(setTimeout( //マスの再生処理
再生する関数,
再生配列[再生配列.length-1].時間,
再生配列
));

return;
}

こんな感じになりました。

これで再生する関数が呼ばれる毎に

1つ前に、また1つ前に…

と元に向かって再生します。

等速でこれを行うと

ただの巻き戻しなので

setTimeout()を利用して再生速度を調整しました。

「ニュル」というのは

速度のなめらかな「ニュ」と素早い速度の「ル」

に分解できます。

ということはニュルニュル感というのは

ゆっくり始まって、急に終わる

感じということですね。

コードで表現しているのは

recovery_time=Math.min(recovery_time*1.2,5000);

の部分で、再生の末端に向かうほど再生速度が遅くなるようにしています。


タピオカに負けないために

さいきんqiitaでタピオカが流行ってましたね。

僕もcssアニメーション大好きとしては、負けていられません。

なので少しアニメーションにもこだわります。

マスを開ける時にパッと開くだけじゃつまらないな

と思ったので

@keyframes dig_shake{

0%{ transform:translate(0px,5px) scale(1.1)}
20%{ transform:translate(5px,0) scale(1.2)}
40%{ transform:translate(-5px,-5px) scale(1.3)}
60%{ transform:translate(0,5px) scale(1.3)}
80%{ transform:translate(5px,0px) scale(1.2)}
100%{ transform:translate(-5px,-5px) scale(1.1)}
}

というアニメーションをマスに付与するようにしてみました。

ココには書けません(重要)が、他にも開けた時の工夫を凝らしました。

これで開けたときに爽快感が出るはずです(マジキチスマイル)!

また再生する際にもパッと戻ったんじゃ味気ないので

transition:200ms background ease-in;

を指定することで、ニュル感が増しました。


STEP3:いざ禁忌を破り、魔との契約

HTML

<!DOCTYPE html>

<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>魔インスイーパー</title>
<script src="script.js" charset="utf-8" defer></script>
<link rel="stylesheet" href="style.css">
</head>

<body>

<main id="screen">
</main>

</body>
</html>

CSS

*{

padding:0;
margin:0;
box-sizing:border-box;
user-select:none;
}

body{
width:100%;
height:100%;
overflow:hidden;
background:#000;
}

:root{
--cell_size:25px;
}

#screen{
overflow:hidden;
position:absolute;
background:#601010;
}

.style_cell{
position:absolute;
overflow:hidden;
width:var(--cell_size);
height:var(--cell_size);
left:calc(var(--x) * var(--cell_size));
top:calc(var(--y) * var(--cell_size));
line-height:var(--cell_size);
text-align:center;
font-weight:900;
font-size:18px;
border:ridge 1px #666;
}

.status_flat{
z-index:1;
background:#c8c8c8;
transition:200ms background ease-in;
cursor:pointer;
}

.status_dig{
z-index:10;
border:solid 1px #580808;
background:rgb(calc(102 - var(--meat)),calc(17 - var(--meat)),calc(17 - var(--meat)));
cursor:default;
animation:dig_shake 100ms ease-out;
}
@keyframes dig_shake{
0%{ transform:translate(0px ,5px) scale(1.1)}
20%{ transform:translate(5px ,0) scale(1.2)}
40%{ transform:translate(-5px ,-5px) scale(1.3)}
60%{ transform:translate(0 ,5px) scale(1.3)}
80%{ transform:translate(5px ,0px) scale(1.2)}
100%{ transform:translate(-5px ,-5px) scale(1.1)}
}
@keyframes beat{
0%{
z-index:20;
transform:scale(1);}
50%{
transform:scale(1.75);}
100%{
transform:scale(1);}
}

.status_flag{
color:#f00 !important;
cursor:crosshair;
}

.status_gameover{
z-index:100;
animation:cell_gameover 250ms infinite ease-in-out both;
}
@keyframes cell_gameover{
0%{ transform:scale(1)}
25%{ transform:scale(2)}
50%{ transform:scale(2.5)}
75%{ transform:scale(2)}
100%{ transform:scale(1)}
}

.style_blood{
display:none;
position:absolute;
width:var(--cell_size);
height:var(--cell_size);
left:calc(var(--rnd_x) + var(--x) * var(--cell_size) - var(--cell_size) / 2);
top:calc(var(--rnd_y) + var(--y) * var(--cell_size) - var(--cell_size) / 2);
line-height:var(--cell_size);
text-align:center;
cursor:default;
}

.status_bleeding{
display:block !important;
animation:bleeding 500ms ease-out both;
}
@keyframes bleeding{
0%{
transform:scale(0.5);
color:rgba(255,0,0,0.75);
z-index:20;}
100%{
transform:scale(4);
color:rgba(255,0,0,0);
z-index:0;}
}

#end_layer{
display:flex;
position:absolute;
z-index:100;
top:0;
left:0;
width:100%;
height:100%;
align-items:center;
justify-content:center;
color:#f00;
font-size:10vw;
font-weight:900;
background:#000;
cursor:crosshair;
animation:end_layer 1500ms 250ms ease-in both;
}
@keyframes end_layer{
0%{
opacity:0}
85%{
opacity:1;
transform:scale(1);
color:rgba(255,0,0,1)}
100%{
transform:scale(2);
color:rgba(255,0,0,0)}
}

#clear_layer{
display:flex;
position:absolute;
z-index:100;
top:0;
left:0;
width:100%;
height:100%;
align-items:center;
justify-content:center;
color:#48f;
font-size:10vw;
font-weight:900;
background:#fff;
cursor:default;
opacity:0.3;
}

JavaScript

const SIZE  =25;    //マスのサイズ

const X_LEN =Math.floor(window.innerWidth/SIZE)-1; //ボードのX長さ
const Y_LEN =Math.floor(window.innerHeight/SIZE)-1; //ボードのY長さ
const MINE =Math.floor(X_LEN*Y_LEN/8); //マインの数
const COLOR =new Array( //ステータスの色
'',
'#005AA0',
'#00873C',
'#DFD000',
'#D28300',
'#C7000B',
'#800073',
'#181878',
'#333'
);

var cell=new Array(X_LEN); //マス配列の宣言
for(let cnt_x=0;cnt_x<X_LEN;cnt_x++){
cell[cnt_x]=new Array(Y_LEN);
}
var mine_flag=true; //マイン設置フラグの宣言
var mine_cells=new Array(MINE); //マインのセルの配列の宣言
var dig_records=[]; //ディグレコードの宣言
var dig_record_cnt=0; //ディグレコードカウンタの宣言
var recovery_time=0; //回復時間の宣言
var click_enabele=true; //クリック可能フラグの宣言
var timer=[]; //タイマー配列の宣言
var beat_flag=true; //ビートフラグの初期化

BoardClear();

//ボードの初期化
function BoardClear(){
mine_flag=true;
beat_flag=true;

let html='';
for(let cnt_x=0;cnt_x<X_LEN;cnt_x++){
for(let cnt_y=0;cnt_y<Y_LEN;cnt_y++){
cell[cnt_x][cnt_y]={
mine :0, //マスの周りのマインの数(10=マイン)
dig :false, //マスを開けたかどうかフラグ
check :false, //周りチェック時のフラグ
flag :false //フラッグフラグ
};
html+='<span style="--x:'+cnt_x+';--y:'+cnt_y+';--rnd_x:'+Math.floor(Math.random()*SIZE)+'px;--rnd_y:'+Math.floor(Math.random()*SIZE)+'px" id="blood_'+cnt_x+'-'+cnt_y+'" class="style_blood">※</span>';
html+='<div role="button" style="--x:'+cnt_x+';--y:'+cnt_y+';--meat:'+Math.floor(Math.random()*15-7)+'" id="cell_'+cnt_x+'-'+cnt_y+'" class="style_cell status_flat" onclick="CellLeftClick('+cnt_x+','+cnt_y+')" oncontextmenu="CellRightClick('+cnt_x+','+cnt_y+');return false"></div>';
}
}
let dom_screen=document.getElementById('screen');
dom_screen.style.left =((window.innerWidth-X_LEN*SIZE)/2)+'px';
dom_screen.style.top =((window.innerHeight-Y_LEN*SIZE)/2)+'px';
dom_screen.style.width =(X_LEN*SIZE)+'px';
dom_screen.style.height =(Y_LEN*SIZE)+'px';
dom_screen.innerHTML =html;

return;
}

//マスをクリックされた時
function CellLeftClick(x,y){
if(cell[x][y].flag || cell[x][y].dig || !click_enabele){
return;
}

if(mine_flag){
mine_flag=false;
BoardMineSet(x,y); //マインの設置
}

if(beat_flag){
beat_flag=false;
timer.push(setTimeout(BeatMove,500+Math.floor(Math.random()*4000)));
timer.push(setTimeout(BeatMove,500+Math.floor(Math.random()*4000)));
}

dig_record_cnt++;
recovery_time=50;
CellOpen(new Array({ //マスを開ける処理
x:x,
y:y
}));

return;
}

//マインの設置
function BoardMineSet(x,y){
let around_cells=AroundCheck(x,y); //クリックしたマスの周辺のマスの配列
let cnt=0;
while(cnt<MINE){ //マインの設置処理
let mine_x=Math.floor(Math.random()*X_LEN); //マイン設置予定X位置
let mine_y=Math.floor(Math.random()*Y_LEN); //マイン設置予定Y位置
if(cell[mine_x][mine_y].mine!==0 || (mine_x===x && mine_y===y)){
continue;
}

let check_flag=false;
for(let cnt_a=0,len_a=around_cells.length;cnt_a<len_a;cnt_a++){
if(around_cells[cnt_a].x===mine_x && around_cells[cnt_a].y===mine_y){
check_flag=true;
break;
}
}
if(check_flag){
continue;
}

cell[mine_x][mine_y].mine=10;
mine_cells[cnt]={
x:mine_x,
y:mine_y
};
cnt++;
}

for(let cnt_m=0,len_m=mine_cells.length;cnt_m<len_m;cnt_m++){ //マイン周りの処理
let cell_m=mine_cells[cnt_m];
let around_cells=AroundCheck(cell_m.x,cell_m.y); //マイン周りのマスの配列
for(let cnt_a=0,len_a=around_cells.length;cnt_a<len_a;cnt_a++){
let cell_a=around_cells[cnt_a];
cell[cell_a.x][cell_a.y].mine=Math.min(cell[cell_a.x][cell_a.y].mine+1,10);
}
}

/*
for(let cnt_x=0;cnt_x<X_LEN;cnt_x++){
for(let cnt_y=0;cnt_y<Y_LEN;cnt_y++){
document.getElementById('cell_'+cnt_x+'-'+cnt_y).innerHTML=cell[cnt_x][cnt_y].mine;
}
}
*/

return;
}

//セルを開ける関数
function CellOpen(open_cells){
click_enabele=false; //クリック不可に

if(!Array.isArray(dig_records[dig_record_cnt])){
dig_records[dig_record_cnt]=[];
}
dig_records[dig_record_cnt].push({
time :recovery_time, //再生時間
cells :open_cells //再生するマスの配列
});
recovery_time=Math.min(recovery_time*1.2,5000);

let next_cells=[];
for(let cnt_o=0,len_o=open_cells.length;cnt_o<len_o;cnt_o++){
let cell_o=open_cells[cnt_o];
cell[cell_o.x][cell_o.y].dig=true;
CellOpenStyle(cell_o.x,cell_o.y); //開けたマスのスタイル更新
if(cell[cell_o.x][cell_o.y].mine!==0){
continue;
}
let around_cells=AroundCheck(cell_o.x,cell_o.y); //開けたマスの周りのマスの配列
for(let cnt_a=0,len_a=around_cells.length;cnt_a<len_a;cnt_a++){
let cell_a=around_cells[cnt_a];
if(cell[cell_a.x][cell_a.y].dig || cell[cell_a.x][cell_a.y].check){
continue;
}
cell[cell_a.x][cell_a.y].check=true;
next_cells.push({
x:cell_a.x,
y:cell_a.y
});
}
}
if(next_cells.length!==0){
timer.push(setTimeout( //マスを開ける処理
CellOpen,
25,
next_cells
));
}else{
click_enabele=true; //クリック可能に
timer.push(setTimeout( //マスの再生処理
CellRecovery,
6000,
dig_records[dig_record_cnt]
));
}

return;
}

//セルを開けたスタイルに更新する関数
function CellOpenStyle(x,y){

let cell_dom=document.getElementById('cell_'+x+'-'+y);
cell_dom.classList.remove('status_flat','status_flag');
cell_dom.classList.add('status_dig');
if(cell[x][y].mine===0){
cell_dom.innerHTML='';
}else if(cell[x][y].mine===10){
cell_dom.innerHTML='';
document.getElementById('cell_'+x+'-'+y).classList.add('status_gameover');
GameOver(); //ゲームオーバー処理
}else{
cell_dom.style.color=COLOR[cell[x][y].mine];
cell_dom.innerHTML=cell[x][y].mine;
document.getElementById('blood_'+x+'-'+y).classList.add('status_bleeding')
}

return;
}

//ゲームオーバー処理
function GameOver(){
document.getElementById('screen').insertAdjacentHTML('beforeend','<div id="end_layer">DEAD END…</div>');
for(let cnt=0,len=timer.length;cnt<len;cnt++){
clearTimeout(timer[cnt]);
}
setTimeout(
BoardClear,
2000
);

return;
}

//マスが再生する関数
function CellRecovery(records){
let record=records[records.length-1];
for(let cnt=0,len=record.cells.length;cnt<len;cnt++){
let cell_r=record.cells[cnt];
cell[cell_r.x][cell_r.y].dig =false;
cell[cell_r.x][cell_r.y].check =false;
cell[cell_r.x][cell_r.y].flag =false;
CellRecoveryStyle(cell_r.x,cell_r.y); //再生したマスのスタイル更新
}

if(records.length===1){
return;
}
records.pop();

timer.push(setTimeout( //マスの再生処理
CellRecovery,
records[records.length-1].time,
records
));

return;
}

//マス回復スタイルに更新する関数
function CellRecoveryStyle(x,y){
let cell_dom=document.getElementById('cell_'+x+'-'+y);
cell_dom.classList.remove('status_dig');
cell_dom.classList.add('status_flat');
cell_dom.innerHTML='';
document.getElementById('blood_'+x+'-'+y).classList.remove('status_bleeding');

return;
}

//ビートする関数
function BeatMove(){
while(true){
let beat_x=Math.floor(Math.random()*X_LEN); //ビート予定X位置
let beat_y=Math.floor(Math.random()*Y_LEN); //ビート予定Y位置
if(!cell[beat_x][beat_y].dig){
document.getElementById('cell_'+beat_x+'-'+beat_y).style.animation='beat 300ms ease-in-out';
break;
}
}
timer.push(setTimeout(BeatMove,500+Math.floor(Math.random()*4000))); //ビート開始

return
}

//マスを右クリックされた時
function CellRightClick(x,y){
if(cell[x][y].dig){
return;
}
let cell_dom=document.getElementById('cell_'+x+'-'+y);
cell_dom.classList.toggle('status_flag');
cell_dom.innerHTML=cell[x][y].flag?'':'×';
cell[x][y].flag=cell[x][y].flag?false:true;

if(document.getElementsByClassName('status_flag').length!==MINE){
return;
}
for(let cnt_x=0;cnt_x<X_LEN;cnt_x++){
for(let cnt_y=0;cnt_y<Y_LEN;cnt_y++){
if(cell[cnt_x][cnt_y].flag && cell[cnt_x][cnt_y].mine!==10){
return;
}
}
}
GameClear(); //ゲームクリア処理

return;
}

//ゲームクリア処理
function GameClear(){
document.getElementById('screen').insertAdjacentHTML('beforeend','<div id="clear_layer">GAME CLEAR!</div>');
for(let cnt=0,len=timer.length;cnt<len;cnt++){
clearTimeout(timer[cnt]);
}

return;
}

//マス周りのチェック関数
function AroundCheck(x,y){
let cells=[];
if(x!==0){ //左
cells.push({
x:x-1,
y:y
});
if(y!==0){ //左上
cells.push({
x:x-1,
y:y-1
});
}
}

if(y!==0){ //上
cells.push({
x:x,
y:y-1
});
if(x!==X_LEN-1){ //右上
cells.push({
x:x+1,
y:y-1
});
}
}

if(x!==X_LEN-1){ //右
cells.push({
x:x+1,
y:y
});
if(y!==Y_LEN-1){ //右下
cells.push({
x:x+1,
y:y+1
});
}
}

if(y!==Y_LEN-1){ //下
cells.push({
x:x,
y:y+1
});
if(x!==0){ //左下
cells.push({
x:x-1,
y:y+1
});
}
}

return cells;
}


STEP4?夐♀縺カ

(微G)

https://s4kd0r.net/game/ma_insweeper/

縺ゥ縺?d繧峨?√?繧、繝ウ繧ケ繧、繝シ繝代↓諢剰ュ倥r荵励▲蜿悶i繧後◆縺ソ縺溘>縺ァ縺吶?

繝槭う繝ウ繧ケ繧、繝シ繝代r菴懊▲縺ヲ縺ソ縺ヲ濶イ縲?ュヲ縺ウ縺後≠縺」縺溘@

讌ス縺励°縺」縺溘?縺ァ縲√が繧ケ繧ケ繝。縺ァ縺呻シ