// 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,
val flagged: Boolean = false
)
/**
* 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)
}
}
}
}
}
/**
* Mark the tile at the given coordinates as flagged.
*
* @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 flag(x: Int, y: Int) {
if (!valid(x, y) || !this[x, y].masked) {
return;
}
with (this[x, y]) {
place(x, y, copy(flagged = true))
}
}
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")
}
}
}