// 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 java.util.Random data class Tile(val adjacentMines: Int = 0, val mine: Boolean = false, val masked: Boolean = true) /** * The Minesweeper "grid", containing instances of [Tile]. */ class Grid(val width: Int, val height: Int, val tiles: Array) { constructor(width: Int = 8, height: Int = 8, mines: Int = 10) : this(width, height, Array(width * height) { Tile() }) { for (i in 0 until mines) { // No access to kotlin.random.Random, so we have to use java.util.Random val x = Random().nextInt(width) val y = Random().nextInt(height) placeMine(x, y) } } private fun index(x: Int, y: Int) = y * width + x 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. * * @throws IllegalArgumentException if the X coordinate is outside * the range of [0, width), or if the Y coordinate is outside the * range of [0, height). */ operator fun get(x: Int, y: Int) = if (valid(x, y)) { tiles[index(x, y)] } else { throw IllegalArgumentException("Invalid coordinates (${x}, ${y})") } /** * Places a mine at the given coordinates. * * @throws IllegalArgumentException if the X coordinate is outside * the range of [0, width), or if the Y coordinate is outside the * range of [0, height). */ fun placeMine(x: Int, y: Int) { if (!valid(x, y)) { throw IllegalArgumentException("Invalid coordinates (${x}, ${y})") } if (this[x, y].mine) { return; } place(x, y, Tile(mine = true)) val xMin = (x - 1).coerceAtLeast(0) val xMax = (x + 1).coerceAtMost(width - 1) val yMin = (y - 1).coerceAtLeast(0) val yMax = (y + 1).coerceAtMost(height - 1) for (y in yMin..yMax) { for (x in xMin..xMax) { with (tiles[index(x, y)]) { if (!mine) { place(x, y, copy(adjacentMines + 1)) } } } } } /** * Reveals a tile at the given coordinates according to the game rules. * * @throws IllegalArgumentException if the X coordinate is outside * the range of [0, width), or if the Y coordinate is outside the * range of [0, height). */ fun reveal(x: Int, y: Int) { if (!valid(x, y) || !this[x, y].masked) { return; } with (this[x, y]) { place(x, y, copy(masked = false)) if (!mine && adjacentMines == 0) { val xMin = (x - 1).coerceAtLeast(0) val xMax = (x + 1).coerceAtMost(width - 1) val yMin = (y - 1).coerceAtLeast(0) val yMax = (y + 1).coerceAtMost(height - 1) for (y in yMin..yMax) { for (x in xMin..xMax) { reveal(x, y) } } } } } override fun toString(): String = buildString { for (y in 0 until height) { for (x in 0 until width) { val tile = this@Grid[x, y] append(when { tile.masked -> "." tile.mine -> "M" tile.adjacentMines == 0 -> " " else -> tile.adjacentMines.toString() } + " ") } append("\n") } } }