diff --git a/.gitignore b/.gitignore index 596a358..8c7305d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin/ doc/ +out/ deliverable/ .classpath .project diff --git a/src/net/hoo2/auth/labyrinth/Board.java b/src/net/hoo2/auth/labyrinth/Board.java index 6d1eac1..9db272c 100644 --- a/src/net/hoo2/auth/labyrinth/Board.java +++ b/src/net/hoo2/auth/labyrinth/Board.java @@ -7,6 +7,7 @@ package net.hoo2.auth.labyrinth; +import java.util.ArrayList; import java.util.function.IntFunction; /** @@ -24,26 +25,27 @@ class Board { * The empty constructor for default initialization */ Board() { - this.N = this.S = this.W = 0; - this.tiles = null; - this.supplies =null; + this.N = 0; + this.S = 0; + this.W = 0; + tiles = null; + supplies =null; + walls = new ArrayList(); } /** * 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 - * @param W The number of walls on the board */ - Board(int N, int S, int W) { - assert (W>= 4*N-1 && W<=(3*N*N+1)/2 ) - : "Boards walls has to be in the range [4N-1, (3N^2+1)/2]"; - + Board(int N, int S) { + assert (N%2 != 0) : "Board's size has to be an odd number."; this.N = Session.boardSize = N; this.S = S; - this.W = W; + this.W = 0; tiles = new Tile[N*N]; supplies = new Supply[S]; + walls = new ArrayList(); } /** @@ -67,6 +69,7 @@ class Board { // Clone arrays this.tiles = b.tiles.clone(); this.supplies = b.supplies.clone(); + this.walls = b.walls; } /** @} */ @@ -79,7 +82,7 @@ class Board { * @param theseusTile * @param minotaurTile */ - void createBoard(int theseusTile, int minotaurTile) throws Exception { + void createBoard(int theseusTile, int minotaurTile) { createTiles(); createSupplies(theseusTile, minotaurTile); } @@ -220,6 +223,15 @@ class Board { void setSupplies(Supply[] supplies) { this.supplies= supplies; } /** @} */ + + /** @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 */ @@ -228,11 +240,11 @@ class Board { /** * This function creates randomly all the tiles of the board */ - private void createTiles() throws Exception { - int wallPool = W; - wallPool -= createBasicTileWalls (); // First create tiles with outer walls - if (createInnerWalls(wallPool) > 0) // Create inner walls with the rest of the requested walls - throw new Exception("Can not create the requested number of walls"); + 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; } /** @@ -254,13 +266,43 @@ class Board { } } - /** @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); } - /** @} */ + /** + * Predicate to check if a wall creates a closed room. + * + * This algorithm has a complexity of O(N^2) where N represents the total + * number of tiles it should be used with care. + * + * @param tileId The tileId of the wall where the wall is. + * @param direction The wall's relative direction from the tile. + * @return True if the wall creates a closed room, false otherwise. + */ + private boolean createsClosedRoom (int tileId, int direction) { + // Get a snapshot of the current walls + ArrayList w = new ArrayList(); + for (Edge it : walls) + w.add(new Edge(it)); + // Create a graph from the current wall(edge) + // and populate the graph with all the edges we can attach. + Graph g = new Graph(new Edge(tileId, direction)); + int size; + do { + size = w.size(); + 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`. @@ -270,6 +312,7 @@ class Board { *
  • 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 @@ -286,12 +329,22 @@ class Board { if (tiles[tileId].hasWall(direction)) return false; switch (direction) { - case Direction.UP: return (tiles[upTileId.apply(tileId)].hasWalls() < Const.maxTileWalls); - case Direction.DOWN: return (tiles[downTileId.apply(tileId)].hasWalls() < Const.maxTileWalls); - case Direction.LEFT: return (tiles[leftTileId.apply(tileId)].hasWalls() < Const.maxTileWalls); - case Direction.RIGHT:return (tiles[rightTileId.apply(tileId)].hasWalls() < Const.maxTileWalls); + 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; } - return false; + if (Session.loopGuard && createsClosedRoom(tileId, direction)) + return false; + return true; } /** @@ -326,16 +379,22 @@ class Board { * @return The number of walls created from the utility. */ private int createBasicTileWalls () { - int tileCount =0; + 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); - tileCount += ((up?1:0) + (down?1:0) + (left?1:0) + (right?1:0)); + wallCount += ((up?1:0) + (down?1:0) + (left?1:0) + (right?1:0)); tiles[i] = new Tile (i, up, down, left, right); + 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 tileCount; + return wallCount; } /** @@ -352,6 +411,8 @@ class Board { Position neighbor = new Position(Position.toRow(tileId), Position.toCol(tileId), dir); tiles[tileId].setWall(dir); tiles[neighbor.getId()].setWall(Direction.opposite(dir)); + if (Session.loopGuard) + walls.add(new Edge(tileId, dir)); } /** @@ -360,23 +421,23 @@ class Board { * @param walls The number of walls to create * @return The number of walls failed to create. */ - private int createInnerWalls (int walls) { + private int createInnerWalls () { ShuffledRange randTiles = new ShuffledRange(0, N*N); - for (int tileId, i =0, shuffleMark =0 ; i walls; /**< + * Array to hold all the walls using the edge representation + * required by the closed room preventing algorithm. + */ /** @} */ } diff --git a/src/net/hoo2/auth/labyrinth/Common.java b/src/net/hoo2/auth/labyrinth/Common.java index 794aec6..cbf87c1 100644 --- a/src/net/hoo2/auth/labyrinth/Common.java +++ b/src/net/hoo2/auth/labyrinth/Common.java @@ -21,9 +21,11 @@ class Const { * Application wide object to hold settings like values for the session. */ class Session { - static int boardSize = 15; /**< Default board's size (if no one set it via command line) */ - static int supplySize = 4; /**< Default board's supply size (if no one set it via command line) */ - static int wallSize = 4*15-1; /**< Default board's wall size (if no one set it via command line) */ + static int boardSize = 15; /**< Default board's size (if no one set it via command line) */ + static int supplySize = 4; /**< Default board's supply size (if no one set it via command line) */ + static int maxRounds = 100; /**< Default number of rounds per game (if no one set it via command line) */ + static boolean loopGuard = false; /**< When true a wall creation guard is added to prevent closed rooms inside the board */ + static boolean interactive = false; /**< When true each round of the game requires user input */ } /** @@ -209,3 +211,179 @@ class ShuffledRange extends Range { Collections.shuffle(numbers); } } + +/** + * @brief + * A utility class used for room prevent algorithm. + * + * This class is the wall representation we use in the room preventing algorithm. + * In this algorithm we represent the crosses between tiles as nodes (V) of a graph and the + * walls as edges. So for example: + * + * _ V = 15 + * / + * +---+---+---+ We have a 4x4=16 vertices board(nodes) and 14 edges(walls). + * | | To represent the vertices on the board we use the + * + +---+ + same trick as the tileId. + * | | | The edges are represented as vertices pairs. + * + + + + <. + * | | | \_ V = 7 + * + +---+---+ + * ^ ^ + * V = 0 V = 3 + * + * @note + * Beside the fact that we prefer this kind of representation of the walls in + * the application we use the one that is suggested from the assignment. This is + * used only in room preventing algorithm. + * @note + * Using this kind of representation we don't have any more the "problem" + * of setting the wall in both neighbor tiles. + */ +class Edge { + /** + * This constructor as as the interface between the application's wall + * representation and the one based on graph. + * @param tileId The tile id of the wall. + * @param direction The direction of the tile where the wall should be. + */ + Edge(int tileId, int direction) { + int N = Session.boardSize +1; + switch (direction) { + case Direction.UP: + v1= (Position.toRow(tileId) + 1)*N + Position.toCol(tileId); + v2= (Position.toRow(tileId) + 1)*N + Position.toCol(tileId) + 1; + break; + case Direction.DOWN: + v1= (Position.toRow(tileId))*N + Position.toCol(tileId); + v2= (Position.toRow(tileId))*N + Position.toCol(tileId) + 1; + break; + case Direction.LEFT: + v1= (Position.toRow(tileId))*N + Position.toCol(tileId); + v2= (Position.toRow(tileId) + 1)*N + Position.toCol(tileId); + break; + case Direction.RIGHT: + v1= (Position.toRow(tileId))*N + Position.toCol(tileId) + 1; + v2= (Position.toRow(tileId) + 1)*N + Position.toCol(tileId) +1; + break; + } + } + /** A deep copy contructor */ + Edge(Edge e) { + v1 = e.getV1(); + v2 = e.getV2(); + } + /** Access of the first node of the edge */ + int getV1() { return v1; } + /** Access of the second node of the edge */ + int getV2() { return v2; } + + private int v1; /**< First vertex of the edge */ + private int v2; /**< Second vertex of the edge */ +} + +/** + * @brief + * Provides a graph functionality for the room preventing algorithm. + * We use a graph to represent the wall structure of the walls. This way + * is easy to find any closed loops. Using graph we transform the problem + * of the closed room in the problem of finding a non simple graph. + * + * If the board has non connected wall structure then we need more than + * one graph to represent it. + * + * An example graph from a board, starting from V=1 is: + *
    + *    6---7   8                (1)
    + *    |       |               /  \
    + *    3   4   5             (4)  (2)
    + *    |   |   |                    \
    + *    0   1---2                    (5)--(8)
    + * 
    + */ +class Graph { + /** + * Constructs a node of the graph using the value of a vertex(node). + * @param v The verteg to attach. + */ + Graph (int v) { + V = v; + E = new ArrayList(); + } + /** + * Constructor that transform an edge into graph. + * @param e The edge to transform. + */ + Graph (Edge e) { + V = e.getV1(); + E = new ArrayList(); + E.add(new Graph(e.getV2())); + } + + /** Access to the current vertex */ + int getV() { return V; } + /** Access to the links of the current vertex */ + ArrayList getE() { return E; } + + /** + * Attach an edge into a graph IFF the graph already has a vertex + * with the same value of one of the vertices of the edge. + * @param e The edge to attach. + * @return The status of the operation. + * @arg True on success + * @arg False on failure + */ + boolean attach (Edge e) { + return tryAttach(e, 0) > 0; + } + + /** + * Counts the number of vertices on the graph with the value of `v` + * @param v The vertex to count + * @return The number of vertices with value `v` + */ + int count (int v) { + return tryCount (v, 0); + } + + /** + * Recursive algorithm that tries to attach an edge into a graph + * IFF the graph already has a vertex. + * with the same value of one of the vertices of the edge. + * @param e The edge to attach. + * @param count An initial count value to feed to the algorithm. + * @return The status of the operation. + * @arg True on success + * @arg False on failure + */ + private int tryAttach (Edge e, int count) { + for (Graph n: E) + count += n.tryAttach (e, count); + if (V == e.getV1()) { + E.add(new Graph(e.getV2())); + ++count; + } + if (V == e.getV2()) { + E.add(new Graph(e.getV1())); + ++count; + } + return count; + } + + /** + * Recursive algorithm that tries to count the number of vertices + * on the graph with the value of `v` + * @param v The vertex to count + * @param count An initial count value to feed to the algorithm. + * @return The number of vertices with value `v` + */ + private int tryCount (int v, int count) { + for (Graph n: E) + count = n.tryCount (v, count); + if (V == v) + return ++count; + return count; + } + private int V; /**< The value of the current vertex/node */ + private ArrayList E; /**< A list of all the child nodes */ +} diff --git a/src/net/hoo2/auth/labyrinth/Game.java b/src/net/hoo2/auth/labyrinth/Game.java index 7842a88..f273bd6 100644 --- a/src/net/hoo2/auth/labyrinth/Game.java +++ b/src/net/hoo2/auth/labyrinth/Game.java @@ -68,33 +68,44 @@ public class Game { Session.boardSize = Integer.parseInt(args[++i]); break; - case "-w": - case "--walls": - if (i+1 < args.length) - Session.wallSize = Integer.parseInt(args[++i]); - break; - case "-s": case "--suplies": if (i+1 < args.length) Session.supplySize = Integer.parseInt(args[++i]); break; + case "-r": + case "--rounds": + if (i+1 < args.length) + Session.maxRounds = Integer.parseInt(args[++i]); + break; + + case "--norooms": + Session.loopGuard = true; + break; + + case "-i": + case "--interactive": + Session.interactive = true; + break; + default: case "-h": case "--help": System.out.println("Labyrinth Game"); System.out.println(""); System.out.println("Usage:"); - System.out.println("labyrinth [-b|--board ] [-w|--walls ] [-s|--supplies ]"); + System.out.println("labyrinth [-b|--board ] [-s|--supplies ] [-r|--rounds ] [--norooms] [-i|--interactive]"); System.out.println("or"); System.out.println("labyrinth -h|--help"); - System.out.println(""); - System.out.println("\t-b | --board: Sets the size of board's edge."); - System.out.println("\t-w | --walls: Sets the number of walls on the board."); - System.out.println("\t-s | --supplies: Sets the number of supplies on the board."); - System.out.println("\t-h | --help: Print this and exit"); - break; + System.out.println("\nOptions\n"); + System.out.println("-b | --board:\n Sets the size of board's edge.\n"); + System.out.println("-s | --supplies:\n Sets the number of supplies on the board.\n"); + System.out.println("-r | --rounds:\n Sets the maximum number of rounds of the game.\n"); + System.out.println("--norooms:\n Prevents the creation of closed rooms inside the board.\n"); + System.out.println("-i | --interactive:\n Each round requires user input in order to continue.\n"); + System.out.println("-h | --help:\n Print this and exits."); + return false; } } return true; @@ -111,9 +122,9 @@ public class Game { // Create a game, a board and 2 players. Game game = new Game(); - Board board = new Board(Session.boardSize, Session.supplySize, Session.wallSize); - Player T = new Player(1, "Theseus", board, 0); - Player M = new Player(2, "Minotaur", board, Position.toID(Session.boardSize/2, Session.boardSize/2)); + Board board = new Board(Session.boardSize, Session.supplySize); + Player T = new Player(1, "Theseus", true, board, 0); + Player M = new Player(2, "Minotaur", false, board, Position.toID(Session.boardSize/2, Session.boardSize/2)); // Populate data to the board board.createBoard(T.playerTileId(), M.playerTileId());