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); + } +}