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

昔のWindowsには標準でインストールされていたマインスイーパーをJavascriptでつくります。

必要なもの

jQuery

HTMLとCSSでゲーム画面をデザインする

マインスイーパーのクリックして開いていくマスをHTMLのtableでつくります。地雷とかヒントの数字はcssのクラスで設定するようにし、Javascriptではそれらを設定・削除することでゲームを実装します。

minesweeper1.htmlのソース

<!DOCTYPE HTML>
<html>
<head>
<link rel='stylesheet' href="minesweeper1.css">
<body>
<div id='game'>
    Bomb: <input id='bombs' value='地雷残り'>
    <table id='board'>
        <tr>
            <td class='cell close'></td>
            <td class='cell open'></td>
            <td class='cell close flag'></td>
        </tr>
        <tr>
            <td class='cell open bomb'></td>
            <td class='cell open hint0'></td>
            <td class='cell open hint1'>1</td>
        </tr>
        <tr>
            <td class='cell open hint2'>2</td>
            <td class='cell open hint3'>3</td>
            <td class='cell open hint4'>4</td>
        </tr>
    </table>
</div>
</body>
</html>

minesweeper.cssのソース

#board {
    border-spacing: 1px;	/* マス間の線 */
    background-color: black;	/* マス間の線の色 */
    border: 8px #ccc ridge;
    border-radius: 4px;
}

#board td.cell {
    width: 20px;	/* マスの幅 */
    height: 20px;	/* マスの高さ */
    overflow: hidden;
    background-color: #ccc;
    padding: 0;
    font-size: 13px;	/* ヒントの数字の大きさ。マスの大きさを超えないようにする。 */
    font-weight: bold;
    text-align: center;	/* 文字の位置 */
    vertical-align: middle;
}

#board td.close {
    border: 4px #ccc outset;	/* outsetでマスが盛り上がったように見せる */
}
/* 地雷があることを示す旗 */
#board td.flag {
    background-color: yellow;
    /*	画像が用意できるとき
    background-image: url('flag.png');
    */
}

#board td.open {
    border: 4px #ccc solid;	/* 開いたときにもマスの大きさが変更されないように背景と同色の枠をつくる */
}
/* 地雷の表示 */
#board td.open.bomb {
    background-color: red;
    /*	画像が用意できるとき
    background-image: url('bomb.png');
    */
    border-color: red;	/* 枠線の幅を変更する必要がないのでborder-colorで指定する */
}
/* ヒントの数字 */
#board td.open.hint0 {
    background-color: blue;
    border-color: blue;
    color: blue;
}
#board td.open.hint1 {
    color: blue;
}
#board td.open.hint2 {
    color: green;
}
#board td.open.hint3 {
    color: #dc143c;
}

/* 地雷の残りを表示するテキストボックス */
#bombs {
    width: 3em;
    text-align: right;
}

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

Javascriptでマスをつくる

たくさんのマスをHTMLでつくるのは面倒であるし、数の変更が容易にできないので、Javascriptを使う。
HTMLの要素を簡単に操作するためにはjQueryを使うとよい。

minesweeper2.htmlのソース

<!DOCTYPE HTML>
<html>
<head>
<link rel='stylesheet' href="minesweeper1.css">
<!-- jQueryの読込 -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<!-- マインスイーパーのプログラム本体 -->
<script type="text/javascript" src="minesweeper2.js"></script>
<body>
<div id='game'>
    Bomb: <input id='bombs' value='地雷残り'>
    <!-- HTMLの要素をjQuery側から見つけやすくするために、id属性を設定しておく -->
    <table id='board'>
    </table>
</div>
</body>
</html>

minesweeper2.jsのソース

/* HTMLの読込が終了した後でブラウザーが実行するために jQueryを使用している	*/
jQuery(function (){
    const rows = 10;	// マスの縦方向の数。とりあえず変更しないのでconstで宣言する。
    const cols = 10;
    const bombs = 15;	// 地雷の数
    
    const board = $('#board');

    for(let r = 0 ; r < rows ; r ++){
        // jQueryでのHTML要素の追加
        // $('<tr>')でtr要素がつくられる。もし$('tr')としてしまうとtr要素を選択することになるので<>は重要
        // $('<tr>')でつくられたものを.appendToメソッドでboard(つまりtable)に追加する。
        // jQueryでは通常メソッドの返り値はそれ自身になっているので、メソッドを順番に適用していくことができる(メソッドチェーンという)
        let row = $('<tr>').appendTo(board);
        for(let c = 0 ; c < cols ; c ++){
            // $('<td>')で要素をつくり、addClassでそれにCSSクラスを追加し、appendToでそれをrow(つまりtr)に追加する。
            let col = $('<td>').addClass('cell').addClass('close').appendTo(row);
        }
    }
});

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

クリックでマスを開くようにする

td要素にクリックされたときの動作を設定する(クリックイベントリスナーを設定する)。

minesweeper3.jsのソース

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

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

    //	マスがクリックされたときの動作
    board.find('td.cell.close').on('click', function (evt){
        // CSSクラスのopenを追加し、closeを外して、クリックイベントの監視をやめる
        $(this).addClass('open').removeClass('close').off('click');
    });
});

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

地雷を配置する

一定数の地雷の設置は、1.順番に決まった数を設置し、2.ランダムに入れ替え、という手順で行います。

minesweeper4.jsのソース

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

    let bomb = bombs; // 地雷の総数(bombs)を変数bombにいれる
    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);

            // bomb --でbombの数が一つ減る。
            // bombに15が入っていたとき、15 > 0が評価されてから14になる。
            // -- bombとすると14になってから14 > 0と評価され、実際に設置される地雷の数が変わるので注意する。
            if(bomb -- > 0){
                col.addClass('bomb');  // 地雷を設置(CSSクラスを設定する)
            }
        }
    }

    // 地雷をランダムに置き換え
        
    // table要素(board)のtr子要素(children('tr'))それぞれについて(each)、function()を適用する
    // このとき、eachに渡される関数内ではtrがthisとして参照できる。
    board.children('tr').each(function (){
        
        // 同じように、tr要素(this)のクラスcellがついたtd子要素(children('td.cell'))それぞれについて(each)、function()を適用する
        $(this).children('td.cell').each(function (){

            // 乱数を使って入れ替え対象を取得する
            let r = Math.floor(Math.random() * rows);
            let c = Math.floor(Math.random() * cols);
            // r番目のtr要素のc番目のtd要素を取得
            let cell = board.children('tr').eq(r).children('td.cell').eq(c);

            // CSSクラスのbombの設定が異なれば、入れ替える
            if($(this).hasClass('bomb') != cell.hasClass('bomb')){
                $(this).toggleClass('bomb');	// toggleClassは指定されたCSSクラスの有無を逆にする
                cell.toggleClass('bomb');
            }
        });
    });

    //	マスがクリックされたときの動作
    board.find('td.cell.close').on('click', function (evt){
        // CSSクラスのopenを追加し、closeを外して、クリックイベントの監視をやめる
        $(this).addClass('open').removeClass('close').off('click');
    });
});

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

周りにある地雷の数を設定する

それぞれのマスの周りにある地雷を数えて、td要素にその値を設定します。HTMLの要素にはカスタム属性というのが設定でき、data-で始まる名前をつけるルールになっています。ここではdata-hintに地雷数を設定します。

minesweeper5.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);
            // 地雷の置かれているマスなら、ヒントの設定は必要ないのでcontinueでforループを次に進める。
            if(cell.hasClass('bomb')) continue;

            bomb = 0;  // 周りの地雷の数
            // [r - 1, r, r + 1]という配列を作って、調べる対象の行を順番に(jQuery.each)調べる。
            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;
                    // 周りのマス(r,c)に地雷があれば、地雷の数を増やす。
                    if(board.children('tr').eq(r).children('td.cell').eq(c).hasClass('bomb')){
                        bomb ++;  // ここでは++ bombでもよい。
                    }
                });
            });
            // 地雷の数をカスタム属性(data-hint)にいれる。
            cell.attr('data-hint', bomb);
            // 地雷の数に応じたCSSクラス(hint0, hint1など)を設定する
            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'));
    });
});

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

自動的に周りを開く

次回に続く。