Generate game
This commit is contained in:
@@ -1,27 +1,39 @@
|
||||
package domain
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class Game(
|
||||
val grid: Grid,
|
||||
val rules: List<GameRule>
|
||||
val clues: List<Clue>
|
||||
) {
|
||||
val horizontalRules = rules.filterIsInstance<HorizontalRule>()
|
||||
val verticalRules = rules.filterIsInstance<VerticalRule>()
|
||||
|
||||
val horizontalClues = clues.filterIsInstance<HorizontalClue>()
|
||||
val verticalClues = clues.filterIsInstance<SameRowClue<ItemClass<*>>>()
|
||||
val positionalClues = clues.filterIsInstance<PositionClue<ItemClass<*>>>()
|
||||
|
||||
init {
|
||||
for (position in positionalClues) {
|
||||
val row = grid[position.item.itemType.companion]
|
||||
row.forEachIndexed { index, gameCell ->
|
||||
if (index == position.index) {
|
||||
gameCell.selection = position.item
|
||||
} else {
|
||||
gameCell.options.remove(position.item)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun areCategoriesValid(): Boolean {
|
||||
val usedCategories = mutableSetOf<KClass<out ItemClass>>()
|
||||
val usedCategories = mutableSetOf<ItemClassCompanion<*>>()
|
||||
for (row in grid.rows) {
|
||||
val category = row.first().options.first()::class
|
||||
if (usedCategories.contains(category)) {
|
||||
if (usedCategories.contains(row.category)) {
|
||||
return false
|
||||
}
|
||||
usedCategories.add(category)
|
||||
usedCategories.add(row.category)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun areRulesViolated(): Boolean = rules
|
||||
fun areRulesViolated(): Boolean = clues
|
||||
.map { it.isRuleViolated(grid) }
|
||||
.reduce { a, b -> a || b }
|
||||
}
|
||||
94
composeApp/src/commonMain/kotlin/domain/clues.kt
Normal file
94
composeApp/src/commonMain/kotlin/domain/clues.kt
Normal file
@@ -0,0 +1,94 @@
|
||||
package domain
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
sealed class Clue {
|
||||
abstract fun isRuleViolated(grid: Grid): Boolean
|
||||
}
|
||||
|
||||
sealed class HorizontalClue : Clue()
|
||||
|
||||
class NeighbourClue<C:ItemClass<C>>(val a: Item<C>, val b: Item<C>) : HorizontalClue() {
|
||||
private val aType = a.itemType
|
||||
private val bType = b.itemType
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val ia = grid.indexOf(aType)
|
||||
val ib = grid.indexOf(bType)
|
||||
|
||||
if (ia == -1 || ib == -1) return false
|
||||
|
||||
return abs(ia - ib) != 1
|
||||
}
|
||||
}
|
||||
|
||||
class OrderClue<C:ItemClass<C>>(val left: Item<C>, val right: Item<C>) : HorizontalClue() {
|
||||
private val leftType = left.itemType
|
||||
private val rightType = right.itemType
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val il = grid.indexOf(leftType)
|
||||
val ir = grid.indexOf(rightType)
|
||||
|
||||
if (il == -1 || ir == -1) return false
|
||||
|
||||
return ir <= il
|
||||
}
|
||||
}
|
||||
|
||||
class TripletClue<C:ItemClass<C>>(val a: Item<C>, val b: Item<C>, val c: Item<C>) : HorizontalClue() {
|
||||
private val aType = a.itemType
|
||||
private val bType = b.itemType
|
||||
private val cType = c.itemType
|
||||
|
||||
private fun isNeighbourRuleViolated(ix: Int, iy: Int): Boolean {
|
||||
if (ix == -1 || iy == -1) return false
|
||||
return abs(ix - iy) != 1
|
||||
}
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val ia = grid.indexOf(aType)
|
||||
val ib = grid.indexOf(bType)
|
||||
val ic = grid.indexOf(cType)
|
||||
|
||||
if (ia == -1 && ic == -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (ia == ib) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isNeighbourRuleViolated(ia, ib) || isNeighbourRuleViolated(ib, ic)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class SameRowClue<C:ItemClass<C>>(val a: Item<C>, val b: Item<C>) : Clue() {
|
||||
private val aType = a.itemType
|
||||
private val bType = b.itemType
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val ia = grid.indexOf(aType)
|
||||
val ib = grid.indexOf(bType)
|
||||
|
||||
if (ia == -1 || ib == -1) return false
|
||||
|
||||
return ia != ib
|
||||
}
|
||||
}
|
||||
|
||||
class PositionClue<C:ItemClass<C>>(val item: Item<C>, val index: Int) : Clue() {
|
||||
private val aType = item.itemType
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val ia = grid.indexOf(aType)
|
||||
|
||||
if (ia == -1) return false
|
||||
|
||||
return ia != index
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,172 @@
|
||||
package domain
|
||||
|
||||
fun generateGame(size: Int = 6): Game {
|
||||
// Here's a simple algorithm making use of your solver:
|
||||
// 0. Select $size classes and $size items per class.
|
||||
// 1. Generate a random puzzle instance.
|
||||
import domain.PuzzleSolution.MULTIPLE_SOLUTIONS
|
||||
import domain.PuzzleSolution.NO_SOLUTION
|
||||
import domain.PuzzleSolution.SOLVABLE
|
||||
import kotlin.random.Random
|
||||
|
||||
fun generateGame(size: Int = 6): Game {
|
||||
// Generate a random puzzle instance.
|
||||
val classes = ItemClass.randomClasses(size)
|
||||
|
||||
val grid: List<List<Item<ItemClass<*>>>> = classes.map { it ->
|
||||
it.randomItems(size).map { item -> Item(item) }
|
||||
}
|
||||
|
||||
// Build a set C of all possible clues that pertain to this puzzle instance.
|
||||
// (There are a finite and in fact quite small number of possible clues: for example
|
||||
// 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.)
|
||||
var clues = getAllClues(grid).shuffled()
|
||||
|
||||
var i = 0
|
||||
|
||||
// If i >= n, we are done. The set of clues is minimal.
|
||||
while (i < clues.size) {
|
||||
// Run your solver on the reduced set of clues and count the number of possible solutions.
|
||||
val temp = clues - clues[i]
|
||||
if (solve(grid.toGrid(), temp) == SOLVABLE) {
|
||||
// If there is exactly one solution, update clues and reset index.
|
||||
clues = temp
|
||||
i = 0
|
||||
}
|
||||
i++
|
||||
}
|
||||
return Game(grid.toGrid(), clues)
|
||||
|
||||
// 2. Build a set C of all possible clues that pertain to this puzzle instance. (There are a finite and in fact quite small number of possible clues: for example 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.)
|
||||
// 3. Pick a random permutation c1, c2, ..., cn of the clues in C.
|
||||
// 4. Set i = 1.
|
||||
// 5. If i > n, we are done. The set C of clues is minimal.
|
||||
// 6. Let D = C − { ci }. Run your solver on the set D of clues and count the number of possible solutions.
|
||||
// 7. If there is exactly one solution, set C = D.
|
||||
// 8. Set i = i + 1 and go back to step 5.
|
||||
// (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.)
|
||||
TODO()
|
||||
}
|
||||
|
||||
private fun solve(grid: Grid, clues: List<Clue>): PuzzleSolution {
|
||||
// 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.
|
||||
val positionClues = clues.filterIsInstance<PositionClue<ItemClass<*>>>().toSet()
|
||||
positionClues.forEach { position ->
|
||||
val row = grid[position.item.itemType.companion]
|
||||
row.forEachIndexed { index, gameCell ->
|
||||
if (index == position.index) {
|
||||
gameCell.selection = position.item
|
||||
} else {
|
||||
gameCell.options.remove(position.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Repeat until no more items can be removed.
|
||||
val otherClues = clues - positionClues
|
||||
var removedOptions = false
|
||||
do {
|
||||
grid.forEach { row ->
|
||||
removedOptions = row.tryOptionsForClues(grid, otherClues) || removedOptions
|
||||
}
|
||||
} while (removedOptions)
|
||||
|
||||
// If any cell has no items left, the puzzle has no solution.
|
||||
grid.flatMap { it }.forEach { cell ->
|
||||
if (cell.options.isEmpty()) {
|
||||
return NO_SOLUTION
|
||||
}
|
||||
}
|
||||
// If all cells have exactly one item left, the puzzle is solved.
|
||||
if (grid.flatMap { it }.all { it.selection != null }) return SOLVABLE
|
||||
|
||||
// If there are still cells with multiple items, pick one and try each item in turn, then go back to step 2.
|
||||
// TODO: Does this need to be implemented? We would need to try all possible options to check if there are multiple solutions.
|
||||
return MULTIPLE_SOLUTIONS
|
||||
}
|
||||
|
||||
private enum class PuzzleSolution {
|
||||
NO_SOLUTION,
|
||||
SOLVABLE,
|
||||
MULTIPLE_SOLUTIONS
|
||||
}
|
||||
|
||||
fun <C : ItemClass<C>> GameRow<C>.tryOptionsForClues(grid: Grid, clues: List<Clue>): Boolean {
|
||||
var removedOptions = false
|
||||
filter { cell -> cell.selection == null }.forEach { cell ->
|
||||
val removed = cell.options.removeIf { option ->
|
||||
cell.selection = option
|
||||
clues.any { it.isRuleViolated(grid) }
|
||||
}
|
||||
cell.selection = null
|
||||
if (removed) {
|
||||
cleanupOptions(cell)
|
||||
}
|
||||
removedOptions = removedOptions || removed
|
||||
}
|
||||
return removedOptions
|
||||
}
|
||||
|
||||
fun <C : ItemClass<C>> GameRow<C>.cleanupOptions(cell: GameCell<C>) {
|
||||
if (cell.options.size == 1) {
|
||||
cell.selection = cell.options.first()
|
||||
forEach { otherCell ->
|
||||
if (otherCell != cell && otherCell.selection == null) {
|
||||
otherCell.options.remove(cell.selection)
|
||||
cleanupOptions(otherCell)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableList<Clue> {
|
||||
val clues = mutableListOf<Clue>()
|
||||
// For optimization reasons we want the positional clues first
|
||||
rows.forEach { row ->
|
||||
row.forEachIndexed { i, item ->
|
||||
clues.add(PositionClue(item, i))
|
||||
}
|
||||
}
|
||||
rows.forEachIndexed { i, columns ->
|
||||
columns.forEachIndexed { j, item ->
|
||||
// Clue: Neighbours
|
||||
if (j > 0) {
|
||||
rows.map { it[j - 1] }.forEach {
|
||||
// We don't want to give away the order with this clue,
|
||||
// but it needs to be displayed consistently.
|
||||
if (Random.nextBoolean()) {
|
||||
clues.add(NeighbourClue(item, it))
|
||||
} else {
|
||||
clues.add(NeighbourClue(it, item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clue: Order
|
||||
if (j > 0) {
|
||||
rows.flatMap { it.take(j - 1) }.forEach {
|
||||
clues.add(OrderClue(it, item))
|
||||
}
|
||||
}
|
||||
|
||||
// Clue: Triplet
|
||||
if (j > 1) {
|
||||
val lefts = rows.map { it[j - 2] }
|
||||
val middles = rows.map { it[j - 1] }
|
||||
|
||||
lefts.forEach { left ->
|
||||
middles.forEach { middle ->
|
||||
// We don't want to give away the order with this clue,
|
||||
// but it needs to be displayed consistently.
|
||||
if (Random.nextBoolean()) {
|
||||
clues.add(TripletClue(left, middle, item))
|
||||
} else {
|
||||
clues.add(TripletClue(item, middle, left))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clue: Same Column
|
||||
rows.map { it[j] }.forEach {
|
||||
if (it != item) {
|
||||
clues.add(SameRowClue(item, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return clues
|
||||
}
|
||||
@@ -1,21 +1,56 @@
|
||||
package domain
|
||||
|
||||
class GameRow<C : ItemClass>(
|
||||
val category: C,
|
||||
class GameRow<C : ItemClass<C>>(
|
||||
val category: ItemClassCompanion<C>,
|
||||
val options: List<Item<C>>,
|
||||
val cells: List<GameCell<C>>
|
||||
) : List<GameCell<C>> by cells
|
||||
|
||||
class Grid(
|
||||
val rows: List<GameRow<*>>
|
||||
) : List<GameRow<*>> by rows {
|
||||
fun indexOf(element: ItemClass): Int {
|
||||
val row = rows.first { it.category == element::class }
|
||||
return row.indexOfFirst { it.selection == element }
|
||||
val rows: List<GameRow<ItemClass<*>>>
|
||||
) : List<GameRow<ItemClass<*>>> by rows {
|
||||
|
||||
fun <C: ItemClass<C>> indexOf(element: C): Int {
|
||||
return this[element.companion]
|
||||
.indexOfFirst { it.selection?.itemType == element }
|
||||
}
|
||||
|
||||
operator fun <C: ItemClass<C>> get(itemType: ItemClassCompanion<C>): GameRow<C> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return rows.first { it.category == itemType } as GameRow<C>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GameCell<C:ItemClass>(
|
||||
var selection: C?,
|
||||
val solution: C,
|
||||
val options: List<C>
|
||||
fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid(
|
||||
map { row ->
|
||||
GameRow(
|
||||
row.first().itemType.companion,
|
||||
row,
|
||||
row.map { GameCell(selection = null, solution = it, options = row.toMutableList()) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
class GameCell<C : ItemClass<C>>(
|
||||
var selection: Item<C>?,
|
||||
val solution: Item<C>,
|
||||
val options: MutableList<Item<C>>
|
||||
)
|
||||
|
||||
class Item<C : ItemClass<C>>(
|
||||
val itemType: C,
|
||||
val symbol: String
|
||||
) {
|
||||
constructor(itemType: C) : this(itemType, itemType.symbols.random())
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Item<*>) return false
|
||||
return itemType == other.itemType
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return itemType.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package domain
|
||||
|
||||
enum class Animals(symbol: String) : ItemClass {
|
||||
enum class Animals(symbol: String) : ItemClass<Animals> {
|
||||
ZEBRA("🦓"),
|
||||
OCTOPUS("🐙"),
|
||||
GOAT("🐐"),
|
||||
@@ -10,9 +10,16 @@ enum class Animals(symbol: String) : ItemClass {
|
||||
ANT("🐜");
|
||||
|
||||
override val symbols: Array<String> = arrayOf(symbol)
|
||||
|
||||
override val companion
|
||||
get() = Animals
|
||||
|
||||
companion object : ItemClassCompanion<Animals> {
|
||||
override val items: List<Animals> = entries
|
||||
}
|
||||
}
|
||||
|
||||
enum class Nationality(symbol: String) : ItemClass {
|
||||
enum class Nationality(symbol: String) : ItemClass<Nationality> {
|
||||
ENGLAND("🇬🇧"),
|
||||
UKRAINE("🇺🇦"),
|
||||
SPAIN("🇪🇸"),
|
||||
@@ -22,9 +29,16 @@ enum class Nationality(symbol: String) : ItemClass {
|
||||
CANADA("🇨🇦");
|
||||
|
||||
override val symbols: Array<String> = arrayOf(symbol)
|
||||
|
||||
override val companion
|
||||
get() = Nationality
|
||||
|
||||
companion object : ItemClassCompanion<Nationality> {
|
||||
override val items: List<Nationality> = entries
|
||||
}
|
||||
}
|
||||
|
||||
enum class Drink(symbol: String) : ItemClass {
|
||||
enum class Drink(symbol: String) : ItemClass<Drink> {
|
||||
MILK("🥛"),
|
||||
WINE("🍷"),
|
||||
COCKTAIL("🍸"),
|
||||
@@ -34,9 +48,16 @@ enum class Drink(symbol: String) : ItemClass {
|
||||
BEVERAGE("🧃");
|
||||
|
||||
override val symbols: Array<String> = arrayOf(symbol)
|
||||
|
||||
override val companion
|
||||
get() = Drink
|
||||
|
||||
companion object : ItemClassCompanion<Drink> {
|
||||
override val items: List<Drink> = entries
|
||||
}
|
||||
}
|
||||
|
||||
enum class Profession(symbol: String) : ItemClass {
|
||||
enum class Profession(symbol: String) : ItemClass<Profession> {
|
||||
ASTRONAUT("\u200D\uD83D\uDE80"),
|
||||
HEALTH_WORKER("\u200D⚕\uFE0F"),
|
||||
FARMER("\u200D\uD83C\uDF3E"),
|
||||
@@ -47,9 +68,16 @@ enum class Profession(symbol: String) : ItemClass {
|
||||
TEACHER("\u200D\uD83C\uDFEB");
|
||||
|
||||
override val symbols: Array<String> = idic(symbol)
|
||||
|
||||
override val companion
|
||||
get() = Profession
|
||||
|
||||
companion object : ItemClassCompanion<Profession> {
|
||||
override val items: List<Profession> = entries
|
||||
}
|
||||
}
|
||||
|
||||
enum class Fruit(symbol: String) : ItemClass {
|
||||
enum class Fruit(symbol: String) : ItemClass<Fruit> {
|
||||
GRAPES("🍇"),
|
||||
WATERMELON("🍉"),
|
||||
LEMON("🍋"),
|
||||
@@ -62,9 +90,16 @@ enum class Fruit(symbol: String) : ItemClass {
|
||||
MANGO("🥭");
|
||||
|
||||
override val symbols: Array<String> = idic(symbol)
|
||||
|
||||
override val companion
|
||||
get() = Fruit
|
||||
|
||||
companion object : ItemClassCompanion<Fruit> {
|
||||
override val items: List<Fruit> = entries
|
||||
}
|
||||
}
|
||||
|
||||
enum class Dessert(symbol: String) : ItemClass {
|
||||
enum class Dessert(symbol: String) : ItemClass<Dessert> {
|
||||
ICE_CREAM("🍨"),
|
||||
DOUGHNUT("🍩"),
|
||||
COOKIE("🍪"),
|
||||
@@ -76,9 +111,16 @@ enum class Dessert(symbol: String) : ItemClass {
|
||||
CUSTARD("🍮");
|
||||
|
||||
override val symbols: Array<String> = idic(symbol)
|
||||
|
||||
override val companion
|
||||
get() = Dessert
|
||||
|
||||
companion object : ItemClassCompanion<Dessert> {
|
||||
override val items: List<Dessert> = entries
|
||||
}
|
||||
}
|
||||
|
||||
enum class Transportation(symbol: String) : ItemClass {
|
||||
enum class Transportation(symbol: String) : ItemClass<Transportation> {
|
||||
BICYCLE("🚲"),
|
||||
MOTOR_SCOOTER("🛵"),
|
||||
SKATEBOARD("🛹"),
|
||||
@@ -88,18 +130,51 @@ enum class Transportation(symbol: String) : ItemClass {
|
||||
BUS("🚌");
|
||||
|
||||
override val symbols: Array<String> = idic(symbol)
|
||||
|
||||
override val companion
|
||||
get() = Transportation
|
||||
|
||||
companion object : ItemClassCompanion<Transportation> {
|
||||
override val items: List<Transportation> = entries
|
||||
}
|
||||
}
|
||||
|
||||
private val GENDERS = arrayOf("\uD83E\uDDD1", "\uD83D\uDC68", "\uD83D\uDC69")
|
||||
private val SKIN_TONES =
|
||||
arrayOf("\uD83C\uDFFB", "\uD83C\uDFFC", "\uD83C\uDFFD", "\uD83C\uDFFE", "\uD83C\uDFFF")
|
||||
|
||||
private fun idic(symbol: String) = Array<String>(GENDERS.size * SKIN_TONES.size) { i ->
|
||||
private fun idic(symbol: String): Array<String> = Array(GENDERS.size * SKIN_TONES.size) { i ->
|
||||
val g = GENDERS[i % GENDERS.size]
|
||||
val t = SKIN_TONES[i / GENDERS.size]
|
||||
g + t + symbol
|
||||
}
|
||||
|
||||
sealed interface ItemClass {
|
||||
sealed interface ItemClass<out SELF : ItemClass<SELF>> {
|
||||
val symbols: Array<String>
|
||||
|
||||
val companion: ItemClassCompanion<SELF>
|
||||
|
||||
companion object {
|
||||
val classes: List<ItemClassCompanion<*>> = listOf(
|
||||
Animals,
|
||||
Nationality,
|
||||
Drink,
|
||||
Profession,
|
||||
Fruit,
|
||||
Dessert,
|
||||
Transportation
|
||||
)
|
||||
|
||||
fun randomClasses(n: Int): List<ItemClassCompanion<*>> {
|
||||
return classes.shuffled().take(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ItemClassCompanion<out C : ItemClass<C>> {
|
||||
val items: List<C>
|
||||
|
||||
fun randomItems(n: Int): List<C> {
|
||||
return items.shuffled().take(n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package domain
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
sealed class GameRule {
|
||||
abstract fun isRuleViolated(grid: Grid): Boolean
|
||||
}
|
||||
|
||||
sealed class HorizontalRule : GameRule()
|
||||
|
||||
class NeighbourRule(val a: ItemClass, val b: ItemClass) : HorizontalRule() {
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val ia = grid.indexOf(a)
|
||||
val ib = grid.indexOf(b)
|
||||
|
||||
if (ia == -1 || ib == -1) return false
|
||||
|
||||
return abs(ia - ib) != 1
|
||||
}
|
||||
}
|
||||
|
||||
class OrderRule(val left: ItemClass, val right: ItemClass) : HorizontalRule() {
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val il = grid.indexOf(left)
|
||||
val ir = grid.indexOf(right)
|
||||
|
||||
if (il == -1 || ir == -1) return false
|
||||
|
||||
return ir <= il
|
||||
}
|
||||
}
|
||||
|
||||
class TripletRule(val a: ItemClass, val b: ItemClass, val c: ItemClass) : HorizontalRule() {
|
||||
private fun isNeighbourRuleViolated(ix: Int, iy: Int): Boolean {
|
||||
if (ix == -1 || iy == -1) return false
|
||||
return abs(ix - iy) != 1
|
||||
}
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val ia = grid.indexOf(a)
|
||||
val ib = grid.indexOf(b)
|
||||
val ic = grid.indexOf(c)
|
||||
|
||||
if (ia == -1 && ic == -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (ia == ib) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isNeighbourRuleViolated(ia, ib) || isNeighbourRuleViolated(ib, ic)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class VerticalRule(val a: ItemClass, val b: ItemClass) : GameRule() {
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val ia = grid.indexOf(a)
|
||||
val ib = grid.indexOf(b)
|
||||
|
||||
if (ia == -1 || ib == -1) return false
|
||||
|
||||
return ia != ib
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,18 @@ package ui
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import domain.Item
|
||||
import domain.ItemClass
|
||||
import domain.ItemCategory
|
||||
import domain.ItemClassCompanion
|
||||
|
||||
@Composable
|
||||
fun Selector(modifier: Modifier = Modifier, category: ItemCategory, selectedItem: ItemClass, onSelectItem: (ItemClass) -> Unit) {
|
||||
fun <C : ItemClass<C>> Selector(
|
||||
modifier: Modifier = Modifier,
|
||||
category: ItemClassCompanion<C>,
|
||||
options: List<Item<C>>,
|
||||
selectedItem: Item<C>,
|
||||
onSelectItem: (Item<C>) -> Unit
|
||||
) {
|
||||
OutlinedCard(modifier = modifier) {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
@file:OptIn(ExperimentalResourceApi::class)
|
||||
package domain
|
||||
|
||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||
//import org.junit.Test
|
||||
import yaep.composeapp.generated.resources.Res
|
||||
import yaep.composeapp.generated.resources.compose_multiplatform
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.feature
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.fail
|
||||
|
||||
class GameTest {
|
||||
|
||||
@Test
|
||||
fun areCategoriesValid() {
|
||||
// Game(
|
||||
// categories = listOf(
|
||||
// category {
|
||||
// item(Res.drawable.compose_multiplatform)
|
||||
// }
|
||||
// ),
|
||||
// Grid(
|
||||
//
|
||||
// )
|
||||
// )
|
||||
fun `ensure generated game is valid`() {
|
||||
val game = generateGame()
|
||||
expect(game) {
|
||||
feature(Game::areCategoriesValid).toEqual(true)
|
||||
feature(Game::areRulesViolated).toEqual(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun areRulesViolated() {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user