Skip to content
Snippets Groups Projects
Commit 63a9814c authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Improve grid solver

parent 6fafd712
No related branches found
No related tags found
1 merge request!4Resolve "Improve grid solver"
Pipeline #7777 passed
Improve/fix grid solver.
Amélioration/correction du résolveur de grille.
......@@ -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:');
......
......@@ -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');
......
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,
};
}
}
......@@ -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(),
],
......
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();
}
}
}
}
}
......@@ -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"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment