From 5b964b1cd136da4d9621cb176a5680c935fe04bd Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 13 Dec 2023 14:19:37 +0100 Subject: [PATCH] Tris - implemented Tris class - added test cases --- .../java/net/berack/upo/ai/problem2/Tris.java | 159 ++++++++++++++--- .../net/berack/upo/ai/problem2/TestTris.java | 160 ++++++++++++++++++ 2 files changed, 299 insertions(+), 20 deletions(-) create mode 100644 src/test/java/net/berack/upo/ai/problem2/TestTris.java diff --git a/src/main/java/net/berack/upo/ai/problem2/Tris.java b/src/main/java/net/berack/upo/ai/problem2/Tris.java index d9c940c..b7d5123 100644 --- a/src/main/java/net/berack/upo/ai/problem2/Tris.java +++ b/src/main/java/net/berack/upo/ai/problem2/Tris.java @@ -1,21 +1,46 @@ package net.berack.upo.ai.problem2; import java.util.Arrays; -import java.util.Objects; +import java.util.Iterator; -public class Tris { +/** + * Classe che rappresenta il classico gioco del tris, dove per vincere bisogna + * mettere il carattere che si gioca in fila di tre prima dell'avversario. + * + * @author Berack96 + */ +public class Tris implements Iterable { public static final int LENGTH = 3; + /** + * Classe di appoggio per la creazione di una lista di possibili coordinate + * da utilizzare per giocare una mossa. + */ + public static class Coordinate { + public final int x; + public final int y; + private Coordinate(int x, int y) { + this.x = x; + this.y = y; + } + @Override public boolean equals(Object obj) { + if(!(obj instanceof Coordinate)) return false; + var curr = (Coordinate) obj; + return this.x == curr.x && this.y == curr.y; + } + } + /** * Possibili stati delle zone */ - public enum State { + public static enum State { EMPTY, VALUE_X, VALUE_O } private State[] tris; + private State currentTurn = State.VALUE_X; /** * Crea una nuova istanza del gioco con tutti gli spazi vuoti @@ -28,13 +53,12 @@ public class Tris { /** * Crea una nuova istanza del gioco a partire da quella passata in input * @param current l'istanza correte - * @param state lo stato che si vuole mettere ad una cella - * @param x la coordinata x in cui mettere lo stato - * @param y la coordinata y in cui mettere lo stato + * @param coord le coordinate in cui il giocatore vuole giocare la sua mossa */ - public Tris(Tris current, State state, int x, int y) { + public Tris(Tris current, Coordinate coord) { Arrays.copyOf(current.tris, current.tris.length); - this.set(state, x, y); + this.currentTurn = current.currentTurn; + this.play(coord.x, coord.y); } /** @@ -44,24 +68,119 @@ public class Tris { * @return il valore della cella */ public State get(int x, int y) { - if(x >= LENGTH) throw new IndexOutOfBoundsException(); - return this.tris[y * LENGTH + x]; + return this.tris[this.index(x, y)]; } /** - * Permette di mettere lo stato scelto nella cella specificata dalle coordinate. - * Nel caso in cui lo stato scelto dalle coordinate non risulti EMPTY - * il metodo lancerà una eccezione. - * @param state lo stato da impostare + * Permette di far avanzare il gioco, facendo giocare il turno al giocatore corrente nella cella specificata dalle coordinate. + * Nel caso in cui lo stato scelto dalle coordinate non risulti EMPTY il metodo lancerà una eccezione. + * Una volta che si ha un vincitore questo metodo non potrà essere piu chiamato e lancerà una eccezione. + * * @param x la coordinata x in cui mettere lo stato * @param y la coordinata y in cui mettere lo stato + * @throws UnsupportedOperationException nel caso in cui si ha già avuto un vincitore */ - public void set(State state, int x, int y) { - if(x >= LENGTH) throw new IndexOutOfBoundsException(); - var index = y * LENGTH + x; + public void play(int x, int y) { + if(this.haveWinner() != State.EMPTY) throw new UnsupportedOperationException("The game has already finished!"); + if(!isPlayAvailable(x, y)) throw new IllegalArgumentException("The state to modify must be Empty!"); - if(this.tris[index] != State.EMPTY) - throw new IllegalArgumentException(); - this.tris[index] = Objects.requireNonNull(state); + this.tris[this.index(x, y)] = this.currentTurn; + this.currentTurn = switch(this.currentTurn) { + case VALUE_X -> State.VALUE_O; + case VALUE_O -> State.VALUE_X; + default -> State.EMPTY; + }; + } + + /** + * Indica se è possibile giocare nella cella indicata. + * Questo metodo non controlla che il gioco sia finito, ma controlla solamente se la mossa + * indicata nei parametri è legale. + * + * @param x la coordinata X + * @param y la coordinata Y + * @return vero se la mossa può essere giocata, altrimenti falso + */ + public boolean isPlayAvailable(int x, int y) { + return this.tris[this.index(x, y)] == State.EMPTY; + } + + /** + * Permette di avere una lista di tutte le coordinate per le mosse possibili. + * Le coordinate sono incapsulate dentro un oggetto Coordintate in cui l'elemento + * X corrsiponde alla coordinata X, mentre l'elemento Y corrisponde alla coordianta Y. + * Nel caso in cui il gioco sia completato, la lista risultante sarà vuota. + * + * @return un array di coordinate disponibili per giocare. + */ + public Coordinate[] availablePlays() { + var count = 0; + for(var i = 0; i < this.tris.length; i++) + if(this.tris[i] == State.EMPTY) + count += 1; + + var res = new Coordinate[count]; + count = 0; + + for(var y = 0; y < LENGTH; y++) + for(var x = 0; x < LENGTH; x++) + if(isPlayAvailable(x, y)) + res[count++] = new Coordinate(x, y); + + return res; + } + + /** + * Indica se si ha un vincitore e restituisce chi ha vinto. + * @return EMPTY se non c'è ancora un vincitore, altrimenti restituisci il vincitore + */ + public State haveWinner() { + // top left corner -> horizontal and vertical + var state = this.tris[0]; + if(state != State.EMPTY) { + if(this.tris[1] == state && this.tris[2] == state) return state; + if(this.tris[3] == state && this.tris[6] == state) return state; + } + + // bottom right corner -> horizontal and vertical + state = this.tris[8]; + if(state != State.EMPTY) { + if(this.tris[7] == state && this.tris[6] == state) return state; + if(this.tris[5] == state && this.tris[2] == state) return state; + } + + // central -> diagonals, horizontal and vertical + state = this.tris[4]; + if(state != State.EMPTY) { + if(this.tris[0] == state && this.tris[8] == state) return state; + if(this.tris[6] == state && this.tris[2] == state) return state; + if(this.tris[3] == state && this.tris[5] == state) return state; + if(this.tris[1] == state && this.tris[7] == state) return state; + } + + return State.EMPTY; + } + + /** + * Calcola l'indice trasformato da 2D a 1D. + * Questo metodo serve internamente per la classe. + * Nel caso in cui si inseriscano delle coordinate non valide lancia una eccezione. + * + * @param x il valore della coordinata X + * @param y il valore della coordinata Y + * @return il valore risultante + */ + private int index(int x, int y) { + if(x >= LENGTH || y >= LENGTH) throw new IndexOutOfBoundsException(); + return x + y * LENGTH; + } + + @Override + public Iterator iterator() { + return new Iterator() { + int current = 0; + @Override public boolean hasNext() { return current < tris.length; } + @Override public State next() { return tris[current++]; } + }; } } diff --git a/src/test/java/net/berack/upo/ai/problem2/TestTris.java b/src/test/java/net/berack/upo/ai/problem2/TestTris.java new file mode 100644 index 0000000..219e6af --- /dev/null +++ b/src/test/java/net/berack/upo/ai/problem2/TestTris.java @@ -0,0 +1,160 @@ +package net.berack.upo.ai.problem2; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; + +import static net.berack.upo.ai.problem2.Tris.State.*; + +import org.junit.jupiter.api.Test; + +import net.berack.upo.ai.problem2.Tris.State; + +public class TestTris { + + @Test + public void testConstructor() { + var tris = new Tris(); + for(var state : tris) assertEquals(EMPTY, state); + } + + @Test + public void testPlay() { + var tris = new Tris(); + tris.play(0, 0); + + var i = 0; + var states = new Tris.State[] {VALUE_X, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY}; + for(var state : tris) assertEquals(states[i++], state); + + tris.play(2, 2); + states[8] = VALUE_O; + i = 0; + for(var state : tris) assertEquals(states[i++], state); + + tris.play(1, 1); + states[4] = VALUE_X; + i = 0; + for(var state : tris) assertEquals(states[i++], state); + + tris.play(0, 1); + states[3] = VALUE_O; + i = 0; + for(var state : tris) assertEquals(states[i++], state); + + tris.play(1, 0); + states[1] = VALUE_X; + i = 0; + for(var state : tris) assertEquals(states[i++], state); + + tris.play(0, 2); + states[6] = VALUE_O; + i = 0; + for(var state : tris) assertEquals(states[i++], state); + } + + @Test + public void testAvailableActions() { + var tris = new Tris(); + var actions = tris.availablePlays(); + + assertEquals(9, actions.length); + for(var i = 0; i < actions.length -2; i++) { + + for(var j = 0; j < actions.length; j++) { + var curr = actions[j]; + assertEquals(j >= i, tris.isPlayAvailable(curr.x, curr.y)); + } + + var array = Arrays.copyOfRange(actions, i, actions.length); + var avail = tris.availablePlays(); + assertArrayEquals(array, avail, "Array differ at iteration " + i); + + var curr = actions[i]; + tris.play(curr.x, curr.y); + } + } + + @Test + public void testPlayException() { + var tris = new Tris(); + tris.play(0, 0); + + assertThrows(IllegalArgumentException.class, () -> tris.play(0, 0)); + + tris.play(1, 1); + assertThrows(IllegalArgumentException.class, () -> tris.play(0, 0)); + assertThrows(IllegalArgumentException.class, () -> tris.play(1, 1)); + + tris.play(0, 2); + assertThrows(IllegalArgumentException.class, () -> tris.play(0, 0)); + assertThrows(IllegalArgumentException.class, () -> tris.play(1, 1)); + assertThrows(IllegalArgumentException.class, () -> tris.play(0, 2)); + + tris.play(2, 1); + assertThrows(IllegalArgumentException.class, () -> tris.play(0, 0)); + assertThrows(IllegalArgumentException.class, () -> tris.play(1, 1)); + assertThrows(IllegalArgumentException.class, () -> tris.play(0, 2)); + assertThrows(IllegalArgumentException.class, () -> tris.play(2, 1)); + + tris.play(0, 1); + for(var i = 0; i < Tris.LENGTH; i++) + for(var j = 0; j < Tris.LENGTH; j++) { + final var x = i; + final var y = j; + assertThrows(UnsupportedOperationException.class, () -> tris.play(x, y)); + } + } + + @Test + public void testWinner() { + + // horizontal 1 line X + var tris = new Tris(); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(1,0); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(1,1); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(0,0); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(1,2); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(2,0); + assertTrue(tris.haveWinner() == State.VALUE_X); + + // diagonal \ O + tris = new Tris(); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(2,1); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(1,1); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(2,0); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(2,2); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(1,2); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(0,0); + assertTrue(tris.haveWinner() == State.VALUE_O); + + // vertical 2 column X + tris = new Tris(); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(1,0); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(0,2); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(1,1); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(0,1); + assertTrue(tris.haveWinner() == State.EMPTY); + tris.play(1,2); + assertTrue(tris.haveWinner() == State.VALUE_X); + } + +}