1. 程式人生 > >三消遊戲核心邏輯的一種實現

三消遊戲核心邏輯的一種實現

前段時間很喜歡玩"開心消消樂"這個休閒小遊戲, 剛好大學的一位同學也在玩,後來就想著如果讓自己來寫這個邏輯要怎麼寫, 經過一天的構思, 找到一種利用深度優先搜尋思想來實現的方式.

三消遊戲的規則

  1. 一個m x n 的棋盤內, 初始狀態下, 填滿若干種不同型別的… 暫且叫Hero吧, 型別的數量根據遊戲難度不同而不同, 一般有4~5種.
  2. 同種型別的Hero連著 >= 3時消除, 橫著豎著都可以消, 斜著的不能消.

本文約定以下5種不同型別的Hero, 對應的值分別為1,2,3,4,5.

下面模擬一個5x5的棋盤, 說明一下規則,
消除前的棋盤:
1 1 2 4 5
5 1 1 1 4
5 2 1 5 4
4 3 3 3 4

消除後的棋盤:
1 1 2 4 5
5 0 0 0 0
5 2 1 5 0
4 0 0 0 0

演算法實現

實現語言最早用的是C++, 後來因為要用Html繪製介面, 所以把程式碼編譯成JavaScript的了, 而且因為現在過年回家了, C++的程式碼在公司, 只有Js的程式碼可以Po出來了.

演算法其實很簡單, 寫一個函式ScanPos(pos), 這個函式用來掃描pos點的左、上、右、下四個鄰近的點,如果某個鄰近的點和當前掃描的點pos的型別一樣,則遞迴呼叫ScanPos(newpos), 繼續遞迴呼叫ScanPos, 直到所有方向的點都不匹配或者到達邊界為止。

用上面那個例子說明一下掃描的過程:
----> x
|
| 座標方向, 為了直觀,原點為1, 1
V
y

假如我們初始掃描的點是(3, 2), HeroType = 1
1 1 2 4 5
5 1 1 1 4
5 2 1 5 4
4 3 3 3 4

Step.1
1 1 2 4 5
5 1 1 1 4
5 2 1 5 4
4 3 3 3 4

Step.2
1 1 2 4 5
5 1 1 1 4
5 2 1 5 4
4 3 3 3 4

Step.3
1 1 2 4 5
5 1 1 1 4
5 2 1 5 4
4 3 3 3 4
這裡掃到5後發現5不匹配, 於是開始往上Scan

Step.4
1 1 2 4 5
5 1 1 1 4
5 2 1 5 4
4 3 3 3 4

Step.5
1 1 2 4 5
5 1 1 1 4
5 2 1 5 4
4 3 3 3 4
Scan到1,1這個點後,左、上、右、下都無效,左上到達邊界,右邊是來時的地方, 所以也不匹配,下邊是5,也不匹配, 所以回退到1,2這個點的ScanPos函式,這個點的上、右也都不匹配, 所以繼續往回退,…… 中間過程類似,不再描述。最後掃描後的棋盤狀態:
1 1 2 4 5
5 1 1 1 4
5 2 1 5 4
4 3 3 3 4

可以發現這樣掃描後,第一行和第三行的3個點,並不符合三消規則,但是也都被掃描到了。這就需要把這些點排除掉。

排除不符合的點

這一步做的比較粗暴,直接兩層for迴圈遍歷整個棋盤,在內層迴圈內對每一行Hero進行遍歷時, 記錄相鄰的相同點的個數,然後把每個點的Match次數儲存起來,最後只有Match >=3 的才會消除。

以上就是核心演算法了,當然還有很多細節需要注意,比如抽象出Cell類,每個Cell有它的狀態以及Match次數, 在每次ScanPos時,都要實時更新Cell的狀態,等等。

JavaScript程式碼實現

function Grid (cols, rows) {
    this.cols = cols;
    this.rows = rows;
    this.board = new Array();

    this.lastClickPosX = -1;
    this.lastClickPosY = -1;

    this.score = 0;
}

Grid.prototype.scanPos = function(scanPosX, scanPosY, parentPosX, parentPosY) {
    scanPosX = parseInt(scanPosX);
    scanPosY = parseInt(scanPosY);
    parentPosX = parseInt(parentPosX);
    parentPosY = parseInt(parentPosY);

    //console.log("scanPos1: %d %d %d %d", scanPosX, scanPosY, parentPosX, parentPosY);
    var parentCell = this.getCellByPos(parentPosX, parentPosY);
    if (parentCell == null) {
        console.log("parentCell is null");
        return;
    }

    //left
    var leftPosX = scanPosX - 1;
    var leftPosY = scanPosY;
    //console.log("(%d %d) (%d %d)", leftPosX, leftPosY, parentPosX, parentPosY);
    var leftCell = this.getCellByPos(leftPosX, leftPosY);   
    //console.log("(%d %d) (%d %d)", leftPosX, leftPosY, parentPosX, parentPosY);
    //console.log("leftCell:", leftCell);   
    if (leftCell != null
        && (leftPosX != parentPosX || leftPosY != parentPosY)
        && leftCell.getState() == CELL_STATE.CS_NORMAL 
        && leftCell.getHeroType() == parentCell.getHeroType()
        )
    {
        //console.log("left fuck!");
        leftCell.updateState(CELL_STATE.CS_SCANED);
        this.scanPos1(leftPosX, leftPosY, scanPosX, scanPosY);
    }
    //console.log("-----scan pos: scanPosX=%d, scanPosY=%d", scanPosX, scanPosY);

    //top
    var topPosX = scanPosX;
    var topPosY = scanPosY - 1;
    //console.log("(%d %d) (%d %d)", topPosX, topPosY, parentPosX, parentPosY);
    var topCell = this.getCellByPos(topPosX, topPosY);
    //console.log("(%d %d) (%d %d)", topPosX, topPosY, parentPosX, parentPosY);
    //console.log("topCell:", topCell);
    if (topCell != null
        && (topPosX != parentPosX || topPosY != parentPosY)
        && topCell.getState() == CELL_STATE.CS_NORMAL 
        && topCell.getHeroType() == parentCell.getHeroType()
        )
    {
        //console.log("top fuck!");
        topCell.updateState(CELL_STATE.CS_SCANED);
        this.scanPos1(topPosX, topPosY, scanPosX, scanPosY);
    }
    //console.log("-----scan pos: scanPosX=%d, scanPosY=%d", scanPosX, scanPosY);

    //right
    var rightPosX = scanPosX + 1;
    var rightPosY = scanPosY;
    //console.log("(%d %d) (%d %d)", rightPosX, rightPosY, parentPosX, parentPosY);
    var rightCell = this.getCellByPos(rightPosX, rightPosY);
    //console.log("(%d %d) (%d %d)", rightPosX, rightPosY, parentPosX, parentPosY);
    //console.log("rightCell:", rightCell);
    if (rightCell != null
        && (rightPosX != parentPosX || rightPosY != parentPosY)
        && rightCell.getState() == CELL_STATE.CS_NORMAL 
        && rightCell.getHeroType() == parentCell.getHeroType()
        )
    {
        //console.log("right fuck!");
        rightCell.updateState(CELL_STATE.CS_SCANED);
        this.scanPos1(rightPosX, rightPosY, scanPosX, scanPosY);
    }
    //console.log("-----scan pos: scanPosX=%d, scanPosY=%d", scanPosX, scanPosY);

    //down
    var downPosX = scanPosX;
    var downPosY = scanPosY + 1;
    //console.log("(%d %d) (%d %d)", downPosX, downPosY, parentPosX, parentPosY);
    var downCell = this.getCellByPos(downPosX, downPosY);
    //console.log("(%d %d) (%d %d)", downPosX, downPosY, parentPosX, parentPosY);
    //console.log("downCell:", downCell);
    if (downCell != null
        && (downPosX != parentPosX || downPosY != parentPosY)
        && downCell.getState() == CELL_STATE.CS_NORMAL 
        && downCell.getHeroType() == parentCell.getHeroType()
        )
    {
        //console.log("down fuck!");
        downCell.updateState(CELL_STATE.CS_SCANED);
        this.scanPos1(downPosX, downPosY, scanPosX, scanPosY);
    }
    //console.log("-----scan pos: scanPosX=%d, scanPosY=%d", scanPosX, scanPosY);
};

function scan_match_three_xsame(xList) {
    for (var itor = 0; itor < xList.length; itor++) {
        var vecCell = xList[itor];
        if (vecCell == null)
            continue;

        if (vecCell.length < 3)
            continue;

        var start = 0;
        var matchCount = 1;
        var i;
        for (i = 1; i < vecCell.length; i++) {
            var prevCell = vecCell[i-1];
            var currCell = vecCell[i];
            //console.assert(prevCell && currCell, "fatal error");
            if ( prevCell == null || currCell == null
                || prevCell.getPosX() != currCell.getPosX() 
                ) 
            {
                console.assert(0, "fatal error");
                return;
            }

            if (currCell.getPosY() == prevCell.getPosY() + 1) {
                matchCount++;//continuous
            }
            else if(currCell.getPosY() > prevCell.getPosY()+ 1) {
                //interrupt
                for (var j = start; j < i; j++) {
                    console.assert(vecCell[j].getRefXSame() == 0);
                    vecCell[j].setRefXSame(matchCount);
                }
                start = i;
                matchCount = 1;
            }
            else {
                console.log("x-wtf?!: ", prevCell, currCell);
                console.assert(0, "vec's data is not sorted!");
            }
        }

        //loop end, update cell's ref
        for (var j = start; j < i; j++) {
            console.assert(vecCell[j].getRefXSame() == 0);
            vecCell[j].setRefXSame(matchCount);
        }
    }
}

因為程式碼太多,只貼出部分程式碼, 如果需要完整程式碼,請在下面留郵箱。

PS. 家裡好冷, 寫這篇文章手都快dong掉了

2016.11.16 更新

之前我在cocos2dx的應用商店裡(是這麼叫吧?還是程式碼庫,反正是cocos2d的一個官方工具內, 類似u3d的asset store), 看到過一完成度更高的三消遊戲, 那個是用cocos2dx(c++)寫的, 資源都有, 邏輯也很完整, 編譯完就可以直接玩的, 具體叫什麼名字忘了, 你們要以去搜下, 就搜”三消”就行.

Github上可能也有吧, 也可以找找看
我的這個版本比較Low, 完成度很低, 如果是直接拿來做單估計還要花不少時間完善和改BUG, 如果是C++的專案,強烈推薦上面說的cocos2dx的版本