Created
May 8, 2020 06:09
-
-
Save ygrenzinger/c560b32b9ebf2ac3d169a3a18f2e9bb1 to your computer and use it in GitHub Desktop.
Minesweeper project from Jetbrain academy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package minesweeper | |
import java.util.* | |
import kotlin.random.Random | |
enum class Command { | |
MARK, EXPLORE | |
} | |
enum class State { | |
PLAYING, LOST, WON | |
} | |
enum class CellState { | |
MARKED, UNEXPLORED, EXPLORED | |
} | |
sealed class Cell() { | |
abstract val pos: Position | |
var state: CellState = CellState.UNEXPLORED | |
private set | |
fun explored() { | |
state = CellState.EXPLORED | |
} | |
fun switchMark() { | |
state = when (state) { | |
CellState.UNEXPLORED -> CellState.MARKED | |
CellState.MARKED -> CellState.UNEXPLORED | |
else -> CellState.EXPLORED | |
} | |
} | |
} | |
data class Mine(override val pos: Position) : Cell() | |
data class SafeCell(override val pos: Position, val nbOfNearMines: Int) : Cell() | |
typealias Position = Pair<Int, Int> | |
typealias Field = List<List<Cell>> | |
class Minesweeper(private val numberOfMines: Int) { | |
private val size = 9 | |
private var state = State.PLAYING | |
private var firstExploration = true | |
private val minePositions: MutableSet<Position> | |
private var field: Field = listOf() | |
init { | |
this.minePositions = randomMinePositions(numberOfMines) | |
initField() | |
} | |
private fun randomMinePositions(numberOfMines: Int): MutableSet<Position> { | |
return (1..numberOfMines).fold(setOf<Position>()) { acc, _ -> | |
var newPos = randomMinePosition() | |
while (newPos in acc) newPos = randomMinePosition() | |
acc + newPos | |
}.toMutableSet() | |
} | |
private fun nbOfNearMines(pos: Position): Int { | |
return neighbours(pos).count { it in minePositions } | |
} | |
private fun neighbours(pos: Position) = (-1..1).flatMap { i -> | |
(-1..1).map { j -> | |
Position(pos.first + i, pos.second + j) | |
} | |
} | |
// private fun euclidianDistance(a: Position, b: Position): Int { | |
// return sqrt((a.first - b.first).toDouble().pow(2) + (a.second - b.second).toDouble().pow(2)).roundToInt() | |
// } | |
private fun randomMinePosition() = Pair(Random.nextInt(size), Random.nextInt(size)) | |
private fun initField() { | |
val markedCells = markedCells() | |
field = (0 until size).map { row -> | |
(0 until size).map { column -> | |
val pos = Pair(row, column) | |
val cell = if (minePositions.contains(pos)) { | |
Mine(pos) | |
} else { | |
SafeCell(pos, nbOfNearMines(pos)) | |
} | |
if (pos in markedCells) { | |
cell.switchMark() | |
} | |
cell | |
} | |
} | |
} | |
private fun symbolOf(cell: Cell): String { | |
return when { | |
state == State.LOST && cell is Mine -> "X" | |
cell.state == CellState.EXPLORED && cell is SafeCell -> if (cell.nbOfNearMines == 0) { | |
"/" | |
} else { | |
cell.nbOfNearMines.toString() | |
} | |
cell.state == CellState.MARKED -> "*" | |
else -> "." | |
} | |
} | |
fun display() { | |
val separator = "—|" + (1..size).joinToString("") { "—" } + "|" | |
println(" |" + (1..size).joinToString("") { it.toString() } + "|") | |
println(separator) | |
field.forEachIndexed { i, row -> | |
println((i + 1).toString() + "|" + row.joinToString("") { symbolOf(it) } + "|") | |
} | |
println(separator) | |
} | |
private fun mark(pos: Position) { | |
cellAt(pos)?.also { | |
it.switchMark() | |
val markedPositions = markedCells() | |
state = if (minePositions == markedPositions) { | |
State.WON | |
} else { | |
State.PLAYING | |
} | |
} | |
} | |
private fun explore(pos: Position) { | |
manageFirstExploration(pos) | |
cellAt(pos)?.also { | |
state = if (it is Mine) { | |
it.explored() | |
State.LOST | |
} else { | |
exploreFill(pos, setOf()) | |
State.PLAYING | |
} | |
} | |
} | |
private fun manageFirstExploration(pos: Position) { | |
if (firstExploration) { | |
if (pos in minePositions) { | |
var newPos = randomMinePosition() | |
while (newPos in minePositions) newPos = randomMinePosition() | |
minePositions.remove(pos) | |
minePositions.add(newPos) | |
initField() | |
} | |
firstExploration = false | |
} | |
} | |
private fun exploreFill(pos: Position, alreadyExplored: Set<Position>): Set<Position> { | |
if (alreadyExplored.contains(pos)) return alreadyExplored | |
return cellAt(pos)?.let { | |
it.explored() | |
var newlyExplored = alreadyExplored + pos | |
if (it is SafeCell && it.nbOfNearMines == 0) { | |
newlyExplored = newlyExplored + exploreFill(Position(pos.first + 1, pos.second + 1), newlyExplored) | |
newlyExplored = newlyExplored + exploreFill(Position(pos.first + 1, pos.second), newlyExplored) | |
newlyExplored = newlyExplored + exploreFill(Position(pos.first + 1, pos.second - 1), newlyExplored) | |
newlyExplored = newlyExplored + exploreFill(Position(pos.first - 1, pos.second + 1), newlyExplored) | |
newlyExplored = newlyExplored + exploreFill(Position(pos.first - 1, pos.second), newlyExplored) | |
newlyExplored = newlyExplored + exploreFill(Position(pos.first - 1, pos.second - 1), newlyExplored) | |
newlyExplored = newlyExplored + exploreFill(Position(pos.first, pos.second + 1), newlyExplored) | |
newlyExplored = newlyExplored + exploreFill(Position(pos.first, pos.second - 1), newlyExplored) | |
newlyExplored = newlyExplored + exploreFill(Position(pos.first, pos.second - 1), newlyExplored) | |
} | |
newlyExplored | |
} ?: alreadyExplored | |
} | |
/* | |
Flood-fill (node, target-color, replacement-color): | |
1. If target-color is equal to replacement-color, return. | |
2. ElseIf the color of node is not equal to target-color, return. | |
3. Else Set the color of node to replacement-color. | |
4. Perform Flood-fill (one step to the south of node, target-color, replacement-color). | |
Perform Flood-fill (one step to the north of node, target-color, replacement-color). | |
Perform Flood-fill (one step to the west of node, target-color, replacement-color). | |
Perform Flood-fill (one step to the east of node, target-color, replacement-color). | |
5. Return. | |
*/ | |
private fun cellAt(pos: Position): Cell? { | |
if (isOutOfIndex(pos)) return null | |
return field[pos.first][pos.second] | |
} | |
private fun isOutOfIndex(pos: Position): Boolean { | |
return isOutOfIndex(pos.first) || isOutOfIndex(pos.second) | |
} | |
private fun isOutOfIndex(index: Int): Boolean { | |
return index < 0 || index >= size | |
} | |
private fun markedCells() = | |
field.mapIndexed { i, row -> | |
row.mapIndexed { j, cell -> if (cell.state == CellState.MARKED) Pair(i, j) else null }.filterNotNull() | |
}.flatten().toSet() | |
companion object { | |
private fun from(input: String): Pair<Command, Pair<Int, Int>> { | |
val splited = input.split(" ") | |
val command = when (splited[2]) { | |
"free" -> Command.EXPLORE | |
else -> Command.MARK | |
} | |
return Pair(command, Pair(splited[1].toInt() - 1, splited[0].toInt() - 1)) | |
} | |
fun play(scanner: Scanner) { | |
print("How many mines do you want on the field? ") | |
val nbOfMines = scanner.nextLine().toInt() | |
val minesweeper = Minesweeper(nbOfMines) | |
minesweeper.display() | |
while (minesweeper.state == State.PLAYING) { | |
print("Set/unset mines marks or claim a cell as free: ") | |
val input = scanner.nextLine() | |
if (input.isBlank()) continue | |
val command = from(input) | |
if (command.first == Command.EXPLORE) { | |
minesweeper.explore(command.second) | |
} else { | |
minesweeper.mark(command.second) | |
} | |
minesweeper.display() | |
} | |
if (minesweeper.state == State.WON) { | |
println("Congratulations! You found all mines!") | |
} else { | |
println("You stepped on a mine and failed!") | |
} | |
} | |
} | |
} | |
fun main() { | |
val scanner = Scanner(System.`in`) | |
Minesweeper.play(scanner) | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment