Restructure project
* use modules instead of source sets
* Kotlin JVM instead of multiplatform (for now) to make things simpler
* Warning: GameCell doesn't use mutableState anymore, this needs to be
fixed for the UI to work!
This commit is contained in:
37
domain/src/main/kotlin/ch/dissem/yaep/domain/Game.kt
Normal file
37
domain/src/main/kotlin/ch/dissem/yaep/domain/Game.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
class Game(
|
||||
val grid: Grid,
|
||||
val clues: List<Clue>
|
||||
) {
|
||||
val horizontalClues = clues.filterIsInstance<HorizontalClue>()
|
||||
val verticalClues = clues.filterIsInstance<SameColumnClue<ItemClass<*>, 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<ItemClassCompanion<*>>()
|
||||
for (row in grid.rows) {
|
||||
if (usedCategories.contains(row.category)) {
|
||||
return false
|
||||
}
|
||||
usedCategories.add(row.category)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun areRulesViolated(): Boolean = clues
|
||||
.any { it.isRuleViolated(grid) }
|
||||
}
|
||||
196
domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt
Normal file
196
domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt
Normal file
@@ -0,0 +1,196 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
sealed class Clue {
|
||||
abstract fun isRuleViolated(grid: Grid): Boolean
|
||||
}
|
||||
|
||||
sealed class HorizontalClue : Clue()
|
||||
|
||||
class NeighbourClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) :
|
||||
HorizontalClue() {
|
||||
private val aType = a.itemType
|
||||
private val bType = b.itemType
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val rowA = grid[aType.companion]
|
||||
val rowB = grid[bType.companion]
|
||||
|
||||
val ia = rowA.indexOf(aType)
|
||||
val ib by lazy { rowB.indexOf(bType) }
|
||||
if (ia != -1) {
|
||||
if (ib != -1) return !(ib == ia - 1 || ib == ia + 1)
|
||||
return !(rowB.getOrNull(ia - 1).mayBe(b) ||
|
||||
rowB.getOrNull(ia + 1).mayBe(b))
|
||||
}
|
||||
|
||||
if (ib != -1) {
|
||||
return !(rowA.getOrNull(ib - 1).mayBe(a) ||
|
||||
rowA.getOrNull(ib + 1).mayBe(a))
|
||||
}
|
||||
|
||||
for (i in 1 until grid.size) {
|
||||
if (rowA[i - 1].mayBe(a) &&
|
||||
rowB[i - 0].mayBe(b)
|
||||
)
|
||||
return false
|
||||
|
||||
if (rowA[i - 0].mayBe(a) &&
|
||||
rowB[i - 1].mayBe(b)
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class OrderClue<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, val right: Item<R>) :
|
||||
HorizontalClue() {
|
||||
private val leftType = left.itemType
|
||||
private val rightType = right.itemType
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val rowLeft = grid[leftType.companion]
|
||||
val rowRight = grid[rightType.companion]
|
||||
|
||||
val iLeft = rowLeft.indexOf(leftType)
|
||||
val iRight by lazy { rowRight.indexOf(rightType) }
|
||||
if (iLeft != -1) {
|
||||
if (iRight != -1) return iRight <= iLeft
|
||||
|
||||
return rowRight.indexOfLast { it.mayBe(right) } in 0..iLeft
|
||||
}
|
||||
|
||||
if (iRight != -1) {
|
||||
return rowLeft.indexOfFirst { it.mayBe(left) } >= iRight
|
||||
}
|
||||
|
||||
return rowLeft.indexOfFirst { it.mayBe(left) } >= rowRight.indexOfLast { it.mayBe(right) }
|
||||
}
|
||||
}
|
||||
|
||||
class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
|
||||
val a: Item<A>,
|
||||
val b: Item<B>,
|
||||
val c: Item<C>
|
||||
) :
|
||||
HorizontalClue() {
|
||||
private val aType = a.itemType
|
||||
private val bType = b.itemType
|
||||
private val cType = c.itemType
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val rowA = grid[aType.companion]
|
||||
val rowB by lazy { grid[bType.companion] }
|
||||
val rowC by lazy { grid[cType.companion] }
|
||||
|
||||
val ia = rowA.indexOf(aType)
|
||||
val ib by lazy { rowB.indexOf(bType) }
|
||||
val ic by lazy { rowC.indexOf(cType) }
|
||||
|
||||
if (ia != -1) {
|
||||
if (ib != -1) {
|
||||
when (ib) {
|
||||
ia - 1 -> {
|
||||
return if (ic != -1) {
|
||||
ic != ia - 2
|
||||
} else {
|
||||
!rowC[ia - 2].mayBe(c)
|
||||
}
|
||||
}
|
||||
|
||||
ia + 1 -> {
|
||||
return if (ic != -1) {
|
||||
ic != ia + 2
|
||||
} else {
|
||||
!rowC[ia + 2].mayBe(c)
|
||||
}
|
||||
}
|
||||
|
||||
else -> return true
|
||||
}
|
||||
}
|
||||
return when (ic) {
|
||||
-1 -> !(rowB.getOrNull(ia - 1).mayBe(b) && rowC.getOrNull(ia - 2).mayBe(c)) &&
|
||||
!(rowB.getOrNull(ia + 1).mayBe(b) && rowC.getOrNull(ia + 2).mayBe(c))
|
||||
|
||||
ia - 2 -> !rowB[ia - 1].mayBe(b)
|
||||
ia + 2 -> !rowB[ia + 1].mayBe(b)
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
if (ib != -1) {
|
||||
if (ib == 0 || ib == rowB.size - 1) return true
|
||||
|
||||
return when (ic) {
|
||||
-1 -> !(rowA.getOrNull(ib - 1).mayBe(a) && rowC.getOrNull(ib + 1).mayBe(c)) &&
|
||||
!(rowA.getOrNull(ib + 1).mayBe(a) && rowC.getOrNull(ib - 1).mayBe(c))
|
||||
|
||||
ib - 1 -> !rowA[ib + 1].mayBe(a)
|
||||
ib + 1 -> !rowA[ib - 1].mayBe(a)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
if (ic != -1) {
|
||||
return !(rowB.getOrNull(ic - 1).mayBe(b) && rowA.getOrNull(ic - 2).mayBe(a)) &&
|
||||
!(rowB.getOrNull(ic + 1).mayBe(b) && rowA.getOrNull(ic + 2).mayBe(a))
|
||||
}
|
||||
|
||||
for (i in 2 until grid.size) {
|
||||
if (rowA[i - 2].mayBe(a) &&
|
||||
rowB[i - 1].mayBe(b) &&
|
||||
rowC[i - 0].mayBe(c)
|
||||
)
|
||||
return false
|
||||
|
||||
if (rowA[i - 0].mayBe(a) &&
|
||||
rowB[i - 1].mayBe(b) &&
|
||||
rowC[i - 2].mayBe(c)
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) : Clue() {
|
||||
private val aType = a.itemType
|
||||
private val bType = b.itemType
|
||||
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val rowA = grid[aType.companion]
|
||||
val rowB = grid[bType.companion]
|
||||
|
||||
val ia = rowA.indexOf(aType)
|
||||
val ib by lazy { rowB.indexOf(bType) }
|
||||
|
||||
if (ia != -1) {
|
||||
return if (ib != -1) {
|
||||
ib != ia
|
||||
} else {
|
||||
!rowB[ia].mayBe(b)
|
||||
}
|
||||
}
|
||||
|
||||
if (ib != -1) {
|
||||
return !rowA[ib].mayBe(a)
|
||||
}
|
||||
|
||||
for (i in 0 until grid.size) {
|
||||
if (rowA[i].mayBe(a) && rowB[i].mayBe(b)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class PositionClue<C : ItemClass<C>>(val item: Item<C>, val index: Int) : Clue() {
|
||||
override fun isRuleViolated(grid: Grid): Boolean {
|
||||
val i = grid.indexOf(item.itemType)
|
||||
if (i != -1) return i != index
|
||||
|
||||
return grid[item].mayBe(item)
|
||||
}
|
||||
}
|
||||
170
domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt
Normal file
170
domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt
Normal file
@@ -0,0 +1,170 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
import ch.dissem.yaep.domain.PuzzleSolution.MULTIPLE_SOLUTIONS
|
||||
import ch.dissem.yaep.domain.PuzzleSolution.NO_SOLUTION
|
||||
import ch.dissem.yaep.domain.PuzzleSolution.SOLVABLE
|
||||
import kotlin.random.Random
|
||||
|
||||
fun generateGame(size: Int = 6): Game {
|
||||
// Generate a random puzzle instance.
|
||||
// val classes = ItemClass.randomClasses(size)
|
||||
val classes = ItemClass.classes.take(size)
|
||||
|
||||
val grid: List<List<Item<ItemClass<*>>>> = classes.map {
|
||||
// it.randomItems(size).map { item -> Item(item) }
|
||||
it.items.take(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).toList() //.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)
|
||||
|
||||
// (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: Collection<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.
|
||||
clues.filterIsInstance<PositionClue<ItemClass<*>>>().forEach { position ->
|
||||
val row = grid[position.item.itemType.companion]
|
||||
val cell = row[position.index]
|
||||
cell.options.retainAll { it == cell.selection }
|
||||
row.cleanupOptions(cell)
|
||||
}
|
||||
|
||||
// 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.filter { it !is PositionClue<*> }
|
||||
var removedOptions: Boolean
|
||||
do {
|
||||
removedOptions = false
|
||||
grid.forEach { row ->
|
||||
removedOptions = row.tryOptionsForClues(grid, otherClues) || removedOptions
|
||||
}
|
||||
} while (removedOptions)
|
||||
|
||||
// If any cell has no items left, the puzzle has no solution.
|
||||
if (grid.flatMap { it }.any { cell -> 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 == null) {
|
||||
cell.selection = cell.options.first()
|
||||
filter { otherCell -> otherCell != cell && otherCell.selection == null }.forEach { otherCell ->
|
||||
otherCell.options.remove(cell.selection)
|
||||
cleanupOptions(otherCell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableSet<Clue> {
|
||||
val clues = mutableSetOf<Clue>()
|
||||
// rows.forEach { row ->
|
||||
// row.forEachIndexed { i, item ->
|
||||
// clues.add(PositionClue(item, i))
|
||||
// }
|
||||
// }
|
||||
|
||||
rows.forEach { row ->
|
||||
row.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) }.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(SameColumnClue(item, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return clues
|
||||
}
|
||||
88
domain/src/main/kotlin/ch/dissem/yaep/domain/grid.kt
Normal file
88
domain/src/main/kotlin/ch/dissem/yaep/domain/grid.kt
Normal file
@@ -0,0 +1,88 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
class GameRow<C : ItemClass<C>>(
|
||||
val category: ItemClassCompanion<C>,
|
||||
val options: List<Item<C>>,
|
||||
val cells: List<GameCell<C>>
|
||||
) : List<GameCell<C>> by cells {
|
||||
fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element }
|
||||
|
||||
fun updateOptions() {
|
||||
val selections = mapNotNull { it.selection }
|
||||
forEach { it.options.removeAll(selections) }
|
||||
}
|
||||
}
|
||||
|
||||
class Grid(
|
||||
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>
|
||||
}
|
||||
|
||||
operator fun <C : ItemClass<C>> get(item: Item<C>): GameCell<C> {
|
||||
return this[item.itemType.companion].first { it.selection == item }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return rows.map { row -> row.map { it.selection?.symbol ?: " " }.joinToString("") }
|
||||
.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid(
|
||||
map { row ->
|
||||
GameRow(
|
||||
category = row.first().itemType.companion,
|
||||
options = row,
|
||||
cells = 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>>
|
||||
)
|
||||
//{
|
||||
// val options = options.toMutableStateList()
|
||||
// var selection by mutableStateOf(selection)
|
||||
//}
|
||||
|
||||
fun <C : ItemClass<C>> GameCell<C>?.mayBe(item: Item<C>) =
|
||||
this != null && (selection == item || (selection == null && options.contains(item)))
|
||||
|
||||
class Item<C : ItemClass<C>>(
|
||||
val itemType: C,
|
||||
val symbol: String = itemType.symbols.random()
|
||||
) {
|
||||
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()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return itemType.toString() + symbol
|
||||
}
|
||||
}
|
||||
180
domain/src/main/kotlin/ch/dissem/yaep/domain/items.kt
Normal file
180
domain/src/main/kotlin/ch/dissem/yaep/domain/items.kt
Normal file
@@ -0,0 +1,180 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
enum class Animals(symbol: String) : ItemClass<Animals> {
|
||||
ZEBRA("🦓"),
|
||||
OCTOPUS("🐙"),
|
||||
GOAT("🐐"),
|
||||
SLOTH("🦥"),
|
||||
DOG("🐕"),
|
||||
SNAIL("🐌"),
|
||||
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<Nationality> {
|
||||
ENGLAND("🇬🇧"),
|
||||
UKRAINE("🇺🇦"),
|
||||
SPAIN("🇪🇸"),
|
||||
NORWAY("🇳🇴"),
|
||||
JAPAN("🇯🇵"),
|
||||
SWITZERLAND("🇨🇭"),
|
||||
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<Drink> {
|
||||
MILK("🥛"),
|
||||
WINE("🍷"),
|
||||
COCKTAIL("🍸"),
|
||||
COFFEE("☕"),
|
||||
TEA("🍵"),
|
||||
BEER("🍺"),
|
||||
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<Profession> {
|
||||
ASTRONAUT("\u200D\uD83D\uDE80"),
|
||||
HEALTH_WORKER("\u200D⚕\uFE0F"),
|
||||
FARMER("\u200D\uD83C\uDF3E"),
|
||||
ROCK_STAR("\u200D\uD83C\uDFA4"),
|
||||
SCIENTIST("\u200D\uD83D\uDD2C"),
|
||||
SOFTWARE_DEV("\u200D\uD83D\uDCBB"),
|
||||
FIREFIGHTER("\u200D\uD83D\uDE92"),
|
||||
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<Fruit> {
|
||||
GRAPES("🍇"),
|
||||
WATERMELON("🍉"),
|
||||
LEMON("🍋"),
|
||||
BANANA("🍌"),
|
||||
PINEAPPLE("🍍"),
|
||||
CHERRIES("🍒"),
|
||||
STRAWBERRY("🍓"),
|
||||
KIWI("🥝"),
|
||||
PEAR("🍐"),
|
||||
MANGO("🥭");
|
||||
|
||||
override val symbols: Array<String> = arrayOf(symbol)
|
||||
|
||||
override val companion
|
||||
get() = Fruit
|
||||
|
||||
companion object : ItemClassCompanion<Fruit> {
|
||||
override val items: List<Fruit> = entries
|
||||
}
|
||||
}
|
||||
|
||||
enum class Dessert(symbol: String) : ItemClass<Dessert> {
|
||||
ICE_CREAM("🍨"),
|
||||
DOUGHNUT("🍩"),
|
||||
COOKIE("🍪"),
|
||||
CAKE("🍰"),
|
||||
CUPCAKE("🧁"),
|
||||
PIE("🥧"),
|
||||
CHOCOLATE("🍫"),
|
||||
LOLLIPOP("🍭"),
|
||||
CUSTARD("🍮");
|
||||
|
||||
override val symbols: Array<String> = arrayOf(symbol)
|
||||
|
||||
override val companion
|
||||
get() = Dessert
|
||||
|
||||
companion object : ItemClassCompanion<Dessert> {
|
||||
override val items: List<Dessert> = entries
|
||||
}
|
||||
}
|
||||
|
||||
enum class Transportation(symbol: String) : ItemClass<Transportation> {
|
||||
BICYCLE("🚲"),
|
||||
MOTOR_SCOOTER("🛵"),
|
||||
SKATEBOARD("🛹"),
|
||||
TAXI("🚕"),
|
||||
LOCOMOTIVE("🚂"),
|
||||
TRAM_CAR("🚋"),
|
||||
BUS("🚌");
|
||||
|
||||
override val symbols: Array<String> = arrayOf(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> = 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<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)
|
||||
}
|
||||
}
|
||||
27
domain/src/test/kotlin/ch/dissem/yaep/domain/ClueTest.kt
Normal file
27
domain/src/test/kotlin/ch/dissem/yaep/domain/ClueTest.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
abstract class ClueTest {
|
||||
protected val size = 6
|
||||
|
||||
protected fun createGrid(
|
||||
selection: (Item<ItemClass<*>>) -> Item<ItemClass<*>>? = { it }
|
||||
) = Grid(
|
||||
ItemClass.randomClasses(size)
|
||||
.map {
|
||||
it.randomItems(size).map { item -> Item(item) }
|
||||
}
|
||||
.map { row ->
|
||||
GameRow(
|
||||
category = row.first().itemType.companion,
|
||||
options = row,
|
||||
cells = row.map {
|
||||
GameCell(
|
||||
selection = selection(it),
|
||||
solution = it,
|
||||
options = mutableListOf()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
45
domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt
Normal file
45
domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.feature
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toBeLessThan
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class GameTest {
|
||||
|
||||
// @Test
|
||||
// fun `try to find the error`() {
|
||||
// val size = 6
|
||||
// val classes = ItemClass.randomClasses(size)
|
||||
//
|
||||
// val grid: List<List<Item<ItemClass<*>>>> = classes.map {
|
||||
// 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.)
|
||||
// val clues = getAllClues(grid).shuffled()
|
||||
//
|
||||
// val gameGrid = grid.toGrid()
|
||||
// gameGrid.flatMap { it }.forEach { it.selection = it.solution }
|
||||
//
|
||||
// expect(clues).toHaveElementsAndAll {
|
||||
// feature { f(it::isRuleViolated, gameGrid) }.toEqual(false)
|
||||
// }
|
||||
// }
|
||||
@Test
|
||||
fun `ensure generated game is valid`() {
|
||||
val game = generateGame()
|
||||
expect(game) {
|
||||
feature(Game::areCategoriesValid).toEqual(true)
|
||||
feature(Game::areRulesViolated).toEqual(false)
|
||||
feature(Game::clues) {
|
||||
feature(List<Clue>::size).toBeLessThan(30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class NeighbourClueTest : ClueTest() {
|
||||
|
||||
@Test
|
||||
fun `ensure actual neighbours are valid`() {
|
||||
val grid = createGrid()
|
||||
for (ia in 0 until size) {
|
||||
for (ib in 0 until size) {
|
||||
for (j in 1 until size) {
|
||||
val a = grid[ia][j - 1]
|
||||
val b = grid[ib][j]
|
||||
|
||||
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure non-neighbours are invalid`() {
|
||||
val grid = createGrid()
|
||||
for (ia in 0 until size) {
|
||||
for (ib in 0 until size) {
|
||||
for (ja in 0 until size) {
|
||||
for (jb in 0 until size) {
|
||||
if (ja == jb + 1 || ja == jb - 1) {
|
||||
continue
|
||||
}
|
||||
val a = grid[ia][ja]
|
||||
val b = grid[ib][jb]
|
||||
|
||||
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure grid with one neighbour not set is considered valid if a neighbour is in the options`() {
|
||||
val grid = createGrid()
|
||||
for (ia in 0 until size) {
|
||||
for (ib in 0 until size) {
|
||||
for (j in 1 until size) {
|
||||
val rowA = grid[ia]
|
||||
val rowB = grid[ib]
|
||||
val a = rowA[j - 1]
|
||||
val b = rowB[j]
|
||||
|
||||
rowA.forEach { it.selection = null; it.options.clear() }
|
||||
rowB.forEach { it.selection = null; it.options.clear() }
|
||||
|
||||
a.selection = null
|
||||
a.options.add(a.solution)
|
||||
b.selection = b.solution
|
||||
b.options.clear()
|
||||
|
||||
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
|
||||
a.selection = a.solution
|
||||
a.options.clear()
|
||||
b.selection = null
|
||||
b.options.add(b.solution)
|
||||
|
||||
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
|
||||
if (j < size - 1) {
|
||||
val notA = rowA[j + 1]
|
||||
|
||||
a.selection = null
|
||||
a.options.clear()
|
||||
notA.options.add(a.solution)
|
||||
b.selection = b.solution
|
||||
b.options.clear()
|
||||
|
||||
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure grid with a and c more than one cell between is not considered valid`() {
|
||||
val grid = createGrid { null }
|
||||
val rowA = grid.random()
|
||||
val rowB = grid.random()
|
||||
val a = rowA[1]
|
||||
val b = rowB[2]
|
||||
|
||||
a.selection = a.solution
|
||||
|
||||
rowB[3].selection = b.solution
|
||||
|
||||
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class OrderClueTest : ClueTest() {
|
||||
@Test
|
||||
fun `ensure items in correct order are valid`() {
|
||||
val grid = createGrid()
|
||||
for (ia in 0 until size) {
|
||||
for (ib in 0 until size) {
|
||||
for (ic in 0 until size) {
|
||||
for (ja in 0 until size - 1) {
|
||||
for (jb in ja + 1 until size) {
|
||||
val a = grid[ia][ja]
|
||||
val b = grid[ib][jb]
|
||||
|
||||
expect(OrderClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class SameColumnClueTest : ClueTest() {
|
||||
@Test
|
||||
fun `ensure fields in the same column are considered valid`() {
|
||||
val grid = createGrid()
|
||||
for (ia in 0 until size - 1) {
|
||||
for (ib in ia + 1 until size) {
|
||||
for (j in 0 until size) {
|
||||
val a = grid[ia][j]
|
||||
val b = grid[ib][j]
|
||||
|
||||
expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure fields in different columns are considered invalid`() {
|
||||
val grid = createGrid()
|
||||
for (ia in 0 until size - 1) {
|
||||
for (ib in ia + 1 until size) {
|
||||
for (ja in 0 until size - 1) {
|
||||
for (jb in ja + 1 until size) {
|
||||
val a = grid[ia][ja]
|
||||
val b = grid[ib][jb]
|
||||
|
||||
expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if a is set, but b is not an option in the same column, it's considered invalid`() {
|
||||
val grid = createGrid { null }
|
||||
|
||||
for (ia in 0 until size - 1) {
|
||||
for (ib in ia + 1 until size) {
|
||||
for (ja in 0 until size - 1) {
|
||||
for (jb in ja + 1 until size) {
|
||||
val a = grid[ia][ja]
|
||||
val b = grid[ib][jb]
|
||||
|
||||
a.selection = a.solution
|
||||
b.selection = null
|
||||
b.options.remove(b.solution)
|
||||
|
||||
expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if there are no options for a and b in the same column, it's considered invalid`() {
|
||||
val grid = createGrid { null }
|
||||
|
||||
val rowA = grid.random()
|
||||
val rowB = grid.filter { it != rowA }.random()
|
||||
|
||||
for (i in 0 until size) {
|
||||
val a = rowA[i]
|
||||
val b = rowB[i]
|
||||
rowA.forEachIndexed { index, gameCell ->
|
||||
if (index < i) {
|
||||
gameCell.options.remove(a.solution)
|
||||
} else {
|
||||
gameCell.options.add(a.solution)
|
||||
}
|
||||
}
|
||||
rowB.forEachIndexed { index, gameCell ->
|
||||
if (index < i) {
|
||||
gameCell.options.add(b.solution)
|
||||
} else {
|
||||
gameCell.options.remove(b.solution)
|
||||
}
|
||||
}
|
||||
|
||||
expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
154
domain/src/test/kotlin/ch/dissem/yaep/domain/TripletClueTest.kt
Normal file
154
domain/src/test/kotlin/ch/dissem/yaep/domain/TripletClueTest.kt
Normal file
@@ -0,0 +1,154 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class TripletClueTest : ClueTest() {
|
||||
|
||||
@Test
|
||||
fun `ensure actual triplets are valid`() {
|
||||
val grid = createGrid()
|
||||
for (ia in 0 until size) {
|
||||
for (ib in 0 until size) {
|
||||
for (ic in 0 until size) {
|
||||
for (j in 2 until size) {
|
||||
val a = grid[ia][j - 2]
|
||||
val b = grid[ib][j - 1]
|
||||
val c = grid[ic][j]
|
||||
|
||||
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure that middle might not be at the corners`() {
|
||||
val grid = createGrid { null }
|
||||
|
||||
val rowA = grid.random()
|
||||
val rowB = grid.random()
|
||||
val rowC = grid.random()
|
||||
val a = rowA[1]
|
||||
val b = rowB[2]
|
||||
val c = rowC[3]
|
||||
|
||||
rowB[0].selection = b.solution
|
||||
|
||||
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
|
||||
rowB[0].selection = null
|
||||
rowB[grid.size - 1].selection = b.solution
|
||||
|
||||
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure grid where a or c and b are not neighbours is invalid`() {
|
||||
val grid = createGrid { null }
|
||||
val ia = 1
|
||||
val rowA = grid[2]
|
||||
val a = rowA[ia]
|
||||
val ib = 2
|
||||
val rowB = grid[0]
|
||||
val b = rowB[ib]
|
||||
val ic = 3
|
||||
val rowC = grid[1]
|
||||
val c = rowC[ic]
|
||||
|
||||
val clue = TripletClue(a.solution, b.solution, c.solution)
|
||||
|
||||
b.selection = b.solution
|
||||
c.options.add(c.solution)
|
||||
|
||||
|
||||
rowA.forEachIndexed { index, notA ->
|
||||
notA.selection = a.solution
|
||||
when {
|
||||
notA == a -> {
|
||||
// ignore
|
||||
}
|
||||
|
||||
index == ic -> {
|
||||
rowC[ia].options.add(c.solution)
|
||||
expect(clue.isRuleViolated(grid)).toEqual(false)
|
||||
}
|
||||
|
||||
else -> {
|
||||
expect(clue.isRuleViolated(grid)).toEqual(true)
|
||||
}
|
||||
}
|
||||
notA.selection = null
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure grid with a and c more than one cell between is considered invalid`() {
|
||||
val grid = createGrid { null }
|
||||
val a = grid[2][1]
|
||||
val b = grid[0][2]
|
||||
val c = grid[1][3]
|
||||
|
||||
a.selection = a.solution
|
||||
grid[1][4].selection = c.solution
|
||||
|
||||
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `grid with a set and b and c as option on the same side is considered valid`() {
|
||||
val grid = createGrid { null }
|
||||
val rowA = grid.rows.random()
|
||||
val rowB = grid.rows.random()
|
||||
val rowC = grid.rows.random()
|
||||
val a = rowA[3]
|
||||
val b = rowB[2]
|
||||
val c = rowC[1]
|
||||
|
||||
a.selection = a.solution
|
||||
b.options.clear()
|
||||
c.options.clear()
|
||||
|
||||
rowB[4].options.add(b.solution)
|
||||
rowC[5].options.add(c.solution)
|
||||
|
||||
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `grid with a set and no b and c as option on the same side is considered invalid`() {
|
||||
val grid = createGrid { null }
|
||||
val a = grid[2][3]
|
||||
val b = grid[0][2]
|
||||
val c = grid[1][1]
|
||||
|
||||
a.selection = a.solution
|
||||
b.options.remove(b.solution)
|
||||
c.options.add(c.solution)
|
||||
|
||||
grid[0][4].options.add(b.solution)
|
||||
grid[1][5].options.remove(c.solution)
|
||||
|
||||
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
|
||||
.toEqual(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user