Javascriptでマインスイーパーをつくる 第2回

Javascriptでマインスイーパーをつくる 第1回

自動的に周りを開く

周りに地雷がないマスを開いたとき、自動的に周りのマスを開くようにします。連鎖的に開いていくためにクリックイベントリスナー内で別のマスのクリックイベントを引き起こす再帰的な処理を行います。
自分自身を呼び出す関数を再帰関数といいます。再帰関数をうまく動かすためには、関数の冒頭で自身を呼ばないようにする処理が必要です。今回はクリックイベントがcloseが設定されたマスにだけ設定されている(td.cell.closeに対してだけon('click’)が設定されている)ので、closeを外した後で再帰処理を行うことが重要です。

minesweeper6.jsのソース

jQuery(function (){
    const rows = 10;	// マスの縦方向の数。とりあえず変更しないのでconstで宣言する。
    const cols = 10;
    const bombs = 15;	// 地雷の数
    
    const board = $('#board');

    let bomb = bombs;
    for(let r = 0 ; r < rows ; r ++){
        let row = $('<tr>').appendTo(board);
        for(let c = 0 ; c < cols ; c ++){
            let col = $('<td>').addClass('cell').addClass('close').appendTo(row);
            if(bomb -- > 0){
                col.addClass('bomb');  // 地雷を設置(CSSクラスを設定する)
            }
        }
    }

    // 地雷をランダムに置き換え
    board.children('tr').each(function (){
        $(this).children('td.cell').each(function (){
            let r = Math.floor(Math.random() * rows);
            let c = Math.floor(Math.random() * cols);
            let cell = board.children('tr').eq(r).children('td.cell').eq(c);
            if($(this).hasClass('bomb') != cell.hasClass('bomb')){
                $(this).toggleClass('bomb');
                cell.toggleClass('bomb');
            }
        });
    });

    //	周りにある地雷を数える
    for(let r = 0 ; r < rows ; r ++){
        let row = board.children('tr').eq(r)
        for(let c = 0 ; c < cols ; c ++){
            let cell = row.children('td.cell').eq(c);
            if(cell.hasClass('bomb')) continue;

            bomb = 0;
            jQuery.each([r - 1, r, r + 1], function (i, r){
                if(r < 0) return;
                if(r >= rows) return;
                jQuery.each([c - 1, c , c + 1], function (i, c){
                    if(c < 0) return;
                    if(c >= cols) return;
                    if(board.children('tr').eq(r).children('td.cell').eq(c).hasClass('bomb')){
                        bomb ++;
                    }
                });
            });
            cell.attr('data-hint', bomb);
            cell.addClass('hint' + bomb);
        }
    }

    //	マスがクリックされたときの動作
    board.find('td.cell.close').on('click', function (evt){
        let cell = $(this);
        cell.addClass('open').removeClass('close').off('click');

        // 地雷の数を表示する
        cell.text(cell.attr('data-hint'));

        // 周りに地雷が1つでもあるときは処理を終える
        if('0' != cell.attr('data-hint')){
            return;
        }

        //	周を開く
        //	周りのマスを操作するためには自分の位置がわからないといけない。
        //	prevAllメソッドで対象の要素と同じ階層にあるそれよりも前の要素を取得できる。
        //	ここでは同じtr内のtdで、自身(cell)よりも前のものを取得し、その数(length)を調べることで自身の列数を変数cにいれている。
        let c = cell.prevAll().length;
        //	cellの上の要素(tr)を取得し、その前にある要素(tr)を数えて(length)、自身の行数を変数rにいれている。
        let r = cell.parent('tr').prevAll().length;
        //	地雷を数えたときと同じように、周りのマスを取得する
        jQuery.each([r - 1, r, r + 1], function (i, r){
            if(r < 0) return;
            if(r >= rows) return;
            jQuery.each([c - 1, c , c + 1], function (i, c){
                if(c < 0) return;
                if(c >= cols) return;
                // 周りのマスを一つ取得し、そのクリックイベントを引き起こす。
                board.children('tr').eq(r).children('td.cell').eq(c).click();
            });
        });
    });
});

下に表示されないときはここをクリック(別ウィンドウが開きます)

右クリックで旗をたてる

地雷があることが分かったマスに旗を立てます。画像が用意できないので、黄色にします。
旗のたっているセルは開けないようにします。

minesweeper7.jsのソース

jQuery(function (){
    const rows = 10;	// マスの縦方向の数。とりあえず変更しないのでconstで宣言する。
    const cols = 10;
    const bombs = 15;	// 地雷の数
    
    const board = $('#board');

    let bomb = bombs;
    for(let r = 0 ; r < rows ; r ++){
        let row = $('<tr>').appendTo(board);
        for(let c = 0 ; c < cols ; c ++){
            let col = $('<td>').addClass('cell').addClass('close').appendTo(row);
            if(bomb -- > 0){
                col.addClass('bomb');  // 地雷を設置(CSSクラスを設定する)
            }
        }
    }

    // 地雷をランダムに置き換え
    board.children('tr').each(function (){
        $(this).children('td.cell').each(function (){
            let r = Math.floor(Math.random() * rows);
            let c = Math.floor(Math.random() * cols);
            let cell = board.children('tr').eq(r).children('td.cell').eq(c);
            if($(this).hasClass('bomb') != cell.hasClass('bomb')){
                $(this).toggleClass('bomb');
                cell.toggleClass('bomb');
            }
        });
    });

    //	周りにある地雷を数える
    for(let r = 0 ; r < rows ; r ++){
        let row = board.children('tr').eq(r)
        for(let c = 0 ; c < cols ; c ++){
            let cell = row.children('td.cell').eq(c);
            if(cell.hasClass('bomb')) continue;

            bomb = 0;
            jQuery.each([r - 1, r, r + 1], function (i, r){
                if(r < 0) return;
                if(r >= rows) return;
                jQuery.each([c - 1, c , c + 1], function (i, c){
                    if(c < 0) return;
                    if(c >= cols) return;
                    if(board.children('tr').eq(r).children('td.cell').eq(c).hasClass('bomb')){
                        bomb ++;
                    }
                });
            });
            cell.attr('data-hint', bomb);
            cell.addClass('hint' + bomb);
        }
    }

    //	マスがクリックされたときの動作
    board.find('td.cell.close').on('click', function (evt){
        let cell = $(this);

        //	旗が立っているときは開かない
        if(cell.hasClass('flag')){
            return true;
        }

        cell.addClass('open').removeClass('close').off('click');

        // 地雷の数を表示する
        cell.text(cell.attr('data-hint'));

        // 周りに地雷が1つでもあるときは処理を終える
        if('0' != cell.attr('data-hint')){
            return;
        }

        //	周を開く
        let c = cell.prevAll().length;
        let r = cell.parent('tr').prevAll().length;
        jQuery.each([r - 1, r, r + 1], function (i, r){
            if(r < 0) return;
            if(r >= rows) return;
            jQuery.each([c - 1, c , c + 1], function (i, c){
                if(c < 0) return;
                if(c >= cols) return;
                board.children('tr').eq(r).children('td.cell').eq(c).click();
            });
        });
    });

    //	右クリックされたときの処理
    board.find('td.cell.close').on('contextmenu', function (evt){
        let cell = $(this);
        
        //	CSSクラスのflagを加えたり、外したりする。
        cell.toggleClass('flag');

        // コンテキストメニュー(右クリックで現れるメニュー)を表示させないためにfalseを返す。
        return false;
    });
});

下に表示されないときはここをクリック(別ウィンドウが開きます)

ゲームを終了させる

マインスイーパーのゲーム終了条件は、1.地雷以外のマスを全て開いた(成功条件)、2.地雷を開いた(失敗条件)、のどちらかです。ゲームを終了させる処理は同じなので、関数を定義してそれを呼び出します。

minesweeper8.jsのソース

jQuery(function (){
    const rows = 10;	// マスの縦方向の数。とりあえず変更しないのでconstで宣言する。
    const cols = 10;
    const bombs = 15;	// 地雷の数
    
    const board = $('#board');

    let bomb = bombs;
    for(let r = 0 ; r < rows ; r ++){
        let row = $('<tr>').appendTo(board);
        for(let c = 0 ; c < cols ; c ++){
            let col = $('<td>').addClass('cell').addClass('close').appendTo(row);
            if(bomb -- > 0){
                col.addClass('bomb');  // 地雷を設置(CSSクラスを設定する)
            }
        }
    }

    // 地雷をランダムに置き換え
    board.children('tr').each(function (){
        $(this).children('td.cell').each(function (){
            let r = Math.floor(Math.random() * rows);
            let c = Math.floor(Math.random() * cols);
            let cell = board.children('tr').eq(r).children('td.cell').eq(c);
            if($(this).hasClass('bomb') != cell.hasClass('bomb')){
                $(this).toggleClass('bomb');
                cell.toggleClass('bomb');
            }
        });
    });

    //	周りにある地雷を数える
    for(let r = 0 ; r < rows ; r ++){
        let row = board.children('tr').eq(r)
        for(let c = 0 ; c < cols ; c ++){
            let cell = row.children('td.cell').eq(c);
            if(cell.hasClass('bomb')) continue;

            bomb = 0;
            jQuery.each([r - 1, r, r + 1], function (i, r){
                if(r < 0) return;
                if(r >= rows) return;
                jQuery.each([c - 1, c , c + 1], function (i, c){
                    if(c < 0) return;
                    if(c >= cols) return;
                    if(board.children('tr').eq(r).children('td.cell').eq(c).hasClass('bomb')){
                        bomb ++;
                    }
                });
            });
            cell.attr('data-hint', bomb);
            cell.addClass('hint' + bomb);
        }
    }

    //	マスがクリックされたときの動作
    board.find('td.cell.close').on('click', function (evt){
        let cell = $(this);

        if(cell.hasClass('flag')){
            return true;
        }

        cell.addClass('open').removeClass('close').off('click');

        //	地雷のないマスを全て開いたとき
        if($('td.cell.close').length == bombs){
            stopGame('Success');
        }

        //	地雷のあるマスを開いたとき
        if(cell.hasClass('bomb')){
            stopGame('The end');
        }

        // 地雷の数を表示する
        cell.text(cell.attr('data-hint'));

        // 周りに地雷が1つでもあるときは処理を終える
        if('0' != cell.attr('data-hint')){
            return;
        }

        //	周を開く
        let c = cell.prevAll().length;
        let r = cell.parent('tr').prevAll().length;
        jQuery.each([r - 1, r, r + 1], function (i, r){
            if(r < 0) return;
            if(r >= rows) return;
            jQuery.each([c - 1, c , c + 1], function (i, c){
                if(c < 0) return;
                if(c >= cols) return;
                board.children('tr').eq(r).children('td.cell').eq(c).click();
            });
        });
    });

    //	右クリックされたときの処理
    board.find('td.cell.close').on('contextmenu', function (evt){
        let cell = $(this);
        
        //	CSSクラスのflagを加えたり、外したりする。
        cell.toggleClass('flag');

        // コンテキストメニュー(右クリックで現れるメニュー)を表示させないためにfalseを返す。
        return false;
    });
});

//	ゲーム終了処理
function stopGame(message){
    $('td.cell.close').off('click contextmenu');
    alert(message);
}

下に表示されないときはここをクリック(別ウィンドウが開きます)

爆弾の残り数を表示させる

addClass('flag’)したときに一つ減らします。

minesweeper9.jsのソース

jQuery(function (){
    const rows = 10;	// マスの縦方向の数。とりあえず変更しないのでconstで宣言する。
    const cols = 10;
    const bombs = 15;	// 地雷の数
    
    const board = $('#board');
    const bomb_info = $('#bombs').val(bombs);	//	地雷の残り

    let bomb = bombs;
    for(let r = 0 ; r < rows ; r ++){
        let row = $('<tr>').appendTo(board);
        for(let c = 0 ; c < cols ; c ++){
            let col = $('<td>').addClass('cell').addClass('close').appendTo(row);
            if(bomb -- > 0){
                col.addClass('bomb');  // 地雷を設置(CSSクラスを設定する)
            }
        }
    }

    // 地雷をランダムに置き換え
    board.children('tr').each(function (){
        $(this).children('td.cell').each(function (){
            let r = Math.floor(Math.random() * rows);
            let c = Math.floor(Math.random() * cols);
            let cell = board.children('tr').eq(r).children('td.cell').eq(c);
            if($(this).hasClass('bomb') != cell.hasClass('bomb')){
                $(this).toggleClass('bomb');
                cell.toggleClass('bomb');
            }
        });
    });

    //	周りにある地雷を数える
    for(let r = 0 ; r < rows ; r ++){
        let row = board.children('tr').eq(r)
        for(let c = 0 ; c < cols ; c ++){
            let cell = row.children('td.cell').eq(c);
            if(cell.hasClass('bomb')) continue;

            bomb = 0;
            jQuery.each([r - 1, r, r + 1], function (i, r){
                if(r < 0) return;
                if(r >= rows) return;
                jQuery.each([c - 1, c , c + 1], function (i, c){
                    if(c < 0) return;
                    if(c >= cols) return;
                    if(board.children('tr').eq(r).children('td.cell').eq(c).hasClass('bomb')){
                        bomb ++;
                    }
                });
            });
            cell.attr('data-hint', bomb);
            cell.addClass('hint' + bomb);
        }
    }

    //	マスがクリックされたときの動作
    board.find('td.cell.close').on('click', function (evt){
        let cell = $(this);

        if(cell.hasClass('flag')){
            return true;
        }

        cell.addClass('open').removeClass('close').off('click');

        //	地雷のないマスを全て開いたとき
        if($('td.cell.close').length == bombs){
            alert('Success');
            stopGame();
        }

        //	地雷のあるマスを開いたとき
        if(cell.hasClass('bomb')){
            alert('The end');
            stopGame();
        }

        // 地雷の数を表示する
        cell.text(cell.attr('data-hint'));

        // 周りに地雷が1つでもあるときは処理を終える
        if('0' != cell.attr('data-hint')){
            return;
        }

        //	周を開く
        let c = cell.prevAll().length;
        let r = cell.parent('tr').prevAll().length;
        jQuery.each([r - 1, r, r + 1], function (i, r){
            if(r < 0) return;
            if(r >= rows) return;
            jQuery.each([c - 1, c , c + 1], function (i, c){
                if(c < 0) return;
                if(c >= cols) return;
                board.children('tr').eq(r).children('td.cell').eq(c).click();
            });
        });
    });

    //	右クリックされたときの処理
    board.find('td.cell.close').on('contextmenu', function (evt){
        let cell = $(this);
        
        cell.toggleClass('flag');

        //	CSSクラスのflagがあるとき(増えたとき)地雷の残りを減らす
        if(cell.hasClass('flag')){
            bomb_info.val(parseInt(bomb_info.val()) - 1);
        }else{
            bomb_info.val(parseInt(bomb_info.val()) + 1);
        }

        // コンテキストメニュー(右クリックで現れるメニュー)を表示させないためにfalseを返す。
        return false;
    });
});

//	ゲーム終了処理
function stopGame(){
    $('td.cell.close').off('click contextmenu');
}

下に表示されないときはここをクリック(別ウィンドウが開きます)

ダブルクリックで周りを開く

周りが0だったときに開くようにしてあった部分を改良して、すでに開かれているマスをダブルクリックしたとき、周りを開くようにします。

minesweeper10.jsのソース

jQuery(function (){
    const rows = 10;	// マスの縦方向の数。とりあえず変更しないのでconstで宣言する。
    const cols = 10;
    const bombs = 15;	// 地雷の数
    
    const board = $('#board');
    const bomb_info = $('#bombs').val(bombs);	//	地雷の残り

    let bomb = bombs;
    for(let r = 0 ; r < rows ; r ++){
        let row = $('<tr>').appendTo(board);
        for(let c = 0 ; c < cols ; c ++){
            let col = $('<td>').addClass('cell').addClass('close').appendTo(row);
            if(bomb -- > 0){
                col.addClass('bomb');  // 地雷を設置(CSSクラスを設定する)
            }
        }
    }

    // 地雷をランダムに置き換え
    board.children('tr').each(function (){
        $(this).children('td.cell').each(function (){
            let r = Math.floor(Math.random() * rows);
            let c = Math.floor(Math.random() * cols);
            let cell = board.children('tr').eq(r).children('td.cell').eq(c);
            if($(this).hasClass('bomb') != cell.hasClass('bomb')){
                $(this).toggleClass('bomb');
                cell.toggleClass('bomb');
            }
        });
    });

    //	周りにある地雷を数える
    for(let r = 0 ; r < rows ; r ++){
        let row = board.children('tr').eq(r)
        for(let c = 0 ; c < cols ; c ++){
            let cell = row.children('td.cell').eq(c);
            if(cell.hasClass('bomb')) continue;

            bomb = 0;
            jQuery.each([r - 1, r, r + 1], function (i, r){
                if(r < 0) return;
                if(r >= rows) return;
                jQuery.each([c - 1, c , c + 1], function (i, c){
                    if(c < 0) return;
                    if(c >= cols) return;
                    if(board.children('tr').eq(r).children('td.cell').eq(c).hasClass('bomb')){
                        bomb ++;
                    }
                });
            });
            cell.attr('data-hint', bomb);
            cell.addClass('hint' + bomb);
        }
    }

    //	マスがクリックされたときの動作
    board.find('td.cell.close').on('click', function (evt){
        let cell = $(this);

        if(cell.hasClass('flag')){
            return true;
        }

        cell.addClass('open').removeClass('close').off('click')

        //	地雷のないマスを全て開いたとき
        if($('td.cell.close').length == bombs){
            alert('Success');
            stopGame();
        }

        //	地雷のあるマスを開いたとき
        if(cell.hasClass('bomb')){
            alert('The end');
            stopGame();
        }

        // 地雷の数を表示する
        cell.text(cell.attr('data-hint'));

        //	開いたマスをダブルクリックしたとき
        cell.on('dblclick', function (evt){
            let cell = $(this);
            //	クリックされたマスの位置を取得
            let c = cell.prevAll().length;
            let r = cell.parent('tr').prevAll().length;

            //	周りの旗を数える
            let bomb = 0;
            jQuery.each([r - 1, r, r + 1], function (i, r){
                if(r < 0) return;
                if(r >= rows) return;
                jQuery.each([c - 1, c , c + 1], function (i, c){
                    if(c < 0) return;
                    if(c >= cols) return;
                    if(board.children('tr').eq(r).children('td.cell').eq(c).hasClass('flag')){
                        bomb ++;
                    }
                });
            });

            //	置かれた旗とヒントの数が一致しないときは終了
            if(bomb != parseInt(cell.attr('data-hint'))){
                return false;
            }

            //	周りを開く
            jQuery.each([r - 1, r, r + 1], function (i, r){
                if(r < 0) return;
                if(r >= rows) return;
                jQuery.each([c - 1, c , c + 1], function (i, c){
                    if(c < 0) return;
                    if(c >= cols) return;
                    board.children('tr').eq(r).children('td.cell').eq(c).click();
                });
            });
            return false;
        });

        // 自動で周りを開く
        cell.dblclick();

        // 上記の処理は cell.text(...).on('dblclick', function (){...}).dblclick() とメソッドチェーンで書くこともできる。
    });

    //	右クリックされたときの処理
    board.find('td.cell.close').on('contextmenu', function (evt){
        let cell = $(this);
        
        cell.toggleClass('flag');

        //	CSSクラスのflagがあるとき(増えたとき)地雷の残りを減らす
        if(cell.hasClass('flag')){
            bomb_info.val(parseInt(bomb_info.val()) - 1);
        }else{
            bomb_info.val(parseInt(bomb_info.val()) + 1);
        }

        // コンテキストメニュー(右クリックで現れるメニュー)を表示させないためにfalseを返す。
        return false;
    });

});

//	ゲーム終了処理
function stopGame(){
    $('td.cell.close').off('click contextmenu');
}

下に表示されないときはここをクリック(別ウィンドウが開きます)

経過時間を表示する

次回に続く。