5
14

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 5 years have passed since last update.

JavaScriptでマインスイーパをつくる

Last updated at Posted at 2019-10-11

動機

暇なときになんとなくマインスイーパを作ってみたら楽しかったので、記録として。
(ゲーム終了判定などは割愛してます。。)

マインスイーパのルールと処理

遊び方はここに分かりやすく載っています。
http://dotpico.com/mine/ja/rule.php

左クリックの処理

  • クリックすると、そのセルを開くことができる
  • そのセルに爆弾が入っていたら、その時点でゲームオーバー
  • そのセルに爆弾が入っていなければ、周囲に存在する爆弾の数が表示される
  • そのセルに爆弾が入っておらず、周囲に1つも爆弾が存在しないとき、周囲のセルが自動的に開かれる

右クリックの処理

  • 1回右クリックすると、そのセルに「フラグ」をつけることができる
  • 2回右クリックすると、そのセルに「?」をつけることができる
  • 3回右クリックすると、元に戻る

ダブルクリックの処理

  • 開いたセルに書いてある数字と、その周囲の「フラグ」の数が一致しているとき、そのセルをダブルクリックすると、「周囲の開かれていないセル」を一度に開くことができる

実装プログラム

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <style>
        td {
            padding: 0px;
            width: 40px;
            height: 40px !important;
            text-align: center;
            border: 1px solid rgb(60, 60, 60);
        }

        /** 未開封のクラス */
        .closed {
            background-color: lightgray;
            color: black;
            cursor: pointer;

        }

        /** 開封して爆弾だったときのクラス */
        .bombed {
            background: red;
            color: white;
            cursor: auto;
        }

        /** 開封して爆弾ではなかったときのクラス */
        .opened {
            background: white;
            color: black;
            cursor: auto;
        }
    </style>
</head>

<body>
    <div id="msg"></div>
    <table>
        <tbody id="target">
            <!-- 中身はJavaScriptでつくる -->
        </tbody>
    </table>

    <script src="main.js" type="text/javascript"></script>
</body>

</html>
main.js
'use strict';

//===================================
// マインスイーパ用のセルクラス
// セルはこのクラスのインスタンスとする
//===================================
class MSCell extends HTMLTableCellElement {

    //-----------------------------------
    // コンストラクタ
    //-----------------------------------
    constructor() {
        super();

        // イベント登録
        this.addEventListener('click', this.clickFunc);
        this.addEventListener('contextmenu', this.clickRightFunc);
        this.addEventListener('dblclick', this.clickDblFunc);
    }

    //-----------------------------------
    // マインスイーパ初期設定
    // x座業、y座標、爆弾かどうかをパラメータにとる
    //-----------------------------------
    init(x, y, bombFlg) {

        // 開封フラグ(未開封のときfalse/開封済みのときtrue)
        this.openedFlg = false;
        // x座標
        this.x = x;
        // y座標
        this.y = y;
        // 爆弾フラグ(爆弾のときtrue/爆弾でなければfalse)
        this.bombFlg = bombFlg;
        // 見た目のクラス
        this.classList.add('closed')

    }

    //-----------------------------------
    // 周辺セルを設定する
    // 周辺セルと、周辺セルの合計爆弾数を設定する
    //-----------------------------------
    setArounds(arounds) {
        // 周辺セル
        this.arounds = arounds;
        // 周辺セルの爆弾数
        this.aroundBombCount = this.arounds.filter(around => around.bombFlg).length;
    }

    //-----------------------------------
    // そのセルの中身を表示する
    //-----------------------------------
    show() {
        if (this.bombFlg) {
            // 爆弾のときは「爆」
            this.textContent = '';

            // 見た目の変更
            this.classList.remove('closed');
            this.classList.add('bombed');
        } else {
            // 爆弾ではないとき
            if (this.aroundBombCount > 0) {
                // 周辺の爆弾数が1個以上のときは数を表示
                this.textContent = this.aroundBombCount;
            }

            // 見た目の変更
            this.classList.remove('closed');
            this.classList.add('opened');
        }
    }

    //-----------------------------------
    // セルを左クリックしたときの関数
    //-----------------------------------
    clickFunc() {

        if (this.openedFlg) {
            // 開封済みのときは何もしない
            return;
        }

        if (this.textContent === '' || this.textContent === '') {
            // 「旗」や「?」がついてるときも何もしない
            return;
        }

        // 開封済みにする
        this.openedFlg = true;

        // このセルを開く
        this.show();

        if (this.bombFlg) {
            // このセルが爆弾のときはゲームオーバーなので全セルを開く
            msCells.forEach(button => button.show());
        } else {
            // このセルが爆弾でないとき
            if (this.aroundBombCount === 0) {
                // 周囲に爆弾が無いときは周囲のセルを全部開く
                this.arounds.forEach(around => around.clickFunc());
            }
        }
    }

    //-----------------------------------
    // セルを右クリックしたときの関数
    //-----------------------------------
    clickRightFunc(e) {

        // 右クリックメニュー禁止
        e.preventDefault();

        if (this.openedFlg) {
            // 既に開かれている場合は何もしない
            return;
        }

        if (this.textContent === '') {
            // 旗を表示
            this.textContent = '';
        } else if (this.textContent === '') {
            // ?を表示
            this.textContent = '';
        } else if (this.textContent === '') {
            // 元に戻す
            this.textContent = '';
        }

    }

    //-----------------------------------
    // セルをダブルクリックしたときの関数
    //-----------------------------------
    clickDblFunc() {
        if (!this.openedFlg) {
            // 既に開かれている場合は何もしない
            return;
        }

        // 周囲の旗の数を取得
        let flgCount = this.arounds.filter(around => around.textContent === '').length;

        // 周囲の旗の数と、クリックしたセルに表示されている爆弾数が一致していれば
        // 周囲のセルをすべて開く
        if (this.aroundBombCount === flgCount) {
            this.arounds.forEach(around => around.clickFunc());
        }
    }

}

//===================================
// カスタム要素の定義
//===================================
customElements.define('ms-td', MSCell, { extends: 'td' });

//===================================
// 全セルを格納しておく変数
//===================================
let msCells = [];

//===================================
// ゲーム初期化用関数
//===================================
let initGame = function (xSize, ySize) {

    // ボタン配置
    for (let y = 0; y < ySize; y++) {
        let tr = document.createElement('tr');
        for (let x = 0; x < xSize; x++) {
            // セルを作る
            let msCell = document.createElement('td', { is: 'ms-td' });
            // セルの初期化
            msCell.init(x, y, Math.random() * 100 < 10);
            // セルをtrにいれておく
            tr.appendChild(msCell);
            // msCellsにも入れておく
            msCells.push(msCell);
        }
        document.getElementById('target').appendChild(tr);
    }

    // aroundsの設定
    msCells.forEach(msCell => {

        // 周囲8マスを取得
        let arounds = msCells.filter(otherCell => {

            if (msCell === otherCell) {
                return false;
            }

            let xArea = [msCell.x - 1, msCell.x, msCell.x + 1];
            let yArea = [msCell.y - 1, msCell.y, msCell.y + 1];

            if (xArea.indexOf(otherCell.x) >= 0) {
                if (yArea.indexOf(otherCell.y) >= 0) {
                    return true;
                }
            }
            return false;
        });

        // 周囲8マスをaroundsとして設定
        msCell.setArounds(arounds);
    });

}

//===================================
// ゲーム初期化
//===================================
initGame(15, 15);

実装のポイント

セルの作り方

JavaScriptのcreateElementでtableタグの中身をつくっていますが、
その中のtdタグは、独自に定義したカスタム要素「MsCell」です。(Msはマインスイーパの略)
MsCellはHTMLTableCellElementを継承しています。

そのため、見た目は普通のtdタグと同じですが、
マインスイーパに必要なopenedFlgなどのプロパティと、clickFuncなどの関数を持っています。

セルに対する動作

ポイントは二つです。

 開いたセルに爆弾が入っていなければ、そのセルの周囲に存在する爆弾の数が表示される

つまり、セルはその周囲のセルの中身を知っている必要があります。
そこで、セルにaroundsというプロパティを持たせ、周囲の8セルを参照させておくことにしました。

 開いたセルに爆弾が入っておらず、そのセルの周囲に1つも爆弾が存在しないとき、周囲のセルが自動的に開かれる

クリックされたセルの周囲の爆弾数で0であれば、そのセルのaroundsもすべて再帰的にクリックしていきます。
また、再帰処理を抜けるための条件として、openedFlg(未開封:false/開封済:true)を持たせています。

参考文献

Custom Elements v1で独自のHTML要素を定義する

5
14
0

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
5
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?