/** * @file Node89978445.java * @brief * File containing the Node class witch represents * the moves of Pacman * * @author Christos Choutouridis 8997 cchoutou@ece.auth.gr * @author Konstantina Tsechelidou 8445 konstsec@ece.auth.gr */ package gr.auth.ee.dsproject.node; import java.util.ArrayList; import gr.auth.ee.dsproject.pacman.PacmanUtilities; import gr.auth.ee.dsproject.pacman.Room; /** * @class Node89978445 * @brief * This class holds each move of the current round and it's evaluation * */ public class Node89978445 { double nodeEvaluation; /** * Pacman's current move evaluation as return value of evaluation () * This is used also as the "return status" of the object * @note * The variable is initialized to Globals.NO_EVAL and calculated only * after the getEvaluation () call */ int[] nodeXY; // Pacman's current x,y coordinate int[][] curGhostPos; // Array holding the Ghost (x,y) pairs int[][] flagPos; // Array holding the flag (x,y) pairs boolean[] curFlagStatus; // Array holding the flag capture status Room[][] Maze; // copy of the current Maze /* * Tree navigation references. These variables are handled by * PacTree */ Node89978445 parent; // reference to parent node in search tree ArrayList children; // references to children nodes in search tree int depth; // the depth of the node in search tree /* * ============ Constructors ============== */ /** * @brief * The simple constructor. Just initialize the data * @note * Using this constructor means that the user MUST call setMaze() * and setPosition() manually after the creation of the Node89978445 object */ public Node89978445 () { this.Maze = null; // Fill members nodeXY = Globals.POSITION_FALSE; nodeEvaluation = Globals.NO_EVAL; parent = null; // allocate objects curGhostPos = new int [PacmanUtilities.numberOfGhosts][2]; flagPos = new int [PacmanUtilities.numberOfFlags][2]; curFlagStatus = new boolean[PacmanUtilities.numberOfFlags]; children = new ArrayList (); } /** * @brief * Constructor for the Node89978445 * @param Maze The current maze object * @param curXY The current pacman's (x, y) position */ public Node89978445 (Room[][] Maze, int[] curXY) { this.Maze = Maze; // Fill members nodeXY = curXY; nodeEvaluation = Globals.NO_EVAL; parent = null; // allocate objects curGhostPos = new int [PacmanUtilities.numberOfGhosts][2]; flagPos = new int [PacmanUtilities.numberOfFlags][2]; curFlagStatus = new boolean[PacmanUtilities.numberOfFlags]; children = new ArrayList (); } /* * ============== Setters =============== */ /** * @brief * SetMaze (Room) to Node object * @param maze The room to set */ public void setMaze (Room[][] maze) { this.Maze = maze; } /** * @brief * Set pacman's position * @param curXY Pacman's current X, Y position */ public void setPosition (int[] curXY) { nodeXY = curXY; } /* * ============== getters ================= */ /** * @brief * If not done calls the evaluation algorithm and returns the result * @return The evaluation result (also stored inside object) * @note * We assume the current position was a valid position. * This routine is not called upon creation of the class */ public double getEvaluation () { // If already evaluated do not re-evaluate the move if (nodeEvaluation == Globals.NO_EVAL) { // Safety filters if (Maze == null) return Globals.NO_EVAL; if (nodeXY == Globals.POSITION_FALSE) return Globals.NO_EVAL; // calculate helper arrays curGhostPos = findGhosts (); flagPos = findFlags (); curFlagStatus = checkFlags (); return nodeEvaluation = evaluate (); } else return nodeEvaluation; } /** * @return Nodes position in maze */ public int[] getCurrentPacmanPos () { return nodeXY; } /** * @return Current ghost position ina maze */ public int[][] getCurrentGhostPos () { return curGhostPos; } /** * @return The depth of the node tin the min-max tree */ public int getDepth () { return depth; } /* * ============= Helper API ============ */ /** * @brief * Convert back a maze position to Room's direction format * @param nextPos The intention (x, y) coordinates * @param curPos The current coordinates * @return The direction * @arg Room.NORTH (x decreases) * @arg Room.SOUTH (x increases) * @arg Room.WEST (y decreases) * @arg Room.EAST (y increases) */ public static int moveConv (int[] nextPos, int[] curPos) { int dx = nextPos[0] - curPos[0]; int dy = nextPos[1] - curPos[1]; if (dx == -1 || dx == PacmanUtilities.numberOfRows-1) return Room.NORTH; else if (dx == 1 || dx == -PacmanUtilities.numberOfRows+1) return Room.SOUTH; else if (dy == -1 || dy == PacmanUtilities.numberOfColumns-1) return Room.WEST; else if (dy == 1 || dy == -PacmanUtilities.numberOfColumns+1) return Room.EAST; else return Globals.INVALID_MOVE; } /** * @param creature creature's (x,y) * @return * Return true if the creature is inside the maze boxes */ public static boolean isInsideBox (int[] creature) { boolean ret = false; //have faith for (int i=0 ; i<4 ; ++i) { if ((creature[0]>=Globals.BOXES[i][0][0] && creature[0]<=Globals.BOXES[i][1][0]) && (creature[1]>=Globals.BOXES[i][0][1] && creature[1]<=Globals.BOXES[i][1][1])) ret = true; } return ret; } /** * @brief * Torus borders are the squares in the outer limits of the maze where * there are no walls. Pacman's gravity can curve the maze plane to * a torus through these squares. This function check if a position is * a torus border position * @param creature creature's (x,y) * @return * Return true if the creature is in a border position. */ public static boolean isTorusSquare (int[] creature) { for (int i=0 ; i= PacmanUtilities.numberOfGhosts) keepGoing = false; } } } return ret; } /** * @brief * Loop entire maze and return an object that holds flags positions * @param none * @return Object with flag positions * The first dimension holds the number of the flag * The 2nd dimension holds the (x, y) coordinates of the flag */ private int[][] findFlags () { int [][] ret = new int [PacmanUtilities.numberOfFlags][2]; // Make an object to return int f = 0; // Flag index boolean keepGoing = true; // Boundary check helper variable // Loop entire Maze (i, j) for (int x=0 ; keepGoing && x= PacmanUtilities.numberOfFlags) keepGoing = false; } } } return ret; } /** * @brief * Loop through flags and check their status * @return Object with flag status */ private boolean[] checkFlags () { boolean[] ret = new boolean [PacmanUtilities.numberOfFlags]; int x, y; for (int i=0 ; i= 0 && newPos[0] < PacmanUtilities.numberOfRows) && (newPos[1] >= 0 && newPos[1] < PacmanUtilities.numberOfColumns))) return Globals.POSITION_FALSE; /* * else if (Maze[newPos[0]][newPos[1]].isFlag ()) * return Globals.POSITION_FALSE; * @note * We comment out the flag part... * This incorrect behavior help evaluation routine to stop treat * flag position as asylum. In other words we remove the discontinuity * of shortestMoveDist() for ghost function. */ else if (!isInsideBox(ghost) && (Maze[ghost[0]][ghost[1]].walls[move] == 0)) return Globals.POSITION_FALSE; else return newPos; } /** * @brief * Check if the requested pacman move is valid * @param pacman pacman coordinates to check the move validation * @param move the move to check * @return * @arg The resulting position in maze if the move is valid. * @arg If it is not valid return Globals.POSITION_FALSE */ private int[] pacmanValidMove (int[] pacman, int move) { int[] newPos = new int[2]; // find hypothetical new position newPos[0] = pacman[0]; newPos[1] = pacman[1]; newPos[0] += (move == Room.SOUTH) ? 1:0; newPos[0] -= (move == Room.NORTH) ? 1:0; newPos[1] += (move == Room.EAST) ? 1:0; newPos[1] -= (move == Room.WEST) ? 1:0; // Pacman curves Maze plane to a Torus if (newPos[0] < 0) newPos[0] = PacmanUtilities.numberOfRows-1; if (newPos[0] >= PacmanUtilities.numberOfRows ) newPos[0] = 0; if (newPos[1] < 0) newPos[1] = PacmanUtilities.numberOfColumns-1; if (newPos[1] >= PacmanUtilities.numberOfColumns ) newPos[1] = 0; // Valid filters if (Maze[pacman[0]][pacman[1]].walls[move] == 0) return Globals.POSITION_FALSE; else if (Maze[newPos[0]][newPos[1]].isGhost ()) return Globals.POSITION_FALSE; else return newPos; } /** * @brief * A Breadth-first search to find the shortest path distance * from an origin point to another point in the maze as if * a creature could walk through origin to xy * @param origin origin's x, y (starting point) * @param xy destination's x, y * @param creature The type of creature for whom the path is for. * This helper variable let us decide which validation * function should call. * @return The number of steps from origin to xy * @note * As long as Java have not function pointers. I prefer this * as a work around over interfaces and lambdas. */ private int shortestMoveDist (int[] origin, int[] xy, Creature creature) { int move; // move direction int steps, qStepItems; // distance and group counters boolean done = false; // algo ending flag int[] xyItem = new int [2]; // Coordinates of the current position of the algo int[] xyNext = new int [2]; // Coordinates of the next valid position of the algo // Queue to feed with possible position Queue2D q = new Queue2D (Globals.DISTANCE_MAX - 1); // 2D array holding all the distances from the origin to each square of the maze int[][] dist = new int [PacmanUtilities.numberOfRows][PacmanUtilities.numberOfColumns]; /* * If target square is inside a box abort with max DISTANCE_FALSE * This is for speeding thing up, as the algorithm would return * the same thing anyway. */ if (isInsideBox(xy)) return Globals.DISTANCE_FALSE; /* * initialize data for algorithm */ for (int i=0 ; i0) { // Have we any item on the current group? xyItem = q.remove (); //load an item of the current group --qStepItems; // mark the removal of the item // loop every valid next position for the current position for (move=0 ; move<4 ; ++move) { switch (creature) { case GHOST: xyNext = ghostValidMove (xyItem, move); break; case PACMAN: xyNext = pacmanValidMove (xyItem, move); break; default: xyNext = Globals.POSITION_FALSE; /* * Function pointer - like behavior */ } if (xyNext != Globals.POSITION_FALSE) { if (dist[xyNext[0]][xyNext[1]] == Globals.DISTANCE_FALSE) { // If we haven't been there dist[xyNext[0]][xyNext[1]] = steps; // mark the distance if ((xyNext[0] == xy[0]) && (xyNext[1] == xy[1])) { /* * first match: The first time we reach destination we * have measured the distance. No need any other try */ done = true; break; } else { // If we are not there yet, feed queue with another position q.insert (xyNext); } } } } } else { // We are done with group, mark how many are for the next one if ((qStepItems = q.size()) <= 0) done = true; // There is no path to destination, abort ++steps; // Update distance counter } } return dist[xy[0]][xy[1]]; } /** * @brief * Normalize a distance to our evaluation interval. * @param v The value to normalize * @param max The maximum input limit value * @param out_min The minimum output limit value * @param out_max The maximum output limit value */ private double normalize (double v, double max, double out_min, double out_max) { return (v / max) * (out_max - out_min) + out_min; } /** * @brief * Evaluate the current move. * The position evaluation is based on shortestMoveDist() results. * We measure the move distance of: * 1) Ghosts to current position, as a ghost path, which represent the minimum steps * needed by a ghost in order to reach the current position avoiding all obstacles * 2) Pacman to all the flags, which represent the minimum steps needed by pacman * in order to reach the flags avoiding all obstacles including the curving * of the plane in the borders * 3) Pacman to all the torus border squares, which represent the minimum steps needed * by pacman in order to reach the torus border squares. @ref Globals.TORUS_BORDERS * * We mix these values in order to produce the final evaluation value */ private double evaluate () { double eval = 0; // mixed evaluation value double ghostEval = 0, gd; // ghost evaluation value double flagEval = 0; // flag evaluation value double torusEval = 0; // torus evaluation value int[] ghostDist = new int[PacmanUtilities.numberOfGhosts]; // hold the ghost distances int[] flagDist = new int[PacmanUtilities.numberOfFlags]; // hold the flag distances int[] torusDist = new int[Globals.TORUS_BORDERS.length]; // hold the torus square distances int minGhostDist = Globals.DISTANCE_MAX+1; // hold the minimum ghost distance int averGhostDist = Globals.DISTANCE_MAX; // hold the average ghost distance int minFlagDist = Globals.DISTANCE_MAX+1; // hold the minimum flag distance int minTorusDist = Globals.DISTANCE_MAX+1; // hold the minimum torus square distance int i; // loop counter /* * === Ghost distances part === */ for (i=0, averGhostDist=0 ; i= 1) { averGhostDist += ghostDist [i]; if (minGhostDist > ghostDist[i]) minGhostDist = ghostDist[i]; } } averGhostDist /= PacmanUtilities.numberOfGhosts; // extract the ghostEval as a combination of the minimum distance and the average gd = Globals.EVAL_GHOSTDIST_MIN_FACTOR * minGhostDist + Globals.EVAL_GHOSTDIST_AVER_FACTOR * averGhostDist; // normalize the ghostEval to our interval if (minGhostDist <= Globals.EVAL_GHOST_BOOST) ghostEval = normalize (gd, Globals.DISTANCE_MAX ,Globals.EVAL_MIN+Globals.EVAL_GHOSTBOOST_OFFSET, Globals.EVAL_MAX); else if (minGhostDist <= Globals.EVAL_GHOST_TERRITORY) ghostEval = normalize (gd, Globals.DISTANCE_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX - Globals.EVAL_GHOSTTERRITORY_OFFSET); else ghostEval = normalize (gd, Globals.DISTANCE_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX); /* * === Flag distances part === */ for (i=0 ; i= 0) { if (minFlagDist > flagDist[i]) minFlagDist = flagDist[i]; } } } // normalize the flagEval to our interval if (minFlagDist <= Globals.EVAL_FLAG_BOOST) flagEval = normalize ((double)Globals.FLAGDIST_MAX - minFlagDist, Globals.FLAGDIST_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX+Globals.EVAL_FLAGBOOST_OFFSET); else flagEval = normalize ((double)Globals.FLAGDIST_MAX - minFlagDist, Globals.FLAGDIST_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX); /* * === Torus borders distances part === */ for (i=0 ; i= 0) { if (minTorusDist > torusDist[i]) minTorusDist = torusDist[i]; } } // normalize the flagEval to our interval torusEval = normalize (Globals.BORDERDIST_MAX-minTorusDist, Globals.BORDERDIST_MAX, Globals.EVAL_MIN, Globals.EVAL_MAX); /* * === Mix up the values and return the normalized value back to caller === */ eval = ghostEval * Globals.EVAL_GHOSTDIST_FACTOR + flagEval * Globals.EVAL_FLAGDIST_FACTOR + torusEval * Globals.EVAL_TORUSDIST_FACTOR; return normalize (eval, Globals.EVAL_FINAL_MAX, Globals.EVAL_FINAL_MIN, Globals.EVAL_FINAL_MAX); } }