import 'dart:math'; import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; import 'package:sudoku/models/game/cell.dart'; import 'package:sudoku/models/game/cell_location.dart'; import 'package:sudoku/models/game/types.dart'; class Board { Board({ required this.cells, }); BoardCells cells = const []; factory Board.createEmpty() { return Board( cells: [], ); } factory Board.createNew({ required BoardCells cells, }) { return Board( cells: cells, ); } factory Board.createFromTemplate({ required String template, required bool isSymetric, }) { // Create board cells from size, with "empty" cells BoardCells createEmptyBoard(final int boardSize) { final BoardCells cells = []; for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) { final List<Cell> row = []; for (int colIndex = 0; colIndex < boardSize; colIndex++) { row.add(Cell( location: CellLocation.go(rowIndex, colIndex), value: 0, isFixed: false, )); } cells.add(row); } return cells; } BoardCells cells = []; final int boardSize = int.parse(pow(template.length, 1 / 2).toStringAsFixed(0)); const String stringValues = '0123456789ABCDEFG'; int index = 0; for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) { final List<Cell> row = []; for (int colIndex = 0; colIndex < boardSize; colIndex++) { final String stringValue = template[index++]; final int value = stringValues.indexOf(stringValue); row.add(Cell( location: CellLocation.go(rowIndex, colIndex), value: value, isFixed: (value != 0), )); } cells.add(row); } const List<String> allowedFlip = ['none', 'horizontal', 'vertical']; List<String> allowedRotate = ['none', 'left', 'right']; // Forbid rotation if blocks are not symetric if (!isSymetric) { allowedRotate = ['none']; } final Random rand = Random(); final String flip = allowedFlip[rand.nextInt(allowedFlip.length)]; final String rotate = allowedRotate[rand.nextInt(allowedRotate.length)]; printlog('flip board: $flip'); printlog('rotate board: $rotate'); switch (flip) { case 'horizontal': { final BoardCells transformedBoard = createEmptyBoard(boardSize); for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) { for (int colIndex = 0; colIndex < boardSize; colIndex++) { transformedBoard[rowIndex][colIndex] = Cell( location: CellLocation.go(rowIndex, colIndex), value: cells[boardSize - rowIndex - 1][colIndex].value, isFixed: false, ); } } cells = transformedBoard; } break; case 'vertical': { final BoardCells transformedBoard = createEmptyBoard(boardSize); for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) { for (int colIndex = 0; colIndex < boardSize; colIndex++) { transformedBoard[rowIndex][colIndex] = Cell( location: CellLocation.go(rowIndex, colIndex), value: cells[rowIndex][boardSize - colIndex - 1].value, isFixed: false, ); } } cells = transformedBoard; } break; } switch (rotate) { case 'left': { final BoardCells transformedBoard = createEmptyBoard(boardSize); for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) { for (int colIndex = 0; colIndex < boardSize; colIndex++) { transformedBoard[rowIndex][colIndex] = Cell( location: CellLocation.go(rowIndex, colIndex), value: cells[colIndex][boardSize - rowIndex - 1].value, isFixed: false, ); } } cells = transformedBoard; } break; case 'right': { final BoardCells transformedBoard = createEmptyBoard(boardSize); for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) { for (int colIndex = 0; colIndex < boardSize; colIndex++) { transformedBoard[rowIndex][colIndex] = Cell( location: CellLocation.go(rowIndex, colIndex), value: cells[boardSize - colIndex - 1][rowIndex].value, isFixed: false, ); } } cells = transformedBoard; } break; } // Fix cells fixed states for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) { for (int colIndex = 0; colIndex < boardSize; colIndex++) { cells[rowIndex][colIndex] = Cell( location: CellLocation.go(rowIndex, colIndex), value: cells[rowIndex][colIndex].value, isFixed: (cells[rowIndex][colIndex].value != 0) ? true : false, ); } } return Board.createNew( cells: cells, ); } Cell get(CellLocation location) { if (location.row < cells.length) { if (location.col < cells[location.row].length) { return cells[location.row][location.col]; } } return Cell.none; } void set(CellLocation location, Cell cell) { cells[location.row][location.col] = cell; } BoardCells copyCells() { final BoardCells copiedGrid = []; for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) { final List<Cell> row = []; for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) { row.add(Cell( location: CellLocation.go(rowIndex, colIndex), value: cells[rowIndex][colIndex].value, isFixed: false, )); } copiedGrid.add(row); } return copiedGrid; } BoardCells getSolvedGrid() { final Board tmpBoard = Board(cells: copyCells()); do { final List<List<int>> cellsWithUniqueAvailableValue = tmpBoard.getEmptyCellsWithUniqueAvailableValue(); if (cellsWithUniqueAvailableValue.isEmpty) { break; } for (int i = 0; i < cellsWithUniqueAvailableValue.length; i++) { final int row = cellsWithUniqueAvailableValue[i][0]; final int col = cellsWithUniqueAvailableValue[i][1]; final int value = cellsWithUniqueAvailableValue[i][2]; tmpBoard.cells[row][col] = Cell( location: CellLocation.go(row, col), value: value, isFixed: tmpBoard.cells[row][col].isFixed, ); } } while (true); return tmpBoard.cells; } List<List<int>> getEmptyCellsWithUniqueAvailableValue() { List<List<int>> candidateCells = []; final int boardSize = cells.length; for (int row = 0; row < boardSize; row++) { for (int col = 0; col < boardSize; col++) { if (cells[row][col].value == 0) { int allowedValuesCount = 0; int candidateValue = 0; for (int value = 1; value <= boardSize; value++) { if (isValueAllowed(CellLocation.go(row, col), value)) { candidateValue = value; allowedValuesCount++; } } if (allowedValuesCount == 1) { candidateCells.add([row, col, candidateValue]); } } } } return candidateCells; } bool isValueAllowed(CellLocation? candidateLocation, int candidateValue) { if ((candidateLocation == null) || (candidateValue == 0)) { return true; } final int boardSize = cells.length; // check lines does not contains a value twice for (int row = 0; row < boardSize; row++) { final List<int> values = []; for (int col = 0; col < boardSize; col++) { int value = cells[row][col].value; if (row == candidateLocation.row && col == candidateLocation.col) { value = candidateValue; } if (value != 0) { values.add(value); } } final List<int> distinctValues = values.toSet().toList(); if (values.length != distinctValues.length) { return false; } } // check columns does not contains a value twice for (int col = 0; col < boardSize; col++) { final List<int> values = []; for (int row = 0; row < boardSize; row++) { int value = cells[row][col].value; if (row == candidateLocation.row && col == candidateLocation.col) { value = candidateValue; } if (value != 0) { values.add(value); } } final List<int> distinctValues = values.toSet().toList(); if (values.length != distinctValues.length) { return false; } } // check blocks does not contains a value twice final int blockSizeVertical = sqrt(cells.length).toInt(); final int blockSizeHorizontal = cells.length ~/ blockSizeVertical; final int horizontalBlocksCount = blockSizeVertical; final int verticalBlocksCount = blockSizeHorizontal; for (int blockRow = 0; blockRow < verticalBlocksCount; blockRow++) { for (int blockCol = 0; blockCol < horizontalBlocksCount; blockCol++) { final List<int> values = []; for (int rowInBlock = 0; rowInBlock < blockSizeVertical; rowInBlock++) { for (int colInBlock = 0; colInBlock < blockSizeHorizontal; colInBlock++) { final int row = (blockRow * blockSizeVertical) + rowInBlock; final int col = (blockCol * blockSizeHorizontal) + colInBlock; int value = cells[row][col].value; if (row == candidateLocation.row && col == candidateLocation.col) { value = candidateValue; } if (value != 0) { values.add(value); } } } final List<int> distinctValues = values.toSet().toList(); if (values.length != distinctValues.length) { return false; } } } return true; } void dump() { printlog(''); printlog('$Board:'); printlog(' cells: $cells'); printlog(''); } @override String toString() { return '$Board(${toJson()})'; } Map<String, dynamic>? toJson() { return <String, dynamic>{ 'cells': cells, }; } }