Skip to content

Commit

Permalink
Merge pull request #52 from teogor/feature/add-sudoku-board-encoding-…
Browse files Browse the repository at this point in the history
…decoding

Enable encoding and decoding Sudoku boards to/from strings
  • Loading branch information
teogor authored Feb 21, 2024
2 parents 852449a + 11eee13 commit 85af5db
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 117 deletions.
3 changes: 3 additions & 0 deletions sudoklify-common/api/sudoklify-common.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
public abstract interface annotation class dev/teogor/sudoklify/common/InternalSudoklifyApi : java/lang/annotation/Annotation {
}

public final class dev/teogor/sudoklify/common/model/Sudoku {
public fun <init> ([[Ljava/lang/String;[[Ljava/lang/String;Ldev/teogor/sudoklify/common/types/Difficulty;Ldev/teogor/sudoklify/common/types/SudokuType;)V
public final fun component1 ()[[Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2024 Teogor (Teodor Grigor)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.teogor.sudoklify.common

@RequiresOptIn(message = "This API is internal to Sudoklify library. Do NOT use it!")
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
annotation class InternalSudoklifyApi
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ package dev.teogor.sudoklify.common.types
/**
* Typealias for representing a single cell value in a game board.
*/
typealias Token = String
typealias BoardCell = String
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

package dev.teogor.sudoklify.common.types

typealias TokenMap = Map<Token, Cell>
typealias TokenMap = Map<BoardCell, Cell>
4 changes: 0 additions & 4 deletions sudoklify-core/api/sudoklify-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@ public final class dev/teogor/sudoklify/core/generation/SudokuGeneratorKt {
}

public final class dev/teogor/sudoklify/core/io/BoardSerializationKt {
public static final fun decodeAsBoard (Ljava/lang/String;Ldev/teogor/sudoklify/common/types/SudokuType;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun encodeAsString (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Ljava/lang/String;
public static final fun generateTokenMap (I)Ljava/util/Map;
public static final fun toNumber (Ljava/lang/String;)I
public static final fun toToken (I)Ljava/lang/String;
}

public final class dev/teogor/sudoklify/core/io/SudokuParser {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import dev.teogor.sudoklify.common.types.Seed
import dev.teogor.sudoklify.common.types.SudokuString
import dev.teogor.sudoklify.common.types.SudokuType
import dev.teogor.sudoklify.common.types.TokenMap
import dev.teogor.sudoklify.core.io.toToken
import dev.teogor.sudoklify.core.tokenizer.Tokenizer
import dev.teogor.sudoklify.core.util.sortRandom
import dev.teogor.sudoklify.core.util.toBoard
import dev.teogor.sudoklify.core.util.toSequenceString
import dev.teogor.sudoklify.ktx.createSeed
import dev.teogor.sudoklify.ktx.toBoardCell
import kotlin.math.sqrt
import kotlin.random.Random

Expand Down Expand Up @@ -228,7 +228,7 @@ internal class SudokuGenerator internal constructor(
val tokenList =
gridList.withIndex().map { (index, _) ->
val value = if (index < boxDigits) (index + 1) else (index - boxDigits + 1)
value.toToken()
value.toBoardCell()
}.shuffled(random)

val tokenMap =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,132 +16,27 @@

package dev.teogor.sudoklify.core.io

import dev.teogor.sudoklify.common.types.SudokuType
import dev.teogor.sudoklify.common.types.Token
import dev.teogor.sudoklify.common.InternalSudoklifyApi
import dev.teogor.sudoklify.common.types.TokenMap

/**
* Converts an integer value to a base-36 string representation for use in
* game board encoding.
*
* - Handles the special case of 0 being represented as "-".
* - Uses nested `when` expressions for efficient character construction.
*
* @receiver The integer value to be converted.
* @return The string representation of the value.
*/
fun Int.toToken(): Token =
when {
this == 0 -> "-"

else -> {
var valueCopy = this
buildString {
while (valueCopy > 0) {
val char =
when (val digit = (valueCopy % 10)) {
0 -> 'j'
else -> ('a' + digit - 1)
}
append(char)
valueCopy /= 10
}
reverse()
this[0] = this[0].uppercaseChar()
}
}
}

/**
* Converts a string representation back to an integer value.
*
* - Handles the special case of "-" being represented as 0.
* - Uses `map` and `fold` with nested `when` expressions for efficient conversion.
*
* @receiver The string representation to be converted.
* @return The integer value represented by the token.
*/
fun Token.toNumber(): Int =
when {
this == "-" -> 0

else ->
map { char ->
when {
char.isUpperCase() -> {
char - 'A' + 1
}

else -> {
char - 'a' + 1
}
}
}.fold(0) { acc, digit -> acc * 10 + digit }
}

/**
* Serializes a list of lists (representing a game board) as a base-36 string.
*
* - Uses the provided valueMapper function to convert individual cell values
* to integers before encoding.
*
* @receiver The grid of values to be serialized.
* @param valueMapper A function to map each cell value to its corresponding
* integer for encoding.
* @return The base-36 string representation of the board.
*/
inline fun <T> List<List<T>>.encodeAsString(crossinline valueMapper: T.() -> Int) =
flatMap { cells ->
cells.map { cell ->
valueMapper(cell).toToken()
}
}.joinToString("")

/**
* Deserializes a base-36 string into a list of lists (representing a game board).
*
* - Uses the provided gameType to validate the board size.
* - Uses the provided valueMapper function to convert decoded integers back to
* cell values.
*
* @receiver The string representation of the grid.
* @param sudokuType The sudoku type, providing information about the expected board
* size.
* @param valueMapper A function to map each decoded integer to its corresponding
* cell value.
* @return The decoded board as a list of lists.
*/
inline fun <T> String.decodeAsBoard(
sudokuType: SudokuType,
crossinline valueMapper: Int.() -> T,
): List<List<T>> {
val regex = Regex("([A-I][a-z]+)|-|[A-I]")
val matches = regex.findAll(this)
val matchedTokens = ArrayList<String>()
matches.forEach { matchedTokens.add(it.value) }
return matchedTokens
.chunked(sudokuType.cells)
.map { row -> row.map { valueMapper(it.toNumber()) } }
}
import dev.teogor.sudoklify.ktx.toBoardCell

/**
* Generates a mapping between token values and their corresponding
* string representations.
*
* TODO annotation for internal use
*
* @param boxDigits The number of digits used to represent each box
* in the Sudoku puzzle.
*
* @return A `TokenMap` containing the mapping between token values
* and their string representations.
*/
@InternalSudoklifyApi
fun generateTokenMap(boxDigits: Int): TokenMap {
val gridList = (1..boxDigits)
val tokenList =
gridList.withIndex().map { (index, _) ->
val value = if (index < boxDigits) (index + 1) else (index - boxDigits + 1)
value.toToken()
value.toBoardCell()
}

val tokenMap =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
* limitations under the License.
*/

@file:OptIn(InternalSudoklifyApi::class)

package dev.teogor.sudoklify.core.io

import dev.teogor.sudoklify.common.InternalSudoklifyApi
import dev.teogor.sudoklify.common.types.SudokuString
import dev.teogor.sudoklify.common.types.SudokuType
import dev.teogor.sudoklify.core.util.toBoard
Expand Down
12 changes: 12 additions & 0 deletions sudoklify-ktx/api/sudoklify-ktx.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
public final class dev/teogor/sudoklify/ktx/BoardCellExtensionsKt {
public static final fun toBoardCell (I)Ljava/lang/String;
public static final fun toInt (Ljava/lang/String;)I
}

public final class dev/teogor/sudoklify/ktx/DifficultyExtensionsKt {
public static final fun toLabel (Ldev/teogor/sudoklify/common/types/Difficulty;[Ljava/lang/String;)Ljava/lang/String;
public static final fun toStars (Ldev/teogor/sudoklify/common/types/Difficulty;)Ljava/lang/String;
Expand All @@ -12,6 +17,13 @@ public final class dev/teogor/sudoklify/ktx/SeedExtensionsKt {
public static final fun toSeed (J)Ldev/teogor/sudoklify/common/types/Seed;
}

public final class dev/teogor/sudoklify/ktx/SudokuBoardExtensionsKt {
public static final fun getCells (Ljava/lang/String;)Ljava/util/ArrayList;
public static final fun mapIndexedToSudokuBoard (Ljava/lang/String;Ldev/teogor/sudoklify/common/types/SudokuType;Lkotlin/jvm/functions/Function3;)Ljava/util/List;
public static final fun mapToSudokuBoard (Ljava/lang/String;Ldev/teogor/sudoklify/common/types/SudokuType;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun mapToSudokuString (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Ljava/lang/String;
}

public final class dev/teogor/sudoklify/ktx/SudokuTypeExtensionsKt {
public static final fun areCellsInSameBox (Ldev/teogor/sudoklify/common/types/SudokuType;II)Z
public static final fun areCellsInSameBox (Ldev/teogor/sudoklify/common/types/SudokuType;IIII)Z
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2024 Teogor (Teodor Grigor)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.teogor.sudoklify.ktx

import dev.teogor.sudoklify.common.types.BoardCell

fun Int.toBoardCell(): BoardCell {
return when {
this == 0 -> "-"

else -> {
var valueCopy = this
buildString {
while (valueCopy > 0) {
val char =
when (val digit = (valueCopy % 10)) {
0 -> 'j'
else -> ('a' + digit - 1)
}
append(char)
valueCopy /= 10
}
reverse()
this[0] = this[0].uppercaseChar()
}
}
}
}

fun BoardCell.toInt(): Int {
return when {
this == "-" -> 0

else ->
map { char ->
when {
char.isUpperCase() -> {
char - 'A' + 1
}

else -> {
char - 'a' + 1
}
}
}.fold(0) { acc, digit -> acc * 10 + digit }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2024 Teogor (Teodor Grigor)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.teogor.sudoklify.ktx

import dev.teogor.sudoklify.common.InternalSudoklifyApi
import dev.teogor.sudoklify.common.types.SudokuType

inline fun <T> List<List<T>>.mapToSudokuString(crossinline valueMapper: T.() -> Int): String {
return flatMap { cells ->
cells.map { cell ->
valueMapper(cell).toBoardCell()
}
}.joinToString("")
}

@OptIn(InternalSudoklifyApi::class)
inline fun <T> String.mapToSudokuBoard(
sudokuType: SudokuType,
crossinline valueMapper: Int.() -> T,
): List<List<T>> {
return getCells()
.chunked(sudokuType.cells)
.map { row -> row.map { valueMapper(it.toInt()) } }
}

@OptIn(InternalSudoklifyApi::class)
inline fun <T> String.mapIndexedToSudokuBoard(
sudokuType: SudokuType,
crossinline valueMapper: (value: Int, row: Int, column: Int) -> T,
): List<List<T>> {
return getCells()
.chunked(sudokuType.cells)
.mapIndexed { row, rowElements ->
rowElements.mapIndexed { column, value ->
valueMapper(value.toInt(), row, column)
}
}
}

@InternalSudoklifyApi
fun String.getCells(): ArrayList<String> {
val regex = Regex("([A-I][a-z]+)|-|[A-I]")
val matches = regex.findAll(this)
val matchedTokens = ArrayList<String>()
matches.forEach { matchedTokens.add(it.value) }
return matchedTokens
}

0 comments on commit 85af5db

Please sign in to comment.