【マインスイーパー開発 #16】難易度を表す3BVを計算する|Android & Kotlinアプリ開発

Android & Kotlin

Android & Kotlinの環境でマインスイーパーを開発する方法を説明します。

今回は、難易度を表す3BVを計算します。

3BVとは、全てのタイルを開くのに必要な最小タップ数のことです。

そそたた
そそたた

ランダムに爆弾を配置するため同じタイル数、爆弾数でも難易度が変わってきます。

3BVの数値が高いのにこのタイムでクリアできたぜ!!みたいな感じでニヤニヤするために使います。

動作イメージ

●との枠の合計数が3BVになります。

数値が大きいほど難易度が高く、小さいほど難易度が低くなります。

3BV=17

3BV=6

3BV=21

ソースコード

開発環境は次の通りです。

PCMacBook Pro(2016年モデル)
IDEAndroid Studio 4.1.1
Android SDKminSdkVersion 21
targetSdkVersion 30
言語Kotlin 1.3.72
package com.sosotata.minesweeper.model

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.sosotata.minesweeper.LOG
import com.sosotata.minesweeper.R

/**
 * 四角タイル制御
 */
class Square() : TileController {

    /** タイル状態定義 */
    enum class TileState(var value: Int) {
        NUM0(0),
        NUM1(1),
        NUM2(2),
        NUM3(3),
        NUM4(4),
        NUM5(5),
        NUM6(6),
        NUM7(7),
        NUM8(8),
        TILE(9),
        TILE_FLAG(10),
        BOMB(11),
        BOMB_HIT(12),
        BOMB_NG(13),
        BOMB_OK(14);

        companion object {
            fun valueFrom(value: Int): TileState {
                return values().firstOrNull { it.value == value } ?: TILE
            }
        }
    }

    /** タイル要素  */
    data class TileElement(var state: TileState, var isBomb: Boolean)

    /** タイル画像リスト */
    private val mTileImages: MutableList<Bitmap> = mutableListOf()

    /** タイル状態管理配列 */
    private lateinit var mTiles: Array<Array<TileElement>>

    /** 爆弾数 */
    private var mNumBomb: Int = 0

    /** フラグ数 */
    private var mCurNumFlag = 0

    /** オープン数 */
    private var mCurNumOpen = 0

    /** ゲーム開始フラグ */
    private var mBegan = false

    /** ゲーム終了フラグ */
    private var mFinished = false

    /** ゲームモード */
    private var mGameMode = GameMode.Bomb

    override var numX: Int = 0
    override var numY: Int = 0
    override var tileWidth: Int = 0
    override var tileHeight: Int = 0
    override var val3BV: Int = 0
    override var updateTileState: ((i: Int, j: Int, image: Bitmap) -> Unit)? = null
    override var updateBombCount: ((Int) -> Unit)? = null
    override var startedGame: (() -> Unit)? = null
    override var gameOver: (() -> Unit)? = null
    override var clearedGame: (() -> Unit)? = null
    override fun initialize(context: Context) {
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.b0))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.b1))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.b2))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.b3))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.b4))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.b5))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.b6))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.b7))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.b8))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.base_square))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.flag))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.bomb))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.bomb_hit))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.bomb_ng))
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.bomb_flag))

        // タイル数、爆弾数を設定
        tileWidth = mTileImages[0].width
        tileHeight = mTileImages[0].height
        numX = 9
        numY = 9
        mNumBomb = 10
        mCurNumFlag = 0
        mCurNumOpen = 0

        // タイル状態初期化
        mTiles = Array(numY) {Array(numX) {
            TileElement(TileState.TILE, false)
        } }

        // 爆弾をランダムにセット
        var count: Int = 0
        while (count < mNumBomb) {
            val x: Int = (0 until numX).random()
            val y: Int = (0 until numY).random()
            if (!mTiles[y][x].isBomb) {
                mTiles[y][x].isBomb = true
                count++
            }
        }

        // 3BVを計算する
        val3BV = calc3BV()

        mBegan = false

        updateBombCount?.invoke(mNumBomb - mCurNumFlag)
    }

    override fun isGaming(): Boolean {
        return (mBegan && !mFinished)
    }

    override fun isNumberState(x: Int, y: Int): Boolean {
        var ret = false
        if (x in 0 until numX && y in 0 until numY) {
            if (mTiles[y][x].state.value <= TileState.NUM8.value) {
                ret = true
            }
        }
        return ret
    }

    override fun isBaseTile(x: Int, y: Int): Boolean {
        var ret = false
        if (x in 0 until numX && y in 0 until numY) {
            if (mTiles[y][x].state == TileState.TILE) {
                ret = true
            }
        }
        return ret
    }

    override fun getTileStateImage(x: Int, y: Int): Bitmap {
        return when {
            x < numX && y < numY -> mTileImages[mTiles[y][x].state.value]
            else -> {
                LOG.e("invalid param x[${x}] y[${y}]")
                mTileImages[mTiles[0][0].state.value]
            }
        }
    }

    override fun open(x: Int, y: Int) {
        if (x in 0 until numX && y in 0 until numY && !mFinished) {
            if (!mBegan) {
                mBegan = true
                startedGame?.invoke()
            }

            if (mGameMode == GameMode.Flag) {
                val tmpState = mTiles[y][x].state
                when (mTiles[y][x].state) {
                    TileState.TILE -> {
                        mTiles[y][x].state = TileState.TILE_FLAG
                        mCurNumFlag++
                        updateBombCount?.invoke(mNumBomb - mCurNumFlag)
                    }

                    TileState.TILE_FLAG -> {
                        mTiles[y][x].state = TileState.TILE
                        mCurNumFlag--
                        updateBombCount?.invoke(mNumBomb - mCurNumFlag)
                    }

                    else -> {
                        if (mTiles[y][x].state.value <= TileState.NUM8.value) {
                            actionNumber(x, y)
                        }
                    }
                }
                if (tmpState != mTiles[y][x].state) {
                    innerUpdateTileState(x, y, mTiles[y][x].state)
                }
            } else {
                if (mTiles[y][x].isBomb) {
                    mTiles[y][x].state = TileState.BOMB
                    innerUpdateTileState(x, y, mTiles[y][x].state)
                    gameOver?.invoke()
                } else if (mTiles[y][x].state.value <= TileState.NUM8.value) {
                    actionNumber(x, y)
                } else {
                    val bombNum = getAroundBombs(x, y)
                    if (bombNum == 0) {
                        actionZero(x, y, mTiles)
                    } else {
                        mCurNumOpen++
                        innerUpdateTileState(x, y, TileState.valueFrom(bombNum))
                    }
                }
            }
        }
    }

    override fun toggleMode(): GameMode {
        mGameMode = when (mGameMode) {
            GameMode.Bomb -> GameMode.Flag
            GameMode.Flag -> GameMode.Bomb
        }
        return mGameMode
    }

    /**
     * タイル状態変更を通知する
    */
    private fun innerUpdateTileState(x: Int, y: Int, state: TileState) {
        mTiles[y][x].state = state
        updateTileState?.invoke(x, y, mTileImages[mTiles[y][x].state.value])
    }

    /**
     * 有効タイル位置判定
     */
    private fun isValidPos(x: Int, y: Int): Boolean {
        return (x in 0 until numX) && (y in 0 until numY)
    }

    /**
     * 指定したタイルの周りにある爆弾数を取得
     */
    private fun getAroundBombs(x: Int, y: Int): Int {
        var ret = 0 // 選択タイルの周りにある爆弾数
        if (isValidPos(x - 1, y - 1) && mTiles[y - 1][x - 1].isBomb) ret++  // 左上
        if (isValidPos(x, y - 1) && mTiles[y - 1][x].isBomb) ret++  // 真上
        if (isValidPos(x + 1, y - 1) && mTiles[y - 1][x + 1].isBomb) ret++  // 右上
        if (isValidPos(x - 1, y) && mTiles[y][x - 1].isBomb) ret++  // 左横
        if (isValidPos(x + 1, y) && mTiles[y][x + 1].isBomb) ret++  // 右横
        if (isValidPos(x - 1, y + 1) && mTiles[y + 1][x - 1].isBomb) ret++  // 左下
        if (isValidPos(x, y + 1) && mTiles[y + 1][x].isBomb) ret++  // 真下
        if (isValidPos(x + 1, y + 1) && mTiles[y + 1][x + 1].isBomb) ret++  // 右下
        return ret
    }

    /**
     * 周りの地雷が0個の場合にタイルを再起的に開く処理
     */
    private fun actionZero(x: Int, y: Int, tiles: Array<Array<TileElement>>) {
        if (x < 0 || y < 0 || numX <= x || numY <= y) {
        } else if (tiles[y][x].state != TileState.TILE) {
        } else {

            val bombNum = getAroundBombs(x, y)
            if (tiles.contentEquals(mTiles)) {
                innerUpdateTileState(x, y, TileState.valueFrom(bombNum))
                mCurNumOpen++
            } else {
                tiles[y][x].state = TileState.valueFrom(bombNum)
            }

            if (bombNum == 0) {
                actionZero(x - 1, y - 1, tiles)
                actionZero(x, y - 1, tiles)
                actionZero(x + 1, y - 1, tiles)
                actionZero(x - 1, y, tiles)
                actionZero(x + 1, y, tiles)
                actionZero(x - 1, y + 1, tiles)
                actionZero(x, y + 1, tiles)
                actionZero(x + 1, y + 1, tiles)
            }
        }
    }

    /**
     * 旗数が正しい数字をタップしたときに旗以外を再起的に開く処理
     */
    private fun actionNumber(x: Int, y: Int) {
        val bombs: Int = getAroundBombs(x, y)
        var flags = 0
        var tmpBoms = 0
        if (isValidPos(x - 1, y - 1) && mTiles[y - 1][x - 1].state == TileState.TILE_FLAG) {
            flags++
            if (mTiles[y - 1][x - 1].isBomb) tmpBoms++
        }
        if (isValidPos(x, y - 1) && mTiles[y - 1][x].state == TileState.TILE_FLAG) {
            flags++
            if (mTiles[y - 1][x].isBomb) tmpBoms++
        }
        if (isValidPos(x + 1, y - 1) && mTiles[y - 1][x + 1].state == TileState.TILE_FLAG) {
            flags++
            if (mTiles[y - 1][x + 1].isBomb) tmpBoms++
        }
        if (isValidPos(x - 1, y) && mTiles[y][x - 1].state == TileState.TILE_FLAG) {
            flags++
            if (mTiles[y][x - 1].isBomb) tmpBoms++
        }
        if (isValidPos(x, y) && mTiles[y][x].state == TileState.TILE_FLAG) {
            flags++
            if (mTiles[y][x].isBomb) tmpBoms++
        }
        if (isValidPos(x + 1, y) && mTiles[y][x + 1].state == TileState.TILE_FLAG) {
            flags++
            if (mTiles[y][x + 1].isBomb) tmpBoms++
        }
        if (isValidPos(x - 1, y + 1) && mTiles[y + 1][x - 1].state == TileState.TILE_FLAG) {
            flags++
            if (mTiles[y + 1][x - 1].isBomb) tmpBoms++
        }
        if (isValidPos(x, y + 1) && mTiles[y + 1][x].state == TileState.TILE_FLAG) {
            flags++
            if (mTiles[y + 1][x].isBomb) tmpBoms++
        }
        if (isValidPos(x + 1, y + 1) && mTiles[y + 1][x + 1].state == TileState.TILE_FLAG) {
            flags++
            if (mTiles[y + 1][x + 1].isBomb) tmpBoms++
        }

        if (bombs == flags) {
            if (bombs == tmpBoms) {
                actionNumberOpenProcess(x - 1, y - 1)
                actionNumberOpenProcess(x, y - 1)
                actionNumberOpenProcess(x + 1, y - 1)
                actionNumberOpenProcess(x - 1, y)
                actionNumberOpenProcess(x + 1, y)
                actionNumberOpenProcess(x - 1, y + 1)
                actionNumberOpenProcess(x, y + 1)
                actionNumberOpenProcess(x + 1, y + 1)
            } else {
                // 旗の位置が間違えているためゲームオーバー
                gameOver?.invoke()
            }
        } else {
            // 旗の数が一致していないため何もしない
        }
    }

    /**
     * 数字実行時のタイルを開く処理
     */
    private fun actionNumberOpenProcess(x: Int, y: Int) {
        if (0 <= x && 0 <= y && x < numX && y < numY) {
            if ((mTiles[y][x].state === TileState.TILE) && !mTiles[y][x].isBomb) {
                val state: Int = getAroundBombs(x, y)
                if (state == 0) {
                    actionZero(x, y, mTiles)
                } else {
                    mCurNumOpen++
                    innerUpdateTileState(x, y, TileState.valueFrom(state))
                }
            }
        }
    }

    /**
     * 3BVを計算する
     */
    private fun calc3BV(): Int {
        var calcVal = 0
        val tmpTiles = Array(numY) {
            Array(numX) {
                TileElement(TileState.TILE, false)
            }
        }

        for (j in 0 until numY) {
            for (i in 0 until numX) {
                tmpTiles[j][i].isBomb = mTiles[j][i].isBomb
            }
        }

        for (j in 0 until numY) {
            for (i in 0 until numX) {
                if (tmpTiles[j][i].state == TileState.TILE && !tmpTiles[j][i].isBomb) {
                    if (getAroundBombs(i, j) == 0) {
                        actionZero(i, j, tmpTiles)
                        calcVal++
                    }
                }
            }
        }

        for (j in 0 until numY) {
            for (i in 0 until numX) {
                if (tmpTiles[j][i].state == TileState.TILE && !tmpTiles[j][i].isBomb) {
                    calcVal++
                }
            }
        }
        LOG.d("@@@@@@@@ 3BV[${calcVal}]")

        return calcVal
    }
}

解説

Squareクラスに3BVを計算するcalc3BVメソッドを実装します。

まずは、3BV計算用にタイル状態管理配列をコピーします。

var calcVal = 0
val tmpTiles = Array(numY) {
    Array(numX) {
        TileElement(TileState.TILE, false)
    }
}

for (j in 0 until numY) {
    for (i in 0 until numX) {
        tmpTiles[j][i].isBomb = mTiles[j][i].isBomb
    }
}

次に、周りの地雷数が0個のタイルを開いて、動作イメージのの枠数に当たる部分を数えます。

for (j in 0 until numY) {
    for (i in 0 until numX) {
        if (tmpTiles[j][i].state == TileState.TILE && !tmpTiles[j][i].isBomb) {
            if (getAroundBombs(i, j) == 0) {
                actionZero(i, j, tmpTiles)
                calcVal++
            }
        }
    }
}

最後に、動作イメージの●に当たる部分として、まだ開いていないタイルから地雷以外のものを数えます。

for (j in 0 until numY) {
    for (i in 0 until numX) {
        if (tmpTiles[j][i].state == TileState.TILE && !tmpTiles[j][i].isBomb) {
            calcVal++
        }
    }
}

コメント

タイトルとURLをコピーしました