diff --git a/.gitignore b/.gitignore index 8a30d25..c0e1299 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,6 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# CUSTOM +target \ No newline at end of file diff --git a/ProgettoImplementativo_A_22-23.pdf b/ProgettoImplementativo_A_22-23.pdf new file mode 100644 index 0000000..d5d9134 Binary files /dev/null and b/ProgettoImplementativo_A_22-23.pdf differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f38f7f7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + net.berack.upo + upo-ai + 1.0-SNAPSHOT + + + 1.17 + 1.17 + + + \ No newline at end of file diff --git a/src/main/java/net/berack/upo/ai/Main.java b/src/main/java/net/berack/upo/ai/Main.java new file mode 100644 index 0000000..b801ed0 --- /dev/null +++ b/src/main/java/net/berack/upo/ai/Main.java @@ -0,0 +1,8 @@ +package net.berack.upo.ai; + +public class Main { + + public static void main(String[] args) { + + } +} \ 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 new file mode 100644 index 0000000..6dca0d5 --- /dev/null +++ b/src/main/java/net/berack/upo/ai/problem1/AStar.java @@ -0,0 +1,159 @@ +package net.berack.upo.ai.problem1; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Una classe che risolve problemi utilizzando l'algoritmo A*. + * @author Berack96 + * @param State la classe degli stati in cui si trova il problema da risolvere + * @param Action la classe di azioni che si possono compiere da uno stato e l'altro + */ +public class AStar { + + private Function actions; + private BiFunction transition; + + private BiFunction heuristic; + private TriFunction cost; + + /** + * Crea una istanza dell'algoritmo A* con una funzione di azioni possibili da compiere dato uno stato + * e una funzione di transizione che dato uno stato permette di raggiungerne un'altro tramite un'azione. + * + * @param actions la funzione che da uno stato permette di vedere le possibili azioni da compiere + * @param transition la funzione di tansizione che ci permette di raggiungere uno stato da un'altro tramite un'azione + */ + public AStar(Function actions, BiFunction transition) { + this.actions = Objects.requireNonNull(actions); + this.transition = Objects.requireNonNull(transition); + + this.cost = (s1,s2, a) -> 1; + this.heuristic = (s1,s2) -> 0; + } + + /** + * Permette di impostare la funzione di costo che indica, facendo un'azione su uno stato e raggiungendone un'altro, quanto devo spendere, ovvero il costo. + * Di default la funzione di costo equivale sempre ad 1 + * @param cost la funzione di costo + * @return se stesso in modo da poter concatenare i vari set (un pò come una factory) + */ + public AStar setCost(TriFunction cost) { + this.cost = Objects.requireNonNull(cost); + return this; + } + + /** + * Permette di impostare una funzione di transizione che indica, facendo un'azione su uno stato, a quale stato possiamo arrivare. + * @param transition la funzione di transizione + * @return se stesso in modo da poter concatenare i vari set (un pò come una factory) + */ + public AStar setTransition(BiFunction transition) { + this.transition = Objects.requireNonNull(transition); + return this; + } + + /** + * Permette di impostare una funzione euristica che indica, prendendo uno stato e uno stato goal, la distanza di costo approssimata per arrivare al goal + * @param heuristic la funzione euristica + * @return se stesso in modo da poter concatenare i vari set (un pò come una factory) + */ + public AStar setHeuristic(BiFunction heuristic) { + this.heuristic = Objects.requireNonNull(heuristic); + return this; + } + + /** + * Permette di impostare una funzione che indica, a partire da uno stato, quali sono le azioni che posso svolgere + * @param actions la funzione di azioni + * @return se stesso in modo da poter concatenare i vari set (un pò come una factory) + */ + public AStar setActions(Function actions) { + this.actions = Objects.requireNonNull(actions); + return this; + } + + /** + * Permette di risolvere il problema dopo aver indicato le varie funzioni. + * In input viene richiesto uno stato iniziale e uno stato goal da raggiungere dallo stato corrente. + * La funzione utilizzata per il raggiungimento dello stato goal è equals(), quindi è richiesto che + * la classe State abbia implementato correttamente la funzione equals(). + * + * @param initial lo stato corrente + * @param goal lo stato da raggiungere + * @return una sequenza di azioni per poter raggiungere lo stato goal oppure null se non è possibile + */ + public List solve(State initial, State goal) { + Objects.requireNonNull(initial); + Objects.requireNonNull(goal); + + NodeState found = null; + var list = new PriorityQueue(); + list.add(new NodeState(null, initial, null, 0)); + + while(list.size() > 0) { + var current = list.poll(); + if(current.state.equals(goal)) { + found = current; + break; + } + + 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)); + + } catch (Exception ignore) {} + } + + if(found == null) return null; + + var path = new ArrayList(); + while(found != null) { + path.add(found.action); + found = found.parent; + } + + Collections.reverse(path); + return path; + } + + + /** + * Classe privata per mantenere i dati all'interno della PriorityQueue. + */ + private class NodeState implements Comparable { + NodeState parent; + State state; + Action action; + int cost; + + NodeState(NodeState parent, State state, Action action, int cost) { + this.parent = parent; + this.state = state; + this.action = action; + this.cost = cost; + } + + @Override + public int compareTo(NodeState other) { + return this.cost - other.cost; + } + } + + /** + * Interfaccia privata per poter indicare una TriFunction, ovvero una funzione che accetta in input 3 parametri + */ + @FunctionalInterface private interface TriFunction { + R apply(T t, U u, V v); + } +} diff --git a/src/main/java/net/berack/upo/ai/problem1/Puzzle8.java b/src/main/java/net/berack/upo/ai/problem1/Puzzle8.java new file mode 100644 index 0000000..97be53c --- /dev/null +++ b/src/main/java/net/berack/upo/ai/problem1/Puzzle8.java @@ -0,0 +1,231 @@ +package net.berack.upo.ai.problem1; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * Classe utilizzata per la rappresentazione del problema delle 8 tessere. + * In essa ci sono dei metodi base per il modello del problema tra cui getter e setter. + * Oltre a questi ci sono dei metodi per la risoluzione del problema sia manuale, + * che automatico tramite l'algoritmo A* + * + * @author Berack96 + */ +public class Puzzle8 implements Iterable { + public static final int LENGTH = 3; + + /** + * Possibili movimenti della tessera "vuota" + */ + public enum Move { + UP, + DOWN, + LEFT, + RIGHT + } + + private int[] puzzle = new int[LENGTH * LENGTH]; + private int blank = 0; + + /** + * Genera una nuova istanza del problema con le tessere posizionate in modo casuale. + */ + public Puzzle8() { + var values = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8); + Collections.shuffle(values); + + for(var i = 0; i < this.puzzle.length; i++) { + this.puzzle[i] = values.get(i).intValue(); + if(this.puzzle[i] == 0) this.blank = i; + } + } + + /** + * Genera una nuova istanza del problema con le tessere posizionate come il + * problema passato in input e dopo aver eseguito la mossa indicata. + * + * @param original il puzzle di partenza che si vuole copiare + * @param move la mossa da eseguire + * + * @throws UnsupportedOperationException nel caso la mossa non sia disponibile. + */ + public Puzzle8(Puzzle8 original, Move move) { + Arrays.copyOf(original.puzzle, this.puzzle.length); + this.blank = original.blank; + + this.move(move); + } + + /** + * Inizializza il problema con gli interi passati. + * L'array inserito deve essere di 9 valori dato che la matrice è di 3x3. + * I valori saranno posizionati a tre a tre nella matrice, quindi i primi 3 saranno nella + * prima riga, i secondi tre nella seconda e i terzi saranno nella terza riga. + * Nel caso in cui vengano passati meno valori una eccezione verrà sollevata. + * Gli elementi inseriti devono essere UNICI e compresi tra 0 e 8 estremi inclusi. + * + * @param values i valori a cui inizializzare il problema. + * @throws IllegalArgumentException nel caso in cui + */ + public Puzzle8(int...values) { + if(values == null || values.length != LENGTH*LENGTH) + throw new IllegalArgumentException("The size of the array must be " + LENGTH*LENGTH); + + var check = new int[LENGTH * LENGTH]; + Arrays.copyOf(values, values.length); + + for(var i = 0; i < this.puzzle.length; i++) { + var curr = this.puzzle[i]; + var ok = true; + + ok = (curr >= 0 && curr < check.length); + if(ok) { + if(curr == 0) this.blank = i; + + check[curr] += 1; + ok = (check[curr] == 1); + } + + if(!ok) throw new IllegalArgumentException("The input values must be UNIQUE integers between 0 and " + (LENGTH*LENGTH -1) + " inclusive"); + } + } + + + /** + * 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 + * 0x0 e l'ultima nella zona 2x2. + * Il valore varia da 1 a 8 ed il valore della tessera "vuota" equivale a 0. + * + * @param x la prima coordinata, i possibili valori sono 0, 1, 2 + * @param y la seconda coordinata, i possibili valori sono 0, 1, 2 + * @return il valore della tessera nelle coordinate selezionate + */ + public int get(int x, int y) { + return puzzle[x * LENGTH + y]; + } + + /** + * Genera un array di mosse disponibili a partire dalla configurazione corrente. + * Questo metodo esiste dato che non tutte le mosse sono disponibili in tutti i casi. + * per esempio se mi trovo nell'angolo in basso a dx potrò fare solo + * le mosse UP e LEFT. + * + * @return un array di mosse disponibili + */ + public Move[] availableMoves() { + return switch(this.blank) { + case 0 -> new Move[] { Move.DOWN, Move.RIGHT }; + case 1 -> new Move[] { Move.DOWN, Move.LEFT, Move.RIGHT }; + case 2 -> new Move[] { Move.DOWN, Move.LEFT }; + case 3 -> new Move[] { Move.UP, Move.DOWN, Move.RIGHT }; + case 4 -> new Move[] { Move.UP, Move.DOWN, Move.LEFT, Move.RIGHT }; + case 5 -> new Move[] { Move.UP, Move.DOWN, Move.LEFT }; + case 6 -> new Move[] { Move.UP, Move.RIGHT }; + case 7 -> new Move[] { Move.UP, Move.LEFT, Move.RIGHT }; + case 8 -> new Move[] { Move.UP, Move.LEFT }; + default -> new Move[] {}; + }; + } + + /** + * Indica se la mossa inserita sia una mossa valida, dato che non tutte le mosse sono disponibili in tutti i casi. + * per esempio se mi trovo nell'angolo in basso a dx potrò fare solo le mosse UP e LEFT. + * @param move la mossa da testare + * @return VERO se la mossa è valida altrimenti falso + */ + public boolean isValidMove(Move move) { + return switch(move) { + case UP -> this.blank >= LENGTH; + case DOWN -> this.blank <= 2*LENGTH - 1; + case LEFT -> this.blank % LENGTH != 0; + case RIGHT -> this.blank % LENGTH != LENGTH - 1; + }; + } + + /** + * Muove la tessera "vuota" in una delle direzioni indicate. + * Essa viene scambiata con una delle tessere adiacenti. + * + * Nota: non tutte le mosse sono disponibili in tutti i casi. + * per esempio se mi trovo nell'angolo in basso a dx potrò fare solo + * le mosse UP e LEFT. + * + * @see #availableMoves() + * @see #isValidMove(Move) + * @see #solve() + * + * @param move + * @throws UnsupportedOperationException nel caso in cui la mossa inserita non sia valida + */ + public void move(Move move) { + if(!this.isValidMove(move)) + throw new UnsupportedOperationException("Move not available"); + + var other = switch(move) { + case UP -> this.blank - LENGTH; + case DOWN -> this.blank + LENGTH; + case LEFT -> this.blank - 1; + case RIGHT -> this.blank + 1; + }; + + this.puzzle[this.blank] = this.puzzle[other]; + this.puzzle[other] = 0; + this.blank = other; + } + + /** + * 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) { + var aStar = new AStar(Puzzle8::availableMoves, (p, m) -> new Puzzle8(p, m)) + .setHeuristic((p1, p2) -> { + var sum = 0; + var n = p1.puzzle.length; + var index = new int[n]; + + for(var i = 0; i < n; i++) index[p1.puzzle[i]] = i; + for(var i = 0; i < n; i++) { + var curr = p2.puzzle[i]; + var i2 = index[curr]; + + sum += Math.abs((i / LENGTH) - (i2 / LENGTH)) + Math.abs((i % LENGTH) - (i2 % LENGTH)); + } + return sum; + }); + + return aStar.solve(this, Objects.requireNonNull(goal)); + } + + + @Override + public boolean equals(Object obj) { + if(!obj.getClass().isInstance(this)) return false; + return Arrays.equals(((Puzzle8) obj).puzzle, this.puzzle); + } + + @Override + public Iterator iterator() { + return new Iterator() { + int current = 0; + @Override public boolean hasNext() { return current < puzzle.length; } + @Override public Integer next() { return puzzle[current++]; } + }; + } +} diff --git a/src/main/java/net/berack/upo/ai/problem2/Tris.java b/src/main/java/net/berack/upo/ai/problem2/Tris.java new file mode 100644 index 0000000..03816bf --- /dev/null +++ b/src/main/java/net/berack/upo/ai/problem2/Tris.java @@ -0,0 +1,29 @@ +package net.berack.upo.ai.problem2; + +import java.util.Arrays; + +public class Tris { + public static final int LENGTH = 3; + + /** + * Possibili stati delle zone + */ + public enum State { + EMPTY, + VALUE_X, + VALUE_O + } + + private State[] tris = new State[LENGTH * LENGTH]; + + /** + * Crea una nuova istanza del gioco con tutti gli spazi vuoti + */ + public Tris() { + Arrays.fill(tris, State.EMPTY); + } + + public Tris(Tris current, State state, int x, int y) { + + } +}