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:
2024-07-02 10:53:04 +02:00
parent 767dc018c6
commit 1f588e6596
65 changed files with 248 additions and 484 deletions

View 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) }
}

View 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)
}
}

View 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
}

View 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
}
}

View 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)
}
}

View 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()
)
}
)
}
)
}

View 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)
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}
}
}
}
}

View File

@@ -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)
}
}
}

View 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)
}
}