Android & Kotlinの環境でマインスイーパーを開発する方法を説明します。
今回は、ゲームを開始してから最初に開くタイルを地雷以外にしてみます。
そそたた
最初のタイルは完全に運に左右されるため、いきなり地雷を開くとなんだか残念な気持ちになってしまう。。
ソースコード
開発環境は次の通りです。
PC | MacBook Pro(2016年モデル) |
IDE | Android Studio 4.1.1 |
Android SDK | minSdkVersion 21 targetSdkVersion 30 |
Kotlin | 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) } } 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 // 地雷をランダムにセット // ※初回オープンは地雷じゃなくする var count = 0 while (count < mNumBomb) { val i = (0 until numX).random() val j = (0 until numY).random() if (!mTiles[j][i].isBomb && (x != i || y != j)) { mTiles[j][i].isBomb = true count++ } } val3BV = calc3BV() 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)) } } } checkGameClear() } } override fun toggleMode(): GameMode { mGameMode = when (mGameMode) { GameMode.Bomb -> GameMode.Flag GameMode.Flag -> GameMode.Bomb } return mGameMode } /** * ゲームクリアを判定する */ private fun checkGameClear() { if (mCurNumOpen == numX * numY - mNumBomb) { for (j in 0 until numY) { for (i in 0 until numX) { if (mTiles[j][i].state == TileState.TILE) { innerUpdateTileState(i, j, TileState.TILE_FLAG) } } } mCurNumFlag = mNumBomb mFinished = true updateBombCount?.invoke(mNumBomb - mCurNumFlag) clearedGame?.invoke() } } /** * タイル状態変更を通知する */ 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 } }
解説
地雷をランダムに配置する処理をinitializeメソッドからopenメソッドに移動して、初回タップ位置を爆弾配置の条件に入れて弾くようにすれば完成です。
override fun open(x: Int, y: Int) { if (x in 0 until numX && y in 0 until numY && !mFinished) { if (!mBegan) { mBegan = true // 地雷をランダムにセット // ※初回オープンは爆弾じゃなくする var count = 0 while (count < mNumBomb) { val i = (0 until numX).random() val j = (0 until numY).random() if (!mTiles[j][i].isBomb && (x != i || y != j)) { mTiles[j][i].isBomb = true count++ } } val3BV = calc3BV() startedGame?.invoke() }
コメント