From ca492509cb09c39e80c679aed28de3ed218e327e Mon Sep 17 00:00:00 2001 From: kaajjo Date: Sun, 14 Jul 2024 17:35:38 +0300 Subject: [PATCH] feat: Killer Sudoku --- .../6.json | 267 ++++++++++++++++++ .../java/com/kaajjo/libresudoku/core/Cell.kt | 3 + .../kaajjo/libresudoku/core/qqwing/Cage.kt | 13 + .../libresudoku/core/qqwing/CageGenerator.kt | 121 ++++++++ .../libresudoku/core/qqwing/GameType.kt | 3 + .../libresudoku/core/utils/SudokuParser.kt | 14 + .../libresudoku/core/utils/SudokuUtils.kt | 6 +- .../libresudoku/data/database/AppDataBase.kt | 5 +- .../database/converters/GameTypeConverter.kt | 6 + .../data/database/model/SudokuBoard.kt | 3 +- .../data/datastore/AppSettingsManager.kt | 6 + .../libresudoku/ui/components/board/Board.kt | 241 ++++++++++------ .../ui/components/board/BoardUtil.kt | 60 ++++ .../components/board/KillerSudokuDrawUtil.kt | 108 +++++++ .../ui/components/board/PositionLines.kt | 107 +++++++ .../kaajjo/libresudoku/ui/game/GameScreen.kt | 5 +- .../libresudoku/ui/game/GameViewModel.kt | 5 + .../ui/gameshistory/GamesHistoryScreen.kt | 3 + .../gameshistory/savedgame/SavedGameScreen.kt | 6 +- .../savedgame/SavedGameViewModel.kt | 5 + .../libresudoku/ui/home/HomeViewModel.kt | 12 +- .../ui/statistics/StatisticsScreen.kt | 5 +- app/src/main/res/values-ru-rRU/strings.xml | 3 + app/src/main/res/values/strings.xml | 5 + 24 files changed, 909 insertions(+), 103 deletions(-) create mode 100644 app/schemas/com.kaajjo.libresudoku.data.database.AppDatabase/6.json create mode 100644 app/src/main/java/com/kaajjo/libresudoku/core/qqwing/Cage.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/core/qqwing/CageGenerator.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/components/board/BoardUtil.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/components/board/KillerSudokuDrawUtil.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/components/board/PositionLines.kt diff --git a/app/schemas/com.kaajjo.libresudoku.data.database.AppDatabase/6.json b/app/schemas/com.kaajjo.libresudoku.data.database.AppDatabase/6.json new file mode 100644 index 00000000..6401c92c --- /dev/null +++ b/app/schemas/com.kaajjo.libresudoku.data.database.AppDatabase/6.json @@ -0,0 +1,267 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "f0a74afcb17b9a4b4078f0765a30360d", + "entities": [ + { + "tableName": "record", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`board_uid` INTEGER NOT NULL, `type` INTEGER NOT NULL, `difficulty` INTEGER NOT NULL, `date` INTEGER NOT NULL, `time` INTEGER NOT NULL, PRIMARY KEY(`board_uid`), FOREIGN KEY(`board_uid`) REFERENCES `board`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "board_uid", + "columnName": "board_uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "difficulty", + "columnName": "difficulty", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "board_uid" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "board", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "board_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "board", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `initial_board` TEXT NOT NULL, `solved_board` TEXT NOT NULL, `difficulty` INTEGER NOT NULL, `type` INTEGER NOT NULL, `folder_id` INTEGER DEFAULT null, `killer_cages` TEXT DEFAULT null, FOREIGN KEY(`folder_id`) REFERENCES `Folder`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "initialBoard", + "columnName": "initial_board", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "solvedBoard", + "columnName": "solved_board", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "difficulty", + "columnName": "difficulty", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "null" + }, + { + "fieldPath": "killerCages", + "columnName": "killer_cages", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "null" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "saved_game", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`board_uid` INTEGER NOT NULL, `current_board` TEXT NOT NULL, `notes` TEXT NOT NULL, `timer` INTEGER NOT NULL, `completed` INTEGER NOT NULL DEFAULT false, `give_up` INTEGER NOT NULL DEFAULT false, `mistakes` INTEGER NOT NULL DEFAULT 0, `can_continue` INTEGER NOT NULL, `last_played` INTEGER, `started_at` INTEGER, `finished_at` INTEGER, PRIMARY KEY(`board_uid`), FOREIGN KEY(`board_uid`) REFERENCES `board`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "board_uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentBoard", + "columnName": "current_board", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timer", + "columnName": "timer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "giveUp", + "columnName": "give_up", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "mistakes", + "columnName": "mistakes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "canContinue", + "columnName": "can_continue", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlayed", + "columnName": "last_played", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startedAt", + "columnName": "started_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "finishedAt", + "columnName": "finished_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "board_uid" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "board", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "board_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "Folder", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `date_created` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "date_created", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f0a74afcb17b9a4b4078f0765a30360d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/core/Cell.kt b/app/src/main/java/com/kaajjo/libresudoku/core/Cell.kt index d871242d..26954476 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/core/Cell.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/core/Cell.kt @@ -1,5 +1,8 @@ package com.kaajjo.libresudoku.core +import kotlinx.serialization.Serializable + +@Serializable data class Cell( val row: Int, val col: Int, diff --git a/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/Cage.kt b/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/Cage.kt new file mode 100644 index 00000000..1bb5544b --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/Cage.kt @@ -0,0 +1,13 @@ +package com.kaajjo.libresudoku.core.qqwing + +import com.kaajjo.libresudoku.core.Cell +import kotlinx.serialization.Serializable + +@Serializable +data class Cage( + val id: Int = 0, + val sum: Int = 0, + val cells: List = emptyList() +) { + fun size() = cells.size +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/CageGenerator.kt b/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/CageGenerator.kt new file mode 100644 index 00000000..3393468b --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/CageGenerator.kt @@ -0,0 +1,121 @@ +package com.kaajjo.libresudoku.core.qqwing + +import android.util.Log +import com.kaajjo.libresudoku.core.Cell +import kotlin.random.Random + +class CageGenerator( + private val board: List>, + private val type: GameType +) { + private var unusedCells = mutableListOf() + + + init { + unusedCells = board.flatten().toMutableList() + } + + private fun generateCage(startCell: Cell, requiredSize: Int = 2, id: Int = 0): Cage? { + if (unusedCells.isEmpty() || !unusedCells.contains(startCell)) { + return null + } + println("Generating cage") + unusedCells.remove(startCell) + + var cage = Cage( + id = id, + cells = listOf(startCell) + ) + + if(getUnusedNeighbors(startCell).isEmpty()) { + println("No neighbors") + return cage + } + + while (cage.size() < requiredSize) { + var neighbors = emptyList() + + val cageCells = cage.cells.toMutableList() + // searching for any cell with at lest 1 unused neighbor + println("Searching neighbors") + for (i in cageCells.indices) { + val cell = cageCells.random() + cageCells.remove(cell) + neighbors = getUnusedNeighbors(cell) + + if (neighbors.isNotEmpty()) { + break + } + } + + if (neighbors.isEmpty()) { + return cage + } + println("Found neighbors") + println("Selecting random cell") + var cell: Cell? = null + for(i in neighbors) { + // select random neighbor to add in cage + val tempCell = neighbors.random() + if (cage.cells.all { it.value != tempCell.value } && unusedCells.contains(tempCell)) { + cell = tempCell + } + } + if (cell != null) { + println("Selected ${cell.toString()}") + unusedCells.remove(cell) + cage = cage.copy( + cells = cage.cells + cell + ) + } else { + println("Couldn't find cell break") + break + } + } + + return cage + } + + fun generate(minSize: Int = 2, maxSize: Int = 6): List { + val cages = mutableListOf() + + var id = 0 + while (unusedCells.isNotEmpty()) { + val cage = generateCage( + startCell = unusedCells.random(), + requiredSize = Random.nextInt(minSize, maxSize + 1), + id = id + ) + if (cage != null) { + cages.add(cage.copy(sum = cage.cells.sumOf { it.value })) + } else { + Log.d("KillerSudokuCageGen", "Couldn't generate cage") + } + id += 1 + } + return cages.toList() + } + + private fun getUnusedNeighbors(cell: Cell): List { + val neighbors = mutableListOf() + val row = cell.row + val col = cell.col + + if (row > 0) { + neighbors.add(board[row - 1][col]) + } + if (col > 0) { + neighbors.add(board[row][col - 1]) + } + if (col < type.size - 1) { + neighbors.add(board[row][col + 1]) + } + if (row < type.size - 1) { + neighbors.add(board[row][col]) + } + + return neighbors + .toList() + .filter { unusedCells.contains(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/GameType.kt b/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/GameType.kt index cfc60052..cd92b66a 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/GameType.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/core/qqwing/GameType.kt @@ -12,4 +12,7 @@ enum class GameType( Default9x9(9, 3, 3, R.string.type_default_9x9), Default12x12(12, 3, 4, R.string.type_default_12x12), Default6x6(6, 2, 3, R.string.type_default_6x6), + Killer9x9(9, 3, 3, R.string.type_killer_9x9), + Killer12x12(12, 3, 4, R.string.type_killer_12x12), + Killer6x6(6, 2, 3, R.string.type_killer_6x6), } \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/core/utils/SudokuParser.kt b/app/src/main/java/com/kaajjo/libresudoku/core/utils/SudokuParser.kt index 07cb87ac..ef026753 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/core/utils/SudokuParser.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/core/utils/SudokuParser.kt @@ -2,7 +2,10 @@ package com.kaajjo.libresudoku.core.utils import com.kaajjo.libresudoku.core.Cell import com.kaajjo.libresudoku.core.Note +import com.kaajjo.libresudoku.core.qqwing.Cage import com.kaajjo.libresudoku.core.qqwing.GameType +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json class SudokuParser { private val emptySeparators = listOf('0', '.') @@ -99,6 +102,17 @@ class SudokuParser { private fun boardDigitToInt(char: Char): Int { return char.digitToInt(radix) } + + fun killerSudokuCagesToString(cages: List): String { + val json = Json { + encodeDefaults = true + } + return json.encodeToString(cages) + } + fun parseKillerSudokuCages(cagesString: String): List { + val json = Json { ignoreUnknownKeys = true } + return json.decodeFromString>(cagesString) + } } class BoardParseException(message: String) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/core/utils/SudokuUtils.kt b/app/src/main/java/com/kaajjo/libresudoku/core/utils/SudokuUtils.kt index b33131a7..6c600b81 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/core/utils/SudokuUtils.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/core/utils/SudokuUtils.kt @@ -139,7 +139,7 @@ class SudokuUtils { } } - GameType.Default9x9 -> { + GameType.Default9x9, GameType.Killer9x9 -> { when (factor) { 1 -> 28.sp 2 -> 36.sp @@ -147,7 +147,7 @@ class SudokuUtils { } } - GameType.Default12x12 -> { + GameType.Default12x12, GameType.Killer12x12 -> { when (factor) { 1 -> 24.sp 2 -> 32.sp @@ -155,7 +155,7 @@ class SudokuUtils { } } - GameType.Default6x6 -> { + GameType.Default6x6, GameType.Killer6x6 -> { when (factor) { 1 -> 34.sp 2 -> 40.sp diff --git a/app/src/main/java/com/kaajjo/libresudoku/data/database/AppDataBase.kt b/app/src/main/java/com/kaajjo/libresudoku/data/database/AppDataBase.kt index de834ddb..b03adf1d 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/data/database/AppDataBase.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/data/database/AppDataBase.kt @@ -22,12 +22,13 @@ import com.kaajjo.libresudoku.data.database.model.SudokuBoard @Database( entities = [Record::class, SudokuBoard::class, SavedGame::class, Folder::class], - version = 5, + version = 6, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(from = 3, to = 4), - AutoMigration(from = 4, to = 5) + AutoMigration(from = 4, to = 5), + AutoMigration(from = 5, to = 6) ] ) @TypeConverters( diff --git a/app/src/main/java/com/kaajjo/libresudoku/data/database/converters/GameTypeConverter.kt b/app/src/main/java/com/kaajjo/libresudoku/data/database/converters/GameTypeConverter.kt index 442918f6..0c25d037 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/data/database/converters/GameTypeConverter.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/data/database/converters/GameTypeConverter.kt @@ -14,6 +14,9 @@ class GameTypeConverter { GameType.Default9x9 -> 1 GameType.Default12x12 -> 2 GameType.Default6x6 -> 3 + GameType.Killer9x9 -> 4 + GameType.Killer12x12 -> 5 + GameType.Killer6x6 -> 6 } } @@ -24,6 +27,9 @@ class GameTypeConverter { 1 -> GameType.Default9x9 2 -> GameType.Default12x12 3 -> GameType.Default6x6 + 4 -> GameType.Killer9x9 + 5 -> GameType.Killer12x12 + 6 -> GameType.Killer6x6 else -> GameType.Unspecified } } diff --git a/app/src/main/java/com/kaajjo/libresudoku/data/database/model/SudokuBoard.kt b/app/src/main/java/com/kaajjo/libresudoku/data/database/model/SudokuBoard.kt index e374402d..ffa929dd 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/data/database/model/SudokuBoard.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/data/database/model/SudokuBoard.kt @@ -26,5 +26,6 @@ data class SudokuBoard( @ColumnInfo(name = "solved_board") val solvedBoard: String, @ColumnInfo(name = "difficulty") val difficulty: GameDifficulty, @ColumnInfo(name = "type") val type: GameType, - @ColumnInfo(name = "folder_id", defaultValue = "null") val folderId: Long? = null + @ColumnInfo(name = "folder_id", defaultValue = "null") val folderId: Long? = null, + @ColumnInfo(name = "killer_cages", defaultValue = "null") val killerCages: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/data/datastore/AppSettingsManager.kt b/app/src/main/java/com/kaajjo/libresudoku/data/datastore/AppSettingsManager.kt index 7ec85cbe..a7088c33 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/data/datastore/AppSettingsManager.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/data/datastore/AppSettingsManager.kt @@ -289,6 +289,9 @@ class AppSettingsManager(context: Context) { GameType.Default9x9 -> "1" GameType.Default12x12 -> "2" GameType.Default6x6 -> "3" + GameType.Killer9x9 -> "4" + GameType.Killer12x12 -> "5" + GameType.Killer6x6 -> "6" } settings[lastSelectedGameDifficultyTypeKey] = difficultyAndType } @@ -318,6 +321,9 @@ class AppSettingsManager(context: Context) { "1" -> GameType.Default9x9 "2" -> GameType.Default12x12 "3" -> GameType.Default6x6 + "4" -> GameType.Killer9x9 + "5" -> GameType.Killer12x12 + "6" -> GameType.Killer6x6 else -> GameType.Default9x9 } } diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/Board.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/Board.kt index 14f6eb5e..f6a2c33c 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/Board.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/Board.kt @@ -1,7 +1,6 @@ package com.kaajjo.libresudoku.ui.components.board import android.graphics.Paint -import android.graphics.Rect import android.util.TypedValue import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectTapGestures @@ -23,8 +22,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke @@ -41,6 +43,7 @@ import androidx.compose.ui.unit.sp import com.kaajjo.libresudoku.LocalBoardColors import com.kaajjo.libresudoku.core.Cell import com.kaajjo.libresudoku.core.Note +import com.kaajjo.libresudoku.core.qqwing.Cage import com.kaajjo.libresudoku.core.qqwing.GameType import com.kaajjo.libresudoku.core.utils.SudokuParser import com.kaajjo.libresudoku.ui.theme.BoardColors @@ -82,7 +85,8 @@ fun Board( cellsToHighlight: List? = null, zoomable: Boolean = false, boardColors: SudokuBoardColors = LocalBoardColors.current, - crossHighlight: Boolean = false + crossHighlight: Boolean = false, + cages: List = emptyList() ) { BoxWithConstraints( modifier = modifier @@ -115,6 +119,7 @@ fun Board( var fontSizePx = with(LocalDensity.current) { mainTextSize.toPx() } var noteSizePx = with(LocalDensity.current) { noteTextSize.toPx() } + var killerSumSizePx = with(LocalDensity.current) { noteTextSize.toPx() * 0.9f } val thinLineWidth = with(LocalDensity.current) { 1.3.dp.toPx() } val thickLineWidth = with(LocalDensity.current) { 1.3.dp.toPx() } @@ -162,6 +167,16 @@ fun Board( ) } + var killerSumPaint by remember { + mutableStateOf( + Paint().apply { + color = notesColor.toArgb() + isAntiAlias = true + textSize = killerSumSizePx + } + ) + } + val context = LocalContext.current LaunchedEffect(mainTextSize, noteTextSize, boardColors) { fontSizePx = TypedValue.applyDimension( @@ -174,6 +189,11 @@ fun Board( noteTextSize.value, context.resources.displayMetrics ) + killerSumSizePx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + noteTextSize.value * 0.9f, + context.resources.displayMetrics + ) textPaint = Paint().apply { color = foregroundColor.toArgb() isAntiAlias = true @@ -194,6 +214,11 @@ fun Board( isAntiAlias = true textSize = fontSizePx } + killerSumPaint = Paint().apply { + color = notesColor.toArgb() + isAntiAlias = true + textSize = killerSumSizePx + } } var zoom by remember(enabled) { mutableFloatStateOf(1f) } @@ -206,8 +231,14 @@ fun Board( onTap = { if (enabled) { val totalOffset = it / zoom + offset - val row = floor((totalOffset.y) / cellSize).toInt().coerceIn(board.indices) - val column = floor((totalOffset.x) / cellSize).toInt().coerceIn(board.indices) + val row = + floor((totalOffset.y) / cellSize) + .toInt() + .coerceIn(board.indices) + val column = + floor((totalOffset.x) / cellSize) + .toInt() + .coerceIn(board.indices) onClick(board[row][column]) } }, @@ -258,34 +289,33 @@ fun Board( Canvas( modifier = if (zoomable) boardModifier.then(zoomModifier) else boardModifier ) { + val cornerRadius = CornerRadius(15f, 15f) + if (selectedCell.row >= 0 && selectedCell.col >= 0) { // current cell - drawRect( - color = highlightColor.copy(alpha = 0.2f), - topLeft = Offset( - x = selectedCell.col * cellSize, - y = selectedCell.row * cellSize + drawRoundCell( + row = selectedCell.row, + col = selectedCell.col, + gameSize = size, + rect = Rect( + offset = Offset( + x = selectedCell.col * cellSize, + y = selectedCell.row * cellSize + ), + size = Size(cellSize, cellSize) ), - size = Size(cellSize, cellSize) + color = highlightColor.copy(alpha = 0.2f), + cornerRadius = cornerRadius ) if (positionLines) { - // vertical position line - drawRect( + drawPositionLines( + row = selectedCell.row, + col = selectedCell.col, + gameSize = size, color = highlightColor.copy(alpha = 0.1f), - topLeft = Offset( - x = selectedCell.col * cellSize, - y = 0f - ), - size = Size(cellSize, maxWidth) - ) - // horizontal position line - drawRect( - color = highlightColor.copy(alpha = 0.1f), - topLeft = Offset( - x = 0f, - y = selectedCell.row * cellSize - ), - size = Size(maxWidth, cellSize) + cellSize = cellSize, + lineLength = maxWidth, + cornerRadius = cornerRadius ) } } @@ -293,26 +323,38 @@ fun Board( for (i in 0 until size) { for (j in 0 until size) { if (board[i][j].value == selectedCell.value && board[i][j].value != 0) { - drawRect( - color = highlightColor.copy(alpha = 0.2f), - topLeft = Offset( - x = board[i][j].col * cellSize, - y = board[i][j].row * cellSize + drawRoundCell( + row = board[i][j].row, + col = board[i][j].col, + gameSize = size, + rect = Rect( + offset = Offset( + x = board[i][j].col * cellSize, + y = board[i][j].row * cellSize + ), + size = Size(cellSize, cellSize) ), - size = Size(cellSize, cellSize) + color = highlightColor.copy(alpha = 0.2f), + cornerRadius = cornerRadius ) } } } } cellsToHighlight?.forEach { - drawRect( + drawRoundCell( + row = it.row, + col = it.col, + gameSize = size, color = highlightColor.copy(alpha = 0.3f), - topLeft = Offset( - x = it.col * cellSize, - y = it.row * cellSize + rect = Rect( + Offset( + x = it.col * cellSize, + y = it.row * cellSize + ), + size = Size(cellSize, cellSize) ), - size = Size(cellSize, cellSize) + cornerRadius = cornerRadius ) } @@ -387,6 +429,35 @@ fun Board( } } } + + if (cages.isNotEmpty()) { + cages.forEach { cage -> + val noteBounds = android.graphics.Rect() + val textToDraw = cage.sum.toString() + killerSumPaint.getTextBounds(textToDraw, 0, textToDraw.length, noteBounds) + // get top left cell + val cellWithSum = cage.cells.minWith( + compareBy { it.row }.thenBy { it.col } + ) + drawIntoCanvas { canvas -> + canvas.nativeCanvas.drawText( + textToDraw, + cellWithSum.col * cellSize + (cellSize * 0.05f), + cellWithSum.row * cellSize + noteBounds.height() + (cellSize * 0.05f), + killerSumPaint + ) + } + + drawKillerCage( + cage = cage, + cellWithSum = cellWithSum, + cellSize = cellSize, + strokeWidth = thickLineWidth, + color = thinLineColor, + cornerTextPadding = Offset(noteBounds.width().toFloat(), noteBounds.height().toFloat()) + ) + } + } } } } @@ -428,7 +499,7 @@ private fun DrawScope.drawNumbers( val textToDraw = if (questions) "?" else board[i][j].value.toString(16).uppercase() - val textBounds = Rect() + val textBounds = android.graphics.Rect() textPaint.getTextBounds(textToDraw, 0, 1, textBounds) val textWidth = paint.measureText(textToDraw) @@ -452,7 +523,7 @@ private fun DrawScope.drawNotes( cellSizeDivWidth: Float, cellSizeDivHeight: Float ) { - val noteBounds = Rect() + val noteBounds = android.graphics.Rect() paint.getTextBounds("1", 0, 1, noteBounds) drawIntoCanvas { canvas -> @@ -475,62 +546,52 @@ private fun DrawScope.drawNotes( } } -private fun getNoteColumnNumber(number: Int, size: Int): Int { - if (size == 9 || size == 6) { - return when (number) { - 1, 2, 3 -> 0 - 4, 5, 6 -> 1 - 7, 8, 9 -> 2 - else -> 0 - } - } else if (size == 12) { - return when (number) { - 1, 2, 3, 4 -> 0 - 5, 6, 7, 8 -> 1 - 9, 10, 11, 12 -> 2 - else -> 0 - } - } - return 0 -} - -private fun getNoteRowNumber(number: Int, size: Int): Int { - if (size == 9 || size == 6) { - return when (number) { - 1, 4, 7 -> 0 - 2, 5, 8 -> 1 - 3, 6, 9 -> 2 - else -> 0 - } - } else if (size == 12) { - return when (number) { - 1, 5, 9 -> 0 - 2, 6, 10 -> 1 - 3, 7, 11 -> 2 - 4, 8, 12 -> 3 - else -> 0 - } +private fun DrawScope.drawRoundCell( + row: Int, + col: Int, + gameSize: Int, + rect: Rect, + color: Color, + cornerRadius: CornerRadius = CornerRadius.Zero +) { + val path = Path().apply { + addRoundRect( + roundRectForCell( + row = row, + col = col, + gameSize = gameSize, + rect = rect, + cornerRadius = cornerRadius + ) + ) } - return 0 + drawPath( + path = path, + color = color + ) } -private fun getSectionHeightForSize(size: Int): Int { - return when (size) { - 6 -> GameType.Default6x6.sectionHeight - 9 -> GameType.Default9x9.sectionHeight - 12 -> GameType.Default12x12.sectionHeight - else -> GameType.Default9x9.sectionHeight - } +private fun roundRectForCell( + row: Int, + col: Int, + gameSize: Int, + rect: Rect, + cornerRadius: CornerRadius +): RoundRect { + val topLeft = if (row == 0 && col == 0) cornerRadius else CornerRadius.Zero + val topRight = if (row == 0 && col == gameSize - 1)cornerRadius else CornerRadius.Zero + val bottomLeft = if (row == gameSize - 1 && col == 0) cornerRadius else CornerRadius.Zero + val bottomRight = if (row == gameSize - 1 && col == gameSize - 1) cornerRadius else CornerRadius.Zero + + return RoundRect( + rect = rect, + topLeft = topLeft, + topRight = topRight, + bottomLeft = bottomLeft, + bottomRight = bottomRight + ) } -private fun getSectionWidthForSize(size: Int): Int { - return when (size) { - 6 -> GameType.Default6x6.sectionWidth - 9 -> GameType.Default9x9.sectionWidth - 12 -> GameType.Default12x12.sectionWidth - else -> GameType.Default9x9.sectionWidth - } -} @LightDarkPreview @Composable diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/BoardUtil.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/BoardUtil.kt new file mode 100644 index 00000000..efa71f38 --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/BoardUtil.kt @@ -0,0 +1,60 @@ +package com.kaajjo.libresudoku.ui.components.board + +import com.kaajjo.libresudoku.core.qqwing.GameType + +fun getNoteColumnNumber(number: Int, size: Int): Int { + if (size == 9 || size == 6) { + return when (number) { + 1, 2, 3 -> 0 + 4, 5, 6 -> 1 + 7, 8, 9 -> 2 + else -> 0 + } + } else if (size == 12) { + return when (number) { + 1, 2, 3, 4 -> 0 + 5, 6, 7, 8 -> 1 + 9, 10, 11, 12 -> 2 + else -> 0 + } + } + return 0 +} + +fun getNoteRowNumber(number: Int, size: Int): Int { + if (size == 9 || size == 6) { + return when (number) { + 1, 4, 7 -> 0 + 2, 5, 8 -> 1 + 3, 6, 9 -> 2 + else -> 0 + } + } else if (size == 12) { + return when (number) { + 1, 5, 9 -> 0 + 2, 6, 10 -> 1 + 3, 7, 11 -> 2 + 4, 8, 12 -> 3 + else -> 0 + } + } + return 0 +} + +fun getSectionHeightForSize(size: Int): Int { + return when (size) { + 6 -> GameType.Default6x6.sectionHeight + 9 -> GameType.Default9x9.sectionHeight + 12 -> GameType.Default12x12.sectionHeight + else -> GameType.Default9x9.sectionHeight + } +} + +fun getSectionWidthForSize(size: Int): Int { + return when (size) { + 6 -> GameType.Default6x6.sectionWidth + 9 -> GameType.Default9x9.sectionWidth + 12 -> GameType.Default12x12.sectionWidth + else -> GameType.Default9x9.sectionWidth + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/KillerSudokuDrawUtil.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/KillerSudokuDrawUtil.kt new file mode 100644 index 00000000..80346a39 --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/KillerSudokuDrawUtil.kt @@ -0,0 +1,108 @@ +package com.kaajjo.libresudoku.ui.components.board + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.DrawScope +import com.kaajjo.libresudoku.core.Cell +import com.kaajjo.libresudoku.core.qqwing.Cage + +fun DrawScope.drawKillerCage( + cage: Cage, + cellWithSum: Cell, + cellSize: Float, + strokeWidth: Float, + color: Color, + cornerTextPadding: Offset +) { + val pathEffect = PathEffect.dashPathEffect(floatArrayOf(cellSize / 12f, cellSize / 12f)) + val padding = cellSize * 0.1f + + cage.cells.forEach { cell -> + val halfPadding = padding / 2f + val xCell = cell.col * cellSize + val yCell = cell.row * cellSize + + val hasPaddingForText = cell == cellWithSum + + val hasNeighborLeft = hasNeighborLeft(cell, cage) + val hasNeighborRight = hasNeighborRight(cell, cage) + val hasNeighborTop = hasNeighborTop(cell, cage) + val hasNeighborBottom = hasNeighborBottom(cell, cage) + + if (!hasNeighborLeft) { + drawLine( + color = color, + start = Offset( + x = xCell + padding, + y = yCell + if (hasPaddingForText) (cornerTextPadding.y + padding) else if (hasNeighborTop) -halfPadding else padding + ), + end = Offset( + x = xCell + padding, + y = yCell + cellSize + if (hasNeighborBottom) halfPadding else -padding + ), + strokeWidth = strokeWidth, + pathEffect = pathEffect + ) + } + + if (!hasNeighborRight) { + drawLine( + color = color, + start = Offset( + x = xCell + cellSize - padding, + y = yCell + if (hasNeighborTop) -halfPadding else padding + ), + end = Offset( + x = xCell + cellSize - padding, + y = yCell + cellSize + if (hasNeighborBottom) halfPadding else -padding + ), + strokeWidth = strokeWidth, + pathEffect = pathEffect + ) + } + if (!hasNeighborTop) { + drawLine( + color = color, + start = Offset( + x = xCell + if (hasPaddingForText) (cornerTextPadding.x + padding) else if (hasNeighborLeft) -halfPadding else padding, + y = yCell + padding + ), + end = Offset( + x = xCell + cellSize + if (hasNeighborRight) halfPadding else -padding, + y = yCell + padding + ), + strokeWidth = strokeWidth, + pathEffect = pathEffect + ) + } + + if (!hasNeighborBottom) { + drawLine( + color = color, + start = Offset( + x = xCell + if (hasNeighborLeft) -halfPadding else padding, + y = yCell + cellSize - padding + ), + end = Offset( + x = xCell + cellSize + if (hasNeighborRight) halfPadding else -padding, + y = yCell + cellSize - padding + ), + strokeWidth = strokeWidth, + pathEffect = pathEffect + ) + } + } +} + +private fun hasNeighborLeft(cell: Cell, cage: Cage) = + cage.cells.any { it.row == cell.row && it.col == cell.col - 1 } + +private fun hasNeighborRight(cell: Cell, cage: Cage) = + cage.cells.any { it.row == cell.row && it.col == cell.col + 1 } + +private fun hasNeighborTop(cell: Cell, cage: Cage) = + cage.cells.any { it.row == cell.row - 1 && it.col == cell.col } + +private fun hasNeighborBottom(cell: Cell, cage: Cage) = + cage.cells.any { it.row == cell.row + 1 && it.col == cell.col } diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/PositionLines.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/PositionLines.kt new file mode 100644 index 00000000..3d6284f3 --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/components/board/PositionLines.kt @@ -0,0 +1,107 @@ +package com.kaajjo.libresudoku.ui.components.board + +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope + +fun DrawScope.drawPositionLines( + row: Int, + col: Int, + gameSize: Int, + cellSize: Float, + lineLength: Float, + color: Color, + cornerRadius: CornerRadius +) { + drawPositionLineVertical( + col = col, + gameSize = gameSize, + cellSize = cellSize, + lineLength = lineLength, + color = color, + cornerRadius = cornerRadius + ) + drawPositionLineHorizontal( + row = row, + gameSize = gameSize, + cellSize = cellSize, + lineLength = lineLength, + color = color, + cornerRadius = cornerRadius + ) +} + +private fun DrawScope.drawPositionLineVertical( + col: Int, + gameSize: Int, + cellSize: Float, + lineLength: Float, + color: Color, + cornerRadius: CornerRadius, +) { + val topLeft = if (col == 0) cornerRadius else CornerRadius.Zero + val topRight = if (col == gameSize - 1) cornerRadius else CornerRadius.Zero + val bottomLeft = if (col == 0) cornerRadius else CornerRadius.Zero + val bottomRight = if (col == gameSize - 1) cornerRadius else CornerRadius.Zero + + drawPath( + path = Path().apply { + addRoundRect( + roundRect = RoundRect( + rect = Rect( + offset = Offset( + x = col * cellSize, + y = 0f + ), + size = Size(cellSize, lineLength) + ), + topLeft = topLeft, + topRight = topRight, + bottomLeft = bottomLeft, + bottomRight = bottomRight, + ) + ) + }, + color = color + ) +} + +private fun DrawScope.drawPositionLineHorizontal( + row: Int, + gameSize: Int, + cellSize: Float, + lineLength: Float, + color: Color, + cornerRadius: CornerRadius, +) { + val topLeft = if (row == 0) cornerRadius else CornerRadius.Zero + val topRight = if (row == 0) cornerRadius else CornerRadius.Zero + val bottomLeft = if (row == gameSize - 1) cornerRadius else CornerRadius.Zero + val bottomRight = if (row == gameSize - 1) cornerRadius else CornerRadius.Zero + + drawPath( + path = Path().apply { + addRoundRect( + roundRect = RoundRect( + rect = Rect( + offset = Offset( + x = 0f, + y = row * cellSize + ), + size = Size(lineLength, cellSize) + ), + topLeft = topLeft, + topRight = topRight, + bottomLeft = bottomLeft, + bottomRight = bottomRight, + ) + ) + }, + color = color + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameScreen.kt index 8599c9ea..c74bc401 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameScreen.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameScreen.kt @@ -303,8 +303,9 @@ fun GameScreen( enabled = viewModel.gamePlaying && !viewModel.endGame, questions = !(viewModel.gamePlaying || viewModel.endGame) && Build.VERSION.SDK_INT < Build.VERSION_CODES.R, renderNotes = renderNotes && !viewModel.showSolution, - zoomable = viewModel.gameType == GameType.Default12x12, - crossHighlight = crossHighlight + zoomable = viewModel.gameType == GameType.Default12x12 || viewModel.gameType == GameType.Killer12x12, + crossHighlight = crossHighlight, + cages = viewModel.cages ) } diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameViewModel.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameViewModel.kt index 3e8aef86..7cad774d 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameViewModel.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope import com.kaajjo.libresudoku.core.Cell import com.kaajjo.libresudoku.core.Note import com.kaajjo.libresudoku.core.PreferencesConstants +import com.kaajjo.libresudoku.core.qqwing.Cage import com.kaajjo.libresudoku.core.qqwing.GameDifficulty import com.kaajjo.libresudoku.core.qqwing.GameType import com.kaajjo.libresudoku.core.qqwing.QQWingController @@ -89,6 +90,9 @@ class GameViewModel @Inject constructor( boardEntity.solvedBoard, boardEntity.type ) + boardEntity.killerCages?.let { cagesString -> + cages = sudokuParser.parseKillerSudokuCages(cagesString) + } for (i in solvedBoard.indices) { for (j in solvedBoard.indices) { solvedBoard[i][j].locked = initialBoard[i][j].locked @@ -183,6 +187,7 @@ class GameViewModel @Inject constructor( private lateinit var initialBoard: List> var gameBoard by mutableStateOf(List(9) { row -> List(9) { col -> Cell(row, col, 0) } }) var solvedBoard = emptyList>() + var cages by mutableStateOf(emptyList()) var currCell by mutableStateOf(Cell(-1, -1, 0)) private var undoRedoManager = UndoRedoManager(GameState(gameBoard, notes)) diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/GamesHistoryScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/GamesHistoryScreen.kt index 97268bc8..d443702b 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/GamesHistoryScreen.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/GamesHistoryScreen.kt @@ -257,6 +257,9 @@ fun GamesHistoryScreen( GameType.Default9x9, GameType.Default6x6, GameType.Default12x12, + GameType.Killer9x9, + GameType.Killer12x12, + GameType.Killer6x6 ) ) { AnimatedIconFilterChip( diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/savedgame/SavedGameScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/savedgame/SavedGameScreen.kt index fae0c481..0ad59264 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/savedgame/SavedGameScreen.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/savedgame/SavedGameScreen.kt @@ -210,7 +210,8 @@ fun SavedGameScreen( mainTextSize = fontSizeValue, selectedCell = Cell(-1, -1), onClick = { }, - crossHighlight = crossHighlight + crossHighlight = crossHighlight, + cages = viewModel.killerCages ) 1 -> Board( @@ -219,7 +220,8 @@ fun SavedGameScreen( mainTextSize = fontSizeValue, selectedCell = Cell(-1, -1), onClick = { }, - crossHighlight = crossHighlight + crossHighlight = crossHighlight, + cages = viewModel.killerCages ) } } diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/savedgame/SavedGameViewModel.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/savedgame/SavedGameViewModel.kt index 98d8b11e..47779cfc 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/savedgame/SavedGameViewModel.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/gameshistory/savedgame/SavedGameViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kaajjo.libresudoku.core.Cell import com.kaajjo.libresudoku.core.Note +import com.kaajjo.libresudoku.core.qqwing.Cage import com.kaajjo.libresudoku.core.utils.SudokuParser import com.kaajjo.libresudoku.core.utils.SudokuUtils import com.kaajjo.libresudoku.data.database.model.Folder @@ -52,6 +53,7 @@ class SavedGameViewModel var parsedInitialBoard by mutableStateOf(emptyList>()) var parsedCurrentBoard by mutableStateOf(emptyList>()) + var killerCages by mutableStateOf(emptyList()) var notes by mutableStateOf(emptyList()) var exportDialog by mutableStateOf(false) @@ -88,6 +90,9 @@ class SavedGameViewModel } } notes = sudokuParser.parseNotes(savedGame.notes) + if (boardEntity.killerCages != null) { + killerCages = sudokuParser.parseKillerSudokuCages(boardEntity.killerCages) + } } } diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/home/HomeViewModel.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/home/HomeViewModel.kt index 944c9c54..999ac3aa 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/home/HomeViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kaajjo.libresudoku.core.Cell import com.kaajjo.libresudoku.core.PreferencesConstants +import com.kaajjo.libresudoku.core.qqwing.CageGenerator import com.kaajjo.libresudoku.core.qqwing.GameDifficulty import com.kaajjo.libresudoku.core.qqwing.GameType import com.kaajjo.libresudoku.core.qqwing.QQWingController @@ -51,7 +52,10 @@ class HomeViewModel private val types = listOf( GameType.Default9x9, GameType.Default6x6, - GameType.Default12x12 + GameType.Default12x12, + GameType.Killer9x9, + GameType.Killer12x12, + GameType.Killer6x6 ) val lastSelectedGameDifficultyType = appSettingsManager.lastSelectedGameDifficultyType @@ -111,6 +115,7 @@ class HomeViewModel val solved = qqWingController.solve(generated, gameTypeToGenerate) isSolving = false + if (!qqWingController.isImpossible && qqWingController.solutionCount == 1) { for (i in 0 until size) { for (j in 0 until size) { @@ -119,6 +124,8 @@ class HomeViewModel } } + val generator = CageGenerator(solvedPuzzle, gameTypeToGenerate) + val cages = generator.generate(2, 5) withContext(Dispatchers.IO) { val sudokuParser = SudokuParser() insertedBoardUid = boardRepository.insert( @@ -127,7 +134,8 @@ class HomeViewModel initialBoard = sudokuParser.boardToString(puzzle), solvedBoard = sudokuParser.boardToString(solvedPuzzle), difficulty = selectedDifficulty, - type = selectedType + type = selectedType, + killerCages = SudokuParser().killerSudokuCagesToString(cages) ) ) } diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/statistics/StatisticsScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/statistics/StatisticsScreen.kt index a8a26dd1..2f6b061f 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/statistics/StatisticsScreen.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/statistics/StatisticsScreen.kt @@ -128,7 +128,10 @@ fun StatisticsScreen( types = listOf( Pair(GameType.Default9x9, stringResource(R.string.type_default_9x9)), Pair(GameType.Default6x6, stringResource(R.string.type_default_6x6)), - Pair(GameType.Default12x12, stringResource(R.string.type_default_12x12)) + Pair(GameType.Default12x12, stringResource(R.string.type_default_12x12)), + Pair(GameType.Killer9x9, stringResource(R.string.type_killer_9x9)), + Pair(GameType.Killer6x6, stringResource(R.string.type_killer_6x6)), + Pair(GameType.Killer12x12, stringResource(R.string.type_killer_12x12)) ), selected = viewModel.selectedType, onSelected = { viewModel.setType(it) } diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 89bf4ea3..a2c6f759 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -20,6 +20,9 @@ 6x6 9x9 12x12 + Киллер 9x9 + Киллер 6x6 + Киллер 12x12 Неопределенный Настройки Больше diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51fb8bd8..67f65a7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,9 @@ 6x6 9x9 12x12 + Killer 9x9 + Killer 6x6 + Killer 12x12 Unspecified Settings More @@ -350,4 +353,6 @@ TON USDT (TRC20) MIR + + \ No newline at end of file