Generate game
This commit is contained in:
@@ -36,14 +36,20 @@ kotlin {
|
|||||||
desktopMain.dependencies {
|
desktopMain.dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
androidNativeTest.dependencies {
|
||||||
// implementation(libs.junit)
|
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
|
implementation(libs.atrium)
|
||||||
|
}
|
||||||
|
commonTest.dependencies {
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
implementation(libs.atrium)
|
||||||
|
|
||||||
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
|
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
|
||||||
implementation(compose.uiTest)
|
implementation(compose.uiTest)
|
||||||
}
|
}
|
||||||
desktopTest.dependencies {
|
desktopTest.dependencies {
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
implementation(libs.atrium)
|
||||||
implementation(compose.desktop.uiTestJUnit4)
|
implementation(compose.desktop.uiTestJUnit4)
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,39 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
class Game(
|
class Game(
|
||||||
val grid: Grid,
|
val grid: Grid,
|
||||||
val rules: List<GameRule>
|
val clues: List<Clue>
|
||||||
) {
|
) {
|
||||||
val horizontalRules = rules.filterIsInstance<HorizontalRule>()
|
val horizontalClues = clues.filterIsInstance<HorizontalClue>()
|
||||||
val verticalRules = rules.filterIsInstance<VerticalRule>()
|
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 {
|
fun areCategoriesValid(): Boolean {
|
||||||
val usedCategories = mutableSetOf<KClass<out ItemClass>>()
|
val usedCategories = mutableSetOf<ItemClassCompanion<*>>()
|
||||||
for (row in grid.rows) {
|
for (row in grid.rows) {
|
||||||
val category = row.first().options.first()::class
|
if (usedCategories.contains(row.category)) {
|
||||||
if (usedCategories.contains(category)) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
usedCategories.add(category)
|
usedCategories.add(row.category)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun areRulesViolated(): Boolean = rules
|
fun areRulesViolated(): Boolean = clues
|
||||||
.map { it.isRuleViolated(grid) }
|
.map { it.isRuleViolated(grid) }
|
||||||
.reduce { a, b -> a || b }
|
.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
|
package domain
|
||||||
|
|
||||||
fun generateGame(size: Int = 6): Game {
|
import domain.PuzzleSolution.MULTIPLE_SOLUTIONS
|
||||||
// Here's a simple algorithm making use of your solver:
|
import domain.PuzzleSolution.NO_SOLUTION
|
||||||
// 0. Select $size classes and $size items per class.
|
import domain.PuzzleSolution.SOLVABLE
|
||||||
// 1. Generate a random puzzle instance.
|
import kotlin.random.Random
|
||||||
|
|
||||||
// 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.)
|
fun generateGame(size: Int = 6): Game {
|
||||||
// 3. Pick a random permutation c1, c2, ..., cn of the clues in C.
|
// Generate a random puzzle instance.
|
||||||
// 4. Set i = 1.
|
val classes = ItemClass.randomClasses(size)
|
||||||
// 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.
|
val grid: List<List<Item<ItemClass<*>>>> = classes.map { it ->
|
||||||
// 7. If there is exactly one solution, set C = D.
|
it.randomItems(size).map { item -> Item(item) }
|
||||||
// 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()
|
// 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)
|
||||||
|
|
||||||
|
// (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 {
|
||||||
|
// 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
|
package domain
|
||||||
|
|
||||||
class GameRow<C : ItemClass>(
|
class GameRow<C : ItemClass<C>>(
|
||||||
val category: C,
|
val category: ItemClassCompanion<C>,
|
||||||
|
val options: List<Item<C>>,
|
||||||
val cells: List<GameCell<C>>
|
val cells: List<GameCell<C>>
|
||||||
) : List<GameCell<C>> by cells
|
) : List<GameCell<C>> by cells
|
||||||
|
|
||||||
class Grid(
|
class Grid(
|
||||||
val rows: List<GameRow<*>>
|
val rows: List<GameRow<ItemClass<*>>>
|
||||||
) : List<GameRow<*>> by rows {
|
) : List<GameRow<ItemClass<*>>> by rows {
|
||||||
fun indexOf(element: ItemClass): Int {
|
|
||||||
val row = rows.first { it.category == element::class }
|
fun <C: ItemClass<C>> indexOf(element: C): Int {
|
||||||
return row.indexOfFirst { it.selection == element }
|
return this[element.companion]
|
||||||
}
|
.indexOfFirst { it.selection?.itemType == element }
|
||||||
}
|
}
|
||||||
|
|
||||||
class GameCell<C:ItemClass>(
|
operator fun <C: ItemClass<C>> get(itemType: ItemClassCompanion<C>): GameRow<C> {
|
||||||
var selection: C?,
|
@Suppress("UNCHECKED_CAST")
|
||||||
val solution: C,
|
return rows.first { it.category == itemType } as GameRow<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
|
package domain
|
||||||
|
|
||||||
enum class Animals(symbol: String) : ItemClass {
|
enum class Animals(symbol: String) : ItemClass<Animals> {
|
||||||
ZEBRA("🦓"),
|
ZEBRA("🦓"),
|
||||||
OCTOPUS("🐙"),
|
OCTOPUS("🐙"),
|
||||||
GOAT("🐐"),
|
GOAT("🐐"),
|
||||||
@@ -10,9 +10,16 @@ enum class Animals(symbol: String) : ItemClass {
|
|||||||
ANT("🐜");
|
ANT("🐜");
|
||||||
|
|
||||||
override val symbols: Array<String> = arrayOf(symbol)
|
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("🇬🇧"),
|
ENGLAND("🇬🇧"),
|
||||||
UKRAINE("🇺🇦"),
|
UKRAINE("🇺🇦"),
|
||||||
SPAIN("🇪🇸"),
|
SPAIN("🇪🇸"),
|
||||||
@@ -22,9 +29,16 @@ enum class Nationality(symbol: String) : ItemClass {
|
|||||||
CANADA("🇨🇦");
|
CANADA("🇨🇦");
|
||||||
|
|
||||||
override val symbols: Array<String> = arrayOf(symbol)
|
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("🥛"),
|
MILK("🥛"),
|
||||||
WINE("🍷"),
|
WINE("🍷"),
|
||||||
COCKTAIL("🍸"),
|
COCKTAIL("🍸"),
|
||||||
@@ -34,9 +48,16 @@ enum class Drink(symbol: String) : ItemClass {
|
|||||||
BEVERAGE("🧃");
|
BEVERAGE("🧃");
|
||||||
|
|
||||||
override val symbols: Array<String> = arrayOf(symbol)
|
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"),
|
ASTRONAUT("\u200D\uD83D\uDE80"),
|
||||||
HEALTH_WORKER("\u200D⚕\uFE0F"),
|
HEALTH_WORKER("\u200D⚕\uFE0F"),
|
||||||
FARMER("\u200D\uD83C\uDF3E"),
|
FARMER("\u200D\uD83C\uDF3E"),
|
||||||
@@ -47,9 +68,16 @@ enum class Profession(symbol: String) : ItemClass {
|
|||||||
TEACHER("\u200D\uD83C\uDFEB");
|
TEACHER("\u200D\uD83C\uDFEB");
|
||||||
|
|
||||||
override val symbols: Array<String> = idic(symbol)
|
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("🍇"),
|
GRAPES("🍇"),
|
||||||
WATERMELON("🍉"),
|
WATERMELON("🍉"),
|
||||||
LEMON("🍋"),
|
LEMON("🍋"),
|
||||||
@@ -62,9 +90,16 @@ enum class Fruit(symbol: String) : ItemClass {
|
|||||||
MANGO("🥭");
|
MANGO("🥭");
|
||||||
|
|
||||||
override val symbols: Array<String> = idic(symbol)
|
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("🍨"),
|
ICE_CREAM("🍨"),
|
||||||
DOUGHNUT("🍩"),
|
DOUGHNUT("🍩"),
|
||||||
COOKIE("🍪"),
|
COOKIE("🍪"),
|
||||||
@@ -76,9 +111,16 @@ enum class Dessert(symbol: String) : ItemClass {
|
|||||||
CUSTARD("🍮");
|
CUSTARD("🍮");
|
||||||
|
|
||||||
override val symbols: Array<String> = idic(symbol)
|
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("🚲"),
|
BICYCLE("🚲"),
|
||||||
MOTOR_SCOOTER("🛵"),
|
MOTOR_SCOOTER("🛵"),
|
||||||
SKATEBOARD("🛹"),
|
SKATEBOARD("🛹"),
|
||||||
@@ -88,18 +130,51 @@ enum class Transportation(symbol: String) : ItemClass {
|
|||||||
BUS("🚌");
|
BUS("🚌");
|
||||||
|
|
||||||
override val symbols: Array<String> = idic(symbol)
|
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 GENDERS = arrayOf("\uD83E\uDDD1", "\uD83D\uDC68", "\uD83D\uDC69")
|
||||||
private val SKIN_TONES =
|
private val SKIN_TONES =
|
||||||
arrayOf("\uD83C\uDFFB", "\uD83C\uDFFC", "\uD83C\uDFFD", "\uD83C\uDFFE", "\uD83C\uDFFF")
|
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 g = GENDERS[i % GENDERS.size]
|
||||||
val t = SKIN_TONES[i / GENDERS.size]
|
val t = SKIN_TONES[i / GENDERS.size]
|
||||||
g + t + symbol
|
g + t + symbol
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ItemClass {
|
sealed interface ItemClass<out SELF : ItemClass<SELF>> {
|
||||||
val symbols: Array<String>
|
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.material3.OutlinedCard
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import domain.Item
|
||||||
import domain.ItemClass
|
import domain.ItemClass
|
||||||
import domain.ItemCategory
|
import domain.ItemClassCompanion
|
||||||
|
|
||||||
@Composable
|
@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) {
|
OutlinedCard(modifier = modifier) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,19 @@
|
|||||||
@file:OptIn(ExperimentalResourceApi::class)
|
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
import ch.tutteli.atrium.api.fluent.en_GB.feature
|
||||||
//import org.junit.Test
|
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||||
import yaep.composeapp.generated.resources.Res
|
import ch.tutteli.atrium.api.verbs.expect
|
||||||
import yaep.composeapp.generated.resources.compose_multiplatform
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.fail
|
|
||||||
|
|
||||||
class GameTest {
|
class GameTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun areCategoriesValid() {
|
fun `ensure generated game is valid`() {
|
||||||
// Game(
|
val game = generateGame()
|
||||||
// categories = listOf(
|
expect(game) {
|
||||||
// category {
|
feature(Game::areCategoriesValid).toEqual(true)
|
||||||
// item(Res.drawable.compose_multiplatform)
|
feature(Game::areRulesViolated).toEqual(false)
|
||||||
// }
|
}
|
||||||
// ),
|
|
||||||
// Grid(
|
|
||||||
//
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun areRulesViolated() {
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ kotlin = "1.9.24"
|
|||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
#junit = { module = "junit:junit", version = "4.13.2"}
|
#junit = { module = "junit:junit", version = "4.13.2"}
|
||||||
|
atrium = { module = "ch.tutteli.atrium:atrium-fluent", version = "1.2.0" }
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
||||||
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
|
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
|
||||||
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
|
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
|
||||||
|
|||||||
Reference in New Issue
Block a user