diff --git a/pom.xml b/pom.xml
index f38f7f7..5617dd0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,4 +13,13 @@
1.17
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.9.2
+ test
+
+
+
\ No newline at end of file
diff --git a/src/main/java/net/berack/upo/ai/problem1/AStar.java b/src/main/java/net/berack/upo/ai/problem1/AStar.java
index 6dca0d5..5d49b9b 100644
--- a/src/main/java/net/berack/upo/ai/problem1/AStar.java
+++ b/src/main/java/net/berack/upo/ai/problem1/AStar.java
@@ -94,7 +94,7 @@ public class AStar {
NodeState found = null;
var list = new PriorityQueue();
- list.add(new NodeState(null, initial, null, 0));
+ list.add(new NodeState(null, initial, null, 0, 0));
while(list.size() > 0) {
var current = list.poll();
@@ -106,19 +106,20 @@ public class AStar {
for(var action : this.actions.apply(current.state)) try {
var next = this.transition.apply(current.state, action);
-
var cost = this.cost.apply(current.state, next, action);
var dist = this.heuristic.apply(next, goal);
- list.add(new NodeState(current, next, action, current.cost + cost + dist));
+ list.add(new NodeState(current, next, action, current.cost + cost, dist));
- } catch (Exception ignore) {}
+ } catch (Exception ignore) {
+ ignore.printStackTrace();
+ }
}
if(found == null) return null;
var path = new ArrayList();
- while(found != null) {
+ while(found != null && found.action != null) {
path.add(found.action);
found = found.parent;
}
@@ -136,17 +137,19 @@ public class AStar {
State state;
Action action;
int cost;
+ int total;
- NodeState(NodeState parent, State state, Action action, int cost) {
+ NodeState(NodeState parent, State state, Action action, int cost, int heuristic) {
this.parent = parent;
this.state = state;
this.action = action;
this.cost = cost;
+ this.total = cost + heuristic;
}
@Override
public int compareTo(NodeState other) {
- return this.cost - other.cost;
+ return this.total - other.total;
}
}
diff --git a/src/main/java/net/berack/upo/ai/problem1/Puzzle8.java b/src/main/java/net/berack/upo/ai/problem1/Puzzle8.java
index 73f0a15..bed0cf0 100644
--- a/src/main/java/net/berack/upo/ai/problem1/Puzzle8.java
+++ b/src/main/java/net/berack/upo/ai/problem1/Puzzle8.java
@@ -16,6 +16,7 @@ import java.util.Random;
*/
public class Puzzle8 implements Iterable {
public static final int LENGTH = 3;
+ public static final int[] DEFAULT_GOAL = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 0};
/**
* Possibili movimenti della tessera "vuota"
@@ -34,18 +35,8 @@ public class Puzzle8 implements Iterable {
* Genera una nuova istanza del problema con le tessere posizionate in modo casuale.
*/
public Puzzle8() {
- var rand = new Random();
- this.puzzle = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8};
-
- for(var i = this.puzzle.length - 1; i > 0; i--) {
- var j = rand.nextInt(i + 1);
-
- var temp = this.puzzle[i];
- this.puzzle[i] = this.puzzle[j];
- this.puzzle[j] = temp;
-
- if(this.puzzle[i] == 0) this.blank = i;
- }
+ this.puzzle = Arrays.copyOf(DEFAULT_GOAL, DEFAULT_GOAL.length);
+ this.shuffle();
}
/**
@@ -58,7 +49,7 @@ public class Puzzle8 implements Iterable {
* @throws UnsupportedOperationException nel caso la mossa non sia disponibile.
*/
public Puzzle8(Puzzle8 original, Move move) {
- this.puzzle = Arrays.copyOf(original.puzzle, this.puzzle.length);
+ this.puzzle = Arrays.copyOf(original.puzzle, original.puzzle.length);
this.blank = original.blank;
this.move(move);
@@ -98,18 +89,6 @@ public class Puzzle8 implements Iterable {
}
}
-
- /**
- * Indica la grandezza della tavoletta che contiene le tessere.
- * Essa è una matrice 3x3.
- *
- * @see #LENGTH
- * @return la grandezza del lato della matrice
- */
- public int size() {
- return LENGTH;
- }
-
/**
* Permette di ricevere il valore della tessera posizionata nella zona richiesta.
* La zona è indicata come una matrice 3x3 dove la prima zona si trova nelle coordinate
@@ -194,12 +173,83 @@ public class Puzzle8 implements Iterable {
this.blank = other;
}
+ /**
+ * Questo metodo mette a caso le tessere del puzzle.
+ * É possibile che, una volta che le tessere sono state spostate a caso,
+ * non siano più raggiungibili alcuni stati.
+ *
+ * @see #isSolvable()
+ * @see #isSolvable(Puzzle8)
+ */
+ public void shuffle() {
+ var rand = new Random();
+ for(var i = this.puzzle.length - 1; i > 0; i--) {
+ var j = rand.nextInt(i + 1);
+
+ var temp = this.puzzle[i];
+ this.puzzle[i] = this.puzzle[j];
+ this.puzzle[j] = temp;
+
+ if(this.puzzle[i] == 0) this.blank = i;
+ }
+ }
+
+ /**
+ * Indica se l'istanza del puzzle corrente sia risolvibile per il caso default.
+ * Con caso default si indica il puzzle composto da {@link #DEFAULT_GOAL}
+ *
+ * @return true nel caso si possa risolvere, false altrimenti
+ */
+ public boolean isSolvable() {
+ return this.isSolvable(new Puzzle8(DEFAULT_GOAL));
+ }
+
+ /**
+ * Metodo per controllare la risolvibilità del puzzle corrente.
+ *
+ * @param goal lo stato a cui si vuole arrivare
+ * @return true nel caso si possa risolvere, false altrimenti
+ */
+ public boolean isSolvable(Puzzle8 goal) {
+ var n = LENGTH * LENGTH;
+ var indexGoal = new int[n];
+
+ for(var i = 0; i < n; i++)
+ indexGoal[goal.puzzle[i]] = i;
+
+ var sum = 0;
+ for(var i = 0; i < n; i++) {
+ var curr = this.puzzle[i];
+ var iGoal = indexGoal[curr];
+
+ if(curr == 0) continue;
+
+ for(var j = n - 1; j > i ; j--) {
+ var val = this.puzzle[j];
+ if(val != 0 && indexGoal[val] < iGoal) sum += 1;
+ }
+ }
+
+ return (sum % 2) == 0;
+ }
+
+ /**
+ * Usa l'algoritmo A* per la soluzione del problema.
+ * Questo metodo cercherà di raggiungere lo stato goal indicato da {@link #DEFAULT_GOAL}
+ * @return una sequenza di mosse per poter risolvere il problema o null se non risolvibile
+ */
+ public List solve() {
+ return this.solve(new Puzzle8(DEFAULT_GOAL));
+ }
+
/**
* Usa l'algoritmo A* per la soluzione del problema
* @param goal lo stato goal in cui arrivare
* @return una sequenza di mosse per poter risolvere il problema o null se non risolvibile
*/
public List solve(Puzzle8 goal) {
+ if(!this.isSolvable(goal)) return null;
+
var aStar = new AStar(Puzzle8::availableMoves, (p, m) -> new Puzzle8(p, m))
.setHeuristic((p1, p2) -> {
var sum = 0;
@@ -219,6 +269,10 @@ public class Puzzle8 implements Iterable {
return aStar.solve(this, Objects.requireNonNull(goal));
}
+ @Override
+ public String toString() {
+ return Arrays.toString(this.puzzle);
+ }
@Override
public boolean equals(Object obj) {
diff --git a/src/test/java/net/berack/upo/ai/problem1/TestPuzzle.java b/src/test/java/net/berack/upo/ai/problem1/TestPuzzle.java
new file mode 100644
index 0000000..f7ff407
--- /dev/null
+++ b/src/test/java/net/berack/upo/ai/problem1/TestPuzzle.java
@@ -0,0 +1,150 @@
+package net.berack.upo.ai.problem1;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static net.berack.upo.ai.problem1.Puzzle8.Move.*;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+public class TestPuzzle {
+
+ private int[] goalArray = new int[] {0,1,2,3,4,5,6,7,8};
+
+ @Test
+ public void testContructors() {
+ var puzzle = new Puzzle8(goalArray);
+
+ for(var i = 0; i < goalArray.length; i++)
+ assertEquals(goalArray[i], puzzle.get(i%3, i/3), "Error in initialization");
+
+ var puzzle2 = new Puzzle8(goalArray);
+ assertEquals(puzzle, puzzle2, "Error in equality");
+
+ }
+
+ @Test
+ public void testMoves() {
+ var puzzle = new Puzzle8(goalArray);
+
+ var moves = new Puzzle8.Move[] {RIGHT, RIGHT, DOWN, LEFT, LEFT, DOWN, RIGHT, RIGHT, UP};
+ var avail = new Puzzle8.Move[][] {
+ {DOWN, RIGHT},
+ {DOWN, LEFT, RIGHT},
+ {DOWN, LEFT},
+ {UP, DOWN, LEFT},
+ {UP, DOWN, LEFT, RIGHT},
+ {UP, DOWN, RIGHT},
+ {UP, RIGHT},
+ {UP, LEFT, RIGHT},
+ {UP, LEFT}
+ };
+ var after = new int[][] {
+ {1,0,2,3,4,5,6,7,8},
+ {1,2,0,3,4,5,6,7,8},
+ {1,2,5,3,4,0,6,7,8},
+ {1,2,5,3,0,4,6,7,8},
+ {1,2,5,0,3,4,6,7,8},
+ {1,2,5,6,3,4,0,7,8},
+ {1,2,5,6,3,4,7,0,8},
+ {1,2,5,6,3,4,7,8,0},
+ {1,2,5,6,3,0,7,8,4}
+ };
+
+ for(var i = 0; i < moves.length; i++) {
+ var valid = puzzle.availableMoves();
+
+ Arrays.sort(avail[i]);
+ Arrays.sort(valid);
+ assertArrayEquals(avail[i], valid, "Available moves not corrected at iteration " + i);
+
+ puzzle.move(moves[i]);
+ assertEquals(new Puzzle8(after[i]), puzzle, "Puzzle Equality not corrected at iteration " + i);
+ }
+ }
+
+ @Test
+ public void testMoveInit() {
+ var puzzle = new Puzzle8(goalArray);
+ var moves = new Puzzle8.Move[] {RIGHT, DOWN, LEFT, UP, DOWN, RIGHT, UP, LEFT};
+
+ for(var move : moves)
+ puzzle = new Puzzle8(puzzle, move);
+ assertEquals(new Puzzle8(goalArray), puzzle, "After useless moves the puzzle must be the same as before!");
+ }
+
+ @Test
+ public void testSolvable() {
+ var solvable = new Puzzle8(5,2,8,4,1,7,0,3,6);
+ var unsolvable = new Puzzle8(1,2,3,4,5,6,0,8,7);
+
+ assertTrue(solvable.isSolvable());
+ assertFalse(unsolvable.isSolvable());
+
+ var goal = new Puzzle8(0,1,2,3,4,5,6,7,8);
+
+ solvable = new Puzzle8(1,0,2,3,4,5,6,7,8);
+ unsolvable = new Puzzle8(2,1,0,3,4,5,6,7,8);
+
+ assertTrue(solvable.isSolvable(goal));
+ assertFalse(unsolvable.isSolvable(goal));
+ }
+
+ @Test
+ public void testSolveSimple() {
+ var goal = new Puzzle8(goalArray);
+
+ assertEquals(0, goal.solve(goal).size());
+
+ var puzzle = new Puzzle8(1,0,2,3,4,5,6,7,8);
+ var solution = new Puzzle8.Move[] {LEFT};
+ var actual = puzzle.solve(goal).toArray(new Puzzle8.Move[0]);
+
+ assertArrayEquals(solution, actual);
+
+ for(var move : actual) puzzle.move(move);
+ assertEquals(new Puzzle8(goalArray), puzzle);
+ }
+
+ @Test
+ public void testSolveSimple2() {
+ var puzzle = new Puzzle8(1,2,5,4,0,8,3,6,7);
+ var goal = new Puzzle8(goalArray);
+ var solution = new Puzzle8.Move[] {LEFT, DOWN, RIGHT, RIGHT, UP, UP, LEFT, LEFT};
+ var actual = puzzle.solve(goal).toArray(new Puzzle8.Move[0]);
+
+ assertArrayEquals(solution, actual);
+
+ for(var move : actual) puzzle.move(move);
+ assertEquals(new Puzzle8(goalArray), puzzle);
+ }
+
+ @Test
+ public void testSolve() {
+ var puzzle = new Puzzle8(3,5,6,1,2,4,0,7,8);
+ var puzzleGoal = new Puzzle8(1,2,3,4,5,6,7,8,0);
+ var solution = new Puzzle8.Move[] {RIGHT, UP, RIGHT, UP, LEFT, LEFT, DOWN, RIGHT, DOWN, RIGHT, UP, UP, LEFT, DOWN, RIGHT, DOWN};
+ var actual = puzzle.solve(puzzleGoal).toArray(new Puzzle8.Move[0]);
+
+ assertArrayEquals(solution, actual);
+
+ for(var move : actual) puzzle.move(move);
+ assertEquals(puzzleGoal, puzzle);
+ }
+
+ @Test
+ public void testSolveRand() {
+ var goal = new Puzzle8(Puzzle8.DEFAULT_GOAL);
+
+ var puzzle = new Puzzle8();
+ while(!puzzle.isSolvable(goal)) puzzle.shuffle();
+
+ var actions = puzzle.solve(goal);
+ for(var move : actions) puzzle.move(move);
+
+ assertEquals(puzzle, goal);
+ }
+}