// Mines is free software: you can redistribute it and/or modify it under the // terms of the GNU General Public License as published by the Free Software // Foundation, either version 3 of the License, or (at your option) any later // version. // Mines is distributed in the hope that it will be useful, but WITHOUT ANY // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR // A PARTICULAR PURPOSE. See the GNU General Public License for more details. // You should have received a copy of the GNU General Public License along with // Mines. If not, see . package space.jakob.mines import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Rect import android.graphics.Paint import android.util.AttributeSet import android.view.View import android.util.Log import kotlin.math.ceil import kotlin.math.floor enum class AtlasEntry { BLANK, MASKED, UNMASKED, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, FLAG, UNKNOWN, MINE, TRIPPED, } class GameView(context: Context, attrs: AttributeSet) : View(context, attrs) { val atlas = BitmapFactory.decodeResource(context.resources, R.drawable.atlas); val tileWidth = atlas.width / 3 val scaledTileWidth: Int get() = (minOf(width, height) / cameraBreadth).toInt() // Reference to GameActivity's grid that should be treated as read-only. var grid: Grid? = null // All of the state for maintaining the view's current positioning. For an // explanation of these values, see `drawGrid`. var cameraX = 0.0 var cameraY = 0.0 var cameraBreadth = 5.0 // Properties for scaling cameraBreadth to the screen resolution. val cameraBreadthX: Double get() = if (width > height) { cameraBreadth * width.toDouble() / height.toDouble() } else { cameraBreadth } val cameraBreadthY: Double get() = if (height > width) { cameraBreadth * height.toDouble() / width.toDouble() } else { cameraBreadth } /** * Returns the grid coordinates for the tile at the given screen coordinates. */ fun toGridCoordinates(x: Int, y: Int) = Pair( // These were derived algebraically from the formula in `drawGrid`. ((x.toDouble() / scaledTileWidth.toDouble()) - cameraBreadthX / 2 + cameraX).toInt(), ((y.toDouble() / scaledTileWidth.toDouble()) - cameraBreadthY / 2 + cameraY).toInt() ) /** * Moves the camera by (@param dx, @param dy), clamping to grid bounds. */ fun moveCamera(dx: Double, dy: Double) { grid?.let { cameraX = (cameraX + dx / 100.0).coerceIn(0.0, (it.width).toDouble()) cameraY = (cameraY + dy / 100.0).coerceIn(0.0, (it.height).toDouble()) } } fun scaleCamera(factor: Double) { val maxBreadth = grid?.width?.toDouble() ?: 0.0 cameraBreadth = (cameraBreadth / factor).coerceIn(5.0, maxBreadth) } /** * Flag and @return the tile at (@param x, @param y). */ fun flagAt(x: Int, y: Int): Tile? = grid?.let { if (it.valid(x, y)) { it.flag(x, y) return it[x, y] } else { return null } } private fun atlasRect(entry: AtlasEntry): Rect { val atlasWidth = 3 val x = (entry.ordinal % atlasWidth) * tileWidth val y = (entry.ordinal / atlasWidth) * tileWidth return Rect(x, y, x + tileWidth, y + tileWidth) } private fun drawTile(canvas: Canvas, tile: Tile, x: Int, y: Int) { val tileSpacing = 2.0 / 27.0 val xSpacing = (scaledTileWidth * tileSpacing).toInt() val ySpacing = (scaledTileWidth * tileSpacing).toInt() val dst = Rect( x + xSpacing, y + ySpacing, x + scaledTileWidth - xSpacing, y + scaledTileWidth - ySpacing ) if (tile.masked) { canvas.drawBitmap(atlas, atlasRect(AtlasEntry.MASKED), dst, null) if (tile.flagged) { canvas.drawBitmap(atlas, atlasRect(AtlasEntry.FLAG), dst, null) } } else { canvas.drawBitmap(atlas, atlasRect(AtlasEntry.UNMASKED), dst, null) val overlay = when { tile.mine -> AtlasEntry.MINE tile.adjacentMines == 1 -> AtlasEntry.ONE tile.adjacentMines == 2 -> AtlasEntry.TWO tile.adjacentMines == 3 -> AtlasEntry.THREE tile.adjacentMines == 4 -> AtlasEntry.FOUR tile.adjacentMines == 5 -> AtlasEntry.FIVE tile.adjacentMines == 6 -> AtlasEntry.SIX tile.adjacentMines == 7 -> AtlasEntry.SEVEN tile.adjacentMines == 8 -> AtlasEntry.EIGHT else -> AtlasEntry.BLANK } canvas.drawBitmap(atlas, atlasRect(overlay), dst, null) } } private fun drawGrid(canvas: Canvas, grid: Grid) { // The state maintained for drawing the view is a position, (`cameraX`, // `cameraY`), representing the centroid of a rectangle whose sides are // of length `cameraBreadthX` and `cameraBreadthY`. Only the tiles // intersecting this rectangle are drawn. // // +============+ // / | | // | | | // | | | // cameraBreadthY < | x | // | | \__________ (cameraX, cameraY) // | | | // \ | | // +============+ // \_____ ______/ // v // cameraBreadthX for (x in 0 until grid.width) { for (y in 0 until grid.height) { drawTile( canvas, grid[x, y], ((x + cameraBreadthX / 2 - cameraX) * scaledTileWidth).toInt(), ((y + cameraBreadthY / 2 - cameraY) * scaledTileWidth).toInt() ) } } } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) grid?.let { drawGrid(canvas, it) } } }