A java PacMan game application for A.U.TH (data structures class)
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

619 Zeilen
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. * @param node The node of interest
  216. * @param move The move to check
  217. * @return The hypothetical new position of pacman
  218. * @arg The resulting position in maze if the move is valid.
  219. * @arg If it is not valid return Globals.POSITION_FALSE
  220. */
  221. public static int[] pacmanValidMove (Node89978445 node, int move) {
  222. return node.pacmanValidMove (node.nodeXY, move);
  223. }
  224. /*
  225. * ============= Node's private methods =============
  226. */
  227. /**
  228. * @brief
  229. * Loop entire maze and return an object that holds Ghost positions
  230. * @param none
  231. * @return Object with Ghost position
  232. * The first dimension holds the number of the ghost
  233. * The 2nd dimension holds the (x, y) coordinates of the ghost
  234. */
  235. private int[][] findGhosts ()
  236. {
  237. int [][] ret = new int [PacmanUtilities.numberOfGhosts][2]; // Make an object to return
  238. int g = 0; // Ghost index
  239. boolean keepGoing = true; // Boundary check helper variable
  240. // Loop entire Maze (i, j)
  241. for (int x=0 ; keepGoing && x<PacmanUtilities.numberOfRows ; ++x) {
  242. for (int y=0 ; keepGoing && y<PacmanUtilities.numberOfColumns ; ++y) {
  243. if (Maze[x][y].isGhost ()) {
  244. // In case of a Ghost save its position to return object
  245. ret[g][0] = x;
  246. ret[g][1] = y;
  247. // boundary check
  248. if (++g >= PacmanUtilities.numberOfGhosts)
  249. keepGoing = false;
  250. }
  251. }
  252. }
  253. return ret;
  254. }
  255. /**
  256. * @brief
  257. * Loop entire maze and return an object that holds flags positions
  258. * @param none
  259. * @return Object with flag positions
  260. * The first dimension holds the number of the flag
  261. * The 2nd dimension holds the (x, y) coordinates of the flag
  262. */
  263. private int[][] findFlags ()
  264. {
  265. int [][] ret = new int [PacmanUtilities.numberOfFlags][2]; // Make an object to return
  266. int f = 0; // Flag index
  267. boolean keepGoing = true; // Boundary check helper variable
  268. // Loop entire Maze (i, j)
  269. for (int x=0 ; keepGoing && x<PacmanUtilities.numberOfRows ; ++x) {
  270. for (int y=0 ; keepGoing && y<PacmanUtilities.numberOfColumns ; ++y) {
  271. if (Maze[x][y].isFlag()) {
  272. // In case of a Ghost save its position to return object
  273. ret[f][0] = x;
  274. ret[f][1] = y;
  275. // boundary check
  276. if (++f >= PacmanUtilities.numberOfFlags)
  277. keepGoing = false;
  278. }
  279. }
  280. }
  281. return ret;
  282. }
  283. /**
  284. * @brief
  285. * Loop through flags and check their status
  286. * @return Object with flag status
  287. */
  288. private boolean[] checkFlags ()
  289. {
  290. boolean[] ret = new boolean [PacmanUtilities.numberOfFlags];
  291. int x, y;
  292. for (int i=0 ; i<PacmanUtilities.numberOfFlags ; ++i) {
  293. x = flagPos[i][0]; // Get row,col coordinates
  294. y = flagPos[i][1];
  295. ret[i] = (Maze[x][y].isCapturedFlag()) ? true : false;
  296. }
  297. return ret;
  298. }
  299. /*
  300. * ============ private evaluation helper methods ==============
  301. */
  302. /**
  303. * Helper enumerator to make a function pointer like trick
  304. * inside @ref moveDist()
  305. */
  306. private enum Creature {
  307. GHOST, PACMAN
  308. }
  309. /**
  310. * @brief
  311. * Check if the requested ghost move is valid
  312. * @param ghost ghost coordinates to check the move validation
  313. * @param move the move to check
  314. * @return
  315. * @arg The resulting position in maze if the move is valid.
  316. * @arg If it is not valid return Globals.POSITION_FALSE
  317. */
  318. private int[] ghostValidMove (int[] ghost, int move)
  319. {
  320. int[] newPos = new int[2];
  321. // find hypothetical new position
  322. newPos[0] = ghost[0];
  323. newPos[1] = ghost[1];
  324. newPos[0] += (move == Room.SOUTH) ? 1:0;
  325. newPos[0] -= (move == Room.NORTH) ? 1:0;
  326. newPos[1] += (move == Room.EAST) ? 1:0;
  327. newPos[1] -= (move == Room.WEST) ? 1:0;
  328. // Valid filters
  329. if (!((newPos[0] >= 0 && newPos[0] < PacmanUtilities.numberOfRows) &&
  330. (newPos[1] >= 0 && newPos[1] < PacmanUtilities.numberOfColumns)))
  331. return Globals.POSITION_FALSE;
  332. /*
  333. * else if (Maze[newPos[0]][newPos[1]].isFlag ())
  334. * return Globals.POSITION_FALSE;
  335. * @note
  336. * We comment out the flag part...
  337. * This incorrect behavior help evaluation routine to stop treat
  338. * flag position as asylum. In other words we remove the discontinuity
  339. * of shortestMoveDist() for ghost function.
  340. */
  341. else if (!isInsideBox(ghost) && (Maze[ghost[0]][ghost[1]].walls[move] == 0))
  342. return Globals.POSITION_FALSE;
  343. else
  344. return newPos;
  345. }
  346. /**
  347. * @brief
  348. * Check if the requested pacman move is valid
  349. * @param pacman pacman coordinates to check the move validation
  350. * @param move the move to check
  351. * @return
  352. * @arg The resulting position in maze if the move is valid.
  353. * @arg If it is not valid return Globals.POSITION_FALSE
  354. */
  355. private int[] pacmanValidMove (int[] pacman, int move)
  356. {
  357. int[] newPos = new int[2];
  358. // find hypothetical new position
  359. newPos[0] = pacman[0];
  360. newPos[1] = pacman[1];
  361. newPos[0] += (move == Room.SOUTH) ? 1:0;
  362. newPos[0] -= (move == Room.NORTH) ? 1:0;
  363. newPos[1] += (move == Room.EAST) ? 1:0;
  364. newPos[1] -= (move == Room.WEST) ? 1:0;
  365. // Pacman curves Maze plane to a Torus
  366. if (newPos[0] < 0) newPos[0] = PacmanUtilities.numberOfRows-1;
  367. if (newPos[0] >= PacmanUtilities.numberOfRows ) newPos[0] = 0;
  368. if (newPos[1] < 0) newPos[1] = PacmanUtilities.numberOfColumns-1;
  369. if (newPos[1] >= PacmanUtilities.numberOfColumns ) newPos[1] = 0;
  370. // Valid filters
  371. if (Maze[pacman[0]][pacman[1]].walls[move] == 0)
  372. return Globals.POSITION_FALSE;
  373. else if (Maze[newPos[0]][newPos[1]].isGhost ())
  374. return Globals.POSITION_FALSE;
  375. else
  376. return newPos;
  377. }
  378. /**
  379. * @brief
  380. * A Breadth-first search to find the shortest path distance
  381. * from an origin point to another point in the maze as if
  382. * a creature could walk through origin to xy
  383. * @param origin origin's x, y (starting point)
  384. * @param xy destination's x, y
  385. * @param creature The type of creature for whom the path is for.
  386. * This helper variable let us decide which validation
  387. * function should call.
  388. * @return The number of steps from origin to xy
  389. * @note
  390. * As long as Java have not function pointers. I prefer this
  391. * as a work around over interfaces and lambdas.
  392. */
  393. private int shortestMoveDist (int[] origin, int[] xy, Creature creature)
  394. {
  395. int move; // move direction
  396. int steps, qStepItems; // distance and group counters
  397. boolean done = false; // algo ending flag
  398. int[] xyItem = new int [2]; // Coordinates of the current position of the algo
  399. int[] xyNext = new int [2]; // Coordinates of the next valid position of the algo
  400. // Queue to feed with possible position
  401. Queue2D q = new Queue2D (Globals.DISTANCE_MAX - 1);
  402. // 2D array holding all the distances from the origin to each square of the maze
  403. int[][] dist = new int [PacmanUtilities.numberOfRows][PacmanUtilities.numberOfColumns];
  404. /*
  405. * If target square is inside a box abort with max DISTANCE_FALSE
  406. * This is for speeding thing up, as the algorithm would return
  407. * the same thing anyway.
  408. */
  409. if (isInsideBox(xy))
  410. return Globals.DISTANCE_FALSE;
  411. /*
  412. * initialize data for algorithm
  413. */
  414. for (int i=0 ; i<PacmanUtilities.numberOfRows ; ++i)
  415. for (int j=0 ; j<PacmanUtilities.numberOfColumns ; ++j)
  416. dist[i][j] = Globals.DISTANCE_FALSE; // dist array starts with -1
  417. dist [origin[0]][origin[1]] = 0; //starting point is the origin position
  418. q.insert (origin); // feed first loop data
  419. qStepItems = 1; // init counters
  420. steps = 1;
  421. /*
  422. * Main loop of the algorithm.
  423. * For every position pulled from queue with the same "steps" value:
  424. * - check all the valid moves around current's position
  425. * - mark them with the current "steps" value in dist[][] array
  426. * - push them to queue
  427. * - count the pushed position with the same "steps" value in queue
  428. * If no other queued position with the current "steps" value remained
  429. * increase "steps" value and continue.
  430. */
  431. while (done == false) {
  432. if (qStepItems>0) { // Have we any item on the current group?
  433. xyItem = q.remove (); //load an item of the current group
  434. --qStepItems; // mark the removal of the item
  435. // loop every valid next position for the current position
  436. for (move=0 ; move<4 ; ++move) {
  437. switch (creature) {
  438. case GHOST: xyNext = ghostValidMove (xyItem, move); break;
  439. case PACMAN: xyNext = pacmanValidMove (xyItem, move); break;
  440. default: xyNext = Globals.POSITION_FALSE;
  441. /*
  442. * Function pointer - like behavior
  443. */
  444. }
  445. if (xyNext != Globals.POSITION_FALSE) {
  446. if (dist[xyNext[0]][xyNext[1]] == Globals.DISTANCE_FALSE) {
  447. // If we haven't been there
  448. dist[xyNext[0]][xyNext[1]] = steps; // mark the distance
  449. if ((xyNext[0] == xy[0]) && (xyNext[1] == xy[1])) {
  450. /*
  451. * first match: The first time we reach destination we
  452. * have measured the distance. No need any other try
  453. */
  454. done = true;
  455. break;
  456. }
  457. else {
  458. // If we are not there yet, feed queue with another position
  459. q.insert (xyNext);
  460. }
  461. }
  462. }
  463. }
  464. }
  465. else {
  466. // We are done with group, mark how many are for the next one
  467. if ((qStepItems = q.size()) <= 0)
  468. done = true; // There is no path to destination, abort
  469. ++steps; // Update distance counter
  470. }
  471. }
  472. return dist[xy[0]][xy[1]];
  473. }
  474. /**
  475. * @brief
  476. * Normalize a distance to our evaluation interval.
  477. * @param v The value to normalize
  478. * @param max The maximum input limit value
  479. * @param out_min The minimum output limit value
  480. * @param out_max The maximum output limit value
  481. */
  482. private double normalize (double v, double max, double out_min, double out_max) {
  483. return (v / max) * (out_max - out_min) + out_min;
  484. }
  485. /**
  486. * @brief
  487. * Evaluate the current move.
  488. * The position evaluation is based on shortestMoveDist() results.
  489. * We measure the move distance of:
  490. * 1) Ghosts to current position, as a ghost path, which represent the minimum steps
  491. * needed by a ghost in order to reach the current position avoiding all obstacles
  492. * 2) Pacman to all the flags, which represent the minimum steps needed by pacman
  493. * in order to reach the flags avoiding all obstacles including the curving
  494. * of the plane in the borders
  495. * 3) Pacman to all the torus border squares, which represent the minimum steps needed
  496. * by pacman in order to reach the torus border squares. @ref Globals.TORUS_BORDERS
  497. *
  498. * We mix these values in order to produce the final evaluation value
  499. */
  500. private double evaluate ()
  501. {
  502. double eval = 0; // mixed evaluation value
  503. double ghostEval = 0, gd; // ghost evaluation value
  504. double flagEval = 0; // flag evaluation value
  505. double torusEval = 0; // torus evaluation value
  506. int[] ghostDist = new int[PacmanUtilities.numberOfGhosts]; // hold the ghost distances
  507. int[] flagDist = new int[PacmanUtilities.numberOfFlags]; // hold the flag distances
  508. int[] torusDist = new int[Globals.TORUS_BORDERS.length]; // hold the torus square distances
  509. int minGhostDist = Globals.DISTANCE_MAX+1; // hold the minimum ghost distance
  510. int averGhostDist = Globals.DISTANCE_MAX; // hold the average ghost distance
  511. int minFlagDist = Globals.DISTANCE_MAX+1; // hold the minimum flag distance
  512. int minTorusDist = Globals.DISTANCE_MAX+1; // hold the minimum torus square distance
  513. int i; // loop counter
  514. /*
  515. * === Ghost distances part ===
  516. */
  517. for (i=0, averGhostDist=0 ; i<PacmanUtilities.numberOfGhosts ; ++i) {
  518. if ((ghostDist [i] = shortestMoveDist (curGhostPos[i], nodeXY, Creature.GHOST)) >= 1) {
  519. averGhostDist += ghostDist [i];
  520. if (minGhostDist > ghostDist[i])
  521. minGhostDist = ghostDist[i];
  522. }
  523. }
  524. averGhostDist /= PacmanUtilities.numberOfGhosts;
  525. // extract the ghostEval as a combination of the minimum distance and the average
  526. gd = Globals.EVAL_GHOSTDIST_MIN_FACTOR * minGhostDist + Globals.EVAL_GHOSTDIST_AVER_FACTOR * averGhostDist;
  527. // normalize the ghostEval to our interval
  528. if (minGhostDist <= Globals.EVAL_GHOST_BOOST)
  529. ghostEval = normalize (gd, Globals.DISTANCE_MAX ,Globals.EVAL_MIN+Globals.EVAL_GHOSTBOOST_OFFSET, Globals.EVAL_MAX);
  530. else if (minGhostDist <= Globals.EVAL_GHOST_TERRITORY)
  531. ghostEval = normalize (gd, Globals.DISTANCE_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX - Globals.EVAL_GHOSTTERRITORY_OFFSET);
  532. else
  533. ghostEval = normalize (gd, Globals.DISTANCE_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX);
  534. /*
  535. * === Flag distances part ===
  536. */
  537. for (i=0 ; i<PacmanUtilities.numberOfFlags ; ++i) {
  538. if (curFlagStatus[i] == false) {
  539. if ((flagDist[i] = shortestMoveDist (nodeXY, flagPos[i], Creature.PACMAN)) >= 0) {
  540. if (minFlagDist > flagDist[i])
  541. minFlagDist = flagDist[i];
  542. }
  543. }
  544. }
  545. // normalize the flagEval to our interval
  546. if (minFlagDist <= Globals.EVAL_FLAG_BOOST)
  547. flagEval = normalize ((double)Globals.FLAGDIST_MAX - minFlagDist, Globals.FLAGDIST_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX+Globals.EVAL_FLAGBOOST_OFFSET);
  548. else
  549. flagEval = normalize ((double)Globals.FLAGDIST_MAX - minFlagDist, Globals.FLAGDIST_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX);
  550. /*
  551. * === Torus borders distances part ===
  552. */
  553. for (i=0 ; i<Globals.TORUS_BORDERS.length ; ++i) {
  554. if ((torusDist[i] = shortestMoveDist (nodeXY, Globals.TORUS_BORDERS[i], Creature.PACMAN)) >= 0) {
  555. if (minTorusDist > torusDist[i])
  556. minTorusDist = torusDist[i];
  557. }
  558. }
  559. // normalize the flagEval to our interval
  560. torusEval = normalize (Globals.BORDERDIST_MAX-minTorusDist, Globals.BORDERDIST_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX);
  561. /*
  562. * === Mix up the values and return the normalized value back to caller ===
  563. */
  564. eval = ghostEval * Globals.EVAL_GHOSTDIST_FACTOR
  565. + flagEval * Globals.EVAL_FLAGDIST_FACTOR
  566. + torusEval * Globals.EVAL_TORUSDIST_FACTOR;
  567. return normalize (eval, Globals.EVAL_FINAL_MAX, Globals.EVAL_FINAL_MIN, Globals.EVAL_FINAL_MAX);
  568. }
  569. }