Generate game
This commit is contained in:
@@ -1,17 +1,172 @@
|
||||
package domain
|
||||
|
||||
fun generateGame(size: Int = 6): Game {
|
||||
// Here's a simple algorithm making use of your solver:
|
||||
// 0. Select $size classes and $size items per class.
|
||||
// 1. Generate a random puzzle instance.
|
||||
import domain.PuzzleSolution.MULTIPLE_SOLUTIONS
|
||||
import domain.PuzzleSolution.NO_SOLUTION
|
||||
import domain.PuzzleSolution.SOLVABLE
|
||||
import kotlin.random.Random
|
||||
|
||||
fun generateGame(size: Int = 6): Game {
|
||||
// Generate a random puzzle instance.
|
||||
val classes = ItemClass.randomClasses(size)
|
||||
|
||||
val grid: List<List<Item<ItemClass<*>>>> = classes.map { it ->
|
||||
it.randomItems(size).map { item -> Item(item) }
|
||||
}
|
||||
|
||||
// Build a set C of all possible clues that pertain to this puzzle instance.
|
||||
// (There are a finite and in fact quite small number of possible clues: for example
|
||||
// if there are 5 houses, there are 5 possible clues of the form "Person A lives in
|
||||
// house B", 8 possible clues of the form "Person A lives next to house B", and so on.)
|
||||
var clues = getAllClues(grid).shuffled()
|
||||
|
||||
var i = 0
|
||||
|
||||
// If i >= n, we are done. The set of clues is minimal.
|
||||
while (i < clues.size) {
|
||||
// Run your solver on the reduced set of clues and count the number of possible solutions.
|
||||
val temp = clues - clues[i]
|
||||
if (solve(grid.toGrid(), temp) == SOLVABLE) {
|
||||
// If there is exactly one solution, update clues and reset index.
|
||||
clues = temp
|
||||
i = 0
|
||||
}
|
||||
i++
|
||||
}
|
||||
return Game(grid.toGrid(), clues)
|
||||
|
||||
// 2. Build a set C of all possible clues that pertain to this puzzle instance. (There are a finite and in fact quite small number of possible clues: for example if there are 5 houses, there are 5 possible clues of the form "Person A lives in house B", 8 possible clues of the form "Person A lives next to house B", and so on.)
|
||||
// 3. Pick a random permutation c1, c2, ..., cn of the clues in C.
|
||||
// 4. Set i = 1.
|
||||
// 5. If i > n, we are done. The set C of clues is minimal.
|
||||
// 6. Let D = C − { ci }. Run your solver on the set D of clues and count the number of possible solutions.
|
||||
// 7. If there is exactly one solution, set C = D.
|
||||
// 8. Set i = i + 1 and go back to step 5.
|
||||
// (You can speed this up by removing clues in batches rather than one at a time, but it makes the algorithm more complicated to describe.)
|
||||
TODO()
|
||||
}
|
||||
|
||||
private fun solve(grid: Grid, clues: List<Clue>): PuzzleSolution {
|
||||
// Start with a grid where each cell is a list of all possible items.
|
||||
|
||||
// First, set the positions of the items that are already known.
|
||||
val positionClues = clues.filterIsInstance<PositionClue<ItemClass<*>>>().toSet()
|
||||
positionClues.forEach { position ->
|
||||
val row = grid[position.item.itemType.companion]
|
||||
row.forEachIndexed { index, gameCell ->
|
||||
if (index == position.index) {
|
||||
gameCell.selection = position.item
|
||||
} else {
|
||||
gameCell.options.remove(position.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each clue, remove any items that violate the clue.
|
||||
// If any cell has only one item left, remove that item from all other cells.
|
||||
// Repeat until no more items can be removed.
|
||||
val otherClues = clues - positionClues
|
||||
var removedOptions = false
|
||||
do {
|
||||
grid.forEach { row ->
|
||||
removedOptions = row.tryOptionsForClues(grid, otherClues) || removedOptions
|
||||
}
|
||||
} while (removedOptions)
|
||||
|
||||
// If any cell has no items left, the puzzle has no solution.
|
||||
grid.flatMap { it }.forEach { cell ->
|
||||
if (cell.options.isEmpty()) {
|
||||
return NO_SOLUTION
|
||||
}
|
||||
}
|
||||
// If all cells have exactly one item left, the puzzle is solved.
|
||||
if (grid.flatMap { it }.all { it.selection != null }) return SOLVABLE
|
||||
|
||||
// If there are still cells with multiple items, pick one and try each item in turn, then go back to step 2.
|
||||
// TODO: Does this need to be implemented? We would need to try all possible options to check if there are multiple solutions.
|
||||
return MULTIPLE_SOLUTIONS
|
||||
}
|
||||
|
||||
private enum class PuzzleSolution {
|
||||
NO_SOLUTION,
|
||||
SOLVABLE,
|
||||
MULTIPLE_SOLUTIONS
|
||||
}
|
||||
|
||||
fun <C : ItemClass<C>> GameRow<C>.tryOptionsForClues(grid: Grid, clues: List<Clue>): Boolean {
|
||||
var removedOptions = false
|
||||
filter { cell -> cell.selection == null }.forEach { cell ->
|
||||
val removed = cell.options.removeIf { option ->
|
||||
cell.selection = option
|
||||
clues.any { it.isRuleViolated(grid) }
|
||||
}
|
||||
cell.selection = null
|
||||
if (removed) {
|
||||
cleanupOptions(cell)
|
||||
}
|
||||
removedOptions = removedOptions || removed
|
||||
}
|
||||
return removedOptions
|
||||
}
|
||||
|
||||
fun <C : ItemClass<C>> GameRow<C>.cleanupOptions(cell: GameCell<C>) {
|
||||
if (cell.options.size == 1) {
|
||||
cell.selection = cell.options.first()
|
||||
forEach { otherCell ->
|
||||
if (otherCell != cell && otherCell.selection == null) {
|
||||
otherCell.options.remove(cell.selection)
|
||||
cleanupOptions(otherCell)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableList<Clue> {
|
||||
val clues = mutableListOf<Clue>()
|
||||
// For optimization reasons we want the positional clues first
|
||||
rows.forEach { row ->
|
||||
row.forEachIndexed { i, item ->
|
||||
clues.add(PositionClue(item, i))
|
||||
}
|
||||
}
|
||||
rows.forEachIndexed { i, columns ->
|
||||
columns.forEachIndexed { j, item ->
|
||||
// Clue: Neighbours
|
||||
if (j > 0) {
|
||||
rows.map { it[j - 1] }.forEach {
|
||||
// We don't want to give away the order with this clue,
|
||||
// but it needs to be displayed consistently.
|
||||
if (Random.nextBoolean()) {
|
||||
clues.add(NeighbourClue(item, it))
|
||||
} else {
|
||||
clues.add(NeighbourClue(it, item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clue: Order
|
||||
if (j > 0) {
|
||||
rows.flatMap { it.take(j - 1) }.forEach {
|
||||
clues.add(OrderClue(it, item))
|
||||
}
|
||||
}
|
||||
|
||||
// Clue: Triplet
|
||||
if (j > 1) {
|
||||
val lefts = rows.map { it[j - 2] }
|
||||
val middles = rows.map { it[j - 1] }
|
||||
|
||||
lefts.forEach { left ->
|
||||
middles.forEach { middle ->
|
||||
// We don't want to give away the order with this clue,
|
||||
// but it needs to be displayed consistently.
|
||||
if (Random.nextBoolean()) {
|
||||
clues.add(TripletClue(left, middle, item))
|
||||
} else {
|
||||
clues.add(TripletClue(item, middle, left))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clue: Same Column
|
||||
rows.map { it[j] }.forEach {
|
||||
if (it != item) {
|
||||
clues.add(SameRowClue(item, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return clues
|
||||
}
|
||||
Reference in New Issue
Block a user