diff --git a/demo/src/main/kotlin/dev/teogor/sudoklify/demo/gen/SudokuDecoder.kt b/demo/src/main/kotlin/dev/teogor/sudoklify/demo/gen/SudokuDecoder.kt index 507d94c..7d4d63d 100644 --- a/demo/src/main/kotlin/dev/teogor/sudoklify/demo/gen/SudokuDecoder.kt +++ b/demo/src/main/kotlin/dev/teogor/sudoklify/demo/gen/SudokuDecoder.kt @@ -16,7 +16,7 @@ package dev.teogor.sudoklify.demo.gen -import dev.teogor.sudoklify.core.io.toToken +import dev.teogor.sudoklify.ktx.toBoardCell import java.util.regex.Pattern class SudokuDecoder { @@ -35,7 +35,7 @@ class SudokuDecoder { if (cellContent == " ") { "-" } else { - cellContent.toInt().toToken() + cellContent.toInt().toBoardCell() } puzzleBuilder.append(parsedNumber) } diff --git a/demo/src/test/kotlin/dev/teogor/sudoklify/demo/SudokuDemoTest.kt b/demo/src/test/kotlin/dev/teogor/sudoklify/demo/SudokuDemoTest.kt new file mode 100644 index 0000000..eacd35c --- /dev/null +++ b/demo/src/test/kotlin/dev/teogor/sudoklify/demo/SudokuDemoTest.kt @@ -0,0 +1,114 @@ +package dev.teogor.sudoklify.demo + +import dev.teogor.sudoklify.common.types.Board +import dev.teogor.sudoklify.common.types.Difficulty +import dev.teogor.sudoklify.common.types.SudokuType +import dev.teogor.sudoklify.core.generation.difficulty +import dev.teogor.sudoklify.core.generation.generateSudoku +import dev.teogor.sudoklify.core.generation.seed +import dev.teogor.sudoklify.core.generation.seeds +import dev.teogor.sudoklify.core.generation.sudokuParamsBuilder +import dev.teogor.sudoklify.core.generation.sudokuType +import dev.teogor.sudoklify.core.util.toBoard +import dev.teogor.sudoklify.core.util.toSequenceString +import dev.teogor.sudoklify.ktx.createSeed +import dev.teogor.sudoklify.seeds.combinedSeeds +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class SudokuDemoTest { + + @Test + fun `test Verify Solution Generation for 4x4 Sudoku`() { + val expectedSolution = "3214412314322341" + + val sudokuParams = sudokuParamsBuilder { + seeds { combinedSeeds } + seed { createSeed(0L) } + sudokuType { SudokuType.Sudoku4x4 } + difficulty { Difficulty.EASY } + } + val sudoku = sudokuParams.generateSudoku() + + assertEquals(expectedSolution, sudoku.solution.toSequenceString()) + } + + @Test + fun `test Verify Solution Generation for 9x9 Sudoku`() { + val expectedSolution = + "395126784178549623642783915531678249964215378827934561486352197759861432213497856" + + val sudokuParams = sudokuParamsBuilder { + seeds { combinedSeeds } + seed { createSeed(0L) } + sudokuType { SudokuType.Sudoku9x9 } + difficulty { Difficulty.EASY } + } + val sudoku = sudokuParams.generateSudoku() + + assertEquals(expectedSolution, sudoku.solution.toSequenceString()) + } + + @Test + fun `test Verify Solution Generation for 16x16 Sudoku`() { + val expectedSolution = + "15101714111269161352834151017141112691613528341441181016131512231956731669182547141512111310121410168315137914625115246127114310161115981381113195104151226714163697145134111081231161521061631315714411128152947513263816115911121014912151141416102581336717391562111681351441011221584161261131497103115161312515108311142147961113107914251561613412813521234791161081611415" + + val sudokuParams = sudokuParamsBuilder { + seeds { combinedSeeds } + seed { createSeed(0L) } + sudokuType { SudokuType.Sudoku16x16 } + difficulty { Difficulty.EASY } + } + val sudoku = sudokuParams.generateSudoku() + + assertEquals(expectedSolution, sudoku.solution.toSequenceString()) + } + + @Test + fun `test Verify Solution Generation for 25x25 Sudoku`() { + val expectedSolution = + "62281315211101618205914112571724122342319207102125931711622113232441952151612814181232123722813191718154211410616205249112511195916142420234825212103221182113176715212332581713414225201292151810196247111162320178526741219141122918115132521162410391214313252281410762115111623172418192025324720111813623412181525102211416195229172411142213120231521181631782519410259712672116151918935102424251281420622111231713510211973262291281420181325161523174124111412231112152524821101719796452201318162321618122023191110113942451781437156252122817423920211618131115256124122210197253141062518216111917241132352079122132281415422913221191618203724511236178124101415251174231612101452125922081922153716111318241514191062249122317211135242511182320168741824141725512215631971623131189122102021258115181572411714610162219209231332124121651211102423192015197184213132281425176218220174512213716112210612324251481519139115924146182191125218131716472051223322101159241461821911252181317164720512233221019136722810152516242313141211291741821520" + + val sudokuParams = sudokuParamsBuilder { + seeds { combinedSeeds } + seed { createSeed(0L) } + sudokuType { SudokuType.Sudoku25x25 } + difficulty { Difficulty.EASY } + } + val sudoku = sudokuParams.generateSudoku() + + assertEquals(expectedSolution, sudoku.solution.toSequenceString()) + } + + @Test + fun `test Verify Puzzle Solutions Against Generated Puzzles`() { + combinedSeeds.forEach { sudoku -> + val isValid = comparePuzzles( + puzzle = sudoku.puzzle.toBoard(sudoku.sudokuType), + solution = sudoku.solution.toBoard(sudoku.sudokuType), + ) + + assertEquals(true, isValid, "Invalid puzzle for seed ${sudoku.solution}") + } + } + + private fun comparePuzzles(puzzle: Board, solution: Board): Boolean { + if (puzzle.size != solution.size || puzzle[0].size != solution[0].size) { + return false + } + + for (row in puzzle.indices) { + for (col in 0..>.mapToSudokuString(): String { + return flatMap { cells -> + cells.map { cell -> + cell.toBoardCell() + } + }.joinToString("") +} + +/** + * Converts a two-dimensional list of elements to a string representation, applying a custom + * value mapper to each element. + * + * @receiver The two-dimensional list of elements. + * @param valueMapper A function that maps each element of the list to an integer to be used + * in the string representation. + * @return The string representation of the Sudoku board, where each cell is encoded using + * the provided [valueMapper]. + */ inline fun List>.mapToSudokuString(crossinline valueMapper: T.() -> Int): String { return flatMap { cells -> cells.map { cell -> @@ -27,6 +54,34 @@ inline fun List>.mapToSudokuString(crossinline valueMapper: T.() -> }.joinToString("") } +/** + * Converts a string representation of a Sudoku board to a two-dimensional list of integers. + * + * @receiver The string representing the Sudoku board. + * @param sudokuType The type of Sudoku board (e.g., 4x4, 9x9), used to determine the size + * and structure of the board. + * @return A two-dimensional list of integers representing the Sudoku board, where each cell + * is converted to an integer using its default decoding. + */ +@OptIn(InternalSudoklifyApi::class) +fun String.mapToSudokuBoard(sudokuType: SudokuType): List> { + return getCells() + .chunked(sudokuType.cells) + .map { row -> row.map { it.toInt() } } +} + +/** + * Converts a string representation of a Sudoku board to a two-dimensional list of custom + * elements, applying a custom value mapper to each cell value. + * + * @receiver The string representing the Sudoku board. + * @param sudokuType The type of Sudoku board (e.g., 4x4, 9x9), used to determine the size + * and structure of the board. + * @param valueMapper A function that maps each integer value in the string representation + * to a custom element. + * @return A two-dimensional list of custom elements representing the Sudoku board, where + * each cell is decoded and mapped using the provided [valueMapper]. + */ @OptIn(InternalSudoklifyApi::class) inline fun String.mapToSudokuBoard( sudokuType: SudokuType, @@ -37,6 +92,18 @@ inline fun String.mapToSudokuBoard( .map { row -> row.map { valueMapper(it.toInt()) } } } +/** + * Converts a string representation of a Sudoku board to a two-dimensional list of custom + * elements, allowing access to row and column indices during mapping. + * + * @receiver The string representing the Sudoku board. + * @param sudokuType The type of Sudoku board (e.g., 4x4, 9x9), used to determine the size + * and structure of the board. + * @param valueMapper A function that maps each integer value in the string representation + * to a custom element, additionally providing the row and column indices of the cell. + * @return A two-dimensional list of custom elements representing the Sudoku board, where + * each cell is decoded and mapped using the provided [valueMapper], incorporating its row and column indices. + */ @OptIn(InternalSudoklifyApi::class) inline fun String.mapIndexedToSudokuBoard( sudokuType: SudokuType, @@ -51,6 +118,13 @@ inline fun String.mapIndexedToSudokuBoard( } } +/** + * Extracts individual cell values from the string representation of a Sudoku board. + * + * @receiver The string representing the Sudoku board. + * @return An `ArrayList` containing each cell value extracted from the string, + * preserving their original representation (e.g., "A1", "5", "-"). + */ @InternalSudoklifyApi fun String.getCells(): ArrayList { val regex = Regex("([A-I][a-z]+)|-|[A-I]")