From ca1a389e97e3eaa17632e419891eb4514ea651e0 Mon Sep 17 00:00:00 2001 From: "Jakob L. Kreuze" Date: Fri, 21 Dec 2018 12:38:37 -0500 Subject: Redesigned the algorithm for drawing a portion of the minesweeper grid --- .../main/java/space/jakob/mines/GameActivity.kt | 103 +++++++++--------- app/src/main/java/space/jakob/mines/GameView.kt | 115 +++++++++++++-------- app/src/main/java/space/jakob/mines/Grid.kt | 7 +- 3 files changed, 132 insertions(+), 93 deletions(-) (limited to 'app') diff --git a/app/src/main/java/space/jakob/mines/GameActivity.kt b/app/src/main/java/space/jakob/mines/GameActivity.kt index 7e8313c..9240dd9 100644 --- a/app/src/main/java/space/jakob/mines/GameActivity.kt +++ b/app/src/main/java/space/jakob/mines/GameActivity.kt @@ -27,61 +27,62 @@ class GameActivity : AppCompatActivity() { // Implements the callbacks for gestureDetector. val gestureListener = object : GestureDetector.SimpleOnGestureListener() { - override fun onDown(e: MotionEvent): Boolean { - with (gameView) { - grid?.let { - val (x, y) = toGridCoordinates(e.x.toInt(), e.y.toInt()) - it.reveal(x, y) - invalidate() - } - } - return false - } - - override fun onScroll(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float): Boolean { - with (gameView) { - cameraX = (cameraX + dx / 100.0).coerceIn(0.0, gameView.maxCameraX) - cameraY = (cameraY + dy / 100.0).coerceIn(0.0, gameView.maxCameraY) - invalidate() - } - return false - } + // TODO: Replace with onUp + // override fun onDown(e: MotionEvent): Boolean { + // with (gameView) { + // grid?.let { + // val (x, y) = toGridCoordinates(e.x.toInt(), e.y.toInt()) + // it.reveal(x, y) + // invalidate() + // } + // } + // return false + // } + + // override fun onScroll(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float): Boolean { + // with (gameView) { + // cameraX = (cameraX + dx / 100.0).coerceIn(0.0, gameView.maxCameraX) + // cameraY = (cameraY + dy / 100.0).coerceIn(0.0, gameView.maxCameraY) + // invalidate() + // } + // return false + // } } // Implements the callbacks for scaleGestureDetector. val scaleListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { - override fun onScale(scaleGestureDetector: ScaleGestureDetector) : Boolean { - with (gameView) { - grid?.let { - val factor = scaleGestureDetector.getScaleFactor() - - var currentDistance = sightX - cameraX - var desiredDistance = currentDistance / factor - var deltaDistance = Math.abs(desiredDistance - currentDistance) / 2 - - if (factor < 1.0) { - cameraX = (cameraX - deltaDistance).coerceAtLeast(0.0) - sightX = (sightX + deltaDistance).coerceAtMost(it.width.toDouble() - cameraX) - } else { - cameraX = cameraX + deltaDistance - sightX = sightX - deltaDistance - } - - currentDistance = sightY - cameraY - desiredDistance = currentDistance / factor - deltaDistance = Math.abs(desiredDistance - currentDistance) / 2 - - if (factor < 1.0) { - cameraY = (cameraY - deltaDistance).coerceAtLeast(0.0) - sightY = (sightY + deltaDistance).coerceAtMost(it.height.toDouble() - cameraY) - } else { - cameraY = cameraY + deltaDistance - sightY = sightY - deltaDistance - } - } - } - return false - } + // override fun onScale(scaleGestureDetector: ScaleGestureDetector) : Boolean { + // with (gameView) { + // grid?.let { + // val factor = scaleGestureDetector.getScaleFactor() + + // var currentDistance = sightX - cameraX + // var desiredDistance = currentDistance / factor + // var deltaDistance = Math.abs(desiredDistance - currentDistance) / 2 + + // if (factor < 1.0) { + // cameraX = (cameraX - deltaDistance).coerceAtLeast(0.0) + // sightX = (sightX + deltaDistance).coerceAtMost(it.width.toDouble() - cameraX) + // } else { + // cameraX = cameraX + deltaDistance + // sightX = sightX - deltaDistance + // } + + // currentDistance = sightY - cameraY + // desiredDistance = currentDistance / factor + // deltaDistance = Math.abs(desiredDistance - currentDistance) / 2 + + // if (factor < 1.0) { + // cameraY = (cameraY - deltaDistance).coerceAtLeast(0.0) + // sightY = (sightY + deltaDistance).coerceAtMost(it.height.toDouble() - cameraY) + // } else { + // cameraY = cameraY + deltaDistance + // sightY = sightY - deltaDistance + // } + // } + // } + // return false + // } } override fun onTouchEvent(event: MotionEvent): Boolean { diff --git a/app/src/main/java/space/jakob/mines/GameView.kt b/app/src/main/java/space/jakob/mines/GameView.kt index 36fe2ec..52a8283 100644 --- a/app/src/main/java/space/jakob/mines/GameView.kt +++ b/app/src/main/java/space/jakob/mines/GameView.kt @@ -22,8 +22,8 @@ import android.util.AttributeSet import android.view.View import android.util.Log -const val atlasWidth = 3 -const val tileSpacing = 2.0 / 27.0 +import kotlin.math.ceil +import kotlin.math.floor enum class AtlasEntry { BLANK, MASKED, UNMASKED, @@ -35,52 +35,47 @@ class GameView(context: Context, attrs: AttributeSet) : View(context, attrs) { val atlas = BitmapFactory.decodeResource(context.resources, R.drawable.atlas); val tileWidth = atlas.width / 3 + // FIXME: Is this correct? `breadth` tiles, excluding halfsies and such? + val scaledTileWidth: Int + get() = (minOf(width, height) / cameraBreadth).toInt() + 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 sightX = 5.0 - var sightY = 5.0 - - val maxCameraX: Double - get() = grid?.let { - it.width - sightX - } ?: 0.0 - val maxCameraY: Double - get() = grid?.let { - it.height - sightY - } ?: 0.0 - - // The size, in pixels, of an individual tile based on the current scale - // factor. - val scaledWidth: Int - get() = width / sightX.toInt() - val scaledHeight: Int - get() = height / sightY.toInt() + var cameraBreadth = 5.0 + // TODO: Break up into multiple expressions. This isn't readable. /** * Returns the grid coordinates for the tile at the given screen coordinates. */ fun toGridCoordinates(x: Int, y: Int) = Pair( - (x + (scaledWidth * (cameraX % 1)).toInt()) / scaledWidth + cameraX.toInt(), - (y + (scaledHeight * (cameraY % 1)).toInt()) / scaledHeight + cameraY.toInt() + (x + (scaledTileWidth * (cameraX % 1)).toInt()) / scaledTileWidth + cameraX.toInt(), + (y + (scaledTileWidth * (cameraY % 1)).toInt()) / scaledTileWidth + cameraY.toInt() ) 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 xSpacing = (scaledWidth * tileSpacing).toInt() - val ySpacing = (scaledWidth * tileSpacing).toInt() + val tileSpacing = 2.0 / 27.0 + + val xSpacing = (scaledTileWidth * tileSpacing).toInt() + val ySpacing = (scaledTileWidth * tileSpacing).toInt() + // TODO: Opportunity for more ASCII art explanations :) val dst = Rect( x + xSpacing, y + ySpacing, - x + scaledWidth - xSpacing, - y + scaledHeight - ySpacing + x + scaledTileWidth - xSpacing, + y + scaledTileWidth - ySpacing ) if (tile.masked) { @@ -106,24 +101,62 @@ class GameView(context: Context, attrs: AttributeSet) : View(context, attrs) { } private fun drawGrid(canvas: Canvas, grid: Grid) { - val startX = cameraX.toInt() // Truncates the fractional component. - val startY = cameraY.toInt() - val howManyX = (sightX.toInt() + 1).coerceAtMost(grid.width - startX - 1) - val howManyY = (sightY.toInt() + 1).coerceAtMost(grid.height - startY - 1) + // 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 + + val cameraBreadthX = if (width > height) { + cameraBreadth * width.toDouble() / height.toDouble() + } else { + cameraBreadth + } + val cameraBreadthY = if (height > width) { + cameraBreadth * height.toDouble() / width.toDouble() + } else { + cameraBreadth + } - for (i in 0..howManyX) { - for (j in 0..howManyY) { - val tile = grid[startX + i, startY + j] + // This is where the fractional component comes into play -- drawing + // tiles that are partially obscured by the limited reaches of the + // camera view. + val offsetX = (scaledTileWidth * ((cameraX + (cameraBreadthX / 2)) % 1)).toInt() + val offsetY = (scaledTileWidth * ((cameraY + (cameraBreadthY / 2)) % 1) / 2).toInt() - val xOffset = (scaledWidth * (cameraX % 1)).toInt() - val yOffset = (scaledHeight * (cameraY % 1)).toInt() + val howManyX = ceil(cameraBreadthX).toInt() + val howManyY = ceil(cameraBreadthY).toInt() - drawTile( - canvas, - tile, - i * scaledWidth - xOffset, - j * scaledHeight - yOffset - ) + val startX = cameraX - (cameraBreadthX / 2) + val startY = cameraY - (cameraBreadthY / 2) + + for (i in 0..howManyX) { + for (j in 0..howManyY) { + // The fractional components here are truncated, giving a + // equivalent indices into `grid`. Of course, these indices + // aren't necessarily within the bounds of the grid. If a tile + // doesn't exist, we'll just draw blank space. + val x = (startX + i.toDouble()).toInt() + val y = (startY + j.toDouble()).toInt() + + if (grid.valid(x, y)) { + val tile = grid[x, y] + val screenX = i * scaledTileWidth + val screenY = j * scaledTileWidth + drawTile(canvas, tile, screenX + offsetX, screenY + offsetY) + } } } } diff --git a/app/src/main/java/space/jakob/mines/Grid.kt b/app/src/main/java/space/jakob/mines/Grid.kt index b61f5f1..2eec411 100644 --- a/app/src/main/java/space/jakob/mines/Grid.kt +++ b/app/src/main/java/space/jakob/mines/Grid.kt @@ -31,11 +31,16 @@ class Grid(val width: Int, val height: Int, val tiles: Array) { } private fun index(x: Int, y: Int) = y * width + x - private fun valid(x: Int, y: Int) = y in 0 until height && x in 0 until width private fun place(x: Int, y: Int, tile: Tile) { tiles[index(x, y)] = tile } + /** + * Returns whether or not @see get on the given index would raise an + * @exception IllegalArgumentException + */ + fun valid(x: Int, y: Int) = y in 0 until height && x in 0 until width + /** * Returns the tile at the given coordinates. * -- cgit v1.3