package simulator; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.io.*; import java.util.Formatter; /* * Customer.java * Created on 2005/1/4 * */ /** * Customer - This class defines the basic customer. It is abstract because * it does not represent any particular decision strategy. It simply provides * the framework for handling clock ticks, filling wish lists and actions to take * when getting on/off rides. * * @author Ping * */ public abstract class Customer implements ClockedThing, Cloneable { public static String CUSTOMER_TYPE = "Default"; // This enumeration defines the possible strategies for deciding when to // exit the park. // EMPTY_WISH_LIST - Exit the park when the wish list is empty // SET_TIME - There is a set time when the user should go home public static enum ExitStrategy { // We can not only create enums, but // we are able to associate data and methods with // each enumerated value // The values within the parens are passed to the constructor // of the enum type (see below) EMPTY_WISH_LIST ("Empty Wish List"), SET_TIME ("Set Time"), PARK_CLOSING_TIME ("Park Closing Time"); // The string representation of the enum name. // This is for comparing against string input from files public String type; // Constructor ExitStrategy(String s) { type = s; } } // This is just a simple structure that contains // the record of a visit to a particular ride protected class RideEntry { public String rideName; public Time enteredLine; public Time enteredRide; public Time exitedRide; public RideEntry(String name) { this.rideName = name; enteredLine = null; enteredRide = null; exitedRide = null; } public RideEntry(String name, Time timeEnteredLine, Time timeEnteredRide, Time timeExitedRide) { rideName = name; try { enteredLine = (Time) timeEnteredLine.clone(); enteredRide = (Time) timeEnteredRide.clone(); exitedRide = (Time) timeExitedRide.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } } public RideEntry(RideEntry copy) { rideName = copy.rideName; try { enteredLine = (Time) copy.enteredLine.clone(); enteredRide = (Time) copy.enteredRide.clone(); exitedRide = (Time) copy.exitedRide.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } } } // This constant defines how many rides we will refill // the wish list by. When refilling, it will grab the closest // number of rides specified by this constant protected static final int CLOSEST_RIDES = 15; // This defines the possible state that the customer may be in // IN_LINE - The customer is in line for a ride // IN_TRANSIT - The customer is currently walking towards a destination // ON_RIDE - The customer is currently on a ride (or being loaded on one) // WAITING - The customer has not decided what to do yet // ARRIVED - The customer has arrived at the destination and waiting // for further instructions protected enum CustomerStatus {IN_LINE, IN_TRANSIT, ON_RIDE, WAITING, ARRIVED} // The name of the customer protected String name; // This hash map stores the name of each ride with a corresponding // boolean represeting whether or not the ride has been visited. protected HashMap ridesVisited; // This queue stores the ride entries of the rides the customer has been // on. We don't need to be able to access any particular ride. Used for // printing the statistics of the ride visits in the order that they were // visited. protected MyQueue rideList; // This hashtable stores the original wish list. The key is the ride name // and the value is the vector of the priorities of desired visits to // the ride. protected Hashtable> originalWishList; // This stores the number of rides on the original wish list that // was visited. protected int numOfOriginalRidesVisited; // This is the actual agenda with the rides ordered by priority protected MyPriorityQueue agenda; // This stores the current ride that the customer is on protected RideEntry currRide; // This specifies what the customer is currently doing protected CustomerStatus customerStatus; // These variables store where the customer is and where he is going protected Location currLocation; // this stores the attraction that the customer is heading towards. // If the customer is headed towards the exit, this is set to null protected Attraction destinationAttraction; protected Location destination; // This keeps track of the number of minutes remaining until // the customer reaches the destination protected int travelTimeRemaining; // This stores the total number of minutes spent in the park protected int totalTimeInPark; // This stores the total number of minutes spent in line protected int totalTimeInLine; // This stores the total number of minutes spent on rides protected int totalRideTime; // this stores the current time // Note: This should just be a reference and not touched. protected Time time; // This is the time that the customer entered the park protected Time timeEntered; // This is the desired time for the customer to exit protected Time desiredExitTime; // Keep a reference of the park and the logger protected AmusementPark park; protected Logger logger; protected boolean isInPark; protected ExitStrategy exitStrategy; /** * Note that the desiredExitTime may be null if the exit * strategy is either when the wish list is empty or when * the park closes * * @param name * @param wishList * @param exitStrategy * @param timeEntered * @param desiredExitTime * @param logger */ public Customer(String name, Collection> wishList, ExitStrategy exitStrategy, Time timeEntered, Time desiredExitTime) { this.name = name; this.exitStrategy = exitStrategy; this.timeEntered = timeEntered; this.desiredExitTime = desiredExitTime; rideList = new MyQueue(); this.agenda = new MyPriorityQueue(); park = AmusementParkSimulation.park; // put all of the ride names in visitedRides, each mapping to // false because they have not been visited Iterator rides = park.getAllRides(); Attraction nextRide; ridesVisited = new HashMap(AttractionReader.numAttractions); while(rides.hasNext()) { nextRide = rides.next(); ridesVisited.put(nextRide.getName(), false); } // Populate the wish list originalWishList = new Hashtable>(); for(PrioritizedElement e : wishList) { ArrayList visits = originalWishList.get(e.getElement()); if(visits == null) { visits = new ArrayList(); } // Taking prioritized element e from the wishlist given: // Integeter priority - add its priority to the vector of visits // String name - then add that vector to the original wishlist, under the ride name visits.add(e.getPriority()); originalWishList.put(e.getElement(), visits); } } /* * This function fills the agenda with rides from the park. * It first fills the list with rides that the customer has not * been to, then fills it up with the closest X rides */ protected void fillAgenda() { Iterator rides = park.getAllRides(); String rideName; Attraction ride; while(rides.hasNext()) { ride = rides.next(); rideName = ride.getName(); if(!ridesVisited.get(rideName)) { // have not visited this ride agenda.enqueue(ride, currLocation.distance(ride.getEntrance())); } } if(agenda.isEmpty()) { // need more rides to fill the agenda, so have to grab // the closest rides MyPriorityQueue, Integer> closeRides = new MyPriorityQueue< PrioritizedElement, Integer>(); rides = park.getAllRides(); while(rides.hasNext()) { // sort all the rides by their distances, closest rides first ride = rides.next(); int distance = currLocation.distance(ride.getEntrance()); closeRides.enqueue(new PrioritizedElement( ride, distance), distance); } while(!closeRides.isEmpty() && agenda.size() < CLOSEST_RIDES) { // now grab the closest few rides PrioritizedElement e = closeRides.dequeue(); agenda.enqueue(e.getElement(), e.getPriority()); } } } /** * This function should be called when the customer first arrives * at a particular ride * @param rideName */ protected void arrivedAtRide(Attraction ride) { numOfOriginalRidesVisited++; currRide = new RideEntry(ride.getName()); try { currRide.enteredLine = (Time) time.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } // This may not be necessary, but just for clarity currLocation = ride.getEntrance(); travelTimeRemaining = 0; boolean isSuccessful = ride.enqueue(this); if(!isSuccessful) { // the ride is closed, so pick another ride agenda.dequeue(); currRide = null; customerStatus = CustomerStatus.WAITING; } else { // tell the park that the customer has arrived // this removes the customer from the transit list park.customerArrivedAtDestination(this); customerStatus = CustomerStatus.IN_LINE; // log the event logger.arriveAtAttraction(this, ride); } } /** * This function determines if it is time for the customer to * head towards the exit. * @return True if it is time to head for the exit, false otherwise. */ @SuppressWarnings("incomplete-switch") protected boolean isTimeToGoHome() { boolean timeToGo = false; // the current time is after when the park closes if(time.after(park.getClosingTime())) { timeToGo = true; } else { switch(exitStrategy) { case EMPTY_WISH_LIST: if(agenda.isEmpty()) { timeToGo = true; } break; case SET_TIME: // the current time is after the desired go home time if(time.after(desiredExitTime)) { timeToGo = true; } break; } } return timeToGo; } /** * This function sends the customer towards the exit * @param exit */ protected void goToExit() { Location exit = park.getExit(); // time to go home, set the destination as the exit destination = exit; destinationAttraction = null; travelTimeRemaining = currLocation.distance(exit); customerStatus = CustomerStatus.IN_TRANSIT; logger.nextAttraction(this, null); } /** * This function should adjust the priority queue appropriately. * This adjustment is based on the specific customer implementation. * This function will also fill up the wish list if it is currently empty. * * Note all subclasses should call this function before doing anything else. * */ protected void selectRide() { if(agenda.isEmpty()) { fillAgenda(); } } /** * @return Returns the name of the customer */ public String getName() { return name; } /** * @return Returns the minutes remaining before reaching * the current destination. */ public int getTravelTimeRemaining() { return travelTimeRemaining; } /** * @return Returns the customer's status. */ public CustomerStatus getCustomerStatus() { return customerStatus; } /** * @return Returns the timeEntered. */ public Time getTimeEntered() { return timeEntered; } /** * @return Returns the total number of minutes spent on rides */ public int getTotalRideTime() { return totalRideTime; } /** * @return Returns the total number of minutes spent in the park */ public int getTotalTimeInPark() { return totalTimeInPark; } /** * @return Returns the total number of minutes spent in waiting line. */ public int getTotalTimeInLine() { return totalTimeInLine; } public void enterPark(AmusementPark newPark, Time newTime, Location newEntrance, Logger newLogger) { park = newPark; time = newTime; logger = newLogger; currLocation = newEntrance; currRide = null; customerStatus = CustomerStatus.WAITING; travelTimeRemaining = 0; totalTimeInPark = 0; totalTimeInLine = 0; totalRideTime = 0; numOfOriginalRidesVisited = 0; isInPark = true; // now load up the agenda Set>> entries = originalWishList.entrySet(); for(Map.Entry> e : entries) { // iterate through each entry in the original wish list // Note: in the agenda, we store a reference to // the actual ride as opposed to just the string name of the // ride. This is for efficiency reasons since we need to // access the rides' time estimation, entrance location and other // functions. Attraction a = park.getAttraction(e.getKey()); if(a == null) { System.err.println("* Encountered unknown ride " + e.getKey()); } else { ArrayList visits = e.getValue(); for(Integer priority : visits) { agenda.enqueue(a, priority); } } } destination = null; destinationAttraction = null; } /** * This function is called when the park is closing. Any customer * thats not in line or already on a ride should start heading towards the * exit * */ public void parkClosing() { if(customerStatus == CustomerStatus.IN_TRANSIT || customerStatus == CustomerStatus.WAITING) { goToExit(); } } /** * This function is called when the customer exits a ride * @param ride The ride the customer just exited from. */ public void exitRide(Attraction ride) { try { currRide.exitedRide = (Time) time.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } ridesVisited.put(currRide.rideName, true); rideList.enqueue(currRide); // remove the ride that we just got out of from the agenda agenda.dequeue(); currRide = null; currLocation = ride.getExit(); customerStatus = CustomerStatus.WAITING; destinationAttraction = null; // The customer has exited the ride and should be put on the // in transit list. park.customerInTransit(this); // log the event that the customer exited the ride logger.exitAttraction(this, ride); } /** * This function is called when the user boards a ride * @param Ride */ public void boardRide(Attraction ride) { try { currRide.enteredRide = (Time) time.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } customerStatus = CustomerStatus.ON_RIDE; // log that the customer got on the ride logger.boardAttraction(this, ride); } /** * This function is called every clock tick * @return boolean - This function will return true if the the customer is still in the park * and should be notified of the clock ticks. When it return false, that signifies that the * customer has left the park */ public boolean onClockTick() { totalTimeInPark++; switch(customerStatus) { case IN_LINE: // the customer is in line. Currently all he can do is wait. totalTimeInLine++; break; case ON_RIDE: totalRideTime++; break; // We handle both in transit and waiting together. // If the customer is waiting, he/she makes a decision // and immediately starts the travel. case IN_TRANSIT: case WAITING: { // sends the customer to the exit if he/she is not // already heading towards it if(isTimeToGoHome() && (destination == null || !destination.equals(park.getExit()))) { goToExit(); } else { if(customerStatus == CustomerStatus.WAITING) { // re-shuffle the agenda such that the // ride we want to go to is in front selectRide(); Attraction ride = agenda.peek(); destination = ride.getEntrance(); destinationAttraction = ride; travelTimeRemaining = currLocation.distance(destination); customerStatus = CustomerStatus.IN_TRANSIT; logger.nextAttraction(this, destinationAttraction); } // the customer has moved travelTimeRemaining--; currLocation = currLocation.moveTowards(destination); if(travelTimeRemaining <= 0) { // the customer has arrived at the destination // Don't process now, wait till the next clock cycle customerStatus = CustomerStatus.ARRIVED; } } break; } case ARRIVED: { currLocation = destination; // arrived at some destination if(destinationAttraction == null) { // got to the exit isInPark = false; } else { // got to some ride arrivedAtRide(destinationAttraction); } break; } } return isInPark; } public Object clone() throws CloneNotSupportedException { Customer clone = (Customer) super.clone(); // full deep clone of the hashmap clone.ridesVisited = new HashMap(); Set keys = this.ridesVisited.keySet(); for(String key : keys) { Boolean isVisited = this.ridesVisited.get(key); clone.ridesVisited.put(key, isVisited); } // This clone is not a full deep clone, but it is ok. // Strings are not modifiable, and we are only reading from // this list, never modifying it. clone.originalWishList = new Hashtable>(this.originalWishList); clone.agenda = this.agenda.clone(); return clone; } /** * This function prints out the various stats of the customer, * such as the rides visited and the total amount of time spent * on various activities. * */ public void printStats() { System.out.println("-------------------------------------------------------------------------------"); System.out.println("Customer: " + name + "\n"); // Establish format to be used when printing String format = "%-30s | %-13s | %-13s | %-13s%n"; System.out.printf(format, "Ride Name", "Time Entered", "Time Boarded", "Time Exited\n"); // Print every ride entry in the queue rideList using the format from above. // Because we are using a queue, we have kept the rides ordered by the time // the customer went on them, and printing them in this order. MyQueue rideListCopy = rideList.clone(); while(!rideListCopy.isEmpty()) { RideEntry r = rideListCopy.dequeue(); System.out.printf(format, r.rideName, r.enteredLine.toString(), r.enteredRide.toString(), r.exitedRide.toString()); } // Print additional statistics about overall time spent doing different activities. System.out.printf( "\n%5d minutes total time in park\n" + "%5d minutes spent in line\n" + "%5d minutes spent on rides\n", totalTimeInPark, totalTimeInLine, totalRideTime); // Calculate percentage of rides on wishlist visited, then print the percentage. float visitedWishListRatio = 0; if(originalWishList.size() != 0) { visitedWishListRatio = (float)numOfOriginalRidesVisited / originalWishList.size(); } System.out.printf("%nPercentage of rides on wishlist visited: %.2f%%%n%n", visitedWishListRatio * 100); System.out.println("-------------------------------------------------------------------------------"); } public void writeStatsToFile(FileWriter outfile) { Formatter formatter = new Formatter(outfile); String format = "%-30s | %-15s | %-15s | %-15s%n"; formatter.format("--------------------------------------------------------------------------------%n"); formatter.format("%nCustomer: %s%n%n", name); formatter.format(format, "Ride Name", "Time Entered", "Time Boarded", "Time Exited"); formatter.format("%n"); MyQueue rideListCopy = rideList.clone(); while(!rideListCopy.isEmpty()) { RideEntry r = rideListCopy.dequeue(); formatter.format(format, r.rideName, r.enteredLine.toString(), r.enteredRide.toString(), r.exitedRide.toString()); } formatter.format("%n%5d minutes total time in park", totalTimeInPark); formatter.format("%n%5d minutes spent in line", totalTimeInLine); formatter.format("%n%5d minutes spent on rides%n", totalRideTime); float visitedWishListRatio = 0; if(originalWishList.size() != 0) { visitedWishListRatio = (float)numOfOriginalRidesVisited / originalWishList.size(); } formatter.format("%nPercentage of rides on wishlist visited: %.2f%%%n%n", visitedWishListRatio * 100); } }