Board.java

package fr.umontpellier.iut.conquest;

import java.util.ArrayList;
import java.util.List;

import fr.umontpellier.iut.conquest.board.Square;
import fr.umontpellier.iut.conquest.board.memento.BoardMemento;

/**
 * Modélise un plateau.
 */
public class Board {

    /**
     * Tableau des pions.
     */
    private Pawn[][] field;

    /**
     * Constructeur.
     *
     * @param size : la taille du plateau.
     */
    public Board(int size) {
        field = new Pawn[size][size];
    }

    /**
     * Constructeur pour Test.
     *
     * @param field : plateau prédéfini.
     */
    public Board(Pawn[][] field) {
        this.field = field;
    }

    /**
     * Les méthodes suivantes sont utilisées pour les tests automatiques. Il ne faut pas les utiliser.
     */
    public Pawn[][] getField() {
        return field;
    }

    /**
     * Retourne la taille du plateau.
     */
    public int getSize() {
        return field.length;
    }

    /**
     * Affiche le plateau.
     */
    public String toString() {
        int size = field.length;
        StringBuilder b = new StringBuilder();
        for (int r = -1; r < size; r++) {
            for (int c = -1; c < size; c++) {
                if (r == -1 && c == -1) {
                    b.append("_");
                } else if (r == -1) {
                    b.append("_").append(c);
                } else if (c == -1) {
                    b.append(r).append("|");
                } else if (field[r][c] == null) {
                    b.append("_ ");
                } else if (field[r][c].getPlayer().getColor() == 1) {
                    b.append("X ");
                } else {
                    b.append("O ");
                }
            }
            b.append("\n");
        }
        b.append("---");
        return b.toString();
    }

    /**
     * Initialise le plateau avec les pions de départ.
     * Rappel :
     * - player1 commence le jeu avec un pion en haut à gauche (0,0) et un pion en bas à droite.
     * - player2 commence le jeu avec un pion en haut à droite et un pion en bas à gauche.
     */
    public void initField(Player player1, Player player2) {
        initPawnsFirstPlayer(player1);
        initPawnsSecondPlayer(player2);
    }

    private void initPawnsFirstPlayer(Player player1) {
        field[0][0] = new Pawn(player1);
        field[field.length - 1][field.length - 1] = new Pawn(player1);
    }

    private void initPawnsSecondPlayer(Player player2) {
        field[0][field.length - 1] = new Pawn(player2);
        field[field.length - 1][0] = new Pawn(player2);
    }

    /**
     * Vérifie si un coup est valide.
     * Rappel :
     * - Les coordonnées du coup doivent être dans le plateau.
     * - Le pion bougé doit appartenir au joueur.
     * - La case d'arrivée doit être libre.
     * - La distance entre la case d'arrivée est au plus 2.
     */
    public boolean isValid(Move move, Player player) {
        if (move != null) {
            Square startingSquare = new Square(move.getRow1(), move.getColumn1());
            Square arrivalSquare = new Square(move.getRow2(), move.getColumn2());

            return coordinatesIsIntoField(move) && isValidPlayer(startingSquare, player)
                    && isValidArrivalSquare(arrivalSquare) && isValidDistance(move);
        }
        return false;
    }

    /**
     * Déplace un pion.
     *
     * @param move : un coup valide.
     *             Rappel :
     *             - Si le pion se déplace à distance 1 alors il se duplique pour remplir la case d'arrivée et la case de départ.
     *             - Si le pion se déplace à distance 2 alors il ne se duplique pas : la case de départ est maintenant vide et la case d'arrivée remplie.
     *             - Dans tous les cas, une fois que le pion est déplacé, tous les pions se trouvant dans les cases adjacentes à sa case d'arrivée prennent sa couleur.
     */
    public void movePawn(Move move) {
        int startingColumn = move.getColumn1();
        int startingRow = move.getRow1();

        Player actualPlayer = field[startingRow][startingColumn].getPlayer();

        Square arrivalSquare = new Square(move.getRow2(), move.getColumn2());

        // on remplie la case d'arrivée
        colorPawnOfSquare(actualPlayer, arrivalSquare);

        // on vide la case de départ s'il a fait une distance supérieur à 1
        if (!distanceIsRespected(move, 1))
            field[startingRow][startingColumn] = null;

        // on colorie les pions autour de la case d'arrivée
        colorAroundPawnsOfSquare(arrivalSquare);
    }

    /**
     * Retourne la liste de tous les coups valides de player.
     * S'il n'y a de coup valide, retourne une liste vide.
     */
    public List<Move> getValidMoves(Player player) {
        List<Move> validMoves = new ArrayList<>();

        // on parcourt toutes les cases
        for (int startingRow = 0; startingRow < field.length; startingRow++) {
            for (int startingColumn = 0; startingColumn < field.length; startingColumn++) {

                // on vérifie si c'est un pion qui appartient au joeur actuel
                if (isValidPlayer(field[startingRow][startingColumn], player))
                    appendValidMovesAroundStartingSquare(validMoves, new Square(startingRow, startingColumn));
            }
        }
        return validMoves;
    }

    /**
     * Retourne le nombre de pions d'un joueur.
     */
    public int getNbPawns(Player player) {
        int sumPawnsPlayer = 0;

        for (Pawn[] pawnsInRow : field)
            for (Pawn pawn : pawnsInRow)
                if (isValidPlayer(pawn, player))
                    sumPawnsPlayer++;

        return sumPawnsPlayer;
    }

    public BoardMemento saveToMemento() {
        return new BoardMemento(this.field);
    }
    
    public void undoFromMemento(BoardMemento memento) {
        this.field = memento.getField();
    }

    // #region Méthodes privé (boite à outils pour méthode public)

    // #region Outils méthode isValid

    // #region coordinatesIsIntoField
    private boolean coordinatesIsIntoField(Move move) {
        Square startingSquare = new Square(move.getRow1(), move.getColumn1());
        Square arrivalSquare = new Square(move.getRow2(), move.getColumn2());

        return isIntoField(startingSquare) && isIntoField(arrivalSquare);
    }

    private boolean isIntoField(Square square) {
        return isIntoField(square.getRow()) && isIntoField(square.getColumn());
    }

    private boolean isIntoField(int coordinate) {
        return 0 <= coordinate && coordinate < field.length;
    }
    // #endregion coordinatesIsIntoField

    // isValidPlayer
    private boolean isValidPlayer(Square startingSquare, Player player) {
        Pawn startingPawn = field[startingSquare.getRow()][startingSquare.getColumn()];

        return isValidPlayer(startingPawn, player);
    }

    // isValidArrivalSquare
    private boolean isValidArrivalSquare(Square arrivalSquare) {
        Pawn arrivalCase = field[arrivalSquare.getRow()][arrivalSquare.getColumn()];

        return arrivalCase == null;
    }

    // #region isValidDistance
    private boolean isValidDistance(Move move) {
        final int AUTHORIZED_DISTANCE = 2;
        return distanceIsRespected(move, AUTHORIZED_DISTANCE);
    }

    private boolean distanceIsRespected(Move move, final int AUTHORIZED_DISTANCE) {
        int startingRow = move.getRow1();
        int arrivalRow = move.getRow2();
        int startingColumn = move.getColumn1();
        int arrivalColumn = move.getColumn2();

        return distanceIsRespected(AUTHORIZED_DISTANCE, startingRow, arrivalRow)
                && distanceIsRespected(AUTHORIZED_DISTANCE, startingColumn, arrivalColumn);
    }

    private boolean distanceIsRespected(final int AUTHORIZED_DISTANCE, int startingDistance, int arrivalDistance) {
        int distance = Math.abs(startingDistance - arrivalDistance);

        return distance <= AUTHORIZED_DISTANCE;
    }
    // #endregion isValidDistance
    // #endregion Outils méthode isValid

    // #region Outils méthode movePawn
    private void colorAroundPawnsOfSquare(Square arrivalSquare) {

        int arrivalRow = arrivalSquare.getRow();
        int arrivalColumn = arrivalSquare.getColumn();

        Player actualPlayer = field[arrivalRow][arrivalColumn].getPlayer();
        int playerColor = actualPlayer.getColor();

        final int MIN_COLUMN = minus1IntoField(arrivalColumn);
        final int MAX_COLUMN = plus1IntoField(arrivalColumn);

        final int MIN_ROW = minus1IntoField(arrivalRow);
        final int MAX_ROW = plus1IntoField(arrivalRow);

        // on parcours les cases autour de la case d'arrivée
        for (int aroundRow = MIN_ROW; aroundRow <= MAX_ROW; aroundRow++) {
            for (int aroundColumn = MIN_COLUMN; aroundColumn <= MAX_COLUMN; aroundColumn++) {

                // on vérifier si c'est un pion qui peut être colorié
                if (canColorWithPlayerColor(playerColor, field[aroundRow][aroundColumn]))
                    colorPawnOfSquare(actualPlayer, new Square(aroundRow, aroundColumn));
            }
        }
    }

    private void colorPawnOfSquare(Player owner, Square aroundSquare) {
        field[aroundSquare.getRow()][aroundSquare.getColumn()] = new Pawn(owner);
    }

    private boolean canColorWithPlayerColor(int playerColor, Pawn targetedPawn) {
        return targetedPawn != null && targetedPawn.getPlayer().getColor() != playerColor;
    }

    private int plus1IntoField(int coordinate) {
        final int ADDER = 1;
        return plusIntoField(coordinate, ADDER);
    }

    private int minus1IntoField(int coordinate) {
        final int SUBTRACTOR = 1;
        return minusIntoField(coordinate, SUBTRACTOR);
    }
    // #endregion Outils méthode movePawn

    // #region Outils méthode getValidMoves
    private boolean isValidPlayer(Pawn startingPawn, Player player) {
        int playerColor = player.getColor();
        boolean pawnNotNull = startingPawn != null;

        return pawnNotNull && (startingPawn.getPlayer().getColor() == playerColor);
    }

    private void appendValidMovesAroundStartingSquare(List<Move> validMoves, Square startingSquare) {
        int startingRow = startingSquare.getRow();
        int startingColumn = startingSquare.getColumn();

        final int MIN_ROW = minus2IntoField(startingRow);
        final int MAX_ROW = plus2IntoField(startingRow);

        final int MIN_COLUMN = minus2IntoField(startingColumn);
        final int MAX_COLUMN = plus2IntoField(startingColumn);

        // on parcourt toutes les cases autour de la case d'arrivée
        for (int aroundRow = MIN_ROW; aroundRow <= MAX_ROW; aroundRow++) {
            for (int aroundColumn = MIN_COLUMN; aroundColumn <= MAX_COLUMN; aroundColumn++) {

                // on vérifie si c'est une case où l'on peut jouer
                if (canPlayInSquare(new Square(aroundRow, aroundColumn)))
                    validMoves.add(new Move(startingRow, startingColumn, aroundRow, aroundColumn));
            }
        }
    }

    private boolean canPlayInSquare(Square targetedSquare) {
        return field[targetedSquare.getRow()][targetedSquare.getColumn()] == null;
    }

    private int plus2IntoField(int coordinate) {
        final int ADDER = 2;
        return plusIntoField(coordinate, ADDER);
    }

    private int minus2IntoField(int coordinate) {
        final int SUBTRACTOR = 2;
        return minusIntoField(coordinate, SUBTRACTOR);
    }
    // #endregion Outils méthode getValidMoves

    private int minusIntoField(int coordinate, final int SUBTRACTOR) {
        final int MINIMUM_FIELD = 0;
        int minimumCoordinate = (coordinate - SUBTRACTOR);
        return (minimumCoordinate < MINIMUM_FIELD) ? MINIMUM_FIELD : minimumCoordinate;
    }

    private int plusIntoField(int coordinate, final int ADDER) {
        final int MAXIMUM_FIELD = field.length - 1;
        int maximumCoordinate = coordinate + ADDER;
        return (maximumCoordinate > MAXIMUM_FIELD) ? MAXIMUM_FIELD : maximumCoordinate;
    }
    // #endregion Méthodes privé (boite à outils pour méthode public)
}