Render puzzle (WIP)

This commit is contained in:
Christian Basler
2024-06-20 17:37:06 +02:00
parent fcbebe802f
commit 1248288e98
10 changed files with 70 additions and 23 deletions

View File

@@ -0,0 +1,5 @@
package ui.theme
import androidx.compose.ui.text.font.FontFamily
actual val emojiFontFamily: FontFamily? = null

View File

@@ -1,5 +1,7 @@
package domain package domain
import androidx.compose.ui.util.fastAny
class Game( class Game(
val grid: Grid, val grid: Grid,
val clues: List<Clue> val clues: List<Clue>
@@ -18,7 +20,6 @@ class Game(
gameCell.options.remove(position.item) gameCell.options.remove(position.item)
} }
} }
} }
} }
@@ -34,6 +35,5 @@ class Game(
} }
fun areRulesViolated(): Boolean = clues fun areRulesViolated(): Boolean = clues
.map { it.isRuleViolated(grid) } .any { it.isRuleViolated(grid) }
.reduce { a, b -> a || b }
} }

View File

@@ -9,7 +9,7 @@ fun generateGame(size: Int = 6): Game {
// Generate a random puzzle instance. // Generate a random puzzle instance.
val classes = ItemClass.randomClasses(size) val classes = ItemClass.randomClasses(size)
val grid: List<List<Item<ItemClass<*>>>> = classes.map { it -> val grid: List<List<Item<ItemClass<*>>>> = classes.map {
it.randomItems(size).map { item -> Item(item) } it.randomItems(size).map { item -> Item(item) }
} }
@@ -18,6 +18,10 @@ fun generateGame(size: Int = 6): Game {
// if there are 5 houses, there are 5 possible clues of the form "Person A lives in // if there are 5 houses, there are 5 possible clues of the form "Person A lives in
// house B", 8 possible clues of the form "Person A lives next to house B", and so on.) // house B", 8 possible clues of the form "Person A lives next to house B", and so on.)
var clues = getAllClues(grid).shuffled() var clues = getAllClues(grid).shuffled()
// var positionClues: MutableSet<PositionClue<out ItemClass<*>>> = grid.flatMap { row ->
// row.mapIndexed { i, item -> PositionClue(item, i) }
// }.toMutableSet()
var i = 0 var i = 0
@@ -37,12 +41,14 @@ fun generateGame(size: Int = 6): Game {
// (You can speed this up by removing clues in batches rather than one at a time, but it makes the algorithm more complicated to describe.) // (You can speed this up by removing clues in batches rather than one at a time, but it makes the algorithm more complicated to describe.)
} }
private fun solve(grid: Grid, clues: List<Clue>): PuzzleSolution { private fun solve(
grid: Grid,
clues: Collection<Clue>
): PuzzleSolution {
// Start with a grid where each cell is a list of all possible items. // Start with a grid where each cell is a list of all possible items.
// First, set the positions of the items that are already known. // First, set the positions of the items that are already known.
val positionClues = clues.filterIsInstance<PositionClue<ItemClass<*>>>().toSet() clues.filterIsInstance<PositionClue<ItemClass<*>>>().forEach { position ->
positionClues.forEach { position ->
val row = grid[position.item.itemType.companion] val row = grid[position.item.itemType.companion]
row.forEachIndexed { index, gameCell -> row.forEachIndexed { index, gameCell ->
if (index == position.index) { if (index == position.index) {
@@ -56,9 +62,10 @@ private fun solve(grid: Grid, clues: List<Clue>): PuzzleSolution {
// For each clue, remove any items that violate the clue. // For each clue, remove any items that violate the clue.
// If any cell has only one item left, remove that item from all other cells. // If any cell has only one item left, remove that item from all other cells.
// Repeat until no more items can be removed. // Repeat until no more items can be removed.
val otherClues = clues - positionClues val otherClues = clues.filter { it !is PositionClue<*> }
var removedOptions = false var removedOptions: Boolean
do { do {
removedOptions = false
grid.forEach { row -> grid.forEach { row ->
removedOptions = row.tryOptionsForClues(grid, otherClues) || removedOptions removedOptions = row.tryOptionsForClues(grid, otherClues) || removedOptions
} }
@@ -112,15 +119,17 @@ fun <C : ItemClass<C>> GameRow<C>.cleanupOptions(cell: GameCell<C>) {
} }
} }
private fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableList<Clue> { private fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableSet<Clue> {
val clues = mutableListOf<Clue>() val clues = mutableSetOf<Clue>()
// For optimization reasons we want the positional clues first // rows.forEach { row ->
rows.forEach { row -> // row.forEachIndexed { i, item ->
row.forEachIndexed { i, item -> // clues.add(PositionClue(item, i))
clues.add(PositionClue(item, i)) // }
} // }
} clues.add(PositionClue(rows.random().first(), 0))
rows.forEachIndexed { i, columns -> clues.add(PositionClue(rows.random()[3], 3))
rows.forEach { columns ->
columns.forEachIndexed { j, item -> columns.forEachIndexed { j, item ->
// Clue: Neighbours // Clue: Neighbours
if (j > 0) { if (j > 0) {

View File

@@ -30,9 +30,9 @@ class Grid(
fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid( fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid(
map { row -> map { row ->
GameRow( GameRow(
row.first().itemType.companion, category = row.first().itemType.companion,
row, options = row,
row.map { GameCell(selection = null, solution = it, options = row.toMutableList()) } cells = row.map { GameCell(selection = null, solution = it, options = row.toMutableList()) }
) )
} }
) )

View File

@@ -23,6 +23,7 @@ import domain.Item
import domain.ItemClass import domain.ItemClass
import domain.ItemClassCompanion import domain.ItemClassCompanion
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
import ui.theme.emojiFontFamily
import kotlin.math.min import kotlin.math.min
@Composable @Composable
@@ -86,7 +87,7 @@ fun <C : ItemClass<C>> DrawItem(
drawText( drawText(
textMeasurer = textMeasurer, textMeasurer = textMeasurer,
text = emoji, text = emoji,
style = TextStyle(fontSize = fontSize), style = TextStyle(fontSize = fontSize, fontFamily = emojiFontFamily ?: TextStyle.Default.fontFamily),
topLeft = offset topLeft = offset
) )
} }

View File

@@ -0,0 +1,5 @@
package ui.theme
import androidx.compose.ui.text.font.FontFamily
expect val emojiFontFamily: FontFamily?

View File

@@ -0,0 +1,14 @@
package ui.theme
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.platform.Font
actual val emojiFontFamily: FontFamily? = FontFamily(
Font(
resource = "NotoColorEmoji-Regular.ttf",
weight = FontWeight.W400,
style = FontStyle.Normal
)
)

View File

@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.3.2" agp = "8.5.0"
android-compileSdk = "34" android-compileSdk = "34"
android-minSdk = "24" android-minSdk = "24"
android-targetSdk = "34" android-targetSdk = "34"

13
proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,13 @@
-libraryjars <java.home>/jmods/java.base.jmod(!**.jar;!module-info.class)
-keepclasseswithmembers public class MainKt {
public static void main(java.lang.String[]);
}
-dontwarn kotlinx.coroutines.debug.*
-keep class kotlin.** { *; }
-keep class kotlinx.coroutines.** { *; }
-keep class org.jetbrains.skia.** { *; }
-keep class org.jetbrains.skiko.** { *; }
-dontwarn kotlinx.datetime.**