A java PacMan game application for A.U.TH (data structures class)
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.

614 lines
23 KiB

  1. /**
  2. * @file Node89978445.java
  3. * @brief
  4. * File containing the Node class witch represents
  5. * the moves of Pacman
  6. *
  7. * @author Christos Choutouridis 8997 cchoutou@ece.auth.gr
  8. * @author Konstantina Tsechelidou 8445 konstsec@ece.auth.gr
  9. */
  10. package gr.auth.ee.dsproject.node;
  11. import java.util.ArrayList;
  12. import gr.auth.ee.dsproject.pacman.PacmanUtilities;
  13. import gr.auth.ee.dsproject.pacman.Room;
  14. /**
  15. * @class Node89978445
  16. * @brief
  17. * This class holds each move of the current round and it's evaluation
  18. *
  19. */
  20. public class Node89978445
  21. {
  22. double nodeEvaluation; /**
  23. * Pacman's current move evaluation as return value of evaluation ()
  24. * This is used also as the "return status" of the object
  25. * @note
  26. * The variable is initialized to Globals.NO_EVAL and calculated only
  27. * after the getEvaluation () call
  28. */
  29. int[] nodeXY; // Pacman's current x,y coordinate
  30. int[][] curGhostPos; // Array holding the Ghost (x,y) pairs
  31. int[][] flagPos; // Array holding the flag (x,y) pairs
  32. boolean[] curFlagStatus; // Array holding the flag capture status
  33. Room[][] Maze; // copy of the current Maze
  34. /*
  35. * Tree navigation references. These variables are handled by
  36. * PacTree
  37. */
  38. Node89978445 parent; // reference to parent node in search tree
  39. ArrayList<Node89978445> children; // references to children nodes in search tree
  40. int depth; // the depth of the node in search tree
  41. /*
  42. * ============ Constructors ==============
  43. */
  44. /**
  45. * @brief
  46. * The simple constructor. Just initialize the data
  47. * @note
  48. * Using this constructor means that the user MUST call setMaze()
  49. * and setPosition() manually after the creation of the Node89978445 object
  50. */
  51. public Node89978445 ()
  52. {
  53. this.Maze = null; // Fill members
  54. nodeXY = Globals.POSITION_FALSE;
  55. nodeEvaluation = Globals.NO_EVAL;
  56. parent = null;
  57. // allocate objects
  58. curGhostPos = new int [PacmanUtilities.numberOfGhosts][2];
  59. flagPos = new int [PacmanUtilities.numberOfFlags][2];
  60. curFlagStatus = new boolean[PacmanUtilities.numberOfFlags];
  61. children = new ArrayList<Node89978445> ();
  62. }
  63. /**
  64. * @brief
  65. * Constructor for the Node89978445
  66. * @param Maze The current maze object
  67. * @param curXY The current pacman's (x, y) position
  68. */
  69. public Node89978445 (Room[][] Maze, int[] curXY)
  70. {
  71. this.Maze = Maze; // Fill members
  72. nodeXY = curXY;
  73. nodeEvaluation = Globals.NO_EVAL;
  74. parent = null;
  75. // allocate objects
  76. curGhostPos = new int [PacmanUtilities.numberOfGhosts][2];
  77. flagPos = new int [PacmanUtilities.numberOfFlags][2];
  78. curFlagStatus = new boolean[PacmanUtilities.numberOfFlags];
  79. children = new ArrayList<Node89978445> ();
  80. }
  81. /*
  82. * ============== Setters ===============
  83. */
  84. /**
  85. * @brief
  86. * SetMaze (Room) to Node object
  87. * @param maze The room to set
  88. */
  89. public void setMaze (Room[][] maze) {
  90. this.Maze = maze;
  91. }
  92. /**
  93. * @brief
  94. * Set pacman's position
  95. * @param curXY Pacman's current X, Y position
  96. */
  97. public void setPosition (int[] curXY) {
  98. nodeXY = curXY;
  99. }
  100. /*
  101. * ============== getters =================
  102. */
  103. /**
  104. * @brief
  105. * If not done calls the evaluation algorithm and returns the result
  106. * @return The evaluation result (also stored inside object)
  107. * @note
  108. * We assume the current position was a valid position.
  109. * This routine is not called upon creation of the class
  110. */
  111. public double getEvaluation ()
  112. {
  113. // If already evaluated do not re-evaluate the move
  114. if (nodeEvaluation == Globals.NO_EVAL) {
  115. // Safety filters
  116. if (Maze == null)
  117. return Globals.NO_EVAL;
  118. if (nodeXY == Globals.POSITION_FALSE)
  119. return Globals.NO_EVAL;
  120. // calculate helper arrays
  121. curGhostPos = findGhosts ();
  122. flagPos = findFlags ();
  123. curFlagStatus = checkFlags ();
  124. return nodeEvaluation = evaluate ();
  125. }
  126. else
  127. return nodeEvaluation;
  128. }
  129. /**
  130. * @return Nodes position in maze
  131. */
  132. public int[] getCurrentPacmanPos () {
  133. return nodeXY;
  134. }
  135. /**
  136. * @return Current ghost position ina maze
  137. */
  138. public int[][] getCurrentGhostPos () {
  139. return curGhostPos;
  140. }
  141. /**
  142. * @return The depth of the node tin the min-max tree
  143. */
  144. public int getDepth () {
  145. return depth;
  146. }
  147. /*
  148. * ============= Helper API ============
  149. */
  150. /**
  151. * @brief
  152. * Convert back a maze position to Room's direction format
  153. * @param nextPos The intention (x, y) coordinates
  154. * @param curPos The current coordinates
  155. * @return The direction
  156. * @arg Room.NORTH (x decreases)
  157. * @arg Room.SOUTH (x increases)
  158. * @arg Room.WEST (y decreases)
  159. * @arg Room.EAST (y increases)
  160. */
  161. public static int moveConv (int[] nextPos, int[] curPos)
  162. {
  163. int dx = nextPos[0] - curPos[0];
  164. int dy = nextPos[1] - curPos[1];
  165. if (dx == -1 || dx == PacmanUtilities.numberOfRows-1)
  166. return Room.NORTH;
  167. else if (dx == 1 || dx == -PacmanUtilities.numberOfRows+1)
  168. return Room.SOUTH;
  169. else if (dy == -1 || dy == PacmanUtilities.numberOfColumns-1)
  170. return Room.WEST;
  171. else if (dy == 1 || dy == -PacmanUtilities.numberOfColumns+1)
  172. return Room.EAST;
  173. else
  174. return Globals.INVALID_MOVE;
  175. }
  176. /**
  177. * @param creature creature's (x,y)
  178. * @return
  179. * Return true if the creature is inside the maze boxes
  180. */
  181. public static boolean isInsideBox (int[] creature)
  182. {
  183. boolean ret = false; //have faith
  184. for (int i=0 ; i<4 ; ++i) {
  185. if ((creature[0]>=Globals.BOXES[i][0][0] && creature[0]<=Globals.BOXES[i][1][0]) &&
  186. (creature[1]>=Globals.BOXES[i][0][1] && creature[1]<=Globals.BOXES[i][1][1]))
  187. ret = true;
  188. }
  189. return ret;
  190. }
  191. /**
  192. * @brief
  193. * Torus borders are the squares in the outer limits of the maze where
  194. * there are no walls. Pacman's gravity can curve the maze plane to
  195. * a torus through these squares. This function check if a position is
  196. * a torus border position
  197. * @param creature creature's (x,y)
  198. * @return
  199. * Return true if the creature is in a border position.
  200. */
  201. public static boolean isTorusSquare (int[] creature)
  202. {
  203. for (int i=0 ; i<Globals.TORUS_BORDERS.length ; ++i) {
  204. if (creature[0] == Globals.TORUS_BORDERS[i][0] &&
  205. creature[1] == Globals.TORUS_BORDERS[i][1]) {
  206. return true;
  207. }
  208. }
  209. return false;
  210. }
  211. /**
  212. * @brief
  213. * Static version of move validation to use outside of the class
  214. * Check if the requested move for a node is valid
  215. */
  216. public static int[] pacmanValidMove (Node89978445 node, int move) {
  217. return node.pacmanValidMove (node.nodeXY, move);
  218. }
  219. /*
  220. * ============= Node's private methods =============
  221. */
  222. /**
  223. * @brief
  224. * Loop entire maze and return an object that holds Ghost positions
  225. * @param none
  226. * @return Object with Ghost position
  227. * The first dimension holds the number of the ghost
  228. * The 2nd dimension holds the (x, y) coordinates of the ghost
  229. */
  230. private int[][] findGhosts ()
  231. {
  232. int [][] ret = new int [PacmanUtilities.numberOfGhosts][2]; // Make an object to return
  233. int g = 0; // Ghost index
  234. boolean keepGoing = true; // Boundary check helper variable
  235. // Loop entire Maze (i, j)
  236. for (int x=0 ; keepGoing && x<PacmanUtilities.numberOfRows ; ++x) {
  237. for (int y=0 ; keepGoing && y<PacmanUtilities.numberOfColumns ; ++y) {
  238. if (Maze[x][y].isGhost ()) {
  239. // In case of a Ghost save its position to return object
  240. ret[g][0] = x;
  241. ret[g][1] = y;
  242. // boundary check
  243. if (++g >= PacmanUtilities.numberOfGhosts)
  244. keepGoing = false;
  245. }
  246. }
  247. }
  248. return ret;
  249. }
  250. /**
  251. * @brief
  252. * Loop entire maze and return an object that holds flags positions
  253. * @param none
  254. * @return Object with flag positions
  255. * The first dimension holds the number of the flag
  256. * The 2nd dimension holds the (x, y) coordinates of the flag
  257. */
  258. private int[][] findFlags ()
  259. {
  260. int [][] ret = new int [PacmanUtilities.numberOfFlags][2]; // Make an object to return
  261. int f = 0; // Flag index
  262. boolean keepGoing = true; // Boundary check helper variable
  263. // Loop entire Maze (i, j)
  264. for (int x=0 ; keepGoing && x<PacmanUtilities.numberOfRows ; ++x) {
  265. for (int y=0 ; keepGoing && y<PacmanUtilities.numberOfColumns ; ++y) {
  266. if (Maze[x][y].isFlag()) {
  267. // In case of a Ghost save its position to return object
  268. ret[f][0] = x;
  269. ret[f][1] = y;
  270. // boundary check
  271. if (++f >= PacmanUtilities.numberOfFlags)
  272. keepGoing = false;
  273. }
  274. }
  275. }
  276. return ret;
  277. }
  278. /**
  279. * @brief
  280. * Loop through flags and check their status
  281. * @return Object with flag status
  282. */
  283. private boolean[] checkFlags ()
  284. {
  285. boolean[] ret = new boolean [PacmanUtilities.numberOfFlags];
  286. int x, y;
  287. for (int i=0 ; i<PacmanUtilities.numberOfFlags ; ++i) {
  288. x = flagPos[i][0]; // Get row,col coordinates
  289. y = flagPos[i][1];
  290. ret[i] = (Maze[x][y].isCapturedFlag()) ? true : false;
  291. }
  292. return ret;
  293. }
  294. /*
  295. * ============ private evaluation helper methods ==============
  296. */
  297. /**
  298. * Helper enumerator to make a function pointer like trick
  299. * inside @ref moveDist()
  300. */
  301. private enum Creature {
  302. GHOST, PACMAN
  303. }
  304. /**
  305. * @brief
  306. * Check if the requested ghost move is valid
  307. * @param ghost ghost coordinates to check the move validation
  308. * @param move the move to check
  309. * @return
  310. * @arg The resulting position in maze if the move is valid.
  311. * @arg If it is not valid return Globals.POSITION_FALSE
  312. */
  313. private int[] ghostValidMove (int[] ghost, int move)
  314. {
  315. int[] newPos = new int[2];
  316. // find hypothetical new position
  317. newPos[0] = ghost[0];
  318. newPos[1] = ghost[1];
  319. newPos[0] += (move == Room.SOUTH) ? 1:0;
  320. newPos[0] -= (move == Room.NORTH) ? 1:0;
  321. newPos[1] += (move == Room.EAST) ? 1:0;
  322. newPos[1] -= (move == Room.WEST) ? 1:0;
  323. // Valid filters
  324. if (!((newPos[0] >= 0 && newPos[0] < PacmanUtilities.numberOfRows) &&
  325. (newPos[1] >= 0 && newPos[1] < PacmanUtilities.numberOfColumns)))
  326. return Globals.POSITION_FALSE;
  327. /*
  328. * else if (Maze[newPos[0]][newPos[1]].isFlag ())
  329. * return Globals.POSITION_FALSE;
  330. * @note
  331. * We comment out the flag part...
  332. * This incorrect behavior help evaluation routine to stop treat
  333. * flag position as asylum. In other words we remove the discontinuity
  334. * of shortestMoveDist() for ghost function.
  335. */
  336. else if (!isInsideBox(ghost) && (Maze[ghost[0]][ghost[1]].walls[move] == 0))
  337. return Globals.POSITION_FALSE;
  338. else
  339. return newPos;
  340. }
  341. /**
  342. * @brief
  343. * Check if the requested pacman move is valid
  344. * @param pacman pacman coordinates to check the move validation
  345. * @param move the move to check
  346. * @return
  347. * @arg The resulting position in maze if the move is valid.
  348. * @arg If it is not valid return Globals.POSITION_FALSE
  349. */
  350. private int[] pacmanValidMove (int[] pacman, int move)
  351. {
  352. int[] newPos = new int[2];
  353. // find hypothetical new position
  354. newPos[0] = pacman[0];
  355. newPos[1] = pacman[1];
  356. newPos[0] += (move == Room.SOUTH) ? 1:0;
  357. newPos[0] -= (move == Room.NORTH) ? 1:0;
  358. newPos[1] += (move == Room.EAST) ? 1:0;
  359. newPos[1] -= (move == Room.WEST) ? 1:0;
  360. // Pacman curves Maze plane to a Torus
  361. if (newPos[0] < 0) newPos[0] = PacmanUtilities.numberOfRows-1;
  362. if (newPos[0] >= PacmanUtilities.numberOfRows ) newPos[0] = 0;
  363. if (newPos[1] < 0) newPos[1] = PacmanUtilities.numberOfColumns-1;
  364. if (newPos[1] >= PacmanUtilities.numberOfColumns ) newPos[1] = 0;
  365. // Valid filters
  366. if (Maze[pacman[0]][pacman[1]].walls[move] == 0)
  367. return Globals.POSITION_FALSE;
  368. else if (Maze[newPos[0]][newPos[1]].isGhost ())
  369. return Globals.POSITION_FALSE;
  370. else
  371. return newPos;
  372. }
  373. /**
  374. * @brief
  375. * A Breadth-first search to find the shortest path distance
  376. * from an origin point to another point in the maze as if
  377. * a creature could walk through origin to xy
  378. * @param origin origin's x, y (starting point)
  379. * @param xy destination's x, y
  380. * @param creature The type of creature for whom the path is for.
  381. * This helper variable let us decide which validation
  382. * function should call.
  383. * @return The number of steps from origin to xy
  384. * @note
  385. * As long as Java have not function pointers. I prefer this
  386. * as a work around over interfaces and lambdas.
  387. */
  388. private int shortestMoveDist (int[] origin, int[] xy, Creature creature)
  389. {
  390. int move; // move direction
  391. int steps, qStepItems; // distance and group counters
  392. boolean done = false; // algo ending flag
  393. int[] xyItem = new int [2]; // Coordinates of the current position of the algo
  394. int[] xyNext = new int [2]; // Coordinates of the next valid position of the algo
  395. // Queue to feed with possible position
  396. Queue2D q = new Queue2D (Globals.DISTANCE_MAX - 1);
  397. // 2D array holding all the distances from the origin to each square of the maze
  398. int[][] dist = new int [PacmanUtilities.numberOfRows][PacmanUtilities.numberOfColumns];
  399. /*
  400. * If target square is inside a box abort with max DISTANCE_FALSE
  401. * This is for speeding thing up, as the algorithm would return
  402. * the same thing anyway.
  403. */
  404. if (isInsideBox(xy))
  405. return Globals.DISTANCE_FALSE;
  406. /*
  407. * initialize data for algorithm
  408. */
  409. for (int i=0 ; i<PacmanUtilities.numberOfRows ; ++i)
  410. for (int j=0 ; j<PacmanUtilities.numberOfColumns ; ++j)
  411. dist[i][j] = Globals.DISTANCE_FALSE; // dist array starts with -1
  412. dist [origin[0]][origin[1]] = 0; //starting point is the origin position
  413. q.insert (origin); // feed first loop data
  414. qStepItems = 1; // init counters
  415. steps = 1;
  416. /*
  417. * Main loop of the algorithm.
  418. * For every position pulled from queue with the same "steps" value:
  419. * - check all the valid moves around current's position
  420. * - mark them with the current "steps" value in dist[][] array
  421. * - push them to queue
  422. * - count the pushed position with the same "steps" value in queue
  423. * If no other queued position with the current "steps" value remained
  424. * increase "steps" value and continue.
  425. */
  426. while (done == false) {
  427. if (qStepItems>0) { // Have we any item on the current group?
  428. xyItem = q.remove (); //load an item of the current group
  429. --qStepItems; // mark the removal of the item
  430. // loop every valid next position for the current position
  431. for (move=0 ; move<4 ; ++move) {
  432. switch (creature) {
  433. case GHOST: xyNext = ghostValidMove (xyItem, move); break;
  434. case PACMAN: xyNext = pacmanValidMove (xyItem, move); break;
  435. default: xyNext = Globals.POSITION_FALSE;
  436. /*
  437. * Function pointer - like behavior
  438. */
  439. }
  440. if (xyNext != Globals.POSITION_FALSE) {
  441. if (dist[xyNext[0]][xyNext[1]] == Globals.DISTANCE_FALSE) {
  442. // If we haven't been there
  443. dist[xyNext[0]][xyNext[1]] = steps; // mark the distance
  444. if ((xyNext[0] == xy[0]) && (xyNext[1] == xy[1])) {
  445. /*
  446. * first match: The first time we reach destination we
  447. * have measured the distance. No need any other try
  448. */
  449. done = true;
  450. break;
  451. }
  452. else {
  453. // If we are not there yet, feed queue with another position
  454. q.insert (xyNext);
  455. }
  456. }
  457. }
  458. }
  459. }
  460. else {
  461. // We are done with group, mark how many are for the next one
  462. if ((qStepItems = q.size()) <= 0)
  463. done = true; // There is no path to destination, abort
  464. ++steps; // Update distance counter
  465. }
  466. }
  467. return dist[xy[0]][xy[1]];
  468. }
  469. /**
  470. * @brief
  471. * Normalize a distance to our evaluation interval.
  472. * @param v The value to normalize
  473. * @param max The maximum input limit value
  474. * @param out_min The minimum output limit value
  475. * @param out_max The maximum output limit value
  476. */
  477. private double normalize (double v, double max, double out_min, double out_max) {
  478. return (v / max) * (out_max - out_min) + out_min;
  479. }
  480. /**
  481. * @brief
  482. * Evaluate the current move.
  483. * The position evaluation is based on shortestMoveDist() results.
  484. * We measure the move distance of:
  485. * 1) Ghosts to current position, as a ghost path, which represent the minimum steps
  486. * needed by a ghost in order to reach the current position avoiding all obstacles
  487. * 2) Pacman to all the flags, which represent the minimum steps needed by pacman
  488. * in order to reach the flags avoiding all obstacles including the curving
  489. * of the plane in the borders
  490. * 3) Pacman to all the torus border squares, which represent the minimum steps needed
  491. * by pacman in order to reach the torus border squares. @ref Globals.TORUS_BORDERS
  492. *
  493. * We mix these values in order to produce the final evaluation value
  494. */
  495. private double evaluate ()
  496. {
  497. double eval = 0; // mixed evaluation value
  498. double ghostEval = 0, gd; // ghost evaluation value
  499. double flagEval = 0; // flag evaluation value
  500. double torusEval = 0; // torus evaluation value
  501. int[] ghostDist = new int[PacmanUtilities.numberOfGhosts]; // hold the ghost distances
  502. int[] flagDist = new int[PacmanUtilities.numberOfFlags]; // hold the flag distances
  503. int[] torusDist = new int[Globals.TORUS_BORDERS.length]; // hold the torus square distances
  504. int minGhostDist = Globals.DISTANCE_MAX+1; // hold the minimum ghost distance
  505. int averGhostDist = Globals.DISTANCE_MAX; // hold the average ghost distance
  506. int minFlagDist = Globals.DISTANCE_MAX+1; // hold the minimum flag distance
  507. int minTorusDist = Globals.DISTANCE_MAX+1; // hold the minimum torus square distance
  508. int i; // loop counter
  509. /*
  510. * === Ghost distances part ===
  511. */
  512. for (i=0, averGhostDist=0 ; i<PacmanUtilities.numberOfGhosts ; ++i) {
  513. if ((ghostDist [i] = shortestMoveDist (curGhostPos[i], nodeXY, Creature.GHOST)) >= 1) {
  514. averGhostDist += ghostDist [i];
  515. if (minGhostDist > ghostDist[i])
  516. minGhostDist = ghostDist[i];
  517. }
  518. }
  519. averGhostDist /= PacmanUtilities.numberOfGhosts;
  520. // extract the ghostEval as a combination of the minimum distance and the average
  521. gd = Globals.EVAL_GHOSTDIST_MIN_FACTOR * minGhostDist + Globals.EVAL_GHOSTDIST_AVER_FACTOR * averGhostDist;
  522. // normalize the ghostEval to our interval
  523. if (minGhostDist <= Globals.EVAL_GHOST_BOOST)
  524. ghostEval = normalize (gd, Globals.DISTANCE_MAX ,Globals.EVAL_MIN+Globals.EVAL_GHOSTBOOST_OFFSET, Globals.EVAL_MAX);
  525. else if (minGhostDist <= Globals.EVAL_GHOST_TERRITORY)
  526. ghostEval = normalize (gd, Globals.DISTANCE_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX - Globals.EVAL_GHOSTTERRITORY_OFFSET);
  527. else
  528. ghostEval = normalize (gd, Globals.DISTANCE_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX);
  529. /*
  530. * === Flag distances part ===
  531. */
  532. for (i=0 ; i<PacmanUtilities.numberOfFlags ; ++i) {
  533. if (curFlagStatus[i] == false) {
  534. if ((flagDist[i] = shortestMoveDist (nodeXY, flagPos[i], Creature.PACMAN)) >= 0) {
  535. if (minFlagDist > flagDist[i])
  536. minFlagDist = flagDist[i];
  537. }
  538. }
  539. }
  540. // normalize the flagEval to our interval
  541. if (minFlagDist <= Globals.EVAL_FLAG_BOOST)
  542. flagEval = normalize ((double)Globals.FLAGDIST_MAX - minFlagDist, Globals.FLAGDIST_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX+Globals.EVAL_FLAGBOOST_OFFSET);
  543. else
  544. flagEval = normalize ((double)Globals.FLAGDIST_MAX - minFlagDist, Globals.FLAGDIST_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX);
  545. /*
  546. * === Torus borders distances part ===
  547. */
  548. for (i=0 ; i<Globals.TORUS_BORDERS.length ; ++i) {
  549. if ((torusDist[i] = shortestMoveDist (nodeXY, Globals.TORUS_BORDERS[i], Creature.PACMAN)) >= 0) {
  550. if (minTorusDist > torusDist[i])
  551. minTorusDist = torusDist[i];
  552. }
  553. }
  554. // normalize the flagEval to our interval
  555. torusEval = normalize (Globals.BORDERDIST_MAX-minTorusDist, Globals.BORDERDIST_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX);
  556. /*
  557. * === Mix up the values and return the normalized value back to caller ===
  558. */
  559. eval = ghostEval * Globals.EVAL_GHOSTDIST_FACTOR
  560. + flagEval * Globals.EVAL_FLAGDIST_FACTOR
  561. + torusEval * Globals.EVAL_TORUSDIST_FACTOR;
  562. return normalize (eval, Globals.EVAL_FINAL_MAX, Globals.EVAL_FINAL_MIN, Globals.EVAL_FINAL_MAX);
  563. }
  564. }