package sudoku.structure; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Random; import sudoku.constraint.BlockConstraint; import sudoku.constraint.ColumnConstraint; import sudoku.constraint.IConstraint; import sudoku.constraint.LineConstraint; import sudoku.solver.Solver; public class SudokuFactory { /** * Générateur de nombre aléatoire. */ private static final Random random = new Random(); /** * Difficulté avec le ration des cases qui seront vides. */ private static final double VERY_EASY = 0.1; private static final double EASY = 0.25; private static final double MEDIUM = 0.5; private static final double HARD = 0.75; /** * Liste des contraintes par défaut d'un Multi- ou Sudoku. * Comprend les contraintes de blocs, de lignes, et de colonnes. */ public static List DEFAULT_CONSTRAINTS = Arrays.asList(new BlockConstraint(), new LineConstraint(), new ColumnConstraint()); /** * Créée des Cells et les met dans une liste de taille size. * @param size int, nombre de Cells à initialiser. * @return List, liste des Cells initialisées. */ private static List initCells(int size) { List cells = new ArrayList<>(size * size); for (int i = 0; i < size * size; i++) { cells.add(new Cell()); } return cells; } /** * Créée des Blocks de taille width par height à partir des cellules données, et les met dans une liste. * @param cells List, liste des Cells à découper en Blocks. * @param width int, largeur des Blocks à créer. * @param height int, hauteur des Blocks à créer. * @return List, liste des Blocks créés. */ private static List initRectangleBlocs(List cells, int width, int height) { List blocs = new ArrayList<>(); int size = width * height; for (int i = 0; i < size; i++) { Block newBlock = new Block(); int blockX = i % height; int blockY = i / height; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = ((y + blockY * height) * size + (x + blockX * width)); Cell blockCell = cells.get(index); blockCell.setBlock(newBlock); // List blockContainers = new ArrayList<>(); // blockContainers.add(newBlock); // blockCell.setBlockContainers(blockContainers); newBlock.addCell(blockCell); } } blocs.add(newBlock); } return blocs; } /** * Créée un MultiDoku vide dont les Blocks sont de taille widthBlock par heightBlock. * @param widthBlock int, largeur des Blocks. * @param heightBlock int, hauteur des Blocks. * @return MultiDoku, MultiDoku vide. */ public static MultiDoku createBasicEmptyRectangleSudoku(int widthBlock, int heightBlock) { Sudoku s = createRectangleSudoku(widthBlock, heightBlock); return new MultiDoku(Arrays.asList(s)); } /** * Créée un MultiDoku vide dont les Blocks sont carrés de longueur size. * @param size int, taille des Blocks. * @return MultiDoku, MultiDoku vide. */ public static MultiDoku createBasicEmptySquareSudoku(int size) { return createBasicEmptyRectangleSudoku(size, size); } /** * Place des Cells immutables de valeurs fournies, aux Coordinate fournies dans le MultiDoku doku fourni. * @param doku MultiDoku, MultiDoku à remplir. * @param immutableCells Map, association de Coordinate coordonnées et Integer valeurs, correspondant aux cases à remplir. */ public static void setImmutableCells(MultiDoku doku, Map immutableCells) { immutableCells.forEach((coordinate, symbol) -> { for (Sudoku sudoku : doku.getSubGrids()) { Cell cell = sudoku.getCell(coordinate.getX(), coordinate.getY()); if (cell != null) { cell.setSymbolIndex(symbol); cell.setImmutable(); } } }); } /** * Créée un MultiDoku de difficulté difficulty à partir d'un MultiDoku fourni. * * @param doku MultiDoku, MultiDoku dont on doit vider des Cells. * @param nbCellsToEmpty int, nombre de cases à retirer. * @return boolean, valant true si un MultiDoku de difficulté donnée peut être créée, false sinon. * @throws Exception si la difficulté n'est pas compatible avec la taille du MultiDoku. */ public static boolean newDokuFromFilledOne (MultiDoku doku, int nbCellsToEmpty) throws Exception { if (nbCellsToEmpty > doku.getCells().size()) { throw new Exception(); } if (nbCellsToEmpty == 0) { return true; } List cellsThatCanBeEmptied = doku.getFilledCells(); while (!cellsThatCanBeEmptied.isEmpty()) { int index = random.nextInt(cellsThatCanBeEmptied.size()); Cell cellToEmpty = cellsThatCanBeEmptied.get(index); int oldSymbol = cellToEmpty.empty(); if (Solver.countSolution(doku) == 1) { if (newDokuFromFilledOne(doku, --nbCellsToEmpty)) { return true; } } cellToEmpty.setSymbolIndex(oldSymbol); cellsThatCanBeEmptied.remove(cellToEmpty); } return false; } /** * Créée un Sudoku vide dont les Blocks sont de taille widthBlock par heightBlock. * @param widthBlock int, largeur des Blocks. * @param heightBlock int, hauteur des Blocks. * @return Sudoku, Sudoku vide. */ private static Sudoku createRectangleSudoku(int widthBlock, int heightBlock) { int symbolCount = widthBlock * heightBlock; List cases = initCells(symbolCount); List blocs = initRectangleBlocs(cases, widthBlock, heightBlock); Sudoku s = new Sudoku(cases, blocs, DEFAULT_CONSTRAINTS); s.setBlockWidth(widthBlock); return s; } /** * Créée un Sudoku vide dont les Blocks sont carrés de longueur size. * @param size int, taille des Blocks. * @return Sudoku, Sudoku vide. */ private static Sudoku createSquareSudoku(int size) { return createRectangleSudoku(size, size); } /** * Connecte deux Sudokus selon la décalage offset fourni. * @param sudoku1 Sudoku, premier sudoku à connecter. * @param sudoku2 Sudoku, second sudoku à connecter. * @param offset Coordinate, décalage entre les deux Sudokus. */ private static void linkSquareSudokus(Sudoku sudoku1, Sudoku sudoku2, Coordinate offset) { int blockWidth = sudoku1.getBlockWidth(); for (int dx = 0; dx < blockWidth; dx++) { for (int dy = 0; dy < blockWidth; dy++) { int block1X = dx + offset.getX(); int block1Y = dy + offset.getY(); int block2X = dx; int block2Y = dy; if ((block1X < blockWidth) && (block1X >= 0) && (block1Y >= 0) && (block1Y < blockWidth)) { Block block1 = sudoku1.getBlocks().get(block1Y * blockWidth + block1X); Block block2 = sudoku2.getBlocks().get(block2Y * blockWidth + block2X); // on remplace le bloc sudoku2.getBlocks().set(block2Y * blockWidth + block2X, block1); // on remplace les cellules for (int i = 0; i < block1.getCells().size(); i++) { Cell newCell = block1.getCells().get(i); Cell oldCell = block2.getCells().get(i); int oldCellIndex = sudoku2.getCells().indexOf(oldCell); sudoku2.getCells().set(oldCellIndex, newCell); } } } } } /** * Créée un MultiDoku de Blocks carrés de taille size composé de cinq Sudokus, dont un central qui partage chacun de ses Blockss d'angle avec un autre Sudoku. * @param size int, largeur des Blocks unitraires des Sudokus à crééer. * @return MultiDoku, MultiDoku de forme X. */ public static MultiDoku createBasicXShapedMultidoku(int size) { assert (size > 1); /* * 2 3 * 1 * 4 5 */ Sudoku sudoku1 = createSquareSudoku(size); Sudoku sudoku2 = createSquareSudoku(size); Sudoku sudoku3 = createSquareSudoku(size); Sudoku sudoku4 = createSquareSudoku(size); Sudoku sudoku5 = createSquareSudoku(size); linkSquareSudokus(sudoku1, sudoku2, new Coordinate(1 - size, 1 - size)); linkSquareSudokus(sudoku1, sudoku3, new Coordinate(size - 1, 1 - size)); linkSquareSudokus(sudoku1, sudoku4, new Coordinate(1 - size, size - 1)); linkSquareSudokus(sudoku1, sudoku5, new Coordinate(size - 1, size - 1)); return new MultiDoku(Arrays.asList(sudoku1, sudoku2, sudoku3, sudoku4, sudoku5)); } public static MultiDoku createBasicRectangleDokuToSolve(int width, int height, double difficulty) throws Exception { MultiDoku doku = createBasicEmptyRectangleSudoku(width, height); Solver.solveRandom(doku, random); int nbCellsToEmpty = (int)(difficulty*doku.getNbCells()); boolean successfull = newDokuFromFilledOne(doku, nbCellsToEmpty); if (!successfull) { throw new Exception("Canno't create this doku with this difficulty"); } doku.setFilledCellsImmutable(); return doku; } public static MultiDoku createBasicSquareDokuToSolve(int size, double difficulty) throws Exception { return createBasicRectangleDokuToSolve(size, size, difficulty); } public static MultiDoku createBasicXShapedMultiDokuToSolve(int size, double difficulty) throws Exception { MultiDoku doku = createBasicXShapedMultidoku(size); Solver.solveRandom(doku, random); int nbCellsToEmpty = (int)(difficulty*doku.getNbCells()); boolean successful = newDokuFromFilledOne(doku, nbCellsToEmpty); if (!successful) { throw new Exception("Cannot create this Doku with this difficulty"); } doku.setFilledCellsImmutable(); return doku; } }