/** * @file Board.java * * @author * Anastasia Foti AEM:8959 * * * @author * Christos Choutouridis AEM:8997 * */ package host.labyrinth; import java.util.ArrayList; import java.util.function.IntFunction; /** * @brief * This class is the representation of the games's board * * The board is the square arrangement of the tiles. This class is also * the owner of the tile and supply objects. */ class Board { /** @name Constructors */ /** @{ */ /** * The empty constructor for default initialization */ Board() { this.N = 0; this.S = 0; this.W = 0; tiles = null; supplies = null; walls = new ArrayList(); moves = new int[Const.numOfPlayers][Player.MOVE_DATA_SIZE]; playerCount =0; } /** * The main constructor for the application * @param N The size of each edge of the board * @param S The number of supplies on the board */ Board(int N, int S) { assert (N%2 != 0) : "Board's size has to be an odd number."; assert (S <= (N*N-2)) : "At least 2 tiles has to be without supplies."; this.N = Session.boardSize = N; this.S = S; this.W = 0; tiles = new Tile[N*N]; supplies = new Supply[S]; walls = new ArrayList(); moves = new int[Const.numOfPlayers][Player.MOVE_DATA_SIZE]; playerCount =0; } /** * Deep copy constructor * @param b The board to copy * * @note * The lack of value semantics in java is (in author's opinion) one of the greatest * weakness of the language and one of the reasons why it will never be a language * to care about. To quote Alexander Stepanof's words in "elements of programming" section 1.5: * "Assignment is a procedure that takes two objects of the same type and makes the first * object equal to the second without modifying the second". * In this class we try to cope with this situation knowing that we can not do anything about * assignment operator. We just add value semantics to the copy constructor and go on with our lifes... */ Board(Board b) { // Copy primitives this.N = b.N; this.S = b.S; this.W = b.W; tiles = new Tile[b.tiles.length]; supplies = new Supply[b.supplies.length]; walls = new ArrayList(); moves = new int[Const.numOfPlayers][Player.MOVE_DATA_SIZE]; playerCount =b.playerCount; // clone moves array of array of primitives for (int i=0 ; i=0 ; --i) { for (String it : sBoard[i]) System.out.print(it); System.out.println(); } } /** * Predicate to check if a direction is Walkable. * * A `walkable` direction is a tile direction where: *
    *
  • The wall is not the DOWN wall from tile (0, 0). *
  • There is not already a wall in the desired direction. (Implies no sentinel tile). *
* * @param tileId The starting tileId. * @param direction The desired direction. * @return True if it is walkable. */ boolean isWalkable(int tileId, int direction) { return !tiles[tileId].hasWall(direction) && !(tileId == 0 && direction == Direction.DOWN); } /** * Predicate to check if a direction is Walkable. * * A `walkable` direction is a tile direction where: *
    *
  • The wall is not the DOWN wall from tile (0, 0). *
  • There is not already a wall in the desired direction. (Implies no sentinel tile). *
* * @param row Row position of the starting tile. * @param col Column position of the starting tile. * @param direction The desired direction. * @return True if it is walkable. */ boolean isWalkable(int row, int col, int direction) { return !tiles[Position.toID(row, col)].hasWall(direction) && !(Position.toID(row, col) == 0 && direction == Direction.DOWN); } /** * Utility function to check if there is a supply on the tile or not * @param tileId The tile to check * @return Yes/no */ boolean hasSupply (int tileId) { return (Const.noSupply != tiles[tileId].hasSupply(supplies)) ? true : false; } /** * Try to pick supply from a tile. If succeed it also erases the * supply from the board. * * @param tileId The tile to check * @return The id of supply. * @arg Const.noSupply if there is none * @arg The ID of supply if there is one. */ int tryPickSupply(int tileId) { int supplyId = tiles[tileId].hasSupply(supplies); if (supplyId != Const.noSupply) { tiles[tileId].pickSupply(supplies, supplyId); } return supplyId; } /** * A plain fair dice functionality provided by the board. * @return A random direction; */ int dice () { ShuffledRange d = new ShuffledRange(DirRange.Begin, DirRange.End, DirRange.Step); return d.get(); } /** @return the size of each site of the board. */ int size () { return N; } /** * Utility function to create player IDs * @return The generated player id. */ int generatePlayerId () throws Exception { if (playerCount < Const.numOfPlayers) return playerCount++; else throw new Exception("Maximum number of players exceeded"); } /** * Boards utility to give access to other player Id. * * @param playerId The id of player who asks * @return The other player's Id. */ int getOpponentId(int playerId) { return Const.numOfPlayers - (playerId +1); } /** * Boards utility to give access to other player moves. * * @param playerId The id of player who asks * @return The moves data of other player */ int[] getOpponentMove (int playerId) { return moves[getOpponentId(playerId)]; } /** * Utility to update the moves of each player. * * This function is used by the players to update their position on the board. * After that a player can read other player positions using getOpponentMoves() * @see getOpponentMoves() * * @param m Reference to new move data * @param playerId The id of the player who update his/her data. */ void updateMove(int[] m, int playerId) { //moves.set(playerId, Arrays.stream(m).boxed().toArray(Integer[]::new)); moves[playerId] = m; } /** @} */ /** * @name Accessor/Mutator interface * @note * Please consider not to use mutator interface. Its the abstraction killer :( */ /** @{ */ int getN() { return N; } int getS() { return S; } int getW() { return W; } /** * @note Use it with care. Any use of this function results to what Sean Parent calls "incidental data-structure". * see also here * @return Reference to inner tiles array. */ Tile[] getTiles() { return tiles; } /** * @note Use it with care. Any use of this function results to what Sean Parent calls "incidental data-structure". * see also here * @return Reference to inner supplies array. */ Supply[] getSupplies() { return supplies; } /** * @note Use it with care. Any use of this function results to what Sean Parent calls "incidental data-structure". * see also here * @return Reference to inner walls array. */ ArrayList getWalls() { return walls; } /** * @note Use it with care. Any use of this function results to what Sean Parent calls "incidental data-structure". * see also here * @return Reference to inner walls array. */ int[][] getMoves() { return moves; } void setN(int N) { this.N = N; } void setS(int S) { this.S = S; } void setW(int W) { this.W = W; } /** * @param tiles Reference to tiles that we want to act as replacement for the inner tiles array. * @note Use with care. * Any call to this function will probably add memory for the garbage collector. */ void setTiles(Tile[] tiles) { this.tiles = tiles; } /** * @param supplies Reference to supplies that we want to act as replacement for the inner supplies array. * @note Use with care. * Any call to this function will probably add memory for the garbage collector. */ void setSupplies(Supply[] supplies) { this.supplies= supplies; } /** * @param walls Reference to walls that we want to act as replacement for the inner walls vector. * @note Use with care. * Any call to this function will probably add memory for the garbage collector. */ void setWalls (ArrayList walls) { this.walls= walls; } /** * @param moves Reference to moves that we want to act as replacement for the inner moves vector. * @note Use with care. * Any call to this function will probably add memory for the garbage collector. */ void setMoves(int[][] moves) { this.moves =moves; } /** @} */ /** @name Sentinel predicates */ /** @{ */ private boolean isLeftSentinel (int tileId) { return (Position.toCol(tileId) == 0); } private boolean isRightSentinel (int tileId) { return (Position.toCol(tileId) == N-1); } private boolean isUpSentinel (int tileId) { return (Position.toRow(tileId) == N-1); } private boolean isDownSentinel (int tileId) { return (Position.toRow(tileId) == 0); } /** @} */ /** * @name private functionality of the object */ /** @{ */ /** * This function creates randomly all the tiles of the board */ private void createTiles() { int wallCount; wallCount = createBasicTileWalls (); // First create tiles with outer walls wallCount += createInnerWalls(); // Greedy create as many inner walls we can W = wallCount; } /** * This function create randomly the board's supplies. * * The supplies has to be in separate tiles and in tiles with no player * * @param theseusTile The tile of the Theseus * @param minotaurTile The tile of the Minotaur */ private void createSupplies(int theseusTile, int minotaurTile) { ShuffledRange rand = new ShuffledRange(0, N*N); // Make a shuffled range of all tiles for (int tileId, i=0 ; i w = new ArrayList(); for (Edge it : walls) w.add(new Edge(it)); // Create the largest possible coherent graph from the list of walls(edges) Graph g = new Graph(new Edge(tileId, direction)); int size; do { size = w.size(); // mark the size (before the pass) for (int i =0, S=w.size() ; i 1) return true; if (g.count(it.getV2()) > 1) return true; } return false; } /** * Predicate to check if a tile direction is `Wallable`. * * A `wallable` direction is a tile direction where: *
    *
  • The wall is not the DOWN wall from tile (0, 0). *
  • There is not already a wall in the desired direction. (Implies no sentinel tile). *
  • The neighbor in this direction has at most `Const.maxTileWalls -1` walls. *
  • The wall does not create a closed room (Optional requirement). *
* * @note * A wallable direction automatically implies that the direction in not an outer wall. * * @param tileId The tile to check. * @param direction The direction to check * @return True if the direction is wallable. */ private boolean isWallableDir (int tileId, int direction) { // Check list if (!isWalkable(tileId, direction)) return false; switch (direction) { case Direction.UP: if (tiles[upTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls) return false; break; case Direction.DOWN: if (tiles[downTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls) return false; break; case Direction.LEFT: if (tiles[leftTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls) return false; break; case Direction.RIGHT: if (tiles[rightTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls) return false; break; } if (Session.loopGuard && isRoomCreator(tileId, direction)) return false; return true; } /** * Predicate to check if a tile is `Wallable`. * * A `wallable` tile is a tile where: *
    *
  • The tile has at most `Const.maxTileWalls -1` walls. *
  • There is at least one wallable direction on the tile. *
* @param tileId The tile to check * @return True if the tile is wallable. */ private boolean isWallable (int tileId) { // Check list if (tileId == Const.noTileId) return false; if (tiles[tileId].hasWalls() >= Const.maxTileWalls) return false; Range dirs = new Range(DirRange.Begin, DirRange.End, DirRange.Step); for (int dir = dirs.get() ; dir != Const.EOR ; dir = dirs.get()) if (isWallableDir(tileId, dir)) return true; return false; } /** * This utility function create/allocate the tiles of the board and create * the outer walls at the same time. * * @return The number of walls created from the utility. */ private int createBasicTileWalls () { int wallCount =0; for (int i =0 ; i< tiles.length ; ++i) { boolean up = isUpSentinel(i); boolean down = isDownSentinel(i) && (i != 0); boolean left = isLeftSentinel(i); boolean right = isRightSentinel(i); wallCount += ((up?1:0) + (down?1:0) + (left?1:0) + (right?1:0)); tiles[i] = new Tile (i, up, down, left, right); // If we have loopGuard enable we populate walls also. if (Session.loopGuard) { if (up) walls.add(new Edge(i, Direction.UP)); if (down) walls.add(new Edge(i, Direction.DOWN)); if (left) walls.add(new Edge(i, Direction.LEFT)); if (right) walls.add(new Edge(i, Direction.RIGHT)); } } return wallCount; } /** * Create randomly a wall in the wallable selected tile. * @param tileId The wallable tile to create the wall */ private void createInnerWall(int tileId) { // Randomly pick a wallable direction in that tile. ShuffledRange randDirections = new ShuffledRange(DirRange.Begin, DirRange.End, DirRange.Step); int dir; do dir = randDirections.get(); while (!isWallableDir(tileId, dir)); // Add wall to tileId and the adjacent tileId Position neighbor = new Position(Position.toRow(tileId), Position.toCol(tileId), dir); tiles[tileId].setWall(dir); tiles[neighbor.getId()].setWall(Direction.opposite(dir)); // If we have loopGuard enable we populate walls also. if (Session.loopGuard) walls.add(new Edge(tileId, dir)); } /** * This utility creates the inner walls of the board. * * @return The number of walls failed to create. */ private int createInnerWalls () { ShuffledRange randTiles = new ShuffledRange(0, N*N); for (int tileId, walls =0, shuffleMark =0 ; true ; ) { // randomly pick a wallable tile. do { if ((tileId = randTiles.get())== Const.EOR) { if (walls == shuffleMark) // Wallable tiles exhausted. return walls; else { // Re-shuffle and continue. randTiles = new ShuffledRange(0, N*N); shuffleMark =walls; } } } while (!isWallable(tileId)); ++walls; createInnerWall(tileId); } } /** * Utility to get the body (center line) of the string representation of the tile. * * @param row What board's row to get. * @param col What board's column to get. * @param theseusTile The current tile of the Theseus. * @param minotaurTile The current tile of the Minotaur. * @return The body string */ private String getTileBody (int row, int col, int theseusTile, int minotaurTile) { int tileId = Position.toID(row, col); boolean T = (tileId == theseusTile) ? true : false; boolean M = (tileId == minotaurTile) ? true : false; int S = tiles[tileId].hasSupply(supplies); if (T && !M) return " T "; else if (T && M) return "T+M"; else if (M) { if (S == Const.noSupply) return " M "; else return "M+s"; } else if (S != Const.noSupply) return String.format("s%02d", S+1); else return " "; } /** * Utility to render the 3 strings of the tile in the representation frame. * * @param frame Reference to the frame to print into. * @param row The board's row to print. * @param col The board's column to print. * @param theseusTile The current tile of the Theseus. * @param minotaurTile The current tile of the Minotaur. */ private void renderTile(String[][] frame, int row, int col, int theseusTile, int minotaurTile) { IntFunction toframe = (r)->{ return 2*r+1; }; int tileId = Position.toID(row, col); frame[toframe.apply(row)+1][col] = tiles[tileId].hasWall(Direction.UP) ? "+---" : "+ "; frame[toframe.apply(row) ][col] = (tiles[tileId].hasWall(Direction.LEFT)? "|" : " ") + getTileBody(row, col, theseusTile, minotaurTile); frame[toframe.apply(row)-1][col] = tiles[tileId].hasWall(Direction.DOWN) ? "+---" : "+ "; } /** * Utility to render the 3 strings of the tile in the representation frame in * the case the tile lies in the east wall. We call these tiles `sentinel tiles` * * @param frame Reference to the frame to print into. * @param row The board's row to print. * @param col The board's column to print. * @param theseusTile The current tile of the Theseus. * @param minotaurTile The current tile of the Minotaur. */ private void renderSentinelTile(String[][] frame, int row, int col, int theseusTile, int minotaurTile ) { IntFunction toframe = (r)->{ return 2*r+1; }; int tileId = Position.toID(row, col); frame[toframe.apply(row)+1][col] = tiles[tileId].hasWall(Direction.UP) ? "+---+" : "+ +"; frame[toframe.apply(row) ][col] = (tiles[tileId].hasWall(Direction.LEFT)? "|" : " ") + getTileBody(row, col, theseusTile, minotaurTile) + (tiles[tileId].hasWall(Direction.RIGHT)? "|" : " "); frame[toframe.apply(row)-1][col] = tiles[tileId].hasWall(Direction.DOWN) ? "+---+" : "+ +"; } /** @} */ /** @name Neighbor access lambdas */ /** @{ */ private IntFunction leftTileId = (id) -> { return Position.toID(Position.toRow(id), Position.toCol(id)-1); }; private IntFunction rightTileId = (id) -> { return Position.toID(Position.toRow(id), Position.toCol(id)+1); }; private IntFunction upTileId = (id) -> { return Position.toID(Position.toRow(id)+1, Position.toCol(id) ); }; private IntFunction downTileId = (id) -> { return Position.toID(Position.toRow(id)-1, Position.toCol(id) ); }; /** @} */ /** @name Class data */ /** @{ */ private int N; /**< The size of each edge of the board */ private int S; /**< The number of the supplies on the board */ private int W; /**< The number of walls on the board */ private Tile[] tiles; /**< Array to hold all the tiles for the board */ private Supply[] supplies; /**< Array to hold all the supplies on the board */ private ArrayList walls; /**< * Array to hold all the walls using the edge representation * required by the closed room preventing algorithm. */ private int[][] moves; private int playerCount; /** @} */ }