Android & Kotlinの環境でマインスイーパーをアプリ開発する方法を説明します。
今回は、タッチしたタイルに地雷が配置されていたなら地雷を表示し、地雷以外なら周りの地雷数をあらわす数字を表示します。
動作イメージ
真ん中4つのタイルを開いたケースの動作イメージです。
ソースコード
開発環境は次の通りです。
PC | MacBook Pro(2016年モデル) |
IDE | Android Studio 4.0.1 |
Android SDK | minSdkVersion 16 targetSdkVersion 30 |
言語 | Kotlin 1.3.72 |
package com.sosotata.minesweeper.model
import android.content.Context
import android.graphics.Bitmap
/**
* タイル制御インターフェース
*/
interface TileController {
companion object {
fun create(type: TileType): TileController = when (type) {
TileType.Square -> Square()
TileType.Triangle -> TODO("Not Implemented")
TileType.Pentagon -> TODO("Not Implemented")
TileType.Hexagon -> TODO("Not Implemented")
}
}
/** 横方向のタイル要素数 */
var numX: Int
/** 縦方向のタイル要素数 */
var numY: Int
/** タイル幅 */
var tileWidth: Int
/** タイル高さ */
var tileHeight: Int
/** タイル状態更新通知 */
var updateTileState: ((Int, Int, Bitmap) -> Unit)?
/** 初期化処理 */
fun initialize(context: Context)
/** 指定したタイル画像の取得 */
fun getTileStateImage(x: Int, y: Int): Bitmap
/** タイルを開く */
fun open(x: Int, y: Int, longPress: Boolean)
}
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),
TILE_QUESTION(11),
BOMB(12),
BOMB_HIT(13),
BOMB_NG(14),
BOMB_OK(15);
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
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 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.question))
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))
// タイル数、爆弾数を設定
numX = 9
numY = 9
mNumBomb = 10
tileWidth = mTileImages[0].width
tileHeight = mTileImages[0].height
// タイル状態初期化
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++
}
}
}
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, longPress: Boolean) {
if (mTiles[y][x].isBomb ){
mTiles[y][x].state = TileState.BOMB
}
else {
val bombNum = getAroundBombs(x, y)
mTiles[y][x].state = TileState.valueFrom(bombNum)
}
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
}
}
package com.sosotata.minesweeper.ui.widgets
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import com.sosotata.minesweeper.LOG
import com.sosotata.minesweeper.model.TileController
/**
* 四角タイルゲーム画面
*/
class SquareTileGameView : SosotataImageView {
/** タイル描画用ビットマップ */
private lateinit var mSourceBitmap: Bitmap
/** タイル描画用キャンバス */
private lateinit var mRenderCanvas: Canvas
/** タイル制御 */
private lateinit var mTile: TileController
/**
* コンストラクタ
*/
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}
/**
* タイルを描画する
*/
fun initialize(tile: TileController) {
mTile = tile
mSourceBitmap = Bitmap.createBitmap(
mTile.tileWidth * mTile.numX, mTile.tileHeight * mTile.numY, Bitmap.Config.ARGB_8888
)
mRenderCanvas = Canvas(this.mSourceBitmap)
for (y in 0 until mTile.numY) {
for (x in 0 until mTile.numX) {
mRenderCanvas.drawBitmap(
mTile.getTileStateImage(x, y),
mTile.tileWidth.toFloat() * x,
mTile.tileHeight.toFloat() * y,
null
)
}
}
setImage(mSourceBitmap)
// タイル更新
mTile.updateTileState = { i, j, image ->
// タッチしたタイルを描画する
mRenderCanvas.drawBitmap(
image, (image.width * i).toFloat(), (image.height * j).toFloat(), null)
}
}
/**
* [android.view.View.onTouchEvent]
*/
override fun onTouchEvent(e: MotionEvent?): Boolean {
if (e != null) {
if (e.action == MotionEvent.ACTION_UP) {
val values = FloatArray(9)
this.mRenderMatrix.getValues(values)
val x = ((e.x - values[Matrix.MTRANS_X]) / values[Matrix.MSCALE_X] / mTile.tileWidth).toInt()
val y = ((e.y - values[Matrix.MTRANS_Y]) / values[Matrix.MSCALE_Y] / mTile.tileHeight).toInt()
LOG.d("横:${x}番目、縦:${y}番目")
mTile.open(x, y, false)
}
}
return super.onTouchEvent(e)
}
}
解説
複雑な処理はないので全体の流れをざっくりと解説します。
今回、TileControllerインターフェースにタイルを開くopenとタイル状態の変更を通知するupdateTileStateを追加しています。
SquareTileGameViewのonTouchEventでタッチアップを検知したときにopenを実行します。
次にupdateTileStateでタイル状態の変更が通知されるため爆弾や数字などの新しい状態にタイルを更新します。
タイルの周りに配置されている地雷数を数える処理は、SquareクラスのgetAroundBombsメソッドで実施しています。
やっていることは単純で2次元配列で管理しているタイル状態を「左上、真上、右上、左横、右横、左下、真下、右下」の順に参照しているだけです。
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
}
そそたた
端っこのタイルをタッチした場合に2次元配列の範囲を超えてしまうのでisValidPosメソッドによりガードしています。
Kotlinでは最初の条件式がfalseだと後の条件式が実行されません。
コメント