diff --git a/fastlane/metadata/android/en-US/changelogs/3.txt b/fastlane/metadata/android/en-US/changelogs/3.txt new file mode 100644 index 0000000000000000000000000000000000000000..23cb99cfd2751705cbe2e209ed3505973b08787a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3.txt @@ -0,0 +1 @@ +Improve/fix grid solver. diff --git a/fastlane/metadata/android/fr-FR/changelogs/3.txt b/fastlane/metadata/android/fr-FR/changelogs/3.txt new file mode 100644 index 0000000000000000000000000000000000000000..dd898d8d2bcee2dcb85a83ecc0ff77cd5f99c6de --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/3.txt @@ -0,0 +1 @@ +Amélioration/correction du résolveur de grille. diff --git a/lib/models/activity/activity.dart b/lib/models/activity/activity.dart index c13811d9970edd17c95af252aa41f2beafbee623..c633981f5338d76828d543c75e9362676db110fe 100644 --- a/lib/models/activity/activity.dart +++ b/lib/models/activity/activity.dart @@ -8,7 +8,9 @@ import 'package:suguru/data/game_data.dart'; import 'package:suguru/models/activity/board.dart'; import 'package:suguru/models/activity/cell.dart'; import 'package:suguru/models/activity/cell_location.dart'; +import 'package:suguru/models/activity/move.dart'; import 'package:suguru/models/activity/types.dart'; +import 'package:suguru/utils/suguru_solver.dart'; class Activity { Activity({ @@ -139,16 +141,12 @@ class Activity { bool get canGiveTip => (buttonTipsCountdown == 0); bool checkBoardIsSolved() { - for (CellLocation location in board.getCellLocations()) { - if (board.get(location).value == 0 || - board.get(location).value != board.solvedCells[location.row][location.col].value) { - return false; - } + if (board.isSolved()) { + printlog('-> ok suguru solved!'); + return true; } - printlog('-> ok suguru solved!'); - - return true; + return false; } ConflictsCount computeConflictsInBoard() { @@ -227,7 +225,7 @@ class Activity { tipGiven = helpSelectCell(activityCubit); } else { // currently selected cell -> set value - tipGiven = helpFillCell(activityCubit); + tipGiven = helpFillCell(activityCubit, selectedCell?.location); } if (tipGiven) { @@ -236,56 +234,66 @@ class Activity { } bool helpSelectCell(ActivityCubit activityCubit) { + CellLocation? cellLocationToFix; + // pick one of wrong value cells, if found - final List<List<int>> wrongValueCells = getCellsWithWrongValue(); + final List<CellLocation> wrongValueCells = getCellsWithWrongValue(); if (wrongValueCells.isNotEmpty) { printlog('pick from wrongValueCells'); - return pickRandomFromList(activityCubit, wrongValueCells); + wrongValueCells.shuffle(); + cellLocationToFix = wrongValueCells[0]; } // pick one of conflicting cells, if found - final List<List<int>> conflictingCells = getCellsWithConflicts(); + final List<CellLocation> conflictingCells = getCellsWithConflicts(); if (conflictingCells.isNotEmpty) { printlog('pick from conflictingCells'); - return pickRandomFromList(activityCubit, conflictingCells); + conflictingCells.shuffle(); + cellLocationToFix = conflictingCells[0]; } - // pick one from "easy" cells (unique empty cell in block) - final List<List<int>> easyFillableCells = board.getLastEmptyCellsInBlocks(); - if (easyFillableCells.isNotEmpty) { - printlog('pick from easyFillableCells'); - return pickRandomFromList(activityCubit, easyFillableCells); + if (cellLocationToFix != null) { + printlog('picked cell with wrong or conflicting value...'); + activityCubit.selectCell(cellLocationToFix); + return true; } - // pick one from cells with unique non-conflicting candidate value - final List<List<int>> candidateCells = board.getEmptyCellsWithUniqueAvailableValue(); - if (candidateCells.isNotEmpty) { - printlog('pick from candidateCells'); - return pickRandomFromList(activityCubit, candidateCells); + final Move? nextMove = SuguruSolver.pickNextMove(board); + + if (nextMove == null) { + printlog('no easy cell to select...'); + return false; } - // pick one from "only cell in this block for this value" - final List<List<int>> onlyCellsWithoutConflict = board.getOnlyCellInBlockWithoutConflict(); - if (onlyCellsWithoutConflict.isNotEmpty) { - printlog('pick from onlyCellsWithoutConflict'); - return pickRandomFromList(activityCubit, onlyCellsWithoutConflict); + activityCubit.selectCell(nextMove.location); + return true; + } + + bool helpFillCell(ActivityCubit activityCubit, CellLocation? location) { + final Move? nextMove = SuguruSolver.pickNextMove(board, location); + + if (nextMove == null) { + printlog('unable to compute cell value for $location'); + activityCubit.unselectCell(); + return false; } - printlog('no easy cell to select...'); - return false; + activityCubit.updateCellValue(nextMove.location, nextMove.value); + activityCubit.unselectCell(); + return true; } - List<List<int>> getCellsWithWrongValue() { + List<CellLocation> getCellsWithWrongValue() { final BoardCells cells = board.cells; final BoardCells cellsSolved = board.solvedCells; - List<List<int>> cellsWithWrongValue = []; + List<CellLocation> cellsWithWrongValue = []; for (int row = 0; row < boardSizeVertical; row++) { for (int col = 0; col < boardSizeHorizontal; col++) { if (cells[row][col].value != 0 && cells[row][col].value != cellsSolved[row][col].value) { - cellsWithWrongValue.add([row, col]); + cellsWithWrongValue.add(CellLocation.go(row, col)); } } } @@ -293,13 +301,13 @@ class Activity { return cellsWithWrongValue; } - List<List<int>> getCellsWithConflicts() { - List<List<int>> cellsWithConflict = []; + List<CellLocation> getCellsWithConflicts() { + List<CellLocation> cellsWithConflict = []; for (int row = 0; row < boardSizeVertical; row++) { for (int col = 0; col < boardSizeHorizontal; col++) { if (boardConflicts[row][col] != 0) { - cellsWithConflict.add([row, col]); + cellsWithConflict.add(CellLocation.go(row, col)); } } } @@ -307,73 +315,6 @@ class Activity { return cellsWithConflict; } - bool pickRandomFromList(ActivityCubit activityCubit, List<List<int>> cellsCoordinates) { - if (cellsCoordinates.isNotEmpty) { - cellsCoordinates.shuffle(); - final List<int> cell = cellsCoordinates[0]; - activityCubit.selectCell(CellLocation.go(cell[0], cell[1])); - return true; - } - - return false; - } - - bool helpFillCell(ActivityCubit activityCubit) { - // Will clean cell if no eligible value found - int eligibleValue = 0; - - // Ensure there is only one eligible value for this cell - int allowedValuesCount = 0; - final int maxValueForThisCell = board.getMaxValueForBlock(selectedCell?.blockId); - for (int value = 1; value <= maxValueForThisCell; value++) { - if (board.isValueAllowed(selectedCell?.location, value)) { - allowedValuesCount++; - eligibleValue = value; - } - } - - if (allowedValuesCount == 1) { - activityCubit.updateCellValue(selectedCell!.location, eligibleValue); - activityCubit.unselectCell(); - return true; - } - - activityCubit.unselectCell(); - return false; - } - - void checkAllTemplates() { - printlog('###############################'); - printlog('## ##'); - printlog('## CHECK TEMPLATES ##'); - printlog('## ##'); - printlog('###############################'); - - final List<String> allowedLevels = ApplicationConfig.config - .getFromCode(ApplicationConfig.parameterCodeDifficultyLevel) - .allowedValues; - final List<String> allowedSizes = ApplicationConfig.config - .getFromCode(ApplicationConfig.parameterCodeBoardSize) - .allowedValues; - - for (String level in allowedLevels) { - printlog('* level: $level'); - for (String size in allowedSizes) { - printlog('** size: $size'); - final List<String> templates = GameData.templates[size]?[level] ?? []; - printlog('*** templates count: ${templates.length}'); - - for (String template in templates) { - printlog(' checking $template'); - - final Board testBoard = Board.createEmpty(); - testBoard.createFromTemplate(template: template); - testBoard.dump(); - } - } - } - } - void dump() { printlog(''); printlog('## Current game dump:'); diff --git a/lib/models/activity/board.dart b/lib/models/activity/board.dart index 663efc3d7fb68a12841d7491bac2a0f31b8cd0a0..3b7bc38fd397179848eaa40055444b24caeb5b1b 100644 --- a/lib/models/activity/board.dart +++ b/lib/models/activity/board.dart @@ -4,7 +4,9 @@ import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; import 'package:suguru/models/activity/cell.dart'; import 'package:suguru/models/activity/cell_location.dart'; +import 'package:suguru/models/activity/move.dart'; import 'package:suguru/models/activity/types.dart'; +import 'package:suguru/utils/suguru_solver.dart'; class Board { Board({ @@ -35,6 +37,9 @@ class Board { void createFromTemplate({ required String template, }) { + printlog('Creating board from template:'); + printlog(template); + final List<String> templateParts = template.split(';'); if (templateParts.length != 3) { printlog('Failed to get grid template (wrong format)...'); @@ -69,49 +74,8 @@ class Board { cells.add(row); } - const List<String> allowedFlip = ['none', 'horizontal', 'vertical']; - List<String> allowedRotate = ['none', 'left', 'right', 'upsidedown']; - - // Limit rotation if board is not symetric - if (boardSizeVertical != boardSizeHorizontal) { - allowedRotate = ['none', 'upsidedown']; - } - - final Random rand = Random(); - final String flip = allowedFlip[rand.nextInt(allowedFlip.length)]; - final String rotate = allowedRotate[rand.nextInt(allowedRotate.length)]; - - switch (flip) { - case 'horizontal': - { - transformFlipHorizontal(); - } - break; - case 'vertical': - { - transformFlipVertical(); - } - break; - } - - switch (rotate) { - case 'left': - { - transformRotateLeft(); - } - break; - case 'right': - { - transformRotateRight(); - } - break; - case 'upsidedown': - { - transformFlipHorizontal(); - transformFlipVertical(); - } - break; - } + // Do some transformations to board + transformBoard(); // Force cells fixed states (all cells with value != 0) for (CellLocation location in getCellLocations()) { @@ -124,7 +88,12 @@ class Board { ); } - resolve(); + final Board solvedBoard = SuguruSolver.resolve(this); + solvedCells = solvedBoard.cells; + + // FIXME: for debug only + // to start with a board (almost) automatically solved + // cells = solvedCells; } // Helper to create board from size, with "empty" cells @@ -166,6 +135,55 @@ class Board { return locations; } + void transformBoard() { + final int boardSizeVertical = cells.length; + final int boardSizeHorizontal = cells[0].length; + + const List<String> allowedFlip = ['none', 'horizontal', 'vertical']; + List<String> allowedRotate = ['none', 'left', 'right', 'upsidedown']; + + // Limit rotation if board is not symetric + if (boardSizeVertical != boardSizeHorizontal) { + allowedRotate = ['none', 'upsidedown']; + } + + final Random rand = Random(); + final String flip = allowedFlip[rand.nextInt(allowedFlip.length)]; + final String rotate = allowedRotate[rand.nextInt(allowedRotate.length)]; + + switch (flip) { + case 'horizontal': + { + transformFlipHorizontal(); + } + break; + case 'vertical': + { + transformFlipVertical(); + } + break; + } + + switch (rotate) { + case 'left': + { + transformRotateLeft(); + } + break; + case 'right': + { + transformRotateRight(); + } + break; + case 'upsidedown': + { + transformFlipHorizontal(); + transformFlipVertical(); + } + break; + } + } + void transformFlipHorizontal() { final BoardCells transformedBoard = copyCells(); final int boardSizeVertical = cells.length; @@ -244,18 +262,25 @@ class Board { cells = transformedBoard; } + bool inBoard(CellLocation location) { + return (location.row >= 0 && + location.row < cells.length && + location.col >= 0 && + location.col < cells[location.row].length); + } + Cell get(CellLocation location) { - if (location.row < cells.length) { - if (location.col < cells[location.row].length) { - return cells[location.row][location.col]; - } + if (inBoard(location)) { + return cells[location.row][location.col]; } return Cell.none; } void setCell(CellLocation location, Cell cell) { - cells[location.row][location.col] = cell; + if (inBoard(location)) { + cells[location.row][location.col] = cell; + } } void setValue(CellLocation location, int value) { @@ -271,6 +296,11 @@ class Board { )); } + void applyMove(Move move) { + // printlog('put ${move.value} in ${move.location}'); + setValue(move.location, move.value); + } + List<String> getBlockIds() { List<String> blockIds = []; for (CellLocation location in getCellLocations()) { @@ -314,48 +344,8 @@ class Board { return copiedGrid; } - resolve() { - final Board solvedBoard = Board(cells: copyCells(), solvedCells: []); - - do { - // last cell in blocks - final List<List<int>> cellsLastEmptyInBlock = solvedBoard.getLastEmptyCellsInBlocks(); - for (var cellData in cellsLastEmptyInBlock) { - solvedBoard.setValue(CellLocation.go(cellData[0], cellData[1]), cellData[2]); - } - - // last cell in blocks - final List<List<int>> cellsSingleInBlockWithoutConflict = - solvedBoard.getOnlyCellInBlockWithoutConflict(); - for (var cellData in cellsSingleInBlockWithoutConflict) { - solvedBoard.setValue(CellLocation.go(cellData[0], cellData[1]), cellData[2]); - } - - // empty cells with unique available value - final List<List<int>> cellsWithUniqueAvailableValue = - solvedBoard.getEmptyCellsWithUniqueAvailableValue(); - for (var cellData in cellsWithUniqueAvailableValue) { - solvedBoard.setValue(CellLocation.go(cellData[0], cellData[1]), cellData[2]); - } - - // no more empty cell to fill - if (cellsLastEmptyInBlock.isEmpty && cellsWithUniqueAvailableValue.isEmpty) { - if (solvedBoard.isSolved()) { - printlog('ok compute solved board'); - } else { - printlog('!!'); - printlog('!! failed to resolve board'); - printlog('!!'); - } - break; - } - } while (true); - - solvedCells = solvedBoard.cells; - } - bool isSolved() { - // check grid is fully completed + // first, check grid is fully completed for (CellLocation location in getCellLocations()) { if (get(location).value == 0) { return false; @@ -387,6 +377,10 @@ class Board { } } + if (boardHasSiblingWithSameValue()) { + return false; + } + return true; } @@ -415,8 +409,8 @@ class Board { return missingValues; } - List<List<int>> getLastEmptyCellsInBlocks() { - List<List<int>> candidateCells = []; + List<Move> getLastEmptyCellsInBlocks() { + List<Move> candidateCells = []; for (CellLocation location in getCellLocations()) { final Cell cell = get(location); @@ -433,7 +427,7 @@ class Board { candidateValue = value; } } - candidateCells.add([location.row, location.col, candidateValue]); + candidateCells.add(Move(location: location, value: candidateValue)); } } } @@ -441,8 +435,8 @@ class Board { return candidateCells; } - List<List<int>> getOnlyCellInBlockWithoutConflict() { - List<List<int>> candidateCells = []; + List<Move> getOnlyCellInBlockWithoutConflict() { + List<Move> candidateCells = []; for (String blockId in getBlockIds()) { List<int> missingValuesInBlock = getMissingValuesInBlock(blockId); @@ -460,7 +454,7 @@ class Board { if (allowedCellsForThisValue.length == 1) { final CellLocation candidateLocation = allowedCellsForThisValue[0]; - candidateCells.add([candidateLocation.row, candidateLocation.col, candidateValue]); + candidateCells.add(Move(location: candidateLocation, value: candidateValue)); } } } @@ -468,8 +462,8 @@ class Board { return candidateCells; } - List<List<int>> getEmptyCellsWithUniqueAvailableValue() { - List<List<int>> candidateCells = []; + List<Move> getEmptyCellsWithUniqueAvailableValue() { + List<Move> candidateCells = []; for (CellLocation location in getCellLocations()) { if (get(location).value == 0) { @@ -485,7 +479,7 @@ class Board { } if (allowedValuesCount == 1) { - candidateCells.add([location.row, location.col, candidateValue]); + candidateCells.add(Move(location: location, value: candidateValue)); } } } @@ -531,24 +525,15 @@ class Board { return false; } - final int boardSizeVertical = cells.length; - final int boardSizeHorizontal = cells[0].length; - final int value = candidateValue ?? get(cellLocation).value; if (value != 0) { - for (int deltaRow in [-1, 0, 1]) { - for (int deltaCol in [-1, 0, 1]) { - if (cellLocation.row + deltaRow >= 0 && - cellLocation.row + deltaRow < boardSizeHorizontal && - cellLocation.col + deltaCol >= 0 && - cellLocation.col + deltaCol < boardSizeVertical && - !(deltaRow == 0 && deltaCol == 0)) { - final CellLocation candidateLocation = - CellLocation.go(cellLocation.row + deltaRow, cellLocation.col + deltaCol); - - final int siblingValue = get(candidateLocation).value; - - if (siblingValue == value) { + for (int deltaCol in [-1, 0, 1]) { + for (int deltaRow in [-1, 0, 1]) { + final CellLocation siblingLocation = + CellLocation.go(cellLocation.row + deltaRow, cellLocation.col + deltaCol); + + if (inBoard(siblingLocation) && !(deltaRow == 0 && deltaCol == 0)) { + if (get(siblingLocation).value == value) { return true; } } @@ -596,7 +581,12 @@ class Board { if (solvedCells.isEmpty) { rowSolved += '*'; } else { - rowSolved += stringValues[solvedCells[rowIndex][colIndex].value]; + final int solvedValue = solvedCells[rowIndex][colIndex].value; + if (solvedValue == 0) { + rowSolved += ' '; + } else { + rowSolved += stringValues[solvedCells[rowIndex][colIndex].value]; + } } } printlog('$rowBlocks | $rowValues | $rowSolved'); diff --git a/lib/models/activity/move.dart b/lib/models/activity/move.dart new file mode 100644 index 0000000000000000000000000000000000000000..e6c6410d82136520320bbb01ba01dcc329172ca7 --- /dev/null +++ b/lib/models/activity/move.dart @@ -0,0 +1,32 @@ +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +import 'package:suguru/models/activity/cell_location.dart'; + +class Move { + const Move({ + required this.location, + required this.value, + }); + + final CellLocation location; + final int value; + + void dump() { + printlog('$Move:'); + printlog(' location: $location'); + printlog(' value: $value'); + printlog(''); + } + + @override + String toString() { + return '$Move(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'location': location.toJson(), + 'value': value, + }; + } +} diff --git a/lib/ui/pages/game.dart b/lib/ui/pages/game.dart index a4c5464f6c516adf919bec0a794272ea835b689a..e5923741d7b97df1d7ad59937dabe6d6dd5a1a47 100644 --- a/lib/ui/pages/game.dart +++ b/lib/ui/pages/game.dart @@ -29,11 +29,6 @@ class PageGame extends StatelessWidget { const GameBoardWidget(), const SizedBox(height: 8), const GameBottomWidget(), - // StyledButton.text( - // onPressed: () => currentActivity.checkAllTemplates(), - // caption: '[debug] test all templates', - // color: Colors.red, - // ), const Expanded(child: SizedBox.shrink()), currentActivity.isFinished ? const GameEndWidget() : const SizedBox.shrink(), ], diff --git a/lib/utils/suguru_solver.dart b/lib/utils/suguru_solver.dart new file mode 100644 index 0000000000000000000000000000000000000000..ed2709e4236cfc690fee4a70d9b2c1ac1bc3fdf7 --- /dev/null +++ b/lib/utils/suguru_solver.dart @@ -0,0 +1,114 @@ +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +import 'package:suguru/config/application_config.dart'; +import 'package:suguru/data/game_data.dart'; +import 'package:suguru/models/activity/board.dart'; +import 'package:suguru/models/activity/cell_location.dart'; +import 'package:suguru/models/activity/move.dart'; +import 'package:suguru/models/activity/types.dart'; + +class SuguruSolver { + static Board resolve(Board board) { + printlog('solving grid...'); + final BoardCells cells = board.copyCells(); + final Board solvedBoard = Board(cells: cells, solvedCells: cells); + solvedBoard.dump(); + + do { + if (solvedBoard.isSolved()) { + printlog('ok compute solved board'); + break; + } else { + final Move? nextMove = pickNextMove(solvedBoard); + if (nextMove != null) { + // found empty cell to fill + solvedBoard.applyMove(nextMove); + // solvedBoard.dump(); + } else { + // no more empty cell to fill + if (!solvedBoard.isSolved()) { + printlog('!!'); + printlog('!! failed to resolve board'); + printlog('!!'); + } + break; + } + } + } while (true); + + return solvedBoard; + } + + static Move? pickNextMove(Board board, [CellLocation? candidateCell]) { + // pick one from "easy" cells (unique empty cell in block) + final List<Move> easyFillableMoves = board.getLastEmptyCellsInBlocks(); + if (easyFillableMoves.isNotEmpty) { + printlog('picked next move from easyFillableMoves'); + return pickRandomFromList(easyFillableMoves); + } + + // pick one from cells with unique non-conflicting candidate value + final List<Move> candidateMoves = board.getEmptyCellsWithUniqueAvailableValue(); + if (candidateMoves.isNotEmpty) { + printlog('picked next move from candidateMoves'); + return pickRandomFromList(candidateMoves); + } + + // pick one from "only cell in this block for this value" + final List<Move> onlyCellsWithoutConflict = board.getOnlyCellInBlockWithoutConflict(); + if (onlyCellsWithoutConflict.isNotEmpty) { + printlog('picked next move from onlyCellsWithoutConflict'); + return pickRandomFromList(onlyCellsWithoutConflict); + } + + printlog('unable to find next move...'); + return null; + } + + static Board applyMove(Board board, Move move) { + board.setValue(move.location, move.value); + + return board; + } + + static Move? pickRandomFromList(List<Move> moves) { + if (moves.isNotEmpty) { + moves.shuffle(); + return moves[0]; + } + + return null; + } + + static void checkAllTemplates() { + printlog('###############################'); + printlog('## ##'); + printlog('## CHECK TEMPLATES ##'); + printlog('## ##'); + printlog('###############################'); + + final List<String> allowedLevels = ApplicationConfig.config + .getFromCode(ApplicationConfig.parameterCodeDifficultyLevel) + .allowedValues; + final List<String> allowedSizes = ApplicationConfig.config + .getFromCode(ApplicationConfig.parameterCodeBoardSize) + .allowedValues; + + for (String level in allowedLevels) { + printlog('* level: $level'); + for (String size in allowedSizes) { + printlog('** size: $size'); + final List<String> templates = GameData.templates[size]?[level] ?? []; + printlog('*** templates count: ${templates.length}'); + + for (String template in templates) { + printlog(' checking $template'); + + final Board testBoard = Board.createEmpty(); + testBoard.createFromTemplate(template: template); + testBoard.dump(); + } + } + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index f03814e44b4a200f7ef46a4f59884f8ef0a72d4e..982dfe16ae68a58a15ab0e374dd7b72a9fcaebfb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A suguru game application. publish_to: "none" -version: 0.0.2+2 +version: 0.0.3+3 environment: sdk: "^3.0.0"