You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

531 lines
20 KiB

  1. /**
  2. * @file Board.java
  3. *
  4. * @author Christos Choutouridis AEM:8997
  5. * @email cchoutou@ece.auth.gr
  6. */
  7. package net.hoo2.auth.labyrinth;
  8. import java.util.ArrayList;
  9. import java.util.function.IntFunction;
  10. /**
  11. * @brief
  12. * This class is the representation of the games's board
  13. *
  14. * The board is the square arrangement of the tiles. This class is also
  15. * the owner of the tile and supply objects.
  16. */
  17. class Board {
  18. /** @name Constructors */
  19. /** @{ */
  20. /**
  21. * The empty constructor for default initialization
  22. */
  23. Board() {
  24. this.N = 0;
  25. this.S = 0;
  26. this.W = 0;
  27. tiles = null;
  28. supplies =null;
  29. walls = new ArrayList<Edge>();
  30. }
  31. /**
  32. * The main constructor for the application
  33. * @param N The size of each edge of the board
  34. * @param S The number of supplies on the board
  35. */
  36. Board(int N, int S) {
  37. assert (N%2 != 0) : "Board's size has to be an odd number.";
  38. this.N = Session.boardSize = N;
  39. this.S = S;
  40. this.W = 0;
  41. tiles = new Tile[N*N];
  42. supplies = new Supply[S];
  43. walls = new ArrayList<Edge>();
  44. }
  45. /**
  46. * Deep copy constructor
  47. * @param b The board to copy
  48. *
  49. * @note
  50. * The lack of value semantics in java is (in author's opinion) one of the greatest
  51. * weakness of the language and one of the reasons why it will never be a language
  52. * to care about. To quote Alexander Stepanof's words in "elements of programming" section 1.5:
  53. * "Assignment is a procedure that takes two objects of the same type and makes the first
  54. * object equal to the second without modifying the second".
  55. * In this class we try to cope with this situation knowing that we can not do anything about
  56. * assignment operator. We just add value semantics to the copy constructor and go on with our lifes...
  57. */
  58. Board(Board b) {
  59. // Copy primitives
  60. this.N = b.N;
  61. this.S = b.S;
  62. this.W = b.W;
  63. // Clone arrays
  64. this.tiles = b.tiles.clone();
  65. this.supplies = b.supplies.clone();
  66. this.walls = b.walls;
  67. }
  68. /** @} */
  69. /** @name Board's main application interface */
  70. /** @{ */
  71. /**
  72. * Creates the board with all the requested walls and supplies.
  73. *
  74. * @param theseusTile
  75. * @param minotaurTile
  76. */
  77. void createBoard(int theseusTile, int minotaurTile) {
  78. createTiles();
  79. createSupplies(theseusTile, minotaurTile);
  80. }
  81. /**
  82. * Returns a 2-D array with the string representation of the board.
  83. *
  84. * The rows of the array represent the Y-coordinate and the columns the X-coordinate.
  85. * The only difference is that between each row there is an extra row with the possible
  86. * walls. This way the number of rows of the returning array are 2N+1 and the number of
  87. * columns N+1.\n
  88. * So each tile of the board is represented by 3 strings. One for the north wall, one for
  89. * the body and one for the south wall.
  90. *
  91. * @param theseusTile The current Theseus tile
  92. * @param minotaurTile The current Minotaur tile
  93. * @return The string representation of the board
  94. */
  95. String[][] getStringRepresentation(int theseusTile, int minotaurTile) {
  96. String[][] frame = new String[2*N+1][N];
  97. for (int row=0 ; row<N ; ++row) {
  98. int col;
  99. for (col =0 ; col<N-1 ; ++col)
  100. renderTile(frame, row, col, theseusTile, minotaurTile);
  101. renderSentinelTile(frame, row, col, theseusTile, minotaurTile);
  102. }
  103. return frame;
  104. }
  105. /**
  106. * Print board utility.
  107. * @param sBoard Reference to string representation of the board to print.
  108. *
  109. * @note
  110. * As the lower row addresses of the string representation of the board contain
  111. * the south rows, in order to view the board correctly we have to print the rows
  112. * in the opposite order.
  113. */
  114. void printBoard (String[][] sBoard) {
  115. for (int i=sBoard.length-1 ; i>=0 ; --i) {
  116. for (String it : sBoard[i])
  117. System.out.print(it);
  118. System.out.println();
  119. }
  120. }
  121. /**
  122. * Predicate to check if a direction is Walkable
  123. * @param tileId The starting tileId.
  124. * @param direction The desired direction.
  125. * @return True if it is walkable.
  126. */
  127. boolean isWalkable(int tileId, int direction) {
  128. return !tiles[tileId].hasWall(direction)
  129. && !(tileId == 0 && direction == Direction.DOWN);
  130. }
  131. /**
  132. * Predicate to check if a direction is Walkable
  133. * @param row Row position of the starting tile.
  134. * @param column Column position of the starting tile.
  135. * @param direction The desired direction.
  136. * @return True if it is walkable.
  137. */
  138. boolean isWalkable(int row, int col, int direction) {
  139. return !tiles[Position.toID(row, col)].hasWall(direction)
  140. && !(Position.toID(row, col) == 0 && direction == Direction.DOWN);
  141. }
  142. /**
  143. * Try to pick supply from a tile. If succeed it also erases the
  144. * supply from the board.
  145. *
  146. * @param tileId The tile to check
  147. * @return The id of supply.
  148. * @arg Const.noSupply if there is none
  149. * @arg The ID of supply if there is one.
  150. */
  151. int tryPickSupply(int tileId) {
  152. int supplyId = tiles[tileId].hasSupply(supplies);
  153. if (supplyId != Const.noSupply) {
  154. tiles[tileId].pickSupply(supplies, supplyId);
  155. }
  156. return supplyId;
  157. }
  158. /**
  159. * A plain fair dice functionality provided by the board.
  160. * @return A random direction;
  161. */
  162. int dice () {
  163. ShuffledRange d = new ShuffledRange(Direction.Begin, Direction.End, Direction.Step);
  164. return d.get();
  165. }
  166. /** @return the size of each site of the board. */
  167. int size () { return N; }
  168. /** @} */
  169. /**
  170. * @name Accessor/Mutator interface
  171. * @note
  172. * Please consider not to use mutator interface. Its the abstraction killer :(
  173. */
  174. /** @{ */
  175. int getN() { return N; }
  176. int getS() { return S; }
  177. int getW() { return W; }
  178. /**
  179. * @note Use it with care. Any use of this function results to what Sean Parent calls "incidental data-structure".
  180. * <a href="https://github.com/sean-parent/sean-parent.github.io/blob/master/better-code/03-data-structures.md"> see also here</a>
  181. * @return Reference to inner tiles array.
  182. */
  183. Tile[] getTiles() { return tiles; }
  184. /**
  185. * @note Use it with care. Any use of this function results to what Sean Parent calls "incidental data-structure".
  186. * <a href="https://github.com/sean-parent/sean-parent.github.io/blob/master/better-code/03-data-structures.md"> see also here</a>
  187. * @return Reference to inner supplies array.
  188. */
  189. Supply[] getSupplies() { return supplies; }
  190. void setN(int N) { this.N = N; }
  191. void setS(int S) { this.S = S; }
  192. void setW(int W) { this.W = W; }
  193. /**
  194. * @param tiles Reference to tiles that we want to act as replacement for the inner tiles array.
  195. * @note Use with care.
  196. * Any call to this function will probably add memory for the garbage collector.
  197. */
  198. void setTiles(Tile[] tiles) { this.tiles = tiles; }
  199. /**
  200. * @param supplies Reference to supplies that we want to act as replacement for the inner supplies array.
  201. * @note Use with care.
  202. * Any call to this function will probably add memory for the garbage collector.
  203. */
  204. void setSupplies(Supply[] supplies) { this.supplies= supplies; }
  205. /** @} */
  206. /** @name Sentinel predicates */
  207. /** @{ */
  208. private boolean isLeftSentinel (int tileId) { return (Position.toCol(tileId) == 0); }
  209. private boolean isRightSentinel (int tileId) { return (Position.toCol(tileId) == N-1); }
  210. private boolean isUpSentinel (int tileId) { return (Position.toRow(tileId) == N-1); }
  211. private boolean isDownSentinel (int tileId) { return (Position.toRow(tileId) == 0); }
  212. /** @} */
  213. /**
  214. * @name private functionality of the object
  215. */
  216. /** @{ */
  217. /**
  218. * This function creates randomly all the tiles of the board
  219. */
  220. private void createTiles() {
  221. int wallCount;
  222. wallCount = createBasicTileWalls (); // First create tiles with outer walls
  223. wallCount += createInnerWalls(); // Greedy create as many inner walls we can
  224. W = wallCount;
  225. }
  226. /**
  227. * This function create randomly the board's supplies.
  228. *
  229. * The supplies has to be in separate tiles and in tiles with no player
  230. *
  231. * @param theseusTile The tile of the Theseus
  232. * @param minotaurTile The tile of the Minotaur
  233. */
  234. private void createSupplies(int theseusTile, int minotaurTile) {
  235. ShuffledRange rand = new ShuffledRange(0, N*N); // Make a shuffled range of all tiles
  236. for (int tileId, i=0 ; i<supplies.length ; ++i) {
  237. // Pick a tile as long as there is no player in it
  238. do
  239. tileId = rand.get();
  240. while (tileId == theseusTile || tileId == minotaurTile);
  241. supplies[i] = new Supply(i, tileId);
  242. }
  243. }
  244. /**
  245. * Predicate to check if a wall creates a closed room.
  246. *
  247. * This algorithm has a complexity of O(N^2) where N represents the total
  248. * number of tiles it should be used with care.
  249. *
  250. * @param tileId The tileId of the wall where the wall is.
  251. * @param direction The wall's relative direction from the tile.
  252. * @return True if the wall creates a closed room, false otherwise.
  253. */
  254. private boolean createsClosedRoom (int tileId, int direction) {
  255. // Get a snapshot of the current walls
  256. ArrayList<Edge> w = new ArrayList<Edge>();
  257. for (Edge it : walls)
  258. w.add(new Edge(it));
  259. // Create a graph from the current wall(edge)
  260. // and populate the graph with all the edges we can attach.
  261. Graph g = new Graph(new Edge(tileId, direction));
  262. int size;
  263. do {
  264. size = w.size();
  265. for (int i =0, S=w.size() ; i<S ; ++i)
  266. if (g.attach(w.get(i))) {
  267. w.remove(i);
  268. --i; --S;
  269. }
  270. } while (size != w.size());
  271. // Search if a vertex is attached more than once.
  272. // This means that there is at least 2 links to the same node
  273. // so the graph has a closed loop
  274. for (Edge it : walls) {
  275. if (g.count(it.getV1()) > 1) return true;
  276. if (g.count(it.getV2()) > 1) return true;
  277. }
  278. return false;
  279. }
  280. /**
  281. * Predicate to check if a tile direction is `Wallable`.
  282. *
  283. * A `wallable` direction is a tile direction where:
  284. * <ul>
  285. * <li>The wall is not the DOWN wall from tile (0, 0).
  286. * <li>There is not already a wall in the desired direction. (Implies no sentinel tile).
  287. * <li>The neighbor in this direction has at most `Const.maxTileWalls -1` walls.
  288. * <li>The wall does not create a closed room (Optional requirement).
  289. * </ul>
  290. *
  291. * @note
  292. * A wallable direction automatically implies that the direction in not an outer wall.
  293. *
  294. * @param tileId The tile to check.
  295. * @param direction The direction to check
  296. * @return True if the direction is wallable.
  297. */
  298. private boolean isWallableDir (int tileId, int direction) {
  299. // Check list
  300. if (tileId == 0 && direction == Direction.DOWN)
  301. return false;
  302. if (tiles[tileId].hasWall(direction))
  303. return false;
  304. switch (direction) {
  305. case Direction.UP:
  306. if (tiles[upTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls) return false;
  307. break;
  308. case Direction.DOWN:
  309. if (tiles[downTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls) return false;
  310. break;
  311. case Direction.LEFT:
  312. if (tiles[leftTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls) return false;
  313. break;
  314. case Direction.RIGHT:
  315. if (tiles[rightTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls) return false;
  316. break;
  317. }
  318. if (Session.loopGuard && createsClosedRoom(tileId, direction))
  319. return false;
  320. return true;
  321. }
  322. /**
  323. * Predicate to check if a tile is `Wallable`.
  324. *
  325. * A `wallable` tile is a tile where:
  326. * <ul>
  327. * <li>The tile has at most `Const.maxTileWalls -1` walls.
  328. * <li>There is at least one wallable direction on the tile.
  329. * </ul>
  330. * @param tileId The tile to check
  331. * @return True if the tile is wallable.
  332. */
  333. private boolean isWallable (int tileId) {
  334. // Check list
  335. if (tileId == Const.noTileId)
  336. return false;
  337. if (tiles[tileId].hasWalls() >= Const.maxTileWalls)
  338. return false;
  339. Range dirs = new Range(Direction.Begin, Direction.End, Direction.Step);
  340. for (int dir ; (dir = dirs.get()) != Const.noTileId ; )
  341. if (isWallableDir(tileId, dir))
  342. return true;
  343. return false;
  344. }
  345. /**
  346. * This utility function create/allocate the tiles of the board and create
  347. * the outer walls at the same time.
  348. *
  349. * @return The number of walls created from the utility.
  350. */
  351. private int createBasicTileWalls () {
  352. int wallCount =0;
  353. for (int i =0 ; i< tiles.length ; ++i) {
  354. boolean up = isUpSentinel(i);
  355. boolean down = isDownSentinel(i) && (i != 0);
  356. boolean left = isLeftSentinel(i);
  357. boolean right = isRightSentinel(i);
  358. wallCount += ((up?1:0) + (down?1:0) + (left?1:0) + (right?1:0));
  359. tiles[i] = new Tile (i, up, down, left, right);
  360. if (Session.loopGuard) {
  361. if (up) walls.add(new Edge(i, Direction.UP));
  362. if (down) walls.add(new Edge(i, Direction.DOWN));
  363. if (left) walls.add(new Edge(i, Direction.LEFT));
  364. if (right) walls.add(new Edge(i, Direction.RIGHT));
  365. }
  366. }
  367. return wallCount;
  368. }
  369. /**
  370. * Create randomly a wall in the wallable selected tile.
  371. * @param tileId The wallable tile to create the wall
  372. */
  373. private void createInnerWall(int tileId) {
  374. // Randomly pick a wallable direction in that tile.
  375. ShuffledRange randDirections = new ShuffledRange(Direction.Begin, Direction.End, Direction.Step);
  376. int dir;
  377. do
  378. dir = randDirections.get();
  379. while (!isWallableDir(tileId, dir));
  380. Position neighbor = new Position(Position.toRow(tileId), Position.toCol(tileId), dir);
  381. tiles[tileId].setWall(dir);
  382. tiles[neighbor.getId()].setWall(Direction.opposite(dir));
  383. if (Session.loopGuard)
  384. walls.add(new Edge(tileId, dir));
  385. }
  386. /**
  387. * This utility creates the inner walls of the board.
  388. *
  389. * @param walls The number of walls to create
  390. * @return The number of walls failed to create.
  391. */
  392. private int createInnerWalls () {
  393. ShuffledRange randTiles = new ShuffledRange(0, N*N);
  394. for (int tileId, i =0, walls =0, shuffleMark =0 ; true ; ) {
  395. // randomly pick a wallable tile.
  396. do {
  397. if ((tileId = randTiles.get())== Const.noTileId) {
  398. if (i == shuffleMark) // Wallable tiles exhausted.
  399. return walls;
  400. else { // Re-shuffle and continue.
  401. randTiles = new ShuffledRange(0, N*N);
  402. shuffleMark =i;
  403. }
  404. }
  405. } while (!isWallable(tileId));
  406. ++walls;
  407. createInnerWall(tileId);
  408. }
  409. }
  410. /**
  411. * Utility to get the body (center line) of the string representation of the tile.
  412. *
  413. * @param row What board's row to get.
  414. * @param col What board's column to get.
  415. * @param theseusTile The current tile of the Theseus.
  416. * @param minotaurTile The current tile of the Minotaur.
  417. * @return The body string
  418. */
  419. private String getTileBody (int row, int col, int theseusTile, int minotaurTile) {
  420. int tileId = Position.toID(row, col);
  421. boolean T = (tileId == theseusTile) ? true : false;
  422. boolean M = (tileId == minotaurTile) ? true : false;
  423. int S = tiles[tileId].hasSupply(supplies);
  424. if (T && !M) return " T ";
  425. else if (T && M) return "T+M";
  426. else if (M) {
  427. if (S == Const.noSupply) return " M ";
  428. else return "M+s";
  429. }
  430. else if (S != Const.noSupply)
  431. return String.format("s%02d", S+1);
  432. else return " ";
  433. }
  434. /**
  435. * Utility to render the 3 strings of the tile in the representation frame.
  436. *
  437. * @param frame Reference to the frame to print into.
  438. * @param row The board's row to print.
  439. * @param col The board's column to print.
  440. * @param theseusTile The current tile of the Theseus.
  441. * @param minotaurTile The current tile of the Minotaur.
  442. */
  443. private void renderTile(String[][] frame, int row, int col, int theseusTile, int minotaurTile) {
  444. IntFunction<Integer> toframe = (r)->{ return 2*r+1; };
  445. int tileId = Position.toID(row, col);
  446. frame[toframe.apply(row)+1][col] = tiles[tileId].hasWall(Direction.UP) ? "+---" : "+ ";
  447. frame[toframe.apply(row) ][col] = (tiles[tileId].hasWall(Direction.LEFT)? "|" : " ")
  448. + getTileBody(row, col, theseusTile, minotaurTile);
  449. frame[toframe.apply(row)-1][col] = tiles[tileId].hasWall(Direction.DOWN) ? "+---" : "+ ";
  450. }
  451. /**
  452. * Utility to render the 3 strings of the tile in the representation frame in
  453. * the case the tile lies in the east wall. We call these tiles `sentinel tiles`
  454. *
  455. * @param frame Reference to the frame to print into.
  456. * @param row The board's row to print.
  457. * @param col The board's column to print.
  458. * @param theseusTile The current tile of the Theseus.
  459. * @param minotaurTile The current tile of the Minotaur.
  460. */
  461. private void renderSentinelTile(String[][] frame, int row, int col, int theseusTile, int minotaurTile ) {
  462. IntFunction<Integer> toframe = (r)->{ return 2*r+1; };
  463. int tileId = Position.toID(row, col);
  464. frame[toframe.apply(row)+1][col] = tiles[tileId].hasWall(Direction.UP) ? "+---+" : "+ +";
  465. frame[toframe.apply(row) ][col] = (tiles[tileId].hasWall(Direction.LEFT)? "|" : " ")
  466. + getTileBody(row, col, theseusTile, minotaurTile)
  467. + (tiles[tileId].hasWall(Direction.RIGHT)? "|" : " ");
  468. frame[toframe.apply(row)-1][col] = tiles[tileId].hasWall(Direction.DOWN) ? "+---+" : "+ +";
  469. }
  470. /** @} */
  471. /** @name Neighbor access lambdas */
  472. /** @{ */
  473. private IntFunction<Integer> leftTileId = (id) -> { return Position.toID(Position.toRow(id), Position.toCol(id)-1); };
  474. private IntFunction<Integer> rightTileId = (id) -> { return Position.toID(Position.toRow(id), Position.toCol(id)+1); };
  475. private IntFunction<Integer> upTileId = (id) -> { return Position.toID(Position.toRow(id)+1, Position.toCol(id) ); };
  476. private IntFunction<Integer> downTileId = (id) -> { return Position.toID(Position.toRow(id)-1, Position.toCol(id) ); };
  477. /** @} */
  478. /** @name Class data */
  479. /** @{ */
  480. private int N; /**< The size of each edge of the board */
  481. private int S; /**< The number of the supplies on the board */
  482. private int W; /**< The number of walls on the board */
  483. private Tile[] tiles; /**< Array to hold all the tiles for the board */
  484. private Supply[] supplies; /**< Array to hold all the supplies on the board */
  485. private ArrayList<Edge> walls; /**<
  486. * Array to hold all the walls using the edge representation
  487. * required by the closed room preventing algorithm.
  488. */
  489. /** @} */
  490. }