diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f04d6a2..9af4f73 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sun Feb 16 00:33:26 CET 2020 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-bin.zip -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/sample-minesweeper/.gitignore b/sample-minesweeper/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample-minesweeper/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample-minesweeper/build.gradle b/sample-minesweeper/build.gradle new file mode 100644 index 0000000..75f5f2b --- /dev/null +++ b/sample-minesweeper/build.gradle @@ -0,0 +1,9 @@ +// Ported from here: https://github.com/soywiz/lunea/tree/master/samples/busca + +apply plugin: "korge" + +korge { + entryPoint = "com.soywiz.korge.samples.minesweeper.main" + jvmMainClassName = "com.soywiz.korge.samples.minesweeper.MainKt" + id = "com.soywiz.samples.minesweeper" +} diff --git a/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/Board.kt b/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/Board.kt new file mode 100644 index 0000000..f2de006 --- /dev/null +++ b/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/Board.kt @@ -0,0 +1,366 @@ +package com.soywiz.korge.samples.minesweeper + +import com.soywiz.klock.* +import com.soywiz.korau.sound.* +import com.soywiz.korge.html.* +import com.soywiz.korge.render.* +import com.soywiz.korge.view.* +import com.soywiz.korim.bitmap.* +import com.soywiz.korio.lang.* +import com.soywiz.korma.random.* +import kotlin.random.* + +// Proceso que se encarga del tablero +class Board( + parent: Container, + val imageset: BmpSlice, + val imagenes: List, + val click: NativeSound, + val boom: NativeSound, + // Se establecen el ancho, el alto y la cantidad de minas + // Características del tablero: ancho, alto, cantidad de minas + var bwidth: Int, + var bheight: Int, + var minas: Int +) : Process(parent) { + // Matriz con el tablero + var board: Array = arrayOf() + // Matriz de máscara (indica que partes del tablero están destapadas) + var mask: Array = arrayOf() + // Matriz de marcado (indica que partes del tablero están marcadas como "posible mina") (click derecho) + var mark: Array = arrayOf() + + // Variables utilizadas para el contador + var stime: DateTime = DateTime.EPOCH + var tstop: DateTime = DateTime.EPOCH + var timeText: Text + + var lastx: Int = 0 + var lasty: Int = 0 + + // Constructor del proceso en el cual se le pasan el ancho, el alto y la cantidad de minas + init { + // Se crea el texto del contador + //timeText = new Text("", 50, 50, Text.Align.center, Text.Align.middle, Color.white, new Font("Arial", 40)); + //timeText = Text("", 50, 50, Text.Align.center, Text.Align.middle, Color.white, Font.fromResource("font.ttf", 40)); + val FONT_HEIGHT = 32.0 + timeText = text("", textSize = FONT_HEIGHT).xy((bwidth * imageset.height) / 2, -FONT_HEIGHT).apply { + //format = Html.Format(align = Html.Alignment.CENTER, size = FONT_HEIGHT.toInt()) + format = Html.Format(align = Html.Alignment.CENTER) + } + // Se pinta el contador como hijo del tablero + //timeText.group.z = this; + // Y se actualiza su texto + updateTimeText() + + // Se centra el tablero en la pantalla + x = Screen.width / 2 - (bwidth * imageset.height) / 2 + y = Screen.height / 2 - (bheight * imageset.height - 10 - FONT_HEIGHT) / 2 + + // Se establecen algunas características del texto, posición, borde y sombra + //timeText.shadow = 5; + //timeText.border = 1; + + // Se reinicia el tablero + clear() + } + + // Destructor, aquí se quita el texto cuando se borra el tablero + override fun onDestroy() { + timeText.removeFromParent() + } + + // Devuelve el tiempo actual (en milisegundos) + val time: DateTime get() = DateTime.now() + + // Resetea el contador + fun resetTimer() { + stime = time + tstop = DateTime.EPOCH + } + + // Para el contador + fun stopTimer() { + tstop = time + } + + // Devuelve el tiempo ha pasado en segundos desde que se inició el contador + val elapsed: Int + get() = run { + var ctime = time + if (tstop != DateTime.EPOCH) ctime = tstop + return (ctime - stime).seconds.toInt() + } + + // Actualiza el texto del contador con el formato %02d:%02d MM:SS + fun updateTimeText() { + timeText.text = "%02d:%02d".format(elapsed / 60, elapsed % 60) + } + + // Función que se encarga de borrar el tablero y crear uno nuevo + fun clear() { + // Ahora que vamos a borrar un nuevo tablero (y que vamos a crear una nueva partida) + // reiniciamos el contador + resetTimer() + + // Creamos las matrices con el tablero, la máscara de visión y la máscara de marcado (de posibles minas) + board = Array(bheight) { IntArray(bwidth) } + mask = Array(bheight) { BooleanArray(bwidth) } + mark = Array(bheight) { BooleanArray(bwidth) } + + // Comprobamos que no se intenten colocar mas minas que posiciones hay, evitando así un bucle infinito + // en realidad solo se colocan como mucho una cantidad de minas igual a las posiciones del tablero - 1 + // para que pueda haber partida (si no se ganaría directamente) + if (minas > bwidth * bheight - 1) minas = bwidth * bheight - 1 + + // Ahora procederemos a colocar las minas en el tablero + for (n in 0 until minas) { + // Declaramos px, py que utilizaremos para almacenar las posiciones temporales de la mina + var px: Int = 0 + var py: Int = 0 + do { + // Obtenemos una posible posición de la mina + px = Random[0, bwidth - 1] + py = Random[0, bheight - 1] + // Comprobamos si en esa posición hay una mina y estaremos buscando posiciones hasta + // que en esa posición no haya mina + } while (board[py][px] == 10) + + // Ahora que sabemos que en esa posición no hay mina, colocamos una + board[py][px] = 10 + } + + // Ahora que hemos colocado las minas, vamos a colocar los números alrededor de ellas + // Esta es una parte interesante del buscaminas, aquí se colcan los numeros alrededor de las minas + + // Nos recorremos el tablero entero + for (y in 0 until bheight) { + for (x in 0 until bwidth) { + // Comprobamos que en esa posición no haya mina, si hay mina, "pasamos", hacemos un continue y seguimos a la siguiente posición + // sin ejecutar lo que viene después + if (board[y][x] == 10) continue + + // Ahora vamos a contar las minas que hay alrededor de esta posición (ya que en esta posición no hay mina y es posible que tengamos + // que poner un número si tiene alguna mina contigua) + var count = 0 + // Recorremos con x1 € [-1,1], y1 € [-1, 1] + for (y1 in -1..+1) { + for (x1 in -1..+1) { + // Ahora x + x1 y y + y1 tomaran posiciones de la matriz contiguas a la posición actual + // empezando por x - 1, y - 1 para acabar en x + 1, y + 1 + // Comprobamos que la posición esté dentro de la matriz, ya que por ejemplo en la posición 0 + // la posición 0 - 1, 0 - 1, sería la -1, -1, que no está dentro de la matriz y si no está dentro + // de los límites de la matriz, pasamos + if (!in_bounds(x + x1, y + y1)) continue + // Si en esta posición contigua hay una mina entonces incrementamos el contador + if (board[y + y1][x + x1] == 10) count++ + } + } + + // Introducimos en el tablero la nueva imagen (puesto que la imagen con 0 posiciones es la 1 y las siguientes + // son 1, 2, 3, 4, 5, 6, 7, 8) ponemos la imagen correspondiente a count + 1 + board[y][x] = count + 1 + } + } + + // Ahora ya tenemos el tablero preparado + } + + // Indica si una posición está dentro de la matriz + fun in_bounds(px: Int, py: Int): Boolean { + // Si la posición es negativa o si la posición está mas a la derecha del ancho del tablero, devuelve false (no está dentro) + if (px < 0 || px >= bwidth) return false + // Si ocurre lo mismo con la posición y, también devolvemos false + if (py < 0 || py >= bheight) return false + // Si no hemos devuelto ya false, quiere decir que la posición si que está dentro del tablero, así que devolvemos true + return true + } + + var fillpos = 0 + + // Rellena una posición (recursivamente; la forma mas clara y sencilla) + suspend fun fill(px: Int, py: Int) { + if (!in_bounds(px, py)) return + if (mask[py][px] || mark[py][px]) return + mask[py][px] = true + + if (fillpos % 7 == 0) audio.play(click) + frame() + fillpos++ + + if (board[py][px] != 1) return + fill(px - 1, py) + fill(px + 1, py) + fill(px, py - 1) + fill(px, py + 1) + fill(px - 1, py - 1) + fill(px + 1, py + 1) + fill(px + 1, py - 1) + fill(px - 1, py + 1) + } + + suspend fun show_board_lose() { + // Subfunción de show_board_lose que se encarga de + // desenmascarar una posición despues de comprobar + // si es correcta + fun unmask(x: Int, y: Int): Boolean { + if (!in_bounds(x, y)) return false + mask[y][x] = true + return true + } + + // Propagación con forma de diamante + var dist = 0 + while (true) { + var drawing = false + + for (n in 0..dist) { + if (unmask(lastx - n + dist, lasty - n)) drawing = true + if (unmask(lastx + n - dist, lasty - n)) drawing = true + if (unmask(lastx - n + dist, lasty + n)) drawing = true + if (unmask(lastx + n - dist, lasty + n)) drawing = true + } + + if (!drawing) break + + dist++ + frame() + //if (dist >= max(width * 2, height * 2)) break; + } + } + + suspend fun show_board_win() { + for (y in 0 until bheight) { + for (x in 0 until bwidth) { + if (board[y][x] == 10) { + mask[y][x] = false + mark[y][x] = true + frame() + } else { + mask[y][x] = true + } + } + } + } + + suspend fun check(px: Int, py: Int): Boolean { + if (!in_bounds(px, py)) return false + + // Guardamos la última posición en la que hicimos click + lastx = px; lasty = py + + // Estamos ante una mina + if (board[py][px] == 10) return true + + // Estamos ante una casilla vacía + if (board[py][px] == 1) { + fps = 140.0 + fillpos = 0 + fill(px, py) + fps = 60.0 + return false + } + + if (!mask[py][px]) { + mask[py][px] = true + audio.play(click) + } + + return false + } + + // Comprueba si el tablero está en un estado en el cual podemos dar por ganada la partida + fun check_win(): Boolean { + var count = 0 + for (y in 0 until bheight) { + for (x in 0 until bwidth) { + if (mask[y][x]) count++ + } + } + + return (count == bwidth * bheight - minas) + } + + // La acción principal redirecciona a la acción de juego + override suspend fun main() = action(::play) + + // La acción principal de juego que se encarga de gestionar los clicks de ratón + suspend fun play() { + while (true) { + //println("Mouse.x: ${Mouse.x}, x=$x") + if (Mouse.x >= x && Mouse.x < x + bwidth * imageset.height) { + if (Mouse.y >= y && Mouse.y < y + bheight * imageset.height) { + val px = ((Mouse.x - x) / imageset.height).toInt() + val py = ((Mouse.y - y) / imageset.height).toInt() + + if (Mouse.released[0]) { + if (!mark[py][px]) { + if (check(px, py)) { + action(::lose) + } else if (check_win()) { + action(::win) + } + } + } else if (Mouse.released[1] || Mouse.released[2]) { + mark[py][px] = !mark[py][px] + } + } + } + + frame() + } + } + + // Acción del tablero que ocurre cuando el jugador ha perdido + suspend fun lose() { + audio.play(boom, 0) + stopTimer() + show_board_lose() + + while (true) { + if (Mouse.left || Mouse.right) { + clear() + for (n in 0 until 10) frame() + action(::play) + } + frame() + } + } + + // Acción del tablero que ocurre cuando el jugador ha ganado + suspend fun win() { + stopTimer() + show_board_win() + + while (true) { + if (Mouse.left || Mouse.right) { + clear() + for (n in 0 until 10) frame() + action(::play) + } + frame() + } + } + + val images = Array(bheight) { py -> + Array(bwidth) { px -> + image(Bitmaps.transparent).xy(px * imageset.height, py * imageset.height).scale(0.9) + } + } + + override fun renderInternal(ctx: RenderContext) { + for (py in 0 until bheight) { + for (px in 0 until bwidth) { + val image = if (!mask[py][px]) { + imagenes[if (mark[py][px]) 11 else 0] + } else { + imagenes[board[py][px]] + } + + images[py][px].texture = image + } + } + + super.renderInternal(ctx) + } +} diff --git a/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/Process.kt b/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/Process.kt new file mode 100644 index 0000000..710784c --- /dev/null +++ b/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/Process.kt @@ -0,0 +1,227 @@ +package com.soywiz.korge.samples.minesweeper + +import com.soywiz.kds.* +import com.soywiz.klock.* +import com.soywiz.kmem.* +import com.soywiz.korau.sound.* +import com.soywiz.korev.* +import com.soywiz.korev.KeysEvents +import com.soywiz.korge.component.* +import com.soywiz.korge.input.* +import com.soywiz.korge.time.* +import com.soywiz.korge.view.* +import com.soywiz.korim.bitmap.* +import com.soywiz.korim.format.* +import com.soywiz.korio.async.* +import com.soywiz.korio.file.std.* +import kotlinx.coroutines.* +import kotlin.reflect.* + +abstract class Process(parent: Container) : Container() { + init { + parent.addChild(this) + } + + override val stage: Stage get() = super.stage!! + val views: Views get() = stage.views + val type: KClass get() = this::class + var fps: Double = 60.0 + + val key get() = stage.views.key + + val Mouse get() = views.mouseV + val Screen get() = views.screenV + val audio get() = views.audioV + + var angle: Double + get() = rotationDegrees + set(value) = run { rotationDegrees = value } + + suspend fun frame() { + //delayFrame() + delay((1.0 / fps).seconds) + } + + fun action(action: KSuspendFunction0) { + throw ChangeActionException(action) + } + + abstract suspend fun main() + + private lateinit var job: Job + + fun destroy() { + removeFromParent() + } + + open fun onDestroy() { + } + + class ChangeActionException(val action: KSuspendFunction0) : Exception() + + init { + job = views.launchAsap { + var action = ::main + while (true) { + try { + action() + break + } catch (e: ChangeActionException) { + action = e.action + } + } + } + + addComponent(object : StageComponent { + override val view: View = this@Process + + override fun added(views: Views) { + //println("added: $views") + } + + override fun removed(views: Views) { + //println("removed: $views") + job.cancel() + onDestroy() + } + }) + } + + fun collision(type: KClass): Boolean { + return false + } +} + +@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") +class extraPropertyFixed(val name: String? = null, val default: () -> T) { + inline operator fun getValue(thisRef: Extra, property: KProperty<*>): T { + if (thisRef.extra == null) thisRef.extra = LinkedHashMap() + return (thisRef.extra!!.getOrPut(name ?: property.name) { default() } as T) + } + + inline operator fun setValue(thisRef: Extra, property: KProperty<*>, value: T): Unit = run { + if (thisRef.extra == null) thisRef.extra = LinkedHashMap() + thisRef.extra!![name ?: property.name] = value as Any? + } +} + +private val Views.componentsInStagePrev by extraPropertyFixed { linkedSetOf() } +private val Views.componentsInStageCur by extraPropertyFixed { linkedSetOf() } +private val Views.componentsInStage by extraPropertyFixed { linkedSetOf() } +private val Views.tempComponents2 by extraPropertyFixed { arrayListOf() } + +fun Views.registerStageComponent() { + onBeforeRender { + componentsInStagePrev.clear() + componentsInStagePrev += componentsInStageCur + componentsInStageCur.clear() + stage.forEachComponent(tempComponents2) { + componentsInStageCur += it + //println("DEMO: $it -- $componentsInStage") + if (it !in componentsInStage) { + componentsInStage += it + it.added(views) + } + } + for (it in componentsInStagePrev) { + if (it !in componentsInStageCur) { + it.removed(views) + } + } + } +} + +/** + * Component with [added] and [removed] methods that are executed + * once the view is going to be displayed, and when the view has been removed + */ +interface StageComponent : Component { + fun added(views: Views) + fun removed(views: Views) +} + +class Key2(val views: Views) { + operator fun get(key: Key): Boolean = views.keysPressed[key] == true +} + + +class MouseV(val views: Views) { + val left: Boolean get() = pressing[0] + val right: Boolean get() = pressing[1] || pressing[2] + val x: Int get() = (views.input.mouse.x / views.stage.scaleX).toInt() + val y: Int get() = (views.input.mouse.y / views.stage.scaleY).toInt() + val pressing = BooleanArray(8) + val pressed = BooleanArray(8) + val released = BooleanArray(8) + val _pressed = BooleanArray(8) + val _released = BooleanArray(8) +} + +class ScreenV(val views: Views) { + val width: Double get() = views.virtualWidth.toDouble() + val height: Double get() = views.virtualHeight.toDouble() +} + +class AudioV(val views: Views) { + fun play(sound: NativeSound, repeat: Int = 0) { + } +} + +val Views.keysPressed by extraPropertyFixed { LinkedHashMap() } +val Views.key by Extra.PropertyThis { Key2(this) } + +val Views.mouseV by Extra.PropertyThis { MouseV(this) } +val Views.screenV by Extra.PropertyThis { ScreenV(this) } +val Views.audioV by Extra.PropertyThis { AudioV(this) } + +fun Views.registerProcessSystem() { + registerStageComponent() + + stage.addEventListener { e -> + when (e.type) { + MouseEvent.Type.MOVE -> Unit + MouseEvent.Type.DRAG -> Unit + MouseEvent.Type.UP -> { + mouseV.pressing[e.button.id] = false + mouseV._released[e.button.id] = true + } + MouseEvent.Type.DOWN -> { + mouseV.pressing[e.button.id] = true + mouseV._pressed[e.button.id] = true + } + MouseEvent.Type.CLICK -> Unit + MouseEvent.Type.ENTER -> Unit + MouseEvent.Type.EXIT -> Unit + MouseEvent.Type.SCROLL -> Unit + } + } + // @TODO: Use onAfterRender + onBeforeRender { + arraycopy(mouseV._released, 0, mouseV.released, 0, 8) + arraycopy(mouseV._pressed, 0, mouseV.pressed, 0, 8) + + mouseV._released.fill(false) + mouseV._pressed.fill(false) + } + stage.addEventListener { e -> + keysPressed[e.key] = e.type == KeyEvent.Type.DOWN + } +} + +suspend fun readImage(path: String) = resourcesVfs[path].readBitmapSlice() +suspend fun readSound(path: String) = resourcesVfs[path].readNativeSoundOptimized() + +// @TODO: Move to KorIM +fun BitmapSlice.split(width: Int, height: Int): List { + val self = this + val nheight = self.height / height + val nwidth = self.width / width + return arrayListOf().apply { + for (y in 0 until nheight) { + for (x in 0 until nwidth) { + add(self.sliceWithSize(x * width, y * height, width, height)) + } + } + } +} + diff --git a/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/RandomLight.kt b/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/RandomLight.kt new file mode 100644 index 0000000..79c597d --- /dev/null +++ b/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/RandomLight.kt @@ -0,0 +1,67 @@ +package com.soywiz.korge.samples.minesweeper + +import com.soywiz.korge.view.* +import com.soywiz.korim.bitmap.* +import com.soywiz.korma.random.* +import kotlin.math.* +import kotlin.random.* + +class RandomLight( + parent: Container, + light: BmpSlice +) : Process(parent) { + var w2: Double = stage.width / 2 + var h2: Double = stage.height / 2 + val random = Random + var sx: Double = 0.0 + var sy: Double = 0.0 + var inca: Double = 0.0 + var incs: Double = 0.0 + var excx: Double = 0.0 + var excy: Double = 0.0 + + init { + image(light, 0.5, 0.5).apply { + this.blendMode = BlendMode.ADD + } + } + + override suspend fun main() { + sx = random[-w2, w2] + sy = random[-h2, h2] + inca = random[0.0001, 0.03] + incs = random[0.5, 2.0] + excx = random[0.7, 1.3] + excy = random[0.7, 1.3] + alpha = random[0.4, 0.7] + alpha = 0.1 + + while (true) { + angle += inca + x = w2 - cos(angle) * w2 * excx + sx + y = h2 - sin(angle) * h2 * excy + sy + scale = 1 + (cos(angle) / 6) * incs + + // Comprueba si una esfera de luz ha chocado con otra + // El sistema de colisión por defecto es inner circle + if (this.collision(this.type)) { + if (alpha <= 0.8) alpha += 0.01 + } else { + if (alpha >= 0.1) alpha -= 0.01 + } + + frame() + } + } + + suspend fun fadeout() { + while (alpha > 0) { + alpha -= 0.1 + frame() + } + } + + fun draw() { + //graph.draw(__x, __y, alpha, 0, size, alpha, alpha, alpha, GL_ONE, GL_ONE); + } +} diff --git a/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/main.kt b/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/main.kt new file mode 100644 index 0000000..e7648c9 --- /dev/null +++ b/sample-minesweeper/src/commonMain/kotlin/com/soywiz/korge/samples/minesweeper/main.kt @@ -0,0 +1,48 @@ +package com.soywiz.korge.samples.minesweeper + +import com.soywiz.korev.* +import com.soywiz.korge.* +import com.soywiz.korge.view.* + +// Ported from here: https://github.com/soywiz/lunea/tree/master/samples/busca + +suspend fun main() = Korge(width = 800, height = 600, virtualWidth = 640, virtualHeight = 480, title = "Minesweeper") { + views.registerProcessSystem() + MainProcess(this) +} + +class MainProcess(parent: Container) : Process(parent) { + val lights = arrayListOf() + + override suspend fun main() { + val light = readImage("light.png") + image(readImage("bg.jpg")) + for (n in 0 until 20) { + lights += RandomLight(this, light) + } + + val imageset = readImage("buscaminas.png") + val imagenes = imageset.split(imageset.height, imageset.height) + val click = readSound("click.wav") + val boom = readSound("boom.wav") + + val board = Board(this, imageset, imagenes, click, boom, 22, 15, 40) + + while (true) { + if (key[Key.ESCAPE]) { + error("ESC!") + } + if (key[Key.UP]) { + lights += RandomLight(this, light) + } + if (key[Key.DOWN]) { + if (lights.isNotEmpty()) { + lights.removeAt(lights.size - 1).destroy() + } + } + board.updateTimeText() + frame() + } + } +} + diff --git a/sample-minesweeper/src/commonMain/resources/bg.jpg b/sample-minesweeper/src/commonMain/resources/bg.jpg new file mode 100644 index 0000000..3f81dc3 Binary files /dev/null and b/sample-minesweeper/src/commonMain/resources/bg.jpg differ diff --git a/sample-minesweeper/src/commonMain/resources/bomba.ico b/sample-minesweeper/src/commonMain/resources/bomba.ico new file mode 100644 index 0000000..09dd7cc Binary files /dev/null and b/sample-minesweeper/src/commonMain/resources/bomba.ico differ diff --git a/sample-minesweeper/src/commonMain/resources/boom.wav b/sample-minesweeper/src/commonMain/resources/boom.wav new file mode 100644 index 0000000..e186a4e Binary files /dev/null and b/sample-minesweeper/src/commonMain/resources/boom.wav differ diff --git a/sample-minesweeper/src/commonMain/resources/buscaminas.png b/sample-minesweeper/src/commonMain/resources/buscaminas.png new file mode 100644 index 0000000..20c2ab3 Binary files /dev/null and b/sample-minesweeper/src/commonMain/resources/buscaminas.png differ diff --git a/sample-minesweeper/src/commonMain/resources/click.wav b/sample-minesweeper/src/commonMain/resources/click.wav new file mode 100644 index 0000000..625e91f Binary files /dev/null and b/sample-minesweeper/src/commonMain/resources/click.wav differ diff --git a/sample-minesweeper/src/commonMain/resources/font.ttf b/sample-minesweeper/src/commonMain/resources/font.ttf new file mode 100644 index 0000000..5664f9e Binary files /dev/null and b/sample-minesweeper/src/commonMain/resources/font.ttf differ diff --git a/sample-minesweeper/src/commonMain/resources/light.png b/sample-minesweeper/src/commonMain/resources/light.png new file mode 100644 index 0000000..a053e9a Binary files /dev/null and b/sample-minesweeper/src/commonMain/resources/light.png differ