【マインスイーパー開発 #9】残りの地雷数を表示する|Android & Kotlinアプリ開発

Android & Kotlin

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

今回は、「全ての地雷数 ー 旗を立てた数」を残りの地雷数として表示します。

動作イメージ

画面上部の左端にある3桁の数字が残りの地雷数となります。

そそたた
そそたた

「全ての地雷数 ー 旗を立てた数」のため地雷数を超えて旗を立てるとマイナス値になります。

ソースコード

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

PCMacBook Pro(2016年モデル)
IDEAndroid Studio 4.0.1
Android SDKminSdkVersion 16
targetSdkVersion 30
言語Kotlin 1.3.72
package com.sosotata.minesweeper.ui.main

import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.ScaleAnimation
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import com.sosotata.minesweeper.R
import com.sosotata.minesweeper.model.TileController
import com.sosotata.minesweeper.model.TileType
import com.sosotata.minesweeper.ui.widgets.PlayTimeCounter
import kotlinx.android.synthetic.main.game_fragment.*

class GameFragment : Fragment() {
    companion object {
        fun newInstance() = GameFragment()
    }

    private lateinit var viewModel: GameViewModel
    private var playTimeCounter: PlayTimeCounter? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View {
        return inflater.inflate(R.layout.game_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

        viewModel.tile = TileController.create(TileType.Square)
        viewModel.tile.startedGame = {
            playTimeCounter?.start()
        }

        viewModel.tile.gameOver = {
            playTimeCounter?.stop()
            animateButton {
                resetButton.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.reset_button_gameover))
            }
        }

        viewModel.tile.clearedGame = {
            playTimeCounter?.stop()
            animateButton {
                resetButton.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.reset_button_victory))
            }
        }

        viewModel.tile.updateBombCount = {count ->
            bombCountText.text = "%03d".format(count)
        }

        viewModel.tile.initialize(requireContext())

        playTimeCounter = PlayTimeCounter(playTimer)
        squareView.initialize(viewModel.tile)

        resetButton.setOnClickListener {
            animateButton {
                viewModel.tile.initialize(requireContext())
                squareView.initialize(viewModel.tile)
                resetButton.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.reset_button_normal))
                playTimeCounter?.stop()
                playTimeCounter?.reset()
            }
        }
    }

    override fun onResume() {
        playTimeCounter?.resume()
        super.onResume()
    }

    override fun onPause() {
        playTimeCounter?.pause()
        super.onPause()
    }

    private fun animateButton(Animated: () -> Unit)
    {
        val btnEffect = ScaleAnimation(
            1.0f, 1.3f, 1.0f, 1.3f,
            Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
        btnEffect.duration = 100
        btnEffect.setAnimationListener(object : Animation.AnimationListener {
            override fun onAnimationStart(animation: Animation) {}
            override fun onAnimationRepeat(animation: Animation) {}
            override fun onAnimationEnd(animation: Animation) {
                Animated()
            }
        })
        resetButton.startAnimation(btnEffect)
    }
}
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 mStarted = false

    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 = 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++
            }
        }

        mStarted = 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, longPress: Boolean) {
        if (x in 0 until numX && y in 0 until numY) {
            if (!mStarted) {
                mStarted = true
                startedGame?.invoke()
            }

            if (longPress) {
                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 -> {}
                }
            } else {
                if (mTiles[y][x].isBomb) {
                    mTiles[y][x].state = TileState.BOMB
                    gameOver?.invoke()
                } else {
                    val bombNum = getAroundBombs(x, y)
                    mTiles[y][x].state = TileState.valueFrom(bombNum)
                }
            }
            mCurNumOpen++
            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
    }
}

解説

GameFragmentクラスで爆弾数が変化したことを通知するupdateBombCountを登録します。

viewModel.tile.updateBombCount = {count ->
    bombCountText.text = "%03d".format(count)
}

updateBombCountの通知タイミングは、初期化時、旗を立てた、旗を解除したの3つです。

override fun initialize(context: Context) {
    ・・・
    updateBombCount?.invoke(mNumBomb - mCurNumFlag)
}
override fun open(x: Int, y: Int, longPress: Boolean) {
    ・・・
            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 -> {}
            }
    ・・・
}

コメント

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