HTML5遊戲開發-掃雷及其算法研究

呂蒙曰:士隔三月【1】,當刮目相看。所以,在下在這三月中發奮圖強,花了約莫8節信息課的時間研究掃雷。嗚呼,由於在下才能尚且不足,所以也就只能勉強打過中級難度的吧。不過,一邊玩的同時,我還一邊對掃雷這個遊戲的製做方法構思了一下。所以說,本文中的算法完全是憑藉自己對掃雷遊戲規則的總結而自行研發出來的,倘若和MS的掃雷玩法有些出入,還望各位看官見諒。

【1】出自《孫權勸學》,原文爲“士別三日”,由於在下這三個月來都不曾發表博客,所以引申到“士隔三月”,各位看官休怪休怪

以下是本次開發的遊戲截圖:

測試地址:http://yuehaowang.github.io/demo/minesweeper/

算法研究

掃雷雖然屬於遊戲開發初級學者研究的範疇,但是由於我是那種好高騖遠的傢伙,所以,雖然接觸遊戲開發約2年了,可是到現在纔來研究掃雷,實在是慚愧慚愧。
首先,我想我們需要對掃雷遊戲的算法進行一系列的研究。

佈雷算法

這個算法嘛,顧名思義,就是說如何在場景中佈雷。這個算法其實很簡單,就是單一地使用隨機取位來完成。不過值得注意的是,可能有些朋友和我一樣,一開始認爲是先把數字標好再去佈雷,其實應該是先佈雷,再根據佈雷情況給每個格子標數字。

智能幫掃算法

其實這個所謂的“智能幫掃算法”是我隨便給這個算法取的一個名字,可能俗氣了點,看官莫怪~什麼是“智能幫掃算法”呢?首先我來對其下個定義:

當玩家掃開到某個方塊後,如果四周沒有雷,或者這個方塊四周的雷全部被掃除了,那麼這時候就需要幫助玩家把四周的方塊全部掃出。這樣的算法叫做智能幫掃算法

如果實現了這個算法,那麼點擊一個方塊,掃開一大片的效果就可以實現了。某些看官可能不理解爲什麼,我可以在這裏羅嗦一下:根據定義我們可以看出,這個算法只是幫助掃出四周的方塊,不過這是一個鏈環的過程,畢竟四周的方塊被掃開後,如果這些方塊滿足執行幫掃算法的條件,那麼幫掃算法就會在被算法掃開的方塊上生效,如此漸進下去,知道被掃開的方塊不滿足條件爲止。如果用流程圖表示就是:

Created with Raphaël 2.1.0開始掃開一個雷四周被標記的雷數等於相應數目掃開四周未被標記的雷結束yesno

如果流程圖還不能理解的話,就舉個Y某我吃巧克力的例子吧。Y某有一天收到M君送來的一盒巧克力,我首先拆開盒子掰下一塊巧克力,放進嘴裏,發現是苦瓜味的,於是我就下定決心,如果我再吃到苦瓜味的,就把周圍的8塊都吃了。不想我接下來吃到一塊就是苦瓜味的,沒有辦法,只有把周圍八塊都吃了,結果剛吃到其中一塊,發現又是苦瓜味的,於是繼續吃……於是就達到了吃開一片的效果。這裏的吃到苦瓜味的巧克力就相當於“四周被標記的雷數等於相應數目”。這個故事的結局和掃雷不同,由於遊戲中的雷數>=1,所以無論如何都會因遇到雷停下來,可是萬惡的M君送來的巧克力盡然全是苦瓜味的(T_T)

要實現這個算法,說難也不算難。只需把對算法的定義翻譯成代碼即可。

實現過程

既然是遊戲,所以要實現的不僅是算法,還有就是用戶界面之類的。

由於純canvas做遊戲很麻煩,所以我這次就直接使用lufylegend遊戲引擎實現界面。
引擎地址http://lufylegend.com/lufylegend
API地址http://lufylegend.com/lufylegend/api
本文中可能多次出現某些類和函數,或者某些函數和類很重要,所以我把它們的API地址放在下面,這樣以來可以方便大家查找:

  1. LExtends:http://lufylegend.com/api/zh_CN/out/classes/%E5%85%A8%E5%B1%80%E5%87%BD%E6%95%B0.html#method_LExtends
  2. LLoadManage:http://lufylegend.com/api/zh_CN/out/classes/LLoadManage.html
  3. LInit:http://lufylegend.com/api/zh_CN/out/classes/%E5%85%A8%E5%B1%80%E5%87%BD%E6%95%B0.html#method_LInit
  4. LSprite:http://lufylegend.com/api/zh_CN/out/classes/LSprite.html
  5. LBitmap:http://lufylegend.com/api/zh_CN/out/classes/LBitmap.html
  6. LBitmapData:http://lufylegend.com/api/zh_CN/out/classes/LBitmapData.html
  7. LTextField:http://lufylegend.com/api/zh_CN/out/classes/LTextField.html
  8. LButton:http://lufylegend.com/api/zh_CN/out/classes/LButton.html
  9. LDropShadowFilter:http://lufylegend.com/api/zh_CN/out/classes/LDropShadowFilter.html

在下面的講解中,我只講一些關鍵的地方,其他地方就交給看官慢慢啃吧。文末會給出完整的代碼下載。

HTML中的代碼

HTML5遊戲嘛,肯定要有html代碼:

<!DOCTYPE html>
<html>
<head>
    <title>Minesweeper</title>
    <meta charset="utf-8" />
    <script type="text/javascript" src="./lib/lufylegend-1.9.9.simple.min.js"></script>
    <script type="text/javascript" src="./lib/lufylegend.LoadingSample1-0.1.0.min.js"></script>
    <script type="text/javascript" src="./js/Main.js"></script>
</head>
<body oncontextmenu="return false;">
    <div id="mylegend"></div>
</body>
</html>

Main.js

LInit(1000 / 30, "mylegend", 540, 640, main);

var dataList = {};
var stage;
var blockXNum = blockYNum = 10, mineNum = 12;

function main () {
    var loadData = [
        {path : "./js/InfoLayer.js"},
        {path : "./js/ButtonTemplate.js"},
        {path : "./js/MineLayer.js"},
        {path : "./js/StageLayer.js"},
        {name : "bg", path : "./images/bg.jpg"},
        {name : "button_sheet", path : "./images/button_sheet.png"},
        {name : "face_happy", path : "./images/face_happy.png"},
        {name : "face_sad", path : "./images/face_sad.png"},
        {name : "face_smile", path : "./images/face_smile.png"},
        {name : "face_surprise", path : "./images/face_surprise.png"},
        {name : "flag", path : "./images/flag.png"},
        {name : "mine", path : "./images/mine.png"}
    ];

    var loadingLayer = new LoadingSample1();
    addChild(loadingLayer);

    LLoadManage.load(
        loadData,
        function (p) {
            loadingLayer.setProgress(p);
        },
        function (r) {
            dataList = r;

            loadingLayer.remove();

            initGame();
        }
    );
}

function initGame () {
    stage = new StageLayer();
    addChild(stage);
}

在Main.js中,我們初始化了界面,加載了資源,以及加入舞臺類(StageLayer),主要用的是lufylegend中的一些API,不熟悉的同學可以參考前面給出的API文檔。不過得注意幾個變量:

  1. blockXNum:代表遊戲中的方塊每行有幾個
  2. blockYNum:代表遊戲中的方塊每列有幾個
  3. mineNum:雷數

StageLayer.js

這個類也比較簡單,先把代碼貼出來:

function StageLayer () {
    var s = this;
    LExtends(s, LSprite, []);

    var bgBmp = new LBitmap(new LBitmapData(dataList["bg"]));
    bgBmp.scaleX = LGlobal.width / bgBmp.getWidth();
    bgBmp.scaleY = LGlobal.height / bgBmp.getHeight();
    s.addChild(bgBmp);

    s.infoLayer = new InfoLayer();
    s.infoLayer.x = (LGlobal.width - s.infoLayer.getWidth()) / 2;
    s.infoLayer.y = 40;
    s.addChild(s.infoLayer);

    s.mineLayer = null;

    s.createMineLayer();
}
StageLayer.prototype.createMineLayer = function () {
    var s = this;

    if (s.mineLayer) {
        s.mineLayer.remove();
    }

    s.mineLayer = new MineLayer();
    s.mineLayer.x = (LGlobal.width - s.mineLayer.getWidth()) / 2;
    s.mineLayer.y = s.infoLayer.y + s.infoLayer.getHeight() + 30;
    s.addChild(s.mineLayer);
};

這個類是舞臺類,既然是舞臺,那裝些顯示對象就是他的義務囉~看了前面的截圖大家可以發現,這個遊戲中主要由“剩餘雷數”,“帶有face的按鈕”,“用去的時間”,“掃雷區”構成。這些部件我大致分了一下類:“剩餘雷數”,“帶有face的按鈕”,“用去的時間”屬於信息層,“掃雷區”屬於地雷層。這樣一來就又誕生了兩個類:InfoLayer,MineLayer。

InfoLayer.js

這個類正如上面所說,用於放置“剩餘雷數”,“帶有face的按鈕”,“用去的時間”這些部件。具體代碼如下:

function InfoLayer () {
    var s = this;
    LExtends(s, LSprite, []);

    s.mineLeftNumTxt = null;
    s.button = null;
    s.timeUsedTxt = null;

    s.timeUsedNum = 0;
    s.preTime = 0;
    s.mineLeftNum = mineNum;
    s.isStart = false;

    s.addMineLeftNumLayer();
    s.addButton();
    s.addTimeUsedLayer();

    s.addEventListener(LEvent.ENTER_FRAME, function () {
        if (!s.isStart) {
            return;
        }

        s.refreshTimeUsedNumTxt();
    });
}

InfoLayer.prototype.addMineLeftNumLayer = function () {
    var s = this;

    var mineLeftNumLayer = new LSprite();
    s.addChild(mineLeftNumLayer);

    s.mineLeftNumTxt = new LTextField();
    s.mineLeftNumTxt.text = 10000000;
    s.mineLeftNumTxt.color = "white";
    s.mineLeftNumTxt.size = 30;
    mineLeftNumLayer.addChild(s.mineLeftNumTxt);

    mineLeftNumLayer.graphics.drawRoundRect(
        2, "white",
        [
            -5, -5,
            s.mineLeftNumTxt.getWidth() + 10,
            s.mineLeftNumTxt.getHeight() + 10,
            3
        ],
        true, "black"
    );

    s.mineLeftNumTxt.text = s.mineLeftNum;
};

InfoLayer.prototype.addButton = function () {
    var s = this, btnBmp = new LBitmap(new LBitmapData(dataList["face_smile"]));

    s.button = new ButtonTemplate(btnBmp, 1.2);
    s.button.x = s.getWidth() + 50;
    s.button.y = -15;
    s.addChild(s.button);
    s.button.addEventListener(LMouseEvent.MOUSE_UP, function () {
        s.timeUsedNum = 0;
        s.preTime = new Date().getTime();
        s.mineLeftNum = mineNum;
        s.isStart = false;

        s.parent.createMineLayer();

        s.refreshMineLeftNumTxt();
        s.refreshTimeUsedNumTxt();
        s.changeFace("smile");
    })
};

InfoLayer.prototype.addTimeUsedLayer = function () {
    var s = this;

    var timeUsedLayer = new LSprite();
    timeUsedLayer.x = s.getWidth() + 50;
    s.addChild(timeUsedLayer);

    s.timeUsedTxt = new LTextField();
    s.timeUsedTxt.text = 10000000;
    s.timeUsedTxt.color = "white";
    s.timeUsedTxt.size = 30;
    timeUsedLayer.addChild(s.timeUsedTxt);

    timeUsedLayer.graphics.drawRoundRect(
        2, "white",
        [
            -5, -5,
            s.timeUsedTxt.getWidth() + 10,
            s.timeUsedTxt.getHeight() + 10,
            3
        ],
        true, "black"
    );

    s.timeUsedTxt.text = s.timeUsedNum;
};

InfoLayer.prototype.changeFace = function (name) {
    this.button.setContent(new LBitmap(new LBitmapData(dataList["face_" + name])));
};

InfoLayer.prototype.refreshMineLeftNumTxt = function () {
    this.mineLeftNumTxt.text = this.mineLeftNum;
};

InfoLayer.prototype.refreshTimeUsedNumTxt = function (e) {
    var s = this, nowTime = new Date().getTime();

    s.timeUsedNum += (nowTime - s.preTime) / 1000;

    s.preTime = nowTime;

    s.timeUsedTxt.text = parseInt(s.timeUsedNum);
};

玩過windows xp掃雷的都知道,在“掃雷區”中點擊一下鼠標,那按鈕上的face就會改變,所以爲了在MineLayer和InfoLayer進行交互,我在InfoLayer上加了一些用於改變剩餘雷數以及更改按鈕上face的函數(refreshMineLeftNumTxt和changeFace)。

這個類中用到了ButtonTemplate這個類,這個類是一個按鈕類,在遊戲中我們就用到了一種按鈕,所以就用ButtonTemplate把這些按鈕的功能統一起來。

ButtonTemplate.js

這個按鈕類出現在前面講的InfoLayer中,還會出現在下面要講的MineLayer中,作爲方塊。
我把按鈕主要分爲兩個部分:按鈕背景,按鈕內容。
我們在按鈕中要用到的功能主要有如下幾個:

  1. 設置按鈕上的內容,如上面說的face
  2. 在雷被掃除後,按鈕背景會消失,所以需要提供刪除按鈕背景的方法
  3. 刪除按鈕上的內容
  4. 掃雷中,如果鼠標的左右兩鍵同時按下,會把四周的方塊突出一下,這裏我用到的是把這些方塊的按鈕背景設置爲禁用(STATE_DISABLE)的狀態,所以需要用到設置按鈕狀態的功能。其實設置爲禁用狀態是種dirty way,但是lufylegend的LButton中就只提供了ENABLE和DISABLE這兩種狀態,所以,就這樣用好了……

Ok,該上代碼了:

function ButtonTemplate (img, btnBmpScale) {
    var s = this;
    LExtends(s, LSprite, []);

    var btnImg = dataList["button_sheet"];
    var normalBmp = new LBitmap(new LBitmapData(btnImg, 0, 0, 48, 48));
    var overBmp = new LBitmap(new LBitmapData(btnImg, 0, 48, 48, 48));
    var downBmp = new LBitmap(new LBitmapData(btnImg, 0, 96, 48, 48));

    s.button = new LButton(normalBmp, overBmp, downBmp.clone(), downBmp.clone());
    s.button.scaleX = s.button.scaleY = btnBmpScale || 1;
    s.button.staticMode = true;
    s.addChild(s.button);

    s.content = null;

    if (typeof img == UNDEFINED || !img) {
        return;
    }

    s.setContent(img)
}

ButtonTemplate.prototype.setContent = function(content) {
    var s = this;

    s.removeContent();

    s.content = content;
    s.content.x = (s.button.getWidth() - s.content.getWidth()) / 2;
    s.content.y = (s.button.getHeight() - s.content.getHeight()) / 2;
    s.addChild(s.content);
};

ButtonTemplate.prototype.removeContent = function() {
    var s = this;

    if (s.content) {
        s.content.remove();

        s.content = null;
    }
};

ButtonTemplate.prototype.removeButton = function() {
    var s = this;

    if (s.button) {
        s.button.remove();
    }
};

ButtonTemplate.prototype.setIntoNormalState = function () {
    this.button.setState(LButton.STATE_ENABLE);
};

ButtonTemplate.prototype.setIntoOverState = function () {
    this.button.setState(LButton.STATE_DISABLE);
};

上面說的按鈕背景就是button屬性,內容就是content。

MineLayer.js

這個類是非常重要,所以需要好好的解釋其中的一些代碼。先看構造器:

function MineLayer () {
    var s = this;
    LExtends(s, LSprite, []);

    s.map = new Array();
    s.waitingTime = 1;
    s.startTimer = false;
    s.timerIndex = 0;
    s.onUpCallback = null;
    s.preMouseButton = null;
    s.doubleDown = false;
    s.completeNum = 0;

    s.create();
    s.addEventListener(LEvent.ENTER_FRAME, s.loop);
}

介紹一下其中的屬性,

  • map:一個數組,用於存放佈雷後的數據,-1表示此位置是雷,>-1表示該位置附近有多少雷,例如:
[
[-1, 2, 1],
[1, 2, -1],
[0, 1, 1]
]
  • waitingTime:這個屬性用於實現鼠標左右兩鍵同時按下的事件
  • timeIndex:同上
  • startTimer:同上
  • onUpCallBack:鼠標鬆開後執行的函數。這個函數在鼠標按下時設定
  • preMouseButton:同waitingTime
  • doubleDown:判斷是否鼠標左右兩鍵同時按下
  • completeNum:標記正確的雷的數量

接下來來看看create函數:

MineLayer.prototype.create = function () {
    var s = this, positionList = new Array();

    for (var i = 0; i < blockYNum; i++) {
        var row = new Array();
        s.map.push(row);

        for (var j = 0; j < blockXNum; j++) {
            var btn = new ButtonTemplate();
            btn.x = j * 48;
            btn.y = i * 48;
            btn.positionInMap = {x : j, y : i};
            btn.isFlag = false;
            btn.isSwept = false;
            s.addChild(btn);
            btn.addEventListener(LMouseEvent.MOUSE_DOWN, function (e) {
                s.onDown(e.currentTarget, e.button);
            });
            btn.addEventListener(LMouseEvent.MOUSE_UP, function (e) {
                s.onUp(e.currentTarget, e.button);
            });

            row.push(0);
            positionList.push({x : j, y : i});
        }
    }

    for (var k = 0; k < mineNum; k++) {
        var mineIndex = Math.floor(Math.random() * positionList.length),
        o = positionList[mineIndex];

        s.map[o.y][o.x] = -1;

        positionList.splice(mineIndex, 1);
    }

    for (var m = 0; m < blockYNum; m++) {
        var row = s.map[m];

        for (var n = 0; n < blockXNum; n++) {
            var count = 0,
            list = null;

            if (row[n] == -1) {
                continue;
            }

            list = s.findBlockAround(n, m);

            for (var f = 0, ll = list.length; f < ll; f++) {
                if (list[f].v == -1) {
                    count++;
                }
            }

            s.map[m][n] = count;
        }
    }
};

create函數致力於佈雷以及把每個方塊標上數字,這個數字就是這個方塊四周有的雷數。
這裏我用到了一個很重要的函數——findBlockAround:

MineLayer.prototype.findBlockAround = function (x, y) {
    var s = this,
    l = blockYNum,
    t = blockXNum,
    di = y + 1,
    ti = y - 1,
    ri = x + 1,
    li = x - 1,
    cr = null,
    rl = new Array();

    if (di < l) {
        cr = s.map[di];

        rl.push({x : x, y : di, v : cr[x]});

        if (li >= 0) {
            rl.push({x : li, y : di, v : cr[li]});
        }

        if (ri < t) {
            rl.push({x : ri, y : di, v : cr[ri]});
        }
    }

    if (ti >= 0) {
        cr = s.map[ti];

        rl.push({x : x, y : ti, v : cr[x]});

        if (li >= 0) {
            rl.push({x : li, y : ti, v : cr[li]});
        }

        if (ri < t) {
            rl.push({x : ri, y : ti, v : cr[ri]});
        }
    }

    if (li >= 0) {
        cr = s.map[y];

        rl.push({x : li, y : y, v : cr[li]});
    }

    if (ri < t) {
        cr = s.map[y];

        rl.push({x : ri, y : y, v : cr[ri]});
    }

    return rl;
};

這個函數是幹啥的呢?噢~原來是用來尋找某個方塊附近一圈的方塊。也就是左上,正上,右上,正左,左下,正下,右下,正右這幾個位置的方塊。由於這個功能很多地方要用,所以我把它單獨封裝進一個函數。

再來看鼠標事件實現部分,主要由onDown,onUp,loop這個三個函數一起合作來完成:

MineLayer.prototype.onDown = function (btn, mouseButton) {
    var s = this;

    s.parent.infoLayer.changeFace("surprise");

    if (
        s.startTimer
        && (mouseButton == 0 || mouseButton == 2)
        && mouseButton != s.preMouseButton
        && !btn.isFlag
        && btn.isSwept
    ) {
        s.startTimer = false;
        s.timerIndex = 0;
        s.doubleDown = true;
        s.preMouseButton = mouseButton;

        if (!s.isMineAroundHasBeenSwept(btn)) {
            var p = btn.positionInMap,
            list = s.findBlockAround(p.x, p.y);

            for (var i = 0, l = list.length; i < l; i++) {
                var o = list[i], b = s.getChildAt(o.y * blockXNum + o.x)

                if (!b.isFlag) {
                    b.setIntoOverState();
                }
            }
        }

        return;
    }

    s.startTimer = true;

    if (mouseButton == 0) {
        s.onUpCallback = function () {
            s.sweepThis(btn, true);
        }
    } else if (mouseButton == 2) {
        s.onUpCallback = function () {
            s.setFlagTo(btn);
        }
    }
};

MineLayer.prototype.onUp = function (btn, mouseButton) {
    var s = this, infoLayer = s.parent.infoLayer;

    infoLayer.changeFace("smile");

    if (s.doubleDown) {
        var p = btn.positionInMap,
        list = s.findBlockAround(p.x, p.y);

        s.doubleDown = false;
        s.startTimer = false;
        s.preMouseButton = null;

        if (s.isMineAroundHasBeenSwept(btn)) {
            s.sweepBlocksAround(btn, false);
        } else {
            for (var i = 0, l = list.length; i < l; i++) {
                var o = list[i], b = s.getChildAt(o.y * blockXNum + o.x);

                if (!b.isFlag) {
                    b.setIntoNormalState();
                }
            }
        }

        return;
    }

    if (typeof s.onUpCallback == "function") {
        if (!infoLayer.isStart) {
            infoLayer.isStart = true;
            infoLayer.preTime = new Date().getTime();
        }

        s.onUpCallback();

        s.onUpCallback = null;
    }
};

MineLayer.prototype.loop = function (e) {
    var s = e.currentTarget;

    if (!s.startTimer) {
        return;
    }

    if (s.timerIndex++ > s.waitingTime) {
        s.timerIndex = 0;
        s.startTimer = false;
    }
};

主要來講講實現左右兩鍵同時按下事件的實現:
首先我們得想象一下我們左右兩鍵同時按下時的操作,大致可以簡化爲兩個按鍵中其中以個按下後,在短暫時間後,另一個按鍵也按下,如果其中任意一個鬆開,那就執行同時按下對應的代碼;如果超出了短暫時間才按下另一個按鍵,那麼我們就把鼠標鬆開後要執行的函數設置爲最後按下的那個鍵對應的代碼;如果壓根就沒第二次按下,那就直接執行第一次按下對應的代碼。想到這裏後,我們要做的就很明確了。短暫時間的計時是交給loop函數來完成,鼠標按下和鬆開就分別交給了onDown和onUp。

接下來是sweepThis和sweepBlocksAround這兩個函數:

MineLayer.prototype.sweepBlocksAround = function (btn) {
    var s = this,
    p = btn.positionInMap,
    list = s.findBlockAround(p.x, p.y);

    for (var i = 0, l = list.length; i < l; i++) {
        var  o = list[i], b = s.getChildAt(o.y * blockXNum + o.x);

        if (o.v >= 0 && !b.isSwept) {
            s.sweepThis(b);
        } else if (o.v == -1 && !b.isFlag) {
            s.sweepThis(b);
        }
    }
};

MineLayer.prototype.sweepThis = function (btn) {
    var s = this, p = btn.positionInMap, value = s.map[p.y][p.x];

    if (btn.isSwept) {
        return;
    }

    if (btn.isFlag) {
        s.setFlagTo(btn);
    }

    if (value == -1) {
        s.gameOver("lose");

        return;
    }

    var contentLayer = new LSprite();
    contentLayer.filters = [new LDropShadowFilter()];
    contentLayer.graphics.drawRect(2, "white", [0, 0, btn.getWidth(), btn.getHeight()], true, "lightgray");
    var txt = new LTextField();
    txt.text = (value == 0) ? "" : value;
    txt.x = (contentLayer.getWidth() - txt.getWidth()) / 2;
    txt.y = (contentLayer.getHeight() - txt.getHeight()) / 2;
    txt.weight = "bold";
    txt.color = "white";
    txt.lineColor = "#0088FF";
    txt.stroke = true;
    txt.lineWidth = 3;
    txt.size = 18;
    contentLayer.addChild(txt);

    btn.isSwept = true;

    btn.removeButton();

    btn.setContent(contentLayer);

    if (s.isMineAroundHasBeenSwept(btn)) {
        s.sweepBlocksAround(btn);
    }
};

sweepThis的主要功能就是把某個方塊(及參數btn)給掃開。然後判斷這個方塊四周被標記的方塊數是不是等於該方塊四周的雷數,如果判斷通過,就通過sweepBlockAround執行“智能幫掃算法”。這個用來判斷四周被標記的方塊數是不是等於該方塊四周的雷數的函數就是isMineAroundHasBeenSwept:

MineLayer.prototype.isMineAroundHasBeenSwept = function (btn) {
    var s = this,
    p = btn.positionInMap,
    count = 0,
    value = s.map[p.y][p.x],
    list = null;

    if (value == 0) {
        return true;
    }

    list = s.findBlockAround(p.x, p.y);

    for (var i = 0, l = list.length; i < l; i++) {
        var o = list[i];

        if (s.getChildAt(o.y * blockXNum + o.x).isFlag) {
            count++;
        }
    }

    if (count == value) {
        return true;
    }

    return false;
};

在sweepBlockAround中,我們要接受一個參數,這個是告訴sweepBlockAround“幫掃算法”對哪個方塊起作用,及以誰爲中心,向四周掃開其他方塊。在這個函數中,我們首先把四周的方塊獲得,並放入一個數組,然後遍歷這個數組,如果遍歷到的按鈕是>=0的,並且沒有被掃開,就把它掃開;如果是=-1,及代表雷,並且又沒被標記就也把它掃開,因爲sweepBlockAround都是在通過isMineAroundHasBeenSwept後才調用的,所以說如果出現上面的情況,說明玩家判斷失誤了。

最後再來看剩餘的及個膚淺易懂的函數:

MineLayer.prototype.setFlagTo = function (btn) {
    var s = this,
    p = btn.positionInMap;
    flagBmp = null,
    infoLayer = null;

    if (btn.isSwept) {
        return;
    }

    flagBmp = new LBitmap(new LBitmapData(dataList["flag"]));

    infoLayer = s.parent.infoLayer;

    if (btn.isFlag) {
        btn.isFlag = false;

        infoLayer.mineLeftNum++;

        if (s.map[p.y][p.x] == -1) {
            s.completeNum--;
        }

        btn.removeContent();
    } else {
        btn.isFlag = true;

        infoLayer.mineLeftNum--;

        if (s.map[p.y][p.x] == -1) {
            s.completeNum++;
        }

        btn.setContent(flagBmp);
    }

    infoLayer.refreshMineLeftNumTxt();

    if (s.completeNum == mineNum && infoLayer.mineLeftNum == 0) {
        for (var i = 0; i < blockYNum; i++) {
            for (var j = 0; j < blockXNum; j++) {
                var b = s.getChildAt(i * blockXNum + j);

                if (!b.isSwept && !b.isFlag) {
                    s.sweepThis(b);
                }
            }
        }

        s.gameOver("win");
    }
};
MineLayer.prototype.gameOver = function (r) {
    var s = this, infoLayer = s.parent.infoLayer;

    for (var i = 0; i < blockYNum; i++) {
        var row = s.map[i];

        for (var j = 0; j < blockXNum; j++) {
            var v = row[j], b = s.getChildAt(i * blockXNum + j);

            b.mouseEnabled = false;
            b.mouseChildren = false;

            if (r == "lose" && v == -1) {
                b.setContent(new LBitmap(new LBitmapData(dataList["mine"])));

                infoLayer.changeFace("sad");
            }
        }
    }

    if (r == "win") {
        infoLayer.changeFace("happy");
    }

    infoLayer.isStart = false;
};

setFlagTo就是右鍵標小旗功能。gameOver就是遊戲結束時調用的。這兩個函數都涉及了和InfoLayer的交互。

運行代碼,就得到了一款掃雷遊戲。

最近掃上癮了,所以就再來幾把吧!!

源代碼下載

下載地址:http://yuehaowang.github.io/downloads/minesweeper.zip


歡迎大家繼續關注我的博客

轉載請註明出處:Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

發佈了67 篇原創文章 · 獲贊 45 · 訪問量 93萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章