Coverage Summary for Class: Board (it.polimi.ingsw.Server.Model)

Class Class, % Method, % Line, %
Board 100% (1/1) 100% (20/20) 83,8% (155/185)


 package it.polimi.ingsw.Server.Model;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.annotations.Expose;
 import it.polimi.ingsw.Common.BoardInterface;
 import it.polimi.ingsw.Common.GsonOptionalSerializer;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystemNotFoundException;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.nio.file.spi.FileSystemProvider;
 import java.rmi.RemoteException;
 import java.rmi.server.UnicastRemoteObject;
 import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 /**
  * Game's Board
  */
 public class Board extends UnicastRemoteObject implements BoardInterface {
     /**
      * Maximum X dimension of the board
      */
     public static final int BOARD_DIM_X = 9;
     /**
      * Maximum Y dimension of the board
      */
     public static final int BOARD_DIM_Y = 9;
     /**
      * Game's minimum number of players
      */
     private final int MINPLAYERS = 2;
     /**
      * Game's maximum number of players
      */
     private final int MAXPLAYERS = 4;
     /**
      * Board Spaces implementation
      */
     @Expose
     private BoardSpace[][] spaces;
     /**
      * Object Cards extracted in the related game
      */
     @Expose(serialize = false, deserialize = true)
     private List<ObjectCard> objectCards = null;
     /**
      * List of the most recent Object Cards taken
      */
     @Expose(serialize = false, deserialize = true)
     private Map<ObjectCard, BoardSpace> lastTaken = null;
 
     /**
      * Manages board creation and initialization, based on pre-built files indicating usable spaces and restrictions related to the number of players.
      *
      * @param playerCount number of players in the game
      * @param objectCards List of extracted Object Cards
      * @throws FileNotFoundException related to file management. In the default environment, necessary files are found in the "/res/model/" folder of the project.
      */
     public Board(int playerCount, List<ObjectCard> objectCards) throws Exception {
         if (playerCount < MINPLAYERS || playerCount > MAXPLAYERS)
             throw new Exception();
 
         spacesDeclaration(playerCount);
 
         // Initialize utilization tracking for Object Cards
         this.objectCards = Objects.requireNonNullElseGet(objectCards, ArrayList::new);
 
         lastTaken = new HashMap<>();
 
         // default reset
         reset();
     }
 
     /**
      * Related to Game's refresh strategy
      */
     private Board(BoardSpace[][] spaces, List<ObjectCard> objectCards, Map<ObjectCard, BoardSpace> lastTaken) throws RemoteException {
         this.spaces = spaces;
         this.objectCards = objectCards;
         this.lastTaken = lastTaken;
     }
 
     /**
      * Takes care of creating the Board Spaces using JSON declaration files
      *
      * @param playerCount number of players in the game
      * @throws URISyntaxException related to resources locators
      * @throws IOException        related to file management
      */
     private void spacesDeclaration(int playerCount) throws URISyntaxException, IOException {
         // JSON Parser
         Gson gson = new GsonBuilder()
                 .excludeFieldsWithoutExposeAnnotation()
                 .registerTypeAdapter(Optional.class, new GsonOptionalSerializer<>())
                 .create();
 
         // resource locator
         URI uri = ClassLoader.getSystemResource("model/board/"
                 .concat(String.valueOf(playerCount))
                 .concat(".json")).toURI();
 
         // FileSystem support structure, related to JAR file structure
         FileSystem fs = null;
         if ("jar".equals(uri.getScheme())) {
             for (FileSystemProvider provider : FileSystemProvider.installedProviders()) {
                 if (provider.getScheme().equalsIgnoreCase("jar")) {
                     try {
                         fs = provider.getFileSystem(uri);
                     } catch (FileSystemNotFoundException e) {
                         // creates a temporary File System for artifacts file scheme
                         fs = provider.newFileSystem(uri, Collections.emptyMap());
                     }
                 }
             }
         }
 
         // create spaces
         String res;
         try (Stream<String> stream = Files.lines(Paths.get(uri), StandardCharsets.UTF_8)) {
             res = stream.map(Object::toString)
                     .collect(Collectors.joining());
         }
 
         // close FileSystem
         if (fs != null) fs.close();
 
         this.spaces = gson.fromJson(res, BoardSpace[][].class);
     }
 
     /**
      * Returns a copy of the spaces array via functional approach.
      *
      * @return copy of spaces
      */
     public synchronized BoardSpace[][] getSpaces() {
         return Arrays.stream(spaces)
                 .map(BoardSpace[]::clone)
                 .toArray(BoardSpace[][]::new);
     }
 
     /**
      * Checks if a space is usable given coordinates
      *
      * @param x coordinate
      * @param y coordinate
      * @return usability of that space
      * @throws Exception if parameters are trying to go out of the board
      */
     public synchronized boolean isSpaceUsable(int x, int y) throws Exception {
         if (x >= 0 && x < BOARD_DIM_X && y >= 0 && y < BOARD_DIM_Y)
             return spaces[x][y].isUsable();
         else throw new Exception();
     }
 
     /**
      * Returns Object Card as an instance (not as Optional.of(card))
      *
      * @param x coordinate
      * @param y coordinate
      * @return the card
      * @throws Exception if parameters are trying to go out of the board
      */
     public synchronized ObjectCard getPlainCardFromSpace(int x, int y) throws Exception {
         if (x >= 0 && x < BOARD_DIM_X && y >= 0 && y < BOARD_DIM_Y)
             return spaces[x][y].getPlainCard();
         else throw new Exception();
     }
 
     /**
      * Returns Object Card's type ordinal, given a Board Space
      *
      * @param x coordinate
      * @param y coordinate
      * @return the card's ordinal
      * @throws Exception if parameters are trying to go out of the board
      */
     public synchronized int getCardOrdinalFromSpace(int x, int y) throws Exception {
         if (x >= 0 && x <= BOARD_DIM_X - 1 && y >= 0 && y <= BOARD_DIM_Y - 1)
             if (spaces[x][y].getCard().isPresent())
                 return spaces[x][y].getCard().get().getType().ordinal();
             else return -1;
         else throw new Exception();
     }
 
     /**
      * Returns Object Card's image path, given a Board Space
      *
      * @param x coordinate
      * @param y coordinate
      * @return the card's image path
      * @throws Exception if parameters are trying to go out of the board
      */
     public synchronized String getCardImageFromSpace(int x, int y) throws Exception {
         if (x >= 0 && x <= BOARD_DIM_X - 1 && y >= 0 && y <= BOARD_DIM_Y - 1)
             if (spaces[x][y].getCard().isPresent())
                 return spaces[x][y].getCard().get().getType().name().concat("-").concat(String.valueOf(spaces[x][y].getCard().get().getImage()));
             else return null;
         else throw new Exception();
     }
 
     /**
      * Returns a list of usable and effectively present cards on the board, based on a functional approach using Optionals.
      *
      * @return list of cards on the board
      */
     public synchronized List<ObjectCard> boardCards() {
         return Stream.of(spaces)
                 .flatMap(Stream::of)
                 .filter(s -> s.isUsable() && s.getCard().isPresent())
                 .map(s -> s.getCard().orElseThrow())
                 .collect(Collectors.toList());
     }
 
     /**
      * Checks if a reset of the board is needed, watching at usable and effectively present spaces
      *
      * @return boolean: reset of the board is compulsory to continue the game
      */
     synchronized boolean resetNeeded() {
         if (boardCards().isEmpty())
             return true;
         for (int i = 0; i < BOARD_DIM_Y; i++) {
             for (int j = 0; j < BOARD_DIM_Y; j++) {
                 if (spaces[i][j].isUsable() && spaces[i][j].getCard().isPresent()) {
                     if (i >= 1)
                         if (spaces[i - 1][j].isUsable() && spaces[i - 1][j].getCard().isPresent())
                             return false;
                     if (i <= BOARD_DIM_Y - 2)
                         if (spaces[i + 1][j].isUsable() && spaces[i + 1][j].getCard().isPresent())
                             return false;
                     if (j >= 1)
                         if (spaces[i][j - 1].isUsable() && spaces[i][j - 1].getCard().isPresent())
                             return false;
                     if (j <= BOARD_DIM_X - 2)
                         if (spaces[i][j + 1].isUsable() && spaces[i][j + 1].getCard().isPresent())
                             return false;
                 }
             }
         }
         return true;
     }
 
     /**
      * Checks the condition for enabling reset on the board (even if not all cards are put on it)
      *
      * @return condition for reset enabling
      */
     private synchronized boolean resetEnabled() {
         return objectCards.size() < ObjectCard.LIMIT;
     }
 
     /**
      * Implements reset logic. The same approach is used in the construction of the board.
      */
     private synchronized void reset() {
         if (resetNeeded() && resetEnabled()) {
             for (int i = 0; i < BOARD_DIM_X; i++) {
                 for (int j = 0; j < BOARD_DIM_Y; j++) {
                     if (spaces[i][j].isUsable() && spaces[i][j].getCard().isEmpty()) {
                         try {
                             spaces[i][j].setCard(Optional.of(new ObjectCard(objectCards)));
                         } catch (Exception ignored) {
 
                         }
                     }
                 }
             }
         }
     }
 
     /**
      * Checks if, given a list of sequentially ordered cards, every card has a "free side".
      * VERSION 2: checks for every card rather than the group
      *
      * @param list of sequentially ordered cards (indexes on the board can be disordered)
      * @return whether if a move is permitted
      */
     public synchronized boolean valid(List<ObjectCard> list) {
         if (list == null || list.isEmpty() || list.size() > 3 || list.contains(null))
             return false;
         if (!boardCards().containsAll(list))
             return false;
 
         List<Integer> x = new ArrayList<>();
         List<Integer> y = new ArrayList<>();
         int counter = 0;
 
         // finds cards on the board
         for (int i = 0; i < BOARD_DIM_Y; i++) {
             for (int j = 0; j < BOARD_DIM_Y; j++) {
                 if (spaces[i][j].isUsable() && spaces[i][j].getCard().isPresent()) {
                     if (counter < list.size() && list.contains(spaces[i][j].getCard().get())) {
                         x.add(i);
                         y.add(j);
                         counter++;
                     }
                 }
             }
         }
         if (x.size() != list.size() || y.size() != list.size())
             return false;
 
         // cards are on the same row
         if (x.stream().distinct().count() == 1) {
             Collections.sort(y);
             if (list.size() == 3) {
                 if (!(y.get(2).equals(y.get(1) + 1) && y.get(1).equals(y.get(0) + 1))) {
                     return false;
                 }
             } else if (list.size() == 2) {
                 if (!(y.get(1).equals(y.get(0) + 1))) {
                     return false;
                 }
             }
         }
 
         // cards are on the same column
         if (y.stream().distinct().count() == 1) {
             Collections.sort(x);
             if (list.size() == 3) {
                 if (!(x.get(2).equals(x.get(1) + 1) && x.get(1).equals(x.get(0) + 1))) {
                     return false;
                 }
             } else if (list.size() == 2) {
                 if (!(x.get(1).equals(x.get(0) + 1))) {
                     return false;
                 }
             }
         }
 
         counter = 0;
         // order independent check of free sides
         for (int i = 0; i < x.stream().distinct().count(); i++) {
             for (int j = 0; j < y.stream().distinct().count(); j++) {
                 if (x.get(i) == 0 || x.get(i) == BOARD_DIM_X - 1) {
                     counter++;
                     continue;
                 }
                 if (y.get(j) == 0 || y.get(j) == BOARD_DIM_Y - 1) {
                     counter++;
                     continue;
                 }
                 if (x.get(i) >= 1)
                     if (spaces[x.get(i) - 1][y.get(j)].getCard().isEmpty()) {
                         counter++;
                         continue;
                     }
                 if (x.get(i) <= BOARD_DIM_Y - 2)
                     if (spaces[x.get(i) + 1][y.get(j)].getCard().isEmpty()) {
                         counter++;
                         continue;
                     }
                 if (x.get(j) >= 1)
                     if (spaces[x.get(i)][y.get(j) - 1].getCard().isEmpty()) {
                         counter++;
                         continue;
                     }
                 if (x.get(j) <= BOARD_DIM_X - 2)
                     if (spaces[x.get(i)][y.get(j) + 1].getCard().isEmpty()) {
                         counter++;
                         continue;
                     }
             }
         }
 
         return counter == list.size();
     }
 
     /**
      * Checks if a group of couples of coordinates is a valid move on the board
      *
      * @param x list ofcoordinates
      * @param y list of coordinates
      * @return move validation
      * @throws Exception to manage board dimension limits
      */
     public synchronized boolean validFromCoordinate(List<Integer> x, List<Integer> y) throws Exception {
         if (x == null || y == null || x.isEmpty() || y.isEmpty() || x.contains(null) || y.contains(null))
             return false;
         if (x.size() != y.size())
             return false;
         List<ObjectCard> arg = getCardsFromCoordinate(x, y);
         return valid(arg);
     }
 
     /**
      * Returns a list of cards contained in a group of coordinates
      *
      * @param x list of coordinates
      * @param y list of coordinates
      * @return the list of cards
      * @throws Exception to manage board dimension limits
      */
     public synchronized List<ObjectCard> getCardsFromCoordinate(List<Integer> x, List<Integer> y) throws Exception {
         if (x == null || y == null || x.isEmpty() || y.isEmpty() || x.contains(null) || y.contains(null))
             throw new Exception();
         if (x.size() != y.size())
             throw new Exception();
         List<ObjectCard> ret = new ArrayList<>();
         for (int i = 0; i < x.size(); i++) {
             ret.add(getPlainCardFromSpace(x.get(i), y.get(i)));
         }
         return ret;
     }
 
     /**
      * Removes from board a list of ordered cards (only valid moves are accepted)
      *
      * @param orderedCards list of ordered cards (also if not internally)
      * @throws Exception if an invalid deletion is tried
      */
     public synchronized void takeFromBoard(List<ObjectCard> orderedCards) throws Exception {
         if (!valid(orderedCards))
             throw new Exception();
         if (!boardCards().containsAll(orderedCards))
             throw new Exception();
 
         lastTaken.clear();
         for (int i = 0; i < BOARD_DIM_Y; i++) {
             for (int j = 0; j < BOARD_DIM_Y; j++) {
                 if (spaces[i][j].getCard().isPresent() && orderedCards.contains(spaces[i][j].getCard().get())) {
                     lastTaken.put(spaces[i][j].getCard().get(), spaces[i][j]);
                     spaces[i][j].setCard(Optional.empty());
                 }
             }
         }
         if (!lastTaken.isEmpty()) {
             reset();
         }
     }
 
     /**
      * Removes from board a group of cards based on their coordinates (only valid moves are accepted)
      *
      * @param x list of coordinates
      * @param y list of coordinates
      * @return action validity
      * @throws Exception if an invalid deletion is tried
      */
     public synchronized List<ObjectCard> takeFromBoardFromCoordinate(List<Integer> x, List<Integer> y) throws Exception {
         if (x == null || y == null || x.isEmpty() || y.isEmpty())
             throw new Exception();
         if (x.size() != y.size())
             throw new Exception();
         List<ObjectCard> arg = getCardsFromCoordinate(x, y);
         takeFromBoard(arg);
         return arg;
     }
 
     /**
      * Restores the last move (group of cards taken from board)
      *
      * @throws Exception if a restore is tried before any take has happened
      */
     public synchronized void restoreLastTaken() throws Exception {
         if (lastTaken == null || lastTaken.isEmpty())
             throw new Exception();
         for (ObjectCard oc : lastTaken.keySet()) {
             spaces[lastTaken.get(oc).getX()][lastTaken.get(oc).getY()].setCard(Optional.of(oc));
         }
         lastTaken.clear();
     }
 
     /**
      * Refreshed copy, after server reload from file
      *
      * @return new copy of the object
      * @throws RemoteException related to RMI
      */
     public Board getCopy() throws RemoteException {
         return new Board(this.spaces, this.objectCards, this.lastTaken);
     }
 
     /**
      * @return extracted Object Cards list
      */
     public List<ObjectCard> getObjectCards() {
         return objectCards;
     }
 }