Merge remote-tracking branch 'origin/master'

# Conflicts:
#	app/src/main/java/sudoku/structure/MultiDoku.java
#	app/src/main/java/sudoku/structure/Sudoku.java
#	app/src/main/java/sudoku/structure/SudokuFactory.java
#	app/src/test/java/sudoku/solver/SolverTest.java
This commit is contained in:
Melvyn
2025-01-29 18:51:23 +01:00
17 changed files with 504 additions and 222 deletions

21
README.md Normal file
View File

@@ -0,0 +1,21 @@
# Sudoku 🧩
## Features 🌟
- Graphical User Interface (GUI)
- Sudoku saves
- Multiplayer
## Develop ☝🤓
### Run 🏃
```sh
./gradlew run
```
### Run tests 🗣️🔥
```sh
./gradlew test
```

View File

@@ -27,6 +27,10 @@ public class RenderableMultidoku {
this.doku = doku;
}
public boolean isResolved() {
return this.doku.isValid();
}
public int getWidth() {
return width;
}

View File

@@ -1,18 +1,23 @@
package gui;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import common.Signal;
import gui.ColorGenerator.Color;
import imgui.ImGui;
import imgui.ImVec2;
import imgui.ImVec4;
import imgui.flag.ImGuiCol;
import imgui.flag.ImGuiStyleVar;
import sudoku.constraint.Constraint;
import sudoku.structure.Block;
import sudoku.structure.Cell;
import sudoku.structure.MultiDoku;
import sudoku.structure.Sudoku;
public class SudokuRenderer {
@@ -22,11 +27,28 @@ public class SudokuRenderer {
private static final ImVec4 BLACK = new ImVec4(0, 0, 0, 1);
private static final ImVec4 TRANSPARENT = new ImVec4();
private static final ImVec4 WHITE = new ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
private static final ImVec2 cellSize = new ImVec2(50, 50);
private final Set<Cell> diagonals = new HashSet<>();
public final Signal onResolve = new Signal();
public SudokuRenderer(MultiDoku doku) {
this.doku = RenderableMultidoku.fromMultidoku(doku);
this.colorPalette = initColors();
initDiagonals();
}
private void initDiagonals() {
for (Sudoku sudoku : this.doku.getDoku().getSubGrids()) {
if (sudoku.hasConstraint(Constraint.Diagonal)) {
for (int i = 0; i < sudoku.getSize(); i++) {
this.diagonals.add(sudoku.getCell(i, i));
this.diagonals.add(sudoku.getCell(sudoku.getSize() - i - 1, i));
}
}
}
}
private Map<Block, Color> initColors() {
@@ -55,6 +77,8 @@ public class SudokuRenderer {
} else {
if (ImGui.button(Integer.toString(i + 1), cellSize)) {
this.doku.setCellValue(currentCell, i);
if (this.doku.isResolved())
this.onResolve.emit();
ImGui.closeCurrentPopup();
}
}
@@ -66,7 +90,11 @@ public class SudokuRenderer {
public void render() {
final float sudokuViewWidth = cellSize.x * doku.getWidth();
final float displayWidth = ImGui.getIO().getDisplaySizeX();
ImGui.setCursorPosX(displayWidth / 2.0f - sudokuViewWidth / 2.0f);
float offsetX = displayWidth / 2.0f - sudokuViewWidth / 2.0f;
// if the grid is too big, don't offset it
if (offsetX > 0) {
ImGui.setCursorPosX(offsetX);
}
ImGui.beginChild(1, new ImVec2(cellSize.x * doku.getWidth(), cellSize.y * doku.getHeight()));
ImGui.pushStyleVar(ImGuiStyleVar.FrameBorderSize, 2.0f);
@@ -82,15 +110,15 @@ public class SudokuRenderer {
ImGui.pushStyleColor(ImGuiCol.Button, TRANSPARENT);
ImGui.button("##" + index, cellSize);
} else {
ImGui.pushStyleColor(ImGuiCol.Border, BLACK);
if (diagonals.contains(cell)) {
ImGui.pushStyleColor(ImGuiCol.Border, WHITE);
} else {
ImGui.pushStyleColor(ImGuiCol.Border, BLACK);
}
int symbol = cell.getSymbolIndex();
Color blockColor = colorPalette.get(cell.getBlock());
if (!cell.isMutable()) {
// ImGui.pushFont(Fonts.ARIAL_BOLD);
blockColor = new Color(blockColor.r - 0.20f, blockColor.g - 0.20f, blockColor.b - 0.20f);
// ImGui.pushStyleColor(ImGuiCol.Text, new ImVec4(0.1f, 0.1f, 0.1f, 1.0f));
} else {
// ImGui.pushFont(Fonts.CHERI);
}
ImGui.pushStyleColor(ImGuiCol.Button, new ImVec4(blockColor.r, blockColor.g, blockColor.b, 1.0f));
String cellText = "";
@@ -100,10 +128,6 @@ public class SudokuRenderer {
ImGui.openPopup("editPopup");
currentCell = cell;
}
if (!cell.isMutable()) {
// ImGui.popStyleColor();
}
// ImGui.popFont();
}
ImGui.popStyleColor(2);
}

View File

@@ -0,0 +1,147 @@
package gui;
import java.util.ArrayList;
import java.util.List;
import common.Signal;
import imgui.ImGui;
import imgui.extension.imguifiledialog.ImGuiFileDialog;
import imgui.extension.imguifiledialog.flag.ImGuiFileDialogFlags;
import imgui.type.ImBoolean;
import imgui.type.ImInt;
import sudoku.constraint.Constraint;
import sudoku.structure.Difficulty;
import sudoku.structure.MultiDoku;
import sudoku.structure.SudokuFactory;
public class SudokuSelector {
public final Signal onSelect = new Signal();
private MultiDoku doku;
private final boolean canGenEmptyGrid;
private final ImInt sudokuType = new ImInt(0);
private final ImInt difficulty = new ImInt(Difficulty.Medium.ordinal());
private final List<ImBoolean> contraints = new ArrayList<>();
private static final String[] sudokuTypes = { "Carré", "Rectangle", "Multidoku" };
private static final int SQUARE = 0, RECTANGLE = 1, MULTIDOKU = 2;
private final ImInt sudokuSize = new ImInt(3);
private final ImInt sudokuWidth = new ImInt(3);
private final ImInt sudokuHeight = new ImInt(3);
public SudokuSelector(boolean canGenEmptyGrid) {
this.canGenEmptyGrid = canGenEmptyGrid;
initConstraints();
}
private List<Constraint> getConstraints() {
List<Constraint> constraints = new ArrayList<>();
for (int i = 0; i < this.contraints.size(); i++) {
if (this.contraints.get(i).get())
constraints.add(Constraint.values()[i]);
}
return constraints;
}
private void initConstraints() {
for (Constraint cons : Constraint.values()) {
contraints.add(new ImBoolean(SudokuFactory.DEFAULT_CONSTRAINTS.contains(cons)));
}
}
private void selectSudoku(MultiDoku doku, boolean empty) {
this.doku = doku;
if (!empty) {
try {
SudokuFactory.fillDoku(doku, Difficulty.values()[difficulty.get()]);
} catch (Exception e) {
e.printStackTrace();
}
}
this.onSelect.emit();
}
public void renderFileDialog() {
if (ImGuiFileDialog.display("browse-sudoku", ImGuiFileDialogFlags.None)) {
if (ImGuiFileDialog.isOk()) {
var selection = ImGuiFileDialog.getSelection();
for (var entry : selection.entrySet()) {
try {
String filePath = entry.getValue();
this.doku = SudokuFactory.fromfile(filePath);
if (this.doku != null)
this.onSelect.emit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
ImGuiFileDialog.close();
}
}
public void render() {
ImGui.combo("Type de Sudoku", sudokuType, sudokuTypes);
ImGui.combo("Difficulté", difficulty, Difficulty.getDifficultyNames());
if (ImGui.treeNode("Constraintes")) {
for (Constraint cons : Constraint.values()) {
ImGui.checkbox(cons.getDisplayName(), contraints.get(cons.ordinal()));
}
ImGui.treePop();
}
switch (sudokuType.get()) {
case SQUARE:
ImGui.inputInt("Taille", sudokuSize);
if (ImGui.button("Résoudre un sudoku")) {
selectSudoku(SudokuFactory.createBasicEmptySquareDoku(sudokuSize.get(), getConstraints()), false);
}
if (canGenEmptyGrid && ImGui.button("Générer une grille vide")) {
selectSudoku(SudokuFactory.createBasicEmptySquareDoku(sudokuSize.get(), getConstraints()), true);
}
break;
case RECTANGLE:
ImGui.inputInt("Largeur", sudokuHeight);
ImGui.inputInt("Longueur", sudokuWidth);
if (ImGui.button("Résoudre un sudoku")) {
selectSudoku(
SudokuFactory.createBasicEmptyRectangleDoku(sudokuWidth.get(), sudokuHeight.get(),
getConstraints()),
false);
}
if (canGenEmptyGrid && ImGui.button("Générer une grille vide")) {
selectSudoku(
SudokuFactory.createBasicEmptyRectangleDoku(sudokuWidth.get(), sudokuHeight.get(),
getConstraints()),
true);
}
break;
case MULTIDOKU:
ImGui.inputInt("Taille", sudokuSize);
if (ImGui.button("Résoudre un sudoku")) {
selectSudoku(SudokuFactory.createBasicXShapedMultidoku(sudokuSize.get(), getConstraints()), false);
}
if (canGenEmptyGrid && ImGui.button("Générer une grille vide")) {
selectSudoku(SudokuFactory.createBasicXShapedMultidoku(sudokuSize.get(), getConstraints()), true);
}
default:
break;
}
if (ImGui.button("À partir d'un fichier")) {
ImGuiFileDialog.openDialog("browse-sudoku", "Choisissez un fichier", ".json", ".");
}
renderFileDialog();
}
public MultiDoku getDoku() {
return doku;
}
}

View File

@@ -1,9 +1,12 @@
package gui.menu;
import java.util.Arrays;
import game.Player;
import imgui.ImGui;
import network.client.Client;
import network.server.Server;
import sudoku.constraint.Constraint;
import sudoku.structure.MultiDoku;
import sudoku.structure.SudokuFactory;
@@ -17,7 +20,8 @@ public class MultiPlayerView extends BaseView {
this.client = client;
this.server = server;
this.client.onDisconnect.connect(this::onDisconnect);
this.client.onGameStarted.connect(() -> this.stateMachine.pushState(new MultiPlayerDokuView(stateMachine, client, server)));
this.client.onGameStarted
.connect(() -> this.stateMachine.pushState(new MultiPlayerDokuView(stateMachine, client, server)));
}
@Override
@@ -36,7 +40,7 @@ public class MultiPlayerView extends BaseView {
} else {
if (ImGui.button("Démarrer")) {
// temp
MultiDoku doku = SudokuFactory.createBasicXShapedMultidoku(3);
MultiDoku doku = SudokuFactory.createBasicXShapedMultidoku(3, Arrays.asList(Constraint.Diagonal));
this.server.startGame(doku);
}
}

View File

@@ -1,88 +1,26 @@
package gui.menu;
import gui.SudokuSelector;
import imgui.ImGui;
import imgui.type.ImInt;
import sudoku.structure.Difficulty;
import sudoku.structure.MultiDoku;
import sudoku.structure.SudokuFactory;
public class SoloMenu extends BaseView {
private final ImInt sudokuType = new ImInt(0);
private final ImInt difficulty = new ImInt(Difficulty.Medium.ordinal());
private final String[] difficulties;
private static final String[] sudokuTypes = { "Carré", "Rectangle", "Multidoku" };
private static final int SQUARE = 0, RECTANGLE = 1, MULTIDOKU = 2;
private final ImInt sudokuSize = new ImInt(3);
private final ImInt sudokuWidth = new ImInt(3);
private final ImInt sudokuHeight = new ImInt(3);
private final SudokuSelector sudokuSelector;
public SoloMenu(StateMachine stateMachine) {
super(stateMachine);
Difficulty[] diffs = Difficulty.values();
difficulties = new String[diffs.length];
for (int i = 0; i < diffs.length; i++) {
difficulties[i] = diffs[i].getDisplayName();
}
this.sudokuSelector = new SudokuSelector(true);
this.sudokuSelector.onSelect.connect(this::pushSudokuState);
}
private void pushSudokuState(MultiDoku doku, boolean empty) {
if (!empty) {
try {
SudokuFactory.fillDoku(doku, Difficulty.values()[difficulty.get()]);
} catch (Exception e) {
e.printStackTrace();
}
}
this.stateMachine.pushState(new SudokuView(stateMachine, doku));
private void pushSudokuState() {
this.stateMachine.pushState(new SudokuView(stateMachine, this.sudokuSelector.getDoku()));
}
@Override
public void render() {
ImGui.text("Solo");
ImGui.combo("Type de Sudoku", sudokuType, sudokuTypes);
ImGui.combo("Difficulté", difficulty, difficulties);
switch (sudokuType.get()) {
case SQUARE:
ImGui.inputInt("Taille", sudokuSize);
if (ImGui.button("Résoudre un sudoku")) {
pushSudokuState(SudokuFactory.createBasicEmptySquareSudoku(sudokuSize.get()), false);
}
if (ImGui.button("Générer une grille vide")) {
pushSudokuState(SudokuFactory.createBasicEmptySquareSudoku(sudokuSize.get()), true);
}
break;
case RECTANGLE:
ImGui.inputInt("Largeur", sudokuHeight);
ImGui.inputInt("Longueur", sudokuWidth);
if (ImGui.button("Résoudre un sudoku")) {
pushSudokuState(
SudokuFactory.createBasicEmptyRectangleSudoku(sudokuWidth.get(), sudokuHeight.get()),
false);
}
if (ImGui.button("Générer une grille vide")) {
pushSudokuState(
SudokuFactory.createBasicEmptyRectangleSudoku(sudokuWidth.get(), sudokuHeight.get()), true);
}
break;
case MULTIDOKU:
ImGui.inputInt("Taille", sudokuSize);
if (ImGui.button("Résoudre un sudoku")) {
pushSudokuState(SudokuFactory.createBasicXShapedMultidoku(sudokuSize.get()), false);
}
if (ImGui.button("Générer une grille vide")) {
pushSudokuState(SudokuFactory.createBasicXShapedMultidoku(sudokuSize.get()), true);
}
default:
break;
}
sudokuSelector.render();
renderReturnButton();
}

View File

@@ -6,6 +6,7 @@ import java.util.concurrent.CancellationException;
import gui.SudokuRenderer;
import imgui.ImGui;
import imgui.ImGuiStyle;
import sudoku.io.SudokuSerializer;
import sudoku.solver.Solver;
import sudoku.structure.MultiDoku;
@@ -14,11 +15,19 @@ public class SudokuView extends BaseView {
private final SudokuRenderer sudokuRenderer;
private Thread resolveThread;
private final MultiDoku doku;
private String lastSavePath = null;
private boolean resolved = false;
public SudokuView(StateMachine stateMachine, MultiDoku doku) {
super(stateMachine);
this.doku = doku;
this.sudokuRenderer = new SudokuRenderer(doku);
this.sudokuRenderer.onResolve.connect(this::onResolve);
}
private void onResolve() {
this.resolved = true;
}
private void stopResolve() {
@@ -63,7 +72,7 @@ public class SudokuView extends BaseView {
boolean beginSolve = false;
if (centeredButton("Résoudre")) {
if (!resolved && centeredButton("Résoudre")) {
beginSolve = true;
}
if (resolveThread != null)
@@ -81,12 +90,29 @@ public class SudokuView extends BaseView {
stopResolve();
});
}
if (resolved) {
ImGui.text("Bravo !");
}
}
private void renderSaveButton() {
if (ImGui.button("Sauvegarder l'état de la grille")) {
lastSavePath = SudokuSerializer.saveMultiDoku(doku);
ImGui.openPopup("saveDone");
}
if (ImGui.beginPopup("saveDone")) {
ImGui.text("Sudoku sauvegardé dans ");
ImGui.text(lastSavePath);
ImGui.endPopup();
}
}
@Override
public void render() {
sudokuRenderer.render();
renderSolveButton();
renderSaveButton();
renderCancelButton();
renderReturnButton();
}

View File

@@ -18,30 +18,29 @@ public class Main {
int blockWidth = 2;
int blockHeight = 2;
var multidoku = SudokuFactory.createBasicEmptyRectangleSudoku(blockWidth, blockHeight);
var multidoku = SudokuFactory.createBasicEmptyRectangleDoku(blockWidth, blockHeight,
SudokuFactory.DEFAULT_CONSTRAINTS);
var sudoku = multidoku.getSubGrid(0);
if(!sudoku.setCellsSymbol(Arrays.asList(0,1,2,3, 2,3,1,1, 1,0,3,2, 3,2,1,1))){
if (!sudoku.setCellsSymbol(Arrays.asList(0, 1, 2, 3, 2, 3, 1, 1, 1, 0, 3, 2, 3, 2, 1, 1))) {
System.out.println("At least one of those values does not respect the constraints.");
}
// sudoku.setCellSymbol(8,3,0);
//sudoku.setCellSymbol(8,3,0);
SudokuPrinter.printRectangleSudoku(multidoku.getSubGrid(0), blockWidth , blockHeight);
SudokuPrinter.printRectangleSudoku(multidoku.getSubGrid(0), blockWidth, blockHeight);
/*
Solver solver = new Solver();
ArrayList<IConstraint> constraints = new ArrayList<>();
constraints.add(new LineConstraint());
constraints.add(new ColumnConstraint());
constraints.add(new BlockConstraint());
try {
solver.solve(multidoku, constraints);
} catch (Exception e) {
System.out.println(e);
}
*/
* Solver solver = new Solver();
* ArrayList<IConstraint> constraints = new ArrayList<>();
* constraints.add(new LineConstraint());
* constraints.add(new ColumnConstraint());
* constraints.add(new BlockConstraint());
* try {
* solver.solve(multidoku, constraints);
* } catch (Exception e) {
* System.out.println(e);
* }
*/
}
}

View File

@@ -0,0 +1,52 @@
package sudoku.constraint;
import java.util.List;
import sudoku.structure.Sudoku;
public enum Constraint {
Block("Bloc", new BlockConstraint()),
Column("Colonne", new ColumnConstraint()),
Line("Ligne", new LineConstraint()),
Diagonal("Diagonale", new DiagonalConstraint());
String displayName;
IConstraint constraint;
private Constraint(String displayName, IConstraint contraint) {
this.constraint = contraint;
this.displayName = displayName;
}
public boolean canBePlaced(Sudoku s, int x, int y, int newValue) {
return getConstraint().canBePlaced(s, x, y, newValue);
}
public List<Integer> getPossibleSymbols(final Sudoku s, int x, int y) {
return getConstraint().getPossibleSymbols(s, x, y);
}
public String getDisplayName() {
return displayName;
}
public IConstraint getConstraint() {
return constraint;
}
private static final String[] constraintNames;
static {
Constraint[] cons = Constraint.values();
constraintNames = new String[cons.length];
for (int i = 0; i < cons.length; i++) {
constraintNames[i] = cons[i].getDisplayName();
}
}
public static String[] getConstraintNames() {
return constraintNames;
}
}

View File

@@ -0,0 +1,24 @@
package sudoku.constraint;
import sudoku.structure.Sudoku;
public class DiagonalConstraint implements IConstraint {
@Override
public boolean canBePlaced(Sudoku s, int x, int y, int newSymbolIndex) {
if (x == y) {
for (int i = 0; i < s.getSize(); i++) {
if (s.getCell(i, i).getSymbolIndex() == newSymbolIndex)
return false;
}
} else if (s.getSize() - x == y) {
for (int i = 0; i < s.getSize(); i++) {
if (s.getCell(s.getSize() - i - 1, i).getSymbolIndex() == newSymbolIndex)
return false;
}
}
// not in diagonal
return true;
}
}

View File

@@ -10,6 +10,7 @@ import java.util.List;
import org.json.JSONArray;
import org.json.JSONObject;
import sudoku.constraint.Constraint;
import sudoku.structure.Block;
import sudoku.structure.Cell;
import sudoku.structure.MultiDoku;
@@ -34,7 +35,6 @@ public class SudokuSerializer {
for (Cell cell : sudoku.getCells()) {
if (!cellIds.contains(cell)) {
cellIds.add(cell);
}
Block block = cell.getBlock();
@@ -74,6 +74,7 @@ public class SudokuSerializer {
JSONObject jsonSudoku = new JSONObject();
JSONArray cellsJsonArray = new JSONArray();
JSONArray blocksJsonArray = new JSONArray();
JSONArray constraintsJsonArray = new JSONArray();
Sudoku sudoku = multidoku.getSubGrid(i);
@@ -91,6 +92,13 @@ public class SudokuSerializer {
blocksJsonArray.put(blockID);
}
// serialize constraints
for (Constraint cons : sudoku.getConstraints()) {
constraintsJsonArray.put(cons.ordinal());
}
jsonSudoku.put("constraints", constraintsJsonArray);
jsonSudoku.put("cells", cellsJsonArray);
jsonSudoku.put("blocks", blocksJsonArray);
jsonSudoku.put("blockWidth", sudoku.getBlockWidth());
@@ -107,9 +115,9 @@ public class SudokuSerializer {
/**
* Save a serialized MultiDoku in a JSON file.
* @param doku MultiDoku, MultiDoku to save.
* @return int, number of the save.
* @return String, the path of the save.
*/
public static int saveMultiDoku(final MultiDoku doku) {
public static String saveMultiDoku(final MultiDoku doku) {
JSONObject jsonRoot = serializeSudoku(doku);
@@ -124,9 +132,9 @@ public class SudokuSerializer {
try (FileWriter file = new FileWriter(f)) {
file.write(jsonRoot.toString(3));
} catch (IOException e) {
e.fillInStackTrace();
e.printStackTrace();
}
return i;
return f.getAbsolutePath();
}
/**
@@ -212,9 +220,11 @@ public class SudokuSerializer {
JSONObject sudokuJsonObject = multidokuJsonObject.getJSONObject(i);
JSONArray sudokuCellsJsonArray = sudokuJsonObject.getJSONArray("cells");
JSONArray sudokuBlocksJsonArray = sudokuJsonObject.getJSONArray("blocks");
JSONArray sudokuConstraintsJsonArray = sudokuJsonObject.getJSONArray("constraints");
List<Cell> sudokuCells = new ArrayList<>();
List<Block> sudokuBlocks = new ArrayList<>();
List<Constraint> sudokuConstraints = new ArrayList<>();
for (int j = 0; j < sudokuCellsJsonArray.length(); j++) {
int cellID = sudokuCellsJsonArray.getInt(j);
@@ -226,7 +236,12 @@ public class SudokuSerializer {
sudokuBlocks.add(blocks.get(blockID));
}
Sudoku s = new Sudoku(sudokuCells, sudokuBlocks, SudokuFactory.DEFAULT_CONSTRAINTS);
for (int j = 0; j < sudokuConstraintsJsonArray.length(); j++) {
int constraintID = sudokuConstraintsJsonArray.getInt(j);
sudokuConstraints.add(Constraint.values()[constraintID]);
}
Sudoku s = new Sudoku(sudokuCells, sudokuBlocks, sudokuConstraints);
s.setBlockWidth(sudokuJsonObject.getInt("blockWidth"));
sudokus.add(s);
}

View File

@@ -5,7 +5,8 @@ import java.util.List;
/**
* Class qui représente les block de chaque sudoku,
* Un block étant un ensemble de cellule avec une contrainte de block qui lui ait associé
* Un block étant un ensemble de cellule avec une contrainte de block qui lui
* ait associé
*/
public class Block {
@@ -18,14 +19,15 @@ public class Block {
* List de sudoku qui contiennent le block
* Pour un acces plus rapide aux sudokus
*/
private List<Sudoku> sudokus;
private final List<Sudoku> sudokus;
public Block(List<Cell> cells) {
this.cells = cells;
this.sudokus = new ArrayList<>();
}
public Block() {
this.cells = new ArrayList<>();
this(new ArrayList<>());
}
public List<Cell> getCells() {
@@ -34,6 +36,7 @@ public class Block {
/**
* Ajoute une Cell au Block
*
* @param newCell Cell, à ajouter
*/
void addCell(Cell newCell) {
@@ -42,6 +45,7 @@ public class Block {
/**
* Cherche si le Block contient déjà un symbole donné.
*
* @param symbolIndex int, un index de symbole
* @return boolean, true s'il contient le symbole et false sinon
*/
@@ -65,8 +69,4 @@ public class Block {
public List<Sudoku> getSudokus() {
return sudokus;
}
void setSudokus(List<Sudoku> sudokus) {
this.sudokus = sudokus;
}
}

View File

@@ -8,7 +8,7 @@ public enum Difficulty {
double factor;
String displayName;
Difficulty(String displayName, double factor) {
private Difficulty(String displayName, double factor) {
this.factor = factor;
this.displayName = displayName;
}
@@ -21,4 +21,18 @@ public enum Difficulty {
return factor;
}
private static final String[] difficultyNames;
static {
Difficulty[] diffs = Difficulty.values();
difficultyNames = new String[diffs.length];
for (int i = 0; i < diffs.length; i++) {
difficultyNames[i] = diffs[i].getDisplayName();
}
}
public static String[] getDifficultyNames() {
return difficultyNames;
}
}

View File

@@ -1,5 +1,7 @@
package sudoku.structure;
import sudoku.constraint.BlockConstraint;
import sudoku.constraint.Constraint;
import sudoku.constraint.IConstraint;
import java.util.ArrayList;
@@ -22,13 +24,13 @@ public class Sudoku {
/**
* Liste des contraintes (TODO) du Sudoku.
*/
private final List<IConstraint> constraints;
private final List<Constraint> constraints;
/**
* Largeur des Blocks s'ils sont rectangulaires, valant 0 si ce n'est pas le cas.
*/
private int blockWidth;
public Sudoku(List<Cell> cells, List<Block> blocks, List<IConstraint> constraints) {
public Sudoku(List<Cell> cells, List<Block> blocks, List<Constraint> constraints) {
this.cells = cells;
this.blocks = blocks;
this.constraints = constraints;
@@ -41,7 +43,7 @@ public class Sudoku {
* @return Coordinate, correspondante à l'index donné.
*/
public Coordinate toCoords(int index) {
return new Coordinate( index % getSize(), index / getSize() );
return new Coordinate(index % getSize(), index / getSize());
}
/**
@@ -73,13 +75,13 @@ public class Sudoku {
/**
* Teste si on peut placer la value dans la Cell aux coordonnées x, y d'après les contraintes du Sudoku.
* @param x int, abscisse de la Cell voulue.
* @param y int, ordonnée de la Cell voulue.
* @param x int, abscisse de la Cell voulue.
* @param y int, ordonnée de la Cell voulue.
* @param value int, index du symbole qu'on veut placer.
* @return boolean, true si on peut la placer et false sinon.
*/
public boolean canBePlaced(int x, int y, int value) {
for (IConstraint constraint : this.constraints) {
for (Constraint constraint : this.constraints) {
if (!constraint.canBePlaced(this, x, y, value)) {
return false;
}
@@ -89,8 +91,8 @@ public class Sudoku {
/**
* Tente de placer le symbole value dans la Cell de coordonnées x, y.
* @param x int, abscisse de la Cell voulue.
* @param y int, coordonnée de la Cell voulue;
* @param x int, abscisse de la Cell voulue.
* @param y int, coordonnée de la Cell voulue;
* @param value int, index du symbole que l'on veut placer.
* @return boolean, true si le symbole a été placé, false sinon
*/
@@ -132,14 +134,14 @@ public class Sudoku {
/**
* Place le symbole d'index value dans la Cell de coordonnées précisées.
* @param x int, abscisse de la Cell voulue.
* @param y int, coordonnée de la Cell voulue.
* @param x int, abscisse de la Cell voulue.
* @param y int, coordonnée de la Cell voulue.
* @param value int, index du symbole à placer.
* @return Cell, la Cell qui a été modifiée.
*/
public Cell setCellSymbol(int x, int y, int value) {
assert (isValidCoords(x, y));
for (IConstraint constraint : this.constraints) {
for (Constraint constraint : this.constraints) {
if (!constraint.canBePlaced(this, x, y, value)) {
return null;
}
@@ -205,7 +207,7 @@ public class Sudoku {
return this.cells.get(i);
}
public List<IConstraint> getConstraints() {
public List<Constraint> getConstraints() {
return constraints;
}
@@ -264,8 +266,8 @@ public class Sudoku {
* Met à jour les symboles possibles des Cells du Sudoku.
*
*/
public void updateSymbolsPossibilities(){
for (IConstraint constraint : constraints) {
public void updateSymbolsPossibilities() {
for (Constraint constraint : constraints) {
List<Cell> cells = this.getCells();
for (Cell cell : cells) {
Coordinate coord = null;
@@ -275,7 +277,11 @@ public class Sudoku {
System.out.println("Cas jamais atteint.");
}
List<Integer> newPossibleSymbols = cell.getPossibleSymbols();
newPossibleSymbols.retainAll(constraint.getPossibleSymbols(this, coord.getX(), coord.getY()));
newPossibleSymbols.retainAll(constraint.getPossibleSymbols(
this,
coord.getX(),
coord.getY()
));
cell.setPossibleSymbols(newPossibleSymbols);
}
@@ -323,7 +329,7 @@ public class Sudoku {
return result;
}
for (int i = 0; i < this.constraints.size(); i++) {
IConstraint constraint = this.constraints.get(i);
Constraint constraint = this.constraints.get(i);
if (i == 0) {
result.addAll(constraint.getPossibleSymbols(this, cellCoordinates.getX(), cellCoordinates.getY()));
} else {
@@ -446,4 +452,8 @@ public class Sudoku {
this.blockWidth = blockWidth;
}
public boolean hasConstraint(Constraint constraint) {
return this.constraints.contains(constraint);
}
}

View File

@@ -1,5 +1,8 @@
package sudoku.structure;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -8,9 +11,10 @@ import java.util.Random;
import sudoku.constraint.BlockConstraint;
import sudoku.constraint.ColumnConstraint;
import sudoku.constraint.Constraint;
import sudoku.constraint.DiagonalConstraint;
import sudoku.constraint.IConstraint;
import sudoku.constraint.LineConstraint;
import sudoku.io.SudokuPrinter;
import sudoku.solver.Solver;
public class SudokuFactory {
@@ -19,18 +23,13 @@ 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<IConstraint> DEFAULT_CONSTRAINTS = Arrays.asList(new BlockConstraint(), new LineConstraint(), new ColumnConstraint());
public static List<Constraint> DEFAULT_CONSTRAINTS = Arrays.asList(Constraint.Block, Constraint.Column,
Constraint.Line);
/**
* Créée des Cells et les met dans une liste de taille size.
@@ -81,9 +80,9 @@ public class SudokuFactory {
* @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));
public static MultiDoku createBasicEmptyRectangleDoku(int widthBlock, int heightBlock,
List<Constraint> constraints) {
return new MultiDoku(Arrays.asList(createRectangleSudoku(widthBlock, heightBlock, constraints)));
}
/**
@@ -91,13 +90,13 @@ public class SudokuFactory {
* @param size int, taille des Blocks.
* @return MultiDoku, MultiDoku vide.
*/
public static MultiDoku createBasicEmptySquareSudoku(int size) {
return createBasicEmptyRectangleSudoku(size, size);
public static MultiDoku createBasicEmptySquareDoku(int size, List<Constraint> constraints) {
return new MultiDoku(Arrays.asList(createSquareSudoku(size, constraints)));
}
/**
* Place des Cells immutables de valeurs fournies, aux Coordinate fournies dans le MultiDoku doku fourni.
* @param doku MultiDoku, MultiDoku à remplir.
* @param doku MultiDoku, MultiDoku à remplir.
* @param immutableCells Map<Coordinate, Integer>, association de Coordinate coordonnées et Integer valeurs, correspondant aux cases à remplir.
*/
public static void setImmutableCells(MultiDoku doku, Map<Coordinate, Integer> immutableCells) {
@@ -120,7 +119,7 @@ public class SudokuFactory {
* @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 {
public static boolean newDokuFromFilledOne(MultiDoku doku, int nbCellsToEmpty) throws Exception {
System.out.println("nbCellsToEmpty : "+nbCellsToEmpty);
if (nbCellsToEmpty >= doku.getCells().size()) {
@@ -152,37 +151,40 @@ public class SudokuFactory {
}
return false;
}
}
/**
* Créée un Sudoku vide dont les Blocks sont de taille widthBlock par heightBlock.
* @param widthBlock int, largeur des Blocks.
* @param widthBlock int, largeur des Blocks.
* @param heightBlock int, hauteur des Blocks.
* @return Sudoku, Sudoku vide.
*/
private static Sudoku createRectangleSudoku(int widthBlock, int heightBlock) {
private static Sudoku createRectangleSudoku(int widthBlock, int heightBlock, List<Constraint> constraints) {
int symbolCount = widthBlock * heightBlock;
List<Cell> cases = initCells(symbolCount);
List<Block> blocs = initRectangleBlocs(cases, widthBlock, heightBlock);
Sudoku s = new Sudoku(cases, blocs, DEFAULT_CONSTRAINTS);
Sudoku s = new Sudoku(cases, blocs, constraints);
for (Block block : s.getBlocks()) {
block.getSudokus().add(s);
}
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);
private static Sudoku createSquareSudoku(int size, List<Constraint> constraints) {
return createRectangleSudoku(size, size, constraints);
}
/**
* 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.
* @param offset Coordinate, décalage entre les deux Sudokus.
*/
private static void linkSquareSudokus(Sudoku sudoku1, Sudoku sudoku2, Coordinate offset) {
int blockWidth = sudoku1.getBlockWidth();
@@ -199,6 +201,7 @@ public class SudokuFactory {
// on remplace le bloc
sudoku2.getBlocks().set(block2Y * blockWidth + block2X, block1);
block1.getSudokus().add(sudoku2);
// on remplace les cellules
for (int i = 0; i < block1.getCells().size(); i++) {
@@ -218,7 +221,7 @@ public class SudokuFactory {
* @param size int, largeur des Blocks unitraires des Sudokus à crééer.
* @return MultiDoku, MultiDoku de forme X.
*/
public static MultiDoku createBasicXShapedMultidoku(int size) {
public static MultiDoku createBasicXShapedMultidoku(int size, List<Constraint> constraints) {
assert (size > 1);
/*
@@ -227,11 +230,11 @@ public class SudokuFactory {
* 4 5
*/
Sudoku sudoku1 = createSquareSudoku(size);
Sudoku sudoku2 = createSquareSudoku(size);
Sudoku sudoku3 = createSquareSudoku(size);
Sudoku sudoku4 = createSquareSudoku(size);
Sudoku sudoku5 = createSquareSudoku(size);
Sudoku sudoku1 = createSquareSudoku(size, constraints);
Sudoku sudoku2 = createSquareSudoku(size, constraints);
Sudoku sudoku3 = createSquareSudoku(size, constraints);
Sudoku sudoku4 = createSquareSudoku(size, constraints);
Sudoku sudoku5 = createSquareSudoku(size, constraints);
linkSquareSudokus(sudoku1, sudoku2, new Coordinate(1 - size, 1 - size));
linkSquareSudokus(sudoku1, sudoku3, new Coordinate(size - 1, 1 - size));
@@ -243,15 +246,22 @@ public class SudokuFactory {
public static void fillDoku(MultiDoku doku, Difficulty difficulty) throws Exception {
Solver.solveRandom(doku, random);
//SudokuPrinter.printRectangleSudoku(doku.getSubGrid(0), 3, 3);
int nbCellsToEmpty = (int)(difficulty.getFactor()*doku.getNbCells());
//System.out.println(nbCellsToEmpty);
boolean successful = newDokuFromFilledOne(doku, nbCellsToEmpty);
if (!successful) {
int nbCellsToEmpty = (int) (difficulty.getFactor() * doku.getNbCells());
boolean successfull = newDokuFromFilledOne(doku, nbCellsToEmpty);
if (!successfull) {
throw new Exception("Canno't create this doku with this difficulty");
}
doku.setFilledCellsImmutable();
}
public static MultiDoku fromfile(String filePath) {
try {
String content = Files.readString(Paths.get(filePath));
MultiDoku doku = SudokuSerializer.deserializeSudoku(content);
return doku;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}

View File

@@ -1,39 +1,41 @@
package sudoku;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.File;
import java.util.Random;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import sudoku.io.SudokuPrinter;
import sudoku.io.SudokuSerializer;
import sudoku.structure.MultiDoku;
import sudoku.structure.SudokuFactory;
import java.util.Random;
public class SudokuSerializerTest {
void testSerializeWithSize(int blockWidth, int blockHeight) {
var sudoku = SudokuFactory.createBasicEmptyRectangleSudoku(blockWidth, blockHeight);
var sudoku = SudokuFactory.createBasicEmptyRectangleDoku(blockWidth, blockHeight,
SudokuFactory.DEFAULT_CONSTRAINTS);
JSONObject data = SudokuSerializer.serializeSudoku(sudoku);
MultiDoku multiDoku = SudokuSerializer.deserializeSudoku(data);
assert(data.toString().equals(SudokuSerializer.serializeSudoku(multiDoku).toString()));
assert (data.toString().equals(SudokuSerializer.serializeSudoku(multiDoku).toString()));
}
void testSaveWithSize(int blockWidth, int blockHeight) {
MultiDoku doku = SudokuFactory.createBasicEmptyRectangleSudoku(blockWidth, blockHeight);
int saveNumber = SudokuSerializer.saveMultiDoku(doku);
MultiDoku doku = SudokuFactory.createBasicEmptyRectangleDoku(blockWidth, blockHeight,
SudokuFactory.DEFAULT_CONSTRAINTS);
String savePath = SudokuSerializer.saveMultiDoku(doku);
MultiDoku otherDoku = null;
try {
otherDoku = SudokuSerializer.getSavedMultiDoku(saveNumber);
otherDoku = SudokuFactory.fromfile(savePath);
assert (otherDoku != null);
// clean file after test
File fileToDelete = new File(savePath);
fileToDelete.delete();
} catch (Exception e) {
e.printStackTrace();
assert false;
}
assert(doku.equals(otherDoku));
assert (doku.equals(otherDoku));
}
@Test
@@ -41,18 +43,15 @@ public class SudokuSerializerTest {
Random r = new Random();
int testCount = 5;
for (int i = 0; i < testCount; i++) {
int blockWidth = r.nextInt(20) + 1;
int blockHeight = r.nextInt(20) + 1;
int blockWidth = r.nextInt(10) + 1;
int blockHeight = r.nextInt(10) + 1;
testSerializeWithSize(blockWidth, blockHeight);
}
for (int i = 0; i < testCount; i++) {
int blockWidth = r.nextInt(20) + 1;
int blockHeight = r.nextInt(20) + 1;
int blockWidth = r.nextInt(10) + 1;
int blockHeight = r.nextInt(10) + 1;
testSaveWithSize(blockWidth, blockHeight);
}
}
}

View File

@@ -16,51 +16,46 @@ class SolverTest {
void solveTest() {
Random rand = new Random();
MultiDoku dokuToTest = SudokuFactory.createBasicEmptySquareSudoku(3);
MultiDoku dokuResult = SudokuFactory.createBasicEmptySquareSudoku(3);
MultiDoku dokuToTest = SudokuFactory.createBasicEmptySquareDoku(3, SudokuFactory.DEFAULT_CONSTRAINTS);
MultiDoku dokuResult = SudokuFactory.createBasicEmptySquareDoku(3, SudokuFactory.DEFAULT_CONSTRAINTS);
Sudoku sudokuToTest = dokuToTest.getSubGrid(0);
Sudoku sudokuResult = dokuResult.getSubGrid(0);
int ns = Cell.NOSYMBOL;
List<Integer> immutableCells = List.of(ns, ns, 0, ns, ns, 2, 8, ns, 1,
ns, 3, ns, ns, 5, 6, 7, ns, ns,
ns, ns, ns, 8, ns, 7, ns, ns, 6,
0, ns, 1, ns, ns, ns, ns, ns, ns,
4, 8, 7, 5, 1, ns, 6, ns, ns,
6, ns, 3, 2, ns, ns, ns, 8, 0,
ns, ns, 6, ns, ns, 8, ns, 7, 5,
8, 0, ns, 7, ns, 5, 2, ns, 3,
5, ns, ns, ns, 3, 1, 0, ns, ns);
assert(sudokuToTest.setImmutableCellsSymbol(immutableCells));
List<Integer> immutableCells = List.of(ns, ns, 0, ns, ns, 2, 8, ns, 1,
ns, 3, ns, ns, 5, 6, 7, ns, ns,
ns, ns, ns, 8, ns, 7, ns, ns, 6,
0, ns, 1, ns, ns, ns, ns, ns, ns,
4, 8, 7, 5, 1, ns, 6, ns, ns,
6, ns, 3, 2, ns, ns, ns, 8, 0,
ns, ns, 6, ns, ns, 8, ns, 7, 5,
8, 0, ns, 7, ns, 5, 2, ns, 3,
5, ns, ns, ns, 3, 1, 0, ns, ns);
assert (sudokuToTest.setImmutableCellsSymbol(immutableCells));
SudokuPrinter.printRectangleSudoku(dokuToTest.getSubGrid(0), 3, 3);
List<Integer> correctCells = List.of(7, 6, 0, 3, 4, 2, 8, 5, 1,
2, 3, 8, 1, 5, 6, 7, 0, 4,
1, 4, 5, 8, 0, 7, 3, 2, 6,
0, 2, 1, 6, 8, 3, 5, 4, 7,
4, 8, 7, 5, 1, 0, 6, 3, 2,
6, 5, 3, 2, 7, 4, 1, 8, 0,
3, 1, 6, 0, 2, 8, 4, 7, 5,
8, 0, 4, 7, 6, 5, 2, 1, 3,
5, 7, 2, 4, 3, 1, 0, 6, 8);
List<Integer> correctCells = List.of(7, 6, 0, 3, 4, 2, 8, 5, 1,
2, 3, 8, 1, 5, 6, 7, 0, 4,
1, 4, 5, 8, 0, 7, 3, 2, 6,
0, 2, 1, 6, 8, 3, 5, 4, 7,
4, 8, 7, 5, 1, 0, 6, 3, 2,
6, 5, 3, 2, 7, 4, 1, 8, 0,
3, 1, 6, 0, 2, 8, 4, 7, 5,
8, 0, 4, 7, 6, 5, 2, 1, 3,
5, 7, 2, 4, 3, 1, 0, 6, 8);
sudokuResult.setCellsSymbol(correctCells);
System.out.println("\n****************************Doku Control\n");
SudokuPrinter.printRectangleSudoku(sudokuResult, 3, 3);
assert(dokuResult.isSolved());
Solver.solveRandom(dokuToTest, rand);
System.out.println("\n****************************\nDoku solved");
SudokuPrinter.printRectangleSudoku(dokuToTest.getSubGrid(0), 3, 3);
@@ -69,25 +64,25 @@ class SolverTest {
assert(dokuToTest.equals(dokuResult));
MultiDoku dokuToTest2 = SudokuFactory.createBasicEmptySquareSudoku(3);
MultiDoku dokuToTest2 = SudokuFactory.createBasicEmptySquareDoku(3, SudokuFactory.DEFAULT_CONSTRAINTS);
Sudoku sudokuToTest2 = dokuToTest2.getSubGrid(0);
List<Integer> immutableCells2 = List.of(ns, ns, 0, ns, ns, 2, 8, ns, 1,
1, 3, ns, ns, 5, 6, 7, ns, ns,
ns, ns, ns, 8, ns, 7, ns, ns, 6,
0, ns, 1, ns, ns, ns, ns, ns, ns,
4, 8, 7, 5, 1, ns, 6, ns, ns,
6, ns, 3, 2, ns, ns, ns, 8, 0,
ns, ns, 6, ns, ns, 8, ns, 7, 5,
8, 0, ns, 7, ns, 5, 2, ns, 3,
5, ns, ns, ns, 3, 1, 0, ns, ns);
List<Integer> immutableCells2 = List.of(ns, ns, 0, ns, ns, 2, 8, ns, 1,
1, 3, ns, ns, 5, 6, 7, ns, ns,
ns, ns, ns, 8, ns, 7, ns, ns, 6,
0, ns, 1, ns, ns, ns, ns, ns, ns,
4, 8, 7, 5, 1, ns, 6, ns, ns,
6, ns, 3, 2, ns, ns, ns, 8, 0,
ns, ns, 6, ns, ns, 8, ns, 7, 5,
8, 0, ns, 7, ns, 5, 2, ns, 3,
5, ns, ns, ns, 3, 1, 0, ns, ns);
sudokuToTest2.setImmutableCellsSymbol(immutableCells2);
boolean isSolved = Solver.solveRandom(dokuToTest2, rand);
assert(!isSolved);
assert (!isSolved);
MultiDoku dokuToTest3 = SudokuFactory.createBasicEmptySquareSudoku(3);
MultiDoku dokuToTest3 = SudokuFactory.createBasicEmptySquareDoku(3, SudokuFactory.DEFAULT_CONSTRAINTS);
Solver.solveRandom(dokuToTest3, rand);