Android & Kotlinの環境でマインスイーパーを開発する方法を説明します。
今回は、数字タイルの周りに正しい数だけ旗を立っている場合に、その数字タイルをタップすると旗以外の周りのタイルを再起的に開く処理を実装します。
動作イメージ
赤線で囲われた数字タイルの周りに旗が正しく1つ立てられています。
この状態からその数字タイルをタップすると、まだ開いていない●を再起的に開きます。
そそたた
この機能を有効に活用するとタイムが一気に縮まりますが、旗を間違えて立ててしまっている場合はゲームオーバーとなります。
マニアックな人は、旗を使わずに競ったりするみたいですけど。。
ソースコード
開発環境は次の通りです。
PC | MacBook Pro(2016年モデル) |
IDE | Android Studio 4.1 |
Android SDK | minSdkVersion 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 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 = 16
numY = 16
mNumBomb = 40
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++
}
}
mBegan = false
updateBombCount?.invoke(mNumBomb - mCurNumFlag)
}
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) {
updateTileState?.invoke(x, y, mTileImages[mTiles[y][x].state.value])
}
} else {
if (mTiles[y][x].isBomb) {
mTiles[y][x].state = TileState.BOMB
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)
} else {
mCurNumOpen++
mTiles[y][x].state = TileState.valueFrom(bombNum)
}
}
updateTileState?.invoke(x, y, mTileImages[mTiles[y][x].state.value])
}
}
}
override fun toggleMode(): GameMode {
mGameMode = when (mGameMode) {
GameMode.Bomb -> GameMode.Flag
GameMode.Flag -> GameMode.Bomb
}
return mGameMode
}
/**
* 有効タイル位置判定
*/
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) {
if (x < 0 || y < 0 || numX <= x || numY <= y) {
} else if (mTiles[y][x].state != TileState.TILE) {
} else {
val bombNum = getAroundBombs(x, y)
mTiles[y][x].state = TileState.valueFrom(bombNum)
updateTileState?.invoke(x, y, mTileImages[mTiles[y][x].state.value])
mCurNumOpen++
if (bombNum == 0) {
actionZero(x - 1, y - 1)
actionZero(x, y - 1)
actionZero(x + 1, y - 1)
actionZero(x - 1, y)
actionZero(x + 1, y)
actionZero(x - 1, y + 1)
actionZero(x, y + 1)
actionZero(x + 1, y + 1)
}
}
}
/**
* 旗数が正しい数字をタップしたときに旗以外を再起的に開く処理
*/
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)
} else {
mCurNumOpen++
mTiles[y][x].state = TileState.valueFrom(state)
updateTileState?.invoke(x, y, mTileImages[mTiles[y][x].state.value])
}
}
}
}
}
解説
旗数が正しい数字をタップしたときに旗以外の周りのタイルを再起的に開く処理をSquareクラスのactionNumberメソッドに実装します。
actionNumberメソッドは、周りの地雷数と旗数が一致している場合のみ動作し、周りの地雷数と正しい旗数が一致していれば再起的に周りのタイルを開くが、一致していないとゲームオーバーにします。
Squareクラスのopenメソッド内でタップしたタイルが数字の場合にactionNumberメソッドを呼び出す処理を追加します。
コメント