diff --git a/Contest/Assignment/resources/log4j2.xml b/Contest/Assignment/resources/log4j2.xml new file mode 100644 index 0000000..1c946b1 --- /dev/null +++ b/Contest/Assignment/resources/log4j2.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/Main.java b/Contest/Assignment/src/org/togetherjava/event/elevator/Main.java index e9e4159..4c1f0ca 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/Main.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/Main.java @@ -1,10 +1,13 @@ package org.togetherjava.event.elevator; -import org.togetherjava.event.elevator.elevators.Elevator; -import org.togetherjava.event.elevator.humans.Human; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.togetherjava.event.elevator.simulation.MoreSimulations; import org.togetherjava.event.elevator.simulation.Simulation; +import org.togetherjava.event.elevator.util.LogUtils; public final class Main { + private static final Logger logger = LogManager.getLogger(); /** * Starts the application. *

@@ -19,28 +22,47 @@ public static void main(final String[] args) { // Eventually try out the randomly generated systems. If you want to debug a problem you encountered // with one of them, note down the seed that it prints at the beginning and then use the variant that takes this seed. // That way, it will generate the same system again, and you can repeat the test. - Simulation simulation = Simulation.createSingleElevatorSingleHumanSimulation(); - // Simulation simulation = Simulation.createSimpleSimulation(); - // Simulation simulation = Simulation.createRandomSimulation(5, 50, 10); - // Simulation simulation = Simulation.createRandomSimulation(putDesiredSeedHere, 5, 50, 10); +// Simulation simulation = Simulation.createSingleElevatorSingleHumanSimulation(); +// Simulation simulation = Simulation.createSimpleSimulation(); +// Simulation simulation = Simulation.createRandomSimulation(5, 50, 10); +// Simulation simulation = Simulation.createRandomSimulation(putDesiredSeedHere, 5, 50, 10); + Simulation simulation = MoreSimulations.createMegaSimulation(); +// Simulation simulation = MoreSimulations.createMegaAdvancedSimulation(); +// Simulation simulation = MoreSimulations.createSimpleFailingSimulation(); +// Simulation simulation = MoreSimulations.createSimpleSucceedingSimulation(); +// Simulation simulation = MoreSimulations.createSimpleThreeStepSimulation(); +// Simulation simulation = Simulation.createRandomSimulation(1, 100, 1000, 50); +// Simulation simulation = MoreSimulations.createSimplePaternosterSimulation(); +// Simulation simulation = MoreSimulations.createSimpleAdvancedSimulation(); +// Simulation simulation = MoreSimulations.createNotMarkoSimulation(); +// Simulation simulation = MoreSimulations.createGoodPaternosterSimulation(); - simulation.printSummary(); + if (simulation.shouldPrintSummary()) { + simulation.printSummary(); + } - System.out.println("Starting simulation..."); + long simulationStart = System.nanoTime(); + logger.info("Starting simulation..."); simulation.start(); - simulation.prettyPrint(); + if (simulation.shouldPrint()) { + simulation.prettyPrint(); + } while (!simulation.isDone()) { - System.out.println("\tSimulation step " + simulation.getStepCount()); - simulation.step(); - simulation.prettyPrint(); + logger.info("Simulation step " + simulation.getStepCount()); + LogUtils.measure("Simulation step", simulation::step); + if (simulation.shouldPrint()) { + simulation.prettyPrint(); + } + simulation.printCurrentStatistics(); if (simulation.getStepCount() >= 100_000) { throw new IllegalStateException("Simulation aborted. All humans should have arrived" + " by now, but they did not. There is likely a bug in your code."); } } - System.out.println("Simulation is done."); + long simulationEnd = System.nanoTime(); + logger.info("Simulation completed in %.3f seconds.%n".formatted((simulationEnd - simulationStart) / 1e9)); simulation.printResult(); } diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/CommonElevator.java b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/CommonElevator.java new file mode 100644 index 0000000..212f7d8 --- /dev/null +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/CommonElevator.java @@ -0,0 +1,225 @@ +package org.togetherjava.event.elevator.elevators; + +import org.jetbrains.annotations.Nullable; +import org.togetherjava.event.elevator.humans.Passenger; +import org.togetherjava.event.elevator.util.CollectionUtils; + +import java.util.ArrayDeque; +import java.util.Comparator; +import java.util.Deque; + +/** + * A single elevator that can serve a given amount of floors. + *

+ * This elevator can take floor requests from either humans or the elevator system itself. + * The elevator will eventually move towards the requested floor and transport humans to their destinations. + */ +public final class CommonElevator extends Elevator { + /** + * Creates a new elevator. + * + * @param minFloor the minimum floor that the elevator can serve, must be greater than or equal to 1. + * @param floorsServed the amount of floors served in total by this elevator, must be greater than or equal to 2. + * Together with the minFloor this forms a consecutive range of floors with no gaps in between. + * @param currentFloor the floor the elevator starts at, must be within the defined range of floors served by the elevator + */ + public CommonElevator(int minFloor, int floorsServed, int currentFloor) { + super(minFloor, floorsServed, currentFloor); + } + + @Override + public boolean canRequestDestinationFloor() { + return true; + } + + /** + * This represents a human or the elevator system + * itself requesting this elevator to eventually move to the given floor. + * The elevator is supposed to memorize the destination in a way that + * it can ensure to eventually reach it.
+ * + * If this method is called by a passenger, it passes itself as the second parameter, + * which helps the elevator selection algorithm. + */ + @Override + public synchronized void requestDestinationFloor(int destinationFloor, @Nullable Passenger passenger) { + rangeCheck(destinationFloor); + + if (passenger != null) { + potentialTargets.remove(passenger); + } + + // Let's check if the work queue already contains the desired floor + if (!willVisitFloor(destinationFloor)) { + addTargetFloor(destinationFloor); + } + } + + /** + * Add a floor to the task queue of this elevator. Either as a new element at the end of the queue, + * or by modifying the last element if it's possible to do so without changing elevator semantics.
+ * After that, try to find an optimal path through all targets and repopulate the targets' collection, if necessary.
+ * It is expected that this method is called in a synchronized context. + */ + private void addTargetFloor(int targetFloor) { + targets.add(targetFloor); + + var optimalTargetsRecord = rearrangeTargets(currentFloor, targets); + var optimalTargets = compressTargets(optimalTargetsRecord.targets()); + + if (!CollectionUtils.equals(targets, optimalTargets)) { + logger.debug(() -> "Elevator %d on floor %d is rearranging targets after receiving new floor %d, would be %s, new queue %s, potential targets %s, queue length in turns is %d" + .formatted(id, currentFloor, targetFloor, targets, optimalTargets, potentialTargets.values(), optimalTargetsRecord.cost())); + targets.clear(); + targets.addAll(optimalTargets); + } else { + logger.debug(() -> "Elevator %d on floor %d has added floor %d to the queue, the queue is now %s, potential targets %s, queue length in turns is %d" + .formatted(id, currentFloor, targetFloor, targets, potentialTargets.values(), optimalTargetsRecord.cost())); + } + } + + /** + * A recursive method that tries to find an optimal path from a specified starting point through all the targets + * in the specified deque.
+ * It is assumed that the input deque is in synchronized context and does not contain duplicates.
+ * No guarantee is made that the deque reference contained in the returned record will be the same + * or different as the input deque. However, if it's the same, it won't get mutated. + */ + private static OptimalTargetsAndCost rearrangeTargets(int from, Deque targets) { + int size = targets.size(); + if (size == 0) { + // No targets - no need to move + return new OptimalTargetsAndCost(from, targets, 0); + } else if (size == 1) { + // One target - calculate distance to it + return new OptimalTargetsAndCost(from, targets, Math.abs(targets.getFirst() - from)); + } else if (size == 2) { + // Two targets - simple enough to do a manual calculation on them + int e1 = targets.getFirst(); + int e2 = targets.getLast(); + // c1 represents cost as is, c2 represents cost if the two elements were flipped + int c1 = Math.abs(e2 - e1) + Math.abs(e1 - from); + int c2 = Math.abs(e1 - e2) + Math.abs(e2 - from); + int cost; + var newDeque = new ArrayDeque(2); + if (c2 < c1) { + // Flip the two elements + cost = c2; + newDeque.addFirst(e2); + newDeque.addLast(e1); + } else { + cost = c1; + newDeque.addFirst(e1); + newDeque.addLast(e2); + } + return new OptimalTargetsAndCost(from, newDeque, cost); + } else { + // Anything with N targets, where N > 2, gets decomposed into N recursive method calls, + // where each element becomes the new starting point and the rest act as new targets + return targets.stream() + // First off, decompose and do recursive calls + .map(nextFrom -> { + var recursiveTargets = new ArrayDeque(targets.size() - 1); + boolean encounteredSameElement = false; + for (Integer e : targets) { + if (!e.equals(nextFrom)) { + recursiveTargets.addLast(e); + } else if (!encounteredSameElement) { + encounteredSameElement = true; + } else { + throw new RuntimeException("Input queue contains a duplicate"); + } + } + return rearrangeTargets(nextFrom, recursiveTargets); + }) + // Then, filter out the result with the smallest potential cost + .min(Comparator.comparingInt(r -> r.cost() + Math.abs(r.from() - from))) + // Finally, perform the object creation since there's just one element left + .map(r -> { + var oldTargets = r.targets(); + var newTargets = new ArrayDeque(oldTargets.size() + 1); + newTargets.addAll(oldTargets); + newTargets.addFirst(r.from()); + return new OptimalTargetsAndCost(from, newTargets, r.cost() + Math.abs(r.from() - from)); + }) + .orElseThrow(() -> new RuntimeException("Target sorting is functioning incorrectly")); + } + } + + /** + * A record that holds intermediate or terminal search results. + * + * @param from starting point + * @param targets a deque holding points to visit, sorted in the optimal order + * @param cost the amount of turns it would take to visit all points + */ + private record OptimalTargetsAndCost(int from, Deque targets, int cost) {} + + /** + * Compress a given target deque, returning a new deque with removed unnecessary intermediate targets.
+ * No guarantee is made that the deque reference contained in the returned record will be the same + * or different as the input deque. However, if it's the same, it won't get mutated.
+ * It is expected that this method is called in a synchronized context. + */ + private Deque compressTargets(Deque deque) { + int size = deque.size(); + if (size <= 1) { + // There is nothing to compress + return deque; + } + boolean wasCompressed = false; + var newDeque = new ArrayDeque(size); + var itr = deque.iterator(); + int first = currentFloor; + int second = itr.next(); + newDeque.addLast(second); + int third; + while (itr.hasNext()) { + third = itr.next(); + if (Integer.compare(first, second) == Integer.compare(second, third)) { + // Compression takes place, so we do not advance first + newDeque.removeLast(); + wasCompressed = true; + } else { + // No compression, advance first + first = second; + } + newDeque.addLast(third); + second = third; + } + if (wasCompressed) { + logger.debug(() -> "Elevator queue was compressed, start at %d, previous %s, new %s".formatted(currentFloor, deque, newDeque)); + } + return newDeque; + } + + @Override + protected void modifyTargetsOnArrival() { + targets.remove(); + } + + /** + * @return whether this elevator is currently on the specified floor + * or will at some point visit that floor before all its tasks are done. + */ + public boolean willVisitFloor(int floor) { + if (!canServe(floor)) { + return false; + } + + if (floor == currentFloor) { + return true; + } + + int min = currentFloor; + int max = currentFloor; + for (int nextTarget : targets) { + min = Math.min(min, nextTarget); + max = Math.max(max, nextTarget); + if (min <= floor && floor <= max) { + return true; + } + } + return false; + } +} diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/Elevator.java b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/Elevator.java index 51333b2..6e88fc7 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/Elevator.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/Elevator.java @@ -1,21 +1,52 @@ package org.togetherjava.event.elevator.elevators; -import java.util.StringJoiner; +import lombok.Getter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; +import org.togetherjava.event.elevator.humans.ElevatorListener; +import org.togetherjava.event.elevator.humans.Passenger; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** - * A single elevator that can serve a given amount of floors. + * Common superclass for all elevators. *

- * An elevator can take floor requests from either humans or the elevator system itself. - * The elevator will eventually move towards the requested floor and transport humans to their destinations. + * An elevator may be able to take floor requests from either humans or the elevator system itself. In that case, + * the elevator will eventually move towards the requested floor and transport humans to their destinations. */ -public final class Elevator implements ElevatorPanel { +public abstract class Elevator implements ElevatorPanel { + protected static final Logger logger = LogManager.getLogger(); private static final AtomicInteger NEXT_ID = new AtomicInteger(0); - private final int id; - private final int minFloor; - private final int floorsServed; - private int currentFloor; + @Getter protected final int id; + @Getter protected final int minFloor; + @Getter protected final int maxFloor; + + /** + * Currently boarded passengers. + */ + @Getter protected final Collection passengers = ConcurrentHashMap.newKeySet(); + + /** + * Target queue which holds floors that this elevator must visit. + */ + protected final Deque targets = new ArrayDeque<>(); + + /** + * A map which holds guesses of next potential targets, populated by the {@link ElevatorSystem}. + * This is the whole reason to make the system aware of passengers who make elevator calls. + * It's not necessary, but is believed to ever so slightly improve the elevator selection algorithm + * over just a simple set/list of potential targets without owners. + */ + protected final Map potentialTargets = new LinkedHashMap<>(); + @Getter protected int currentFloor; + /** + * An elevator should be aware of the system it belongs to. + */ + protected ElevatorSystem elevatorSystem; /** * Creates a new elevator. @@ -26,65 +57,242 @@ public final class Elevator implements ElevatorPanel { * @param currentFloor the floor the elevator starts at, must be within the defined range of floors served by the elevator */ public Elevator(int minFloor, int floorsServed, int currentFloor) { - if (minFloor <= 0 || floorsServed < 2) { - throw new IllegalArgumentException("Min floor must at least 1, floors served at least 2."); + if (minFloor < 1) { + throw new IllegalArgumentException("Minimum floor must at least 1, got " + minFloor); + } + if (floorsServed < 2) { + throw new IllegalArgumentException("Amount of served floors must be at least 2, got " + floorsServed); } - if (currentFloor < minFloor || currentFloor >= minFloor + floorsServed) { - throw new IllegalArgumentException("The current floor must be between the floors served by the elevator."); + int maxFloor = minFloor + floorsServed - 1; + if (currentFloor < minFloor || maxFloor < currentFloor) { + throw new IllegalArgumentException("The current floor for this elevator must be between %d and %d, got %d".formatted(minFloor, maxFloor, currentFloor)); } this.id = NEXT_ID.getAndIncrement(); this.minFloor = minFloor; + this.maxFloor = maxFloor; this.currentFloor = currentFloor; - this.floorsServed = floorsServed; } - @Override - public int getId() { - return id; + public int getFloorsServed() { + return maxFloor - minFloor + 1; } - public int getMinFloor() { - return minFloor; + public int getTaskCount() { + return targets.size(); } - public int getFloorsServed() { - return floorsServed; + void setElevatorSystem(ElevatorSystem elevatorSystem) { + if (elevatorSystem == null) { + throw new IllegalArgumentException("Elevator system must not be null"); + } + this.elevatorSystem = elevatorSystem; } - @Override - public int getCurrentFloor() { - return currentFloor; + public void boardPassenger(Passenger passenger) { + if (elevatorSystem == null) { + throw new IllegalStateException("Elevator is not connected to an elevator system"); + } + if (passengers.contains(passenger)) { + throw new IllegalArgumentException("Attempt to add a passenger which is already in the elevator"); + } + passengers.add(passenger); + elevatorSystem.passengerEnteredElevator(passenger); + } + + public void removePassenger(Passenger passenger, boolean arrived) { + if (elevatorSystem == null) { + throw new IllegalStateException("Elevator is not connected to an elevator system"); + } + if (!passengers.contains(passenger)) { + throw new IllegalArgumentException("Attempt to remove a passenger which is not in the elevator"); + } + passengers.remove(passenger); + elevatorSystem.passengerLeftElevator(passenger, arrived); } + /** + * Whether this elevator accepts requests to move to a floor. + * For example, a paternoster elevator does not, because his movement pattern is predetermined forever. + * @see #requestDestinationFloor(int, Passenger) + */ + public abstract boolean canRequestDestinationFloor(); + + /** + * This represents a human or the elevator system + * itself requesting this elevator to eventually move to the given floor. + * The elevator is supposed to memorize the destination in a way that + * it can ensure to eventually reach it. + * + * @throws UnsupportedOperationException if the operation is not supported by this elevator + * @see #canRequestDestinationFloor() + */ @Override - public void requestDestinationFloor(int destinationFloor) { - // TODO Implement. This represents a human or the elevator system - // itself requesting this elevator to eventually move to the given floor. - // The elevator is supposed to memorize the destination in a way that - // it can ensure to eventually reach it. - System.out.println("Request for destination floor received"); + public abstract void requestDestinationFloor(int destinationFloor, @Nullable Passenger passenger); + + /** + * Add a potential future target for this elevator. Improves the elevator selection algorithm. + */ + synchronized void addPotentialTarget(int potentialTarget, ElevatorListener listener) { + if (!potentialTargets.containsKey(listener) && !potentialTargets.containsValue(potentialTarget)) { + potentialTargets.put(listener, clampFloor(potentialTarget)); + logger.debug(() -> "Elevator %d on floor %d has added potential target %d, the queue is now %s, potential targets %s".formatted(id, currentFloor, potentialTarget, targets, potentialTargets.values())); + } } + /** + * Essentially there are three possibilities: + *

+ * The elevator is supposed to move in a way that it will eventually reach + * the floors requested by Humans via {@link #requestDestinationFloor(int, Passenger)}, ideally "fast" but also "fair", + * meaning that the average time waiting (either in corridor or inside the elevator) + * is minimized across all humans. + * It is essential that this method updates the currentFloor field accordingly. + */ public void moveOneFloor() { - // TODO Implement. Essentially there are three possibilities: - // - move up one floor - // - move down one floor - // - stand still - // The elevator is supposed to move in a way that it will eventually reach - // the floors requested by Humans via requestDestinationFloor(), ideally "fast" but also "fair", - // meaning that the average time waiting (either in corridor or inside the elevator) - // is minimized across all humans. - // It is essential that this method updates the currentFloor field accordingly. - System.out.println("Request to move a floor received"); + if (!targets.isEmpty()) { + int target = targets.element(); + if (currentFloor < target) { + currentFloor++; + } else if (currentFloor > target) { + currentFloor--; + } else { + throw new IllegalArgumentException("Elevator has current floor as next target, this is a bug"); + } + if (currentFloor == target) { + // We arrived at the next target + modifyTargetsOnArrival(); + } + } else if (!potentialTargets.isEmpty()) { + logger.debug(() -> "Elevator %d on floor %d is idling and will clear its potential targets".formatted(id, currentFloor)); + potentialTargets.clear(); + } } + /** + * The action to perform once the elevator reaches a target. + */ + protected abstract void modifyTargetsOnArrival(); + @Override public synchronized String toString() { - return new StringJoiner(", ", Elevator.class.getSimpleName() + "[", "]").add("id=" + id) + return new StringJoiner(", ", getClass().getSimpleName() + "[", "]").add("id=" + id) .add("minFloor=" + minFloor) - .add("floorsServed=" + floorsServed) + .add("maxFloor=" + maxFloor) .add("currentFloor=" + currentFloor) + .add("passengers=" + passengers.size()) .toString(); } + + @Override + public int hashCode() { + return id; + } + + /** + * @return whether this elevator can serve all the specified floors. + */ + public boolean canServe(int... floors) { + for (int floor : floors) { + if (floor < minFloor || floor > maxFloor) { + return false; + } + } + return true; + } + + /** + * @throws IllegalArgumentException if the specified floor cannot be served by this elevator. + */ + protected void rangeCheck(int floor) { + if (!canServe(floor)) { + throw new IllegalArgumentException("Elevator cannot serve floor %d, only %d to %d are available".formatted(floor, minFloor, maxFloor)); + } + } + + /** + * Returns a floor value clamped between max and min floors. + */ + protected int clampFloor(int floor) { + return Math.max(Math.min(floor, maxFloor), minFloor); + } + + /** + * @return whether this elevator is currently on the specified floor + * or will at some point visit that floor before all its tasks are done. + */ + public abstract boolean willVisitFloor(int floor); + + /** + * @return the minimum amount of turns it would take for this elevator to visit a specified sequence of floors + * (either indirectly by passing by or by creating new tasks), + * taking into account all potential targets of this elevator, + * or -1 if it's impossible or of the input array is empty + * @implNote a choice was made to use {@code int} over {@link OptionalInt OptionalInt} + * since the amount of turns cannot be negative + */ + public synchronized int turnsToVisit(int... floors) { + if (floors.length == 0) { + return -1; + } + + int count = 0; + int previousTarget = currentFloor; + + Collection allTargets = new ArrayList<>(targets.size() + potentialTargets.size()); + allTargets.addAll(targets); + allTargets.addAll(potentialTargets.values()); + Iterator targetItr = allTargets.iterator(); + Iterator floorItr = Arrays.stream(floors).iterator(); + + int nextFloor = floorItr.next(); + if (!canServe(nextFloor)) { + return -1; + } + + while (targetItr.hasNext()) { + int nextTarget = targetItr.next(); + + // While the next floor we're interested in lies on the path, + // we "chop off" part of the path, adding the length of that part to count + // we also advance the floor iterator, or return if the floor was last + while (previousTarget <= nextFloor && nextFloor <= nextTarget || previousTarget >= nextFloor && nextFloor >= nextTarget) { + count += Math.abs(nextFloor - previousTarget); + previousTarget = nextFloor; + if (floorItr.hasNext()) { + nextFloor = floorItr.next(); + if (!canServe(nextFloor)) { + return -1; + } + } else { + return count; + } + } + // If there are more floors remaining to check, add what's left of currently inspected path + count += Math.abs(nextTarget - previousTarget); + + previousTarget = nextTarget; + } + + // The floor currently at nextFloor is guaranteed to be unprocessed + count += Math.abs(nextFloor - previousTarget); + previousTarget = nextFloor; + // If after traversing the queue we haven't covered all floors that we wanted, + // simulate adding them to the queue + while (floorItr.hasNext()) { + nextFloor = floorItr.next(); + if (!canServe(nextFloor)) { + return -1; + } + + count += Math.abs(nextFloor - previousTarget); + previousTarget = nextFloor; + } + + return count; + } } diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/ElevatorPanel.java b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/ElevatorPanel.java index 386ec77..cfee5ab 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/ElevatorPanel.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/ElevatorPanel.java @@ -1,5 +1,8 @@ package org.togetherjava.event.elevator.elevators; +import org.jetbrains.annotations.Nullable; +import org.togetherjava.event.elevator.humans.Passenger; + /** * The system inside an elevator which provides information about the elevator and can be * used to request a destination floor. @@ -20,9 +23,60 @@ public interface ElevatorPanel { int getCurrentFloor(); /** - * Requesting the elevator to eventually move to the given destination floor, for humans to exit. + * Whether this elevator accepts requests to move to a floor. + * For example, a paternoster elevator does not, because his movement patters is predetermined forever. + * @see #requestDestinationFloor(int) + */ + boolean canRequestDestinationFloor(); + + /** + * Requesting the elevator to eventually move to the given destination floor, for humans to exit or enter.
+ * By default, behaves the same way as {@link #requestDestinationFloor(int, Passenger)} with + * {@code null} as the second argument. + * + * @param destinationFloor the desired destination, must be within the range served by this elevator + * @throws UnsupportedOperationException if the operation is not supported by this elevator + * @see #canRequestDestinationFloor() + */ + default void requestDestinationFloor(int destinationFloor) { + requestDestinationFloor(destinationFloor, null); + } + + // New methods below + + /** + * The lowest floor the elevator can travel to. + */ + int getMinFloor(); + + /** + * The highest floor the elevator can travel to. + */ + int getMaxFloor(); + + /** + * Ask the elevator to accept this passenger and to tell the system to remove it from the floor. + * @param passenger the passenger + */ + void boardPassenger(Passenger passenger); + + /** + * Ask the elevator to remove this passenger and to tell the system to add it to the floor, if necessary. + * @param passenger the passenger + * @param arrived whether the passenger has reached its final destination + * @throws IllegalArgumentException if the elevator does not have the specified passenger + */ + void removePassenger(Passenger passenger, boolean arrived); + + /** + * Requesting the elevator to eventually move to the given destination floor, for humans to exit or enter.
+ * Calling the method with {@code null} as second argument will mean that the request was made by the system. * * @param destinationFloor the desired destination, must be within the range served by this elevator + * @param passenger passenger that requested this operation, null if the operation was requested by the system itself, + * sanctioned by not marko + * @throws UnsupportedOperationException if the operation is not supported by this elevator + * @see #canRequestDestinationFloor() */ - void requestDestinationFloor(int destinationFloor); + void requestDestinationFloor(int destinationFloor, @Nullable Passenger passenger); } diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/ElevatorSystem.java b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/ElevatorSystem.java index fadfe56..0b223f4 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/ElevatorSystem.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/ElevatorSystem.java @@ -1,9 +1,11 @@ package org.togetherjava.event.elevator.elevators; import org.togetherjava.event.elevator.humans.ElevatorListener; +import org.togetherjava.event.elevator.humans.Passenger; +import org.togetherjava.event.elevator.util.ConcurrentUtils; +import org.togetherjava.event.elevator.util.LogUtils; -import java.util.ArrayList; -import java.util.List; +import java.util.*; /** * System controlling all elevators of a building. @@ -13,37 +15,185 @@ * the system can be made ready using {@link #ready()}. */ public final class ElevatorSystem implements FloorPanelSystem { - private final List elevators = new ArrayList<>(); - private final List elevatorListeners = new ArrayList<>(); + private final Collection elevators = new HashSet<>(); + private final NavigableMap floors = new TreeMap<>(); public void registerElevator(Elevator elevator) { elevators.add(elevator); + elevator.setElevatorSystem(this); + + for (int i = elevator.getMinFloor(); i <= elevator.getMaxFloor(); i++) { + floors.computeIfAbsent(i, Floor::new); + } + + floors.get(elevator.getCurrentFloor()).addElevator(elevator); } public void registerElevatorListener(ElevatorListener listener) { - elevatorListeners.add(listener); + if (listener instanceof Passenger passenger && passenger.wantsToMove()) { + floors.get(passenger.getCurrentFloor()).addPassenger(passenger); + } } /** - * Upon calling this, the system is ready to receive elevator requests. Elevators may now start moving. + * Upon calling this, the system is ready to receive elevator requests. Elevators may now start moving.
+ *
+ * Additionally, elevator arrival events are fired so that humans can immediately enter them. */ public void ready() { - elevatorListeners.forEach(listener -> listener.onElevatorSystemReady(this)); + LogUtils.measure("Elevator requests", () -> ConcurrentUtils.performTasksInParallel(floors.values(), f -> f.fireElevatorRequestEvents(this))); + LogUtils.measure("Elevator arrivals", () -> ConcurrentUtils.performTasksInParallel(floors.values(), Floor::fireElevatorArrivalEvents)); } + void passengerEnteredElevator(Passenger passenger) { + floors.get(passenger.getCurrentFloor()).removePassenger(passenger); + } + + void passengerLeftElevator(Passenger passenger, boolean arrived) { + if (!arrived) { + floors.get(passenger.getCurrentFloor()).addPassenger(passenger); + } + } + + /** + * This represents a human standing in the corridor, + * requesting that an elevator comes to pick them up for travel into the given direction. + * The system is supposed to make sure that an elevator will eventually reach this floor to pick up the human. + * The human can then enter the elevator and request their actual destination within the elevator. + * Ideally this has to select the best elevator among all which can reduce the time + * for the human spending waiting (either in corridor or in the elevator itself). + * + * @param atFloor the floor to pick up the human at, must be within the range served by the system + * @param desiredTravelDirection the direction the human wants to travel into, + * can be used for determination of the best elevator + * @param listener (NEW) the listener that requested the operation, only used to slightly + * improve the elevator selection algorithm, think an array of surveillance cameras + * and fingerprint sensors in buttons (scary to think about though), sanctioned by + * not marko + * @return the id of the elevator that was recommended by the system + */ @Override - public void requestElevator(int atFloor, TravelDirection desiredTravelDirection) { - // TODO Implement. This represents a human standing in the corridor, - // requesting that an elevator comes to pick them up for travel into the given direction. - // The system is supposed to make sure that an elevator will eventually reach this floor to pick up the human. - // The human can then enter the elevator and request their actual destination within the elevator. - // Ideally this has to select the best elevator among all which can reduce the time - // for the human spending waiting (either in corridor or in the elevator itself). - System.out.println("Request for elevator received"); + public int requestElevator(int atFloor, TravelDirection desiredTravelDirection, ElevatorListener listener) { + Elevator elevator; + + int target = calculateAverageTarget(atFloor, desiredTravelDirection) + .orElseThrow(() -> new IllegalArgumentException("Impossible to travel %s from floor %d".formatted(desiredTravelDirection.name(), atFloor))); + + synchronized (elevators) { + if (elevators.isEmpty()) { + throw new IllegalStateException("An elevator was requested, but there are none registered in the system"); + } + + elevator = elevators.stream() + .filter(e -> e.canServe(atFloor, atFloor + (desiredTravelDirection == TravelDirection.UP ? 1 : -1))) + .min((e1, e2) -> { + // Calculate the time it would take for both elevators to reach the request floor and the target + int t1 = e1.turnsToVisit(atFloor, target); + int t2 = e2.turnsToVisit(atFloor, target); + // If they both can actually reach it, just compare the numbers + if (t1 >= 0 && t2 >= 0) { + return Integer.compare(t1, t2); + } + // If one of them cannot reach it, prefer the one that can + else if (t1 >= 0) { + return -1; + } else if (t2 >= 0) { + return 1; + } + // At this point, the target lies outside the range of both elevators + // In this case, choose the elevator which boundaries lie closest to the target + return Integer.compare( + Math.min(Math.abs(e1.getMaxFloor() - target), Math.abs(e1.getMinFloor() - target)), + Math.min(Math.abs(e2.getMaxFloor() - target), Math.abs(e2.getMinFloor() - target)) + ); + }) + .orElseThrow(() -> new IllegalStateException("No elevators can go %s from floor %d".formatted(desiredTravelDirection.name(), atFloor))); + + } + + if (elevator.canRequestDestinationFloor()) { + elevator.requestDestinationFloor(atFloor); + elevator.addPotentialTarget(target, listener); + } + + return elevator.getId(); + } + + public int getFloorAmount() { + return floors.size(); + } + + public int getMinFloor() { + return floors.firstEntry().getKey(); + } + + public int getMaxFloor() { + return floors.lastEntry().getKey(); + } + + /** + * A helper method to determine whether the simulation is still running. + * Created to avoid streaming the entire Human registry. + */ + public boolean hasActivePassengers() { + return floors.values().stream() + .map(Floor::getActivePassengersCount) + .reduce(0, Integer::sum) > 0; } public void moveOneFloor() { - elevators.forEach(Elevator::moveOneFloor); - elevators.forEach(elevator -> elevatorListeners.forEach(listener -> listener.onElevatorArrivedAtFloor(elevator))); + LogUtils.measure("Moving elevators", this::moveElevators); + LogUtils.measure("Listener firing", this::fireFloorListeners); + } + + /** + * Estimate average target floor that a user might select given a starting floor and a direction. + * Picks the middle floor between the starting floor and the one farthest in the given direction. + * @return {@link OptionalInt} describing the calculated floor number, or empty if the request doesn't make sense + * (going up from the topmost floor or down from the bottom floor) + */ + private OptionalInt calculateAverageTarget(int floorFrom, TravelDirection desiredTravelDirection) { + if (desiredTravelDirection == TravelDirection.UP) { + int maxFloor = floors.lastEntry().getKey(); + if (floorFrom >= maxFloor) { + return OptionalInt.empty(); + } else { + int delta = maxFloor - floorFrom; + delta = delta % 2 == 0 ? delta / 2 : delta / 2 + 1; + return OptionalInt.of(floorFrom + delta); + } + } else { + int minFloor = floors.firstEntry().getKey(); + if (floorFrom <= minFloor) { + return OptionalInt.empty(); + } else { + int delta = floorFrom - minFloor; + delta = delta % 2 == 0 ? delta / 2 : delta / 2 + 1; + return OptionalInt.of(floorFrom - delta); + } + } + } + + private void moveElevators() { + ConcurrentUtils.performTasksInParallel(elevators, e -> { + floors.get(e.getCurrentFloor()).removeElevator(e); + e.moveOneFloor(); + floors.get(e.getCurrentFloor()).addElevator(e); + }); + } + + /** + * Elevator passengers are notified first, giving them a chance to exit and potentially remove themselves + * from tracking if they have reached their destination. In the majority of cases, this will be a performance + * improvement compared to firing waiting passenger events first. + * Lastly, once everyone who wanted to board did so, notify any remaining idle passengers that they can request + * an elevator. This helps to introduce new passengers to the system. + */ + private void fireFloorListeners() { + ConcurrentUtils.performTasksInParallel(floors.values(), f -> { + f.fireElevatorPassengerEvents(); + f.fireElevatorArrivalEvents(); + f.fireElevatorRequestEvents(this); + }); } } diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/Floor.java b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/Floor.java new file mode 100644 index 0000000..557d35e --- /dev/null +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/Floor.java @@ -0,0 +1,93 @@ +package org.togetherjava.event.elevator.elevators; + +import org.togetherjava.event.elevator.humans.ElevatorListener; +import org.togetherjava.event.elevator.humans.Passenger; + +import java.util.Collection; +import java.util.StringJoiner; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Class that holds data for each floor, package-private as technically all this is considered + * implementation detail. + */ +class Floor { + private final int number; + /** + * Passengers that currently wait for an elevator, should not include passengers that have arrived + * at their destination. + */ + private final Collection passengers = ConcurrentHashMap.newKeySet(); + /** + * Elevators currently stopping at this floor. + */ + private final Collection elevators = ConcurrentHashMap.newKeySet(); + + Floor(int number) { + this.number = number; + } + + @Override + public String toString() { + return new StringJoiner(", ", Floor.class.getSimpleName() + "[", "]") + .add("number=" + number) + .toString(); + } + + @Override + public int hashCode() { + return number; + } + + void addPassenger(Passenger passenger) { + passengers.add(passenger); + } + + void removePassenger(Passenger passenger) { + passengers.remove(passenger); + } + + void addElevator(Elevator elevator) { + elevators.add(elevator); + } + + void removeElevator(Elevator elevator) { + elevators.remove(elevator); + } + + synchronized int getActivePassengersCount() { + return passengers.size() + elevators.stream().map(e -> e.getPassengers().size()).reduce(0, Integer::sum); + } + + + /** + * Notify all passengers of all elevators on this floor that they may exit the elevator if they wish. + */ + synchronized void fireElevatorPassengerEvents() { + for (Elevator elevator : elevators) { + for (ElevatorListener passenger : elevator.getPassengers()) { + passenger.onElevatorArrivedAtFloor(elevator); + } + } + } + + /** + * Notify all passengers on this floor that they may enter an elevator if they wish. + */ + synchronized void fireElevatorArrivalEvents() { + for (Passenger passenger : passengers) { + for (Elevator elevator : elevators) { + passenger.onElevatorArrivedAtFloor(elevator); + } + } + } + + /** + * Notify all passengers on this floor that they may request an elevator if they wish. + */ + synchronized void fireElevatorRequestEvents(FloorPanelSystem floorPanelSystem) { + for (Passenger passenger : passengers) { + passenger.onElevatorSystemReady(floorPanelSystem); + } + } +} diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/FloorPanelSystem.java b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/FloorPanelSystem.java index 8043228..d4c9718 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/FloorPanelSystem.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/FloorPanelSystem.java @@ -1,5 +1,7 @@ package org.togetherjava.event.elevator.elevators; +import org.togetherjava.event.elevator.humans.ElevatorListener; + /** * The system in corridors that allows requesting elevators to the current floor. */ @@ -10,8 +12,10 @@ public interface FloorPanelSystem { * @param atFloor the floor to pick up the human at, must be within the range served by the system * @param desiredTravelDirection the direction the human wants to travel into, * can be used for determination of the best elevator + * @param listener (NEW) the listener that requested the operation + * @return the id of the elevator that was recommended by the system * @apiNote This represents a human standing in the corridor, pressing a button on the wall, * requesting that an elevator comes to pick them up for travel into the given direction. */ - void requestElevator(int atFloor, TravelDirection desiredTravelDirection); + int requestElevator(int atFloor, TravelDirection desiredTravelDirection, ElevatorListener listener); } diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/PaternosterElevator.java b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/PaternosterElevator.java new file mode 100644 index 0000000..f9d2a6f --- /dev/null +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/PaternosterElevator.java @@ -0,0 +1,75 @@ +package org.togetherjava.event.elevator.elevators; + +import org.jetbrains.annotations.Nullable; +import org.togetherjava.event.elevator.humans.Passenger; + +/** + * A Paternoster lift which endlessly travels in a range between two floors. It cannot take requests. + */ +public final class PaternosterElevator extends Elevator { + /** + * Creates a new Paternoster elevator. + * + * @param minFloor the minimum floor that the elevator can serve, must be greater than or equal to 1. + * @param floorsServed the amount of floors served in total by this elevator, must be greater than or equal to 2. + * Together with the minFloor this forms a consecutive range of floors with no gaps in between. + * @param currentFloor the floor the elevator starts at, must be within the defined range of floors served by the elevator + */ + public PaternosterElevator(int minFloor, int floorsServed, int currentFloor) { + this(minFloor, floorsServed,currentFloor, TravelDirection.UP); + } + + /** + * Creates a new Paternoster elevator. + * + * @param minFloor the minimum floor that the elevator can serve, must be greater than or equal to 1. + * @param floorsServed the amount of floors served in total by this elevator, must be greater than or equal to 2. + * Together with the minFloor this forms a consecutive range of floors with no gaps in between. + * @param currentFloor the floor the elevator starts at, must be within the defined range of floors served by the elevator + * @param startingDirection desired starting direction, will be overridden if the elevator is currently + * at the end of the path and cannot move further in that direction + */ + public PaternosterElevator(int minFloor, int floorsServed, int currentFloor, TravelDirection startingDirection) { + super(minFloor, floorsServed, currentFloor); + if (this.currentFloor == this.minFloor || this.currentFloor != this.maxFloor && startingDirection == TravelDirection.UP) { + targets.add(this.maxFloor); + targets.add(this.minFloor); + } else { + targets.add(this.minFloor); + targets.add(this.maxFloor); + } + } + + /** + * Cannot request floors since this elevator has predetermined movement + */ + @Override + public boolean canRequestDestinationFloor() { + return false; + } + + @Override + public synchronized void requestDestinationFloor(int destinationFloor, @Nullable Passenger passenger) { + throw new UnsupportedOperationException("Paternoster elevator does not accept requests"); + } + + /** + * Reinsert the target at the back of the queue + */ + @Override + protected void modifyTargetsOnArrival() { + targets.add(targets.remove()); + } + + /** + * Since this is a Paternoster elevator, it will always be able to visit floor in his range, + * and will never visit other floors. + * + * @return whether this elevator is currently on the specified floor + * or will at some point visit that floor before all its tasks are done. + */ + @Override + public boolean willVisitFloor(int floor) { + return canServe(floor); + } +} diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/TravelDirection.java b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/TravelDirection.java index b1c01c0..114a5b8 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/TravelDirection.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/elevators/TravelDirection.java @@ -2,5 +2,6 @@ public enum TravelDirection { UP, - DOWN, + DOWN + } diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/humans/Human.java b/Contest/Assignment/src/org/togetherjava/event/elevator/humans/Human.java index 0af2511..580bf95 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/humans/Human.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/humans/Human.java @@ -1,10 +1,13 @@ package org.togetherjava.event.elevator.humans; +import lombok.Getter; import org.togetherjava.event.elevator.elevators.ElevatorPanel; import org.togetherjava.event.elevator.elevators.FloorPanelSystem; +import org.togetherjava.event.elevator.elevators.TravelDirection; import java.util.OptionalInt; import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicInteger; /** * A single human that starts at a given floor and wants to @@ -13,15 +16,21 @@ * The class mainly acts upon given elevator events it listens to, * for example requesting an elevator, eventually entering and exiting them. */ -public final class Human implements ElevatorListener { - private State currentState; - private final int startingFloor; - private final int destinationFloor; +public final class Human implements Passenger { + private static final AtomicInteger NEXT_ID = new AtomicInteger(0); + + @Getter private final int id; + @Getter private final int startingFloor; + @Getter private final int destinationFloor; + @Getter private State currentState; + @Getter private int currentFloor; + private int nextDestination; + private int expectedElevatorId = -1; /** - * If the human is currently inside an elevator, this is its unique ID. + * If the human is currently inside an elevator, this the reference to it. * Otherwise, this is {@code null} to indicate that the human is currently on the corridor. */ - private Integer currentEnteredElevatorId; + private ElevatorPanel currentEnteredElevator; /** * Creates a new human. @@ -37,46 +46,115 @@ public Human(int startingFloor, int destinationFloor) { throw new IllegalArgumentException("Floors must be at least 1"); } + this.id = NEXT_ID.getAndIncrement(); this.startingFloor = startingFloor; + this.currentFloor = startingFloor; this.destinationFloor = destinationFloor; - currentState = State.IDLE; + if (startingFloor == destinationFloor) { + currentState = State.ARRIVED; + } else { + currentState = State.IDLE; + } } - public State getCurrentState() { - return currentState; + /** + * The system is now ready and the human should leave + * their initial IDLE state, requesting an elevator by clicking on the buttons of + * the floor panel system. The human will now enter the WAITING_FOR_ELEVATOR state. + */ + @Override + public synchronized void onElevatorSystemReady(FloorPanelSystem floorPanelSystem) { + if (currentState == State.IDLE) { + TravelDirection direction; + if (currentFloor < destinationFloor) { + direction = TravelDirection.UP; + } else if (currentFloor > destinationFloor) { + direction = TravelDirection.DOWN; + } else { + // Do nothing. Why did this human come to the elevator hall? :thinking: + System.out.printf("A human has matching source and destination floors, it will be counted as arrived: %d and %d%n", startingFloor, destinationFloor); + currentState = State.ARRIVED; + return; + } + expectedElevatorId = floorPanelSystem.requestElevator(currentFloor, direction, this); + currentState = State.WAITING_FOR_ELEVATOR; + } } - public int getStartingFloor() { - return startingFloor; - } + /** + * If the human is currently waiting for an elevator and + * this event represents arrival at the humans current floor, the human can now enter the + * elevator and request their actual destination floor. The state has to change to TRAVELING_WITH_ELEVATOR. + * If the human is currently traveling with this elevator and the event represents + * arrival at the human's destination floor, the human can now exit the elevator. + */ + @Override + public synchronized void onElevatorArrivedAtFloor(ElevatorPanel elevatorPanel) { + if (shouldBoardElevator(elevatorPanel)) { + if (currentFloor < destinationFloor) { + + int elevatorMaxFloor = elevatorPanel.getMaxFloor(); + if (elevatorMaxFloor <= currentFloor) { + // This human wants to go up, but the elevator cannot go up, skip + return; + } + + elevatorPanel.boardPassenger(this); + nextDestination = Math.min(elevatorMaxFloor, destinationFloor); - public int getDestinationFloor() { - return destinationFloor; + if (elevatorPanel.canRequestDestinationFloor()) { + elevatorPanel.requestDestinationFloor(nextDestination, this); + } + } else if (currentFloor > destinationFloor) { + + int elevatorMinFloor = elevatorPanel.getMinFloor(); + if (elevatorMinFloor >= currentFloor) { + // This human wants to go down, but the elevator cannot go down, skip + return; + } + + elevatorPanel.boardPassenger(this); + nextDestination = Math.max(elevatorMinFloor, destinationFloor); + + if (elevatorPanel.canRequestDestinationFloor()) { + elevatorPanel.requestDestinationFloor(nextDestination, this); + } + } else { + throw new RuntimeException("A human's current floor matches destination floor, but they are waiting for an elevator, this is a bug"); + } + + currentEnteredElevator = elevatorPanel; + currentState = State.TRAVELING_WITH_ELEVATOR; + } else if (isInElevator(elevatorPanel)) { + currentFloor = elevatorPanel.getCurrentFloor(); + + if (currentFloor == nextDestination) { + boolean arrived = nextDestination == destinationFloor; + elevatorPanel.removePassenger(this, arrived); + currentEnteredElevator = null; + expectedElevatorId = -1; + if (arrived) { + currentState = State.ARRIVED; + } else { + currentState = State.IDLE; + } + } + } } - @Override - public void onElevatorSystemReady(FloorPanelSystem floorPanelSystem) { - // TODO Implement. The system is now ready and the human should leave - // their initial IDLE state, requesting an elevator by clicking on the buttons of - // the floor panel system. The human will now enter the WAITING_FOR_ELEVATOR state. - System.out.println("Ready-event received"); + private boolean shouldBoardElevator(ElevatorPanel elevatorPanel) { + return currentState == State.WAITING_FOR_ELEVATOR && elevatorPanel.getId() == expectedElevatorId; } - @Override - public void onElevatorArrivedAtFloor(ElevatorPanel elevatorPanel) { - // TODO Implement. If the human is currently waiting for an elevator and - // this event represents arrival at the humans current floor, the human can now enter the - // elevator and request their actual destination floor. The state has to change to TRAVELING_WITH_ELEVATOR. - // If the human is currently traveling with this elevator and the event represents - // arrival at the human's destination floor, the human can now exit the elevator. - System.out.println("Arrived-event received"); + private boolean isInElevator(ElevatorPanel elevatorPanel) { + return currentState == State.TRAVELING_WITH_ELEVATOR && elevatorPanel.equals(currentEnteredElevator); } public OptionalInt getCurrentEnteredElevatorId() { - return currentEnteredElevatorId == null + return currentEnteredElevator == null ? OptionalInt.empty() - : OptionalInt.of(currentEnteredElevatorId); + : OptionalInt.of(currentEnteredElevator.getId()); } @Override @@ -84,11 +162,17 @@ public String toString() { return new StringJoiner(", ", Human.class.getSimpleName() + "[", "]") .add("currentState=" + currentState) .add("startingFloor=" + startingFloor) + .add("currentFloor=" + currentFloor) .add("destinationFloor=" + destinationFloor) - .add("currentEnteredElevatorId=" + currentEnteredElevatorId) + .add("currentEnteredElevatorId=" + (currentEnteredElevator == null ? "null" : currentEnteredElevator.getId())) .toString(); } + @Override + public int hashCode() { + return id; + } + public enum State { IDLE, WAITING_FOR_ELEVATOR, diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/humans/Passenger.java b/Contest/Assignment/src/org/togetherjava/event/elevator/humans/Passenger.java new file mode 100644 index 0000000..62eaa84 --- /dev/null +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/humans/Passenger.java @@ -0,0 +1,28 @@ +package org.togetherjava.event.elevator.humans; + +/** + * A passenger that wants to travel from one floor to another. + */ +public interface Passenger extends ElevatorListener { + /** + * Where is the passenger travelling from. + */ + int getStartingFloor(); + + /** + * Where the passenger wants to travel to. + */ + int getDestinationFloor(); + + /** + * Passenger's current floor. + */ + int getCurrentFloor(); + + /** + * Whether this passenger currently wants to move to a different floor. + */ + default boolean wantsToMove() { + return getCurrentFloor() != getDestinationFloor(); + } +} diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/AdvancedSimulation.java b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/AdvancedSimulation.java new file mode 100644 index 0000000..16cf62f --- /dev/null +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/AdvancedSimulation.java @@ -0,0 +1,46 @@ +package org.togetherjava.event.elevator.simulation; + +import org.togetherjava.event.elevator.elevators.Elevator; +import org.togetherjava.event.elevator.humans.Human; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * A simulation that adds humans while it's running. + */ +public final class AdvancedSimulation extends Simulation { + + public AdvancedSimulation(List elevators, List humans) { + super(elevators, humans); + } + + public static AdvancedSimulation createRandomSimulation(long seed, int amountOfElevators, int amountOfHumans, int floorsServed) { + return Simulation.createRandomSimulation(seed, amountOfElevators, amountOfHumans, floorsServed, AdvancedSimulation::new); + } + + @Override + public void step() { + if (stepCount > 0 && stepCount % elevatorSystem.getFloorAmount() == 0) { +// addMoreHumans(elevators.size() / 2); + addMoreHumans(1); + } + super.step(); + } + + private void addMoreHumans(int amount) { + var random = ThreadLocalRandom.current(); + + int maxFloor = elevatorSystem.getMaxFloor(); + int minFloor = elevatorSystem.getMinFloor(); + for (int i = 0; i < amount; i++) { + Human human = new Human( + random.nextInt(maxFloor - minFloor + 1) + minFloor, + random.nextInt(maxFloor - minFloor + 1) + minFloor); + elevatorSystem.registerElevatorListener(human); + humans.add(human); + humanStatistics.add(new HumanStatistics(human)); + System.out.printf("Added a new human at floor %d which wants to travel to floor %d%n", human.getCurrentFloor(), human.getDestinationFloor()); + } + } +} diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/HumanStatistics.java b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/HumanStatistics.java index 9c2c778..7913e3e 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/HumanStatistics.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/HumanStatistics.java @@ -1,12 +1,13 @@ package org.togetherjava.event.elevator.simulation; +import lombok.Getter; import org.togetherjava.event.elevator.humans.Human; import java.util.EnumMap; import java.util.Map; final class HumanStatistics { - private final Human human; + @Getter private final Human human; private final Map stateToStepCount = new EnumMap<>(Human.State.class); HumanStatistics(Human human) { diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/MoreSimulations.java b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/MoreSimulations.java new file mode 100644 index 0000000..586e356 --- /dev/null +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/MoreSimulations.java @@ -0,0 +1,80 @@ +package org.togetherjava.event.elevator.simulation; + +import org.togetherjava.event.elevator.elevators.CommonElevator; +import org.togetherjava.event.elevator.elevators.PaternosterElevator; +import org.togetherjava.event.elevator.humans.Human; + +import java.util.List; +import java.util.stream.IntStream; + +/** + * Separate file to avoid bloating {@link Simulation} with static methods + */ +public class MoreSimulations { + + public static Simulation createSimpleFailingSimulation() { + return new Simulation( + List.of( + new CommonElevator(6, 5, 10), + new CommonElevator(1, 5, 5)), + List.of( + new Human(1, 7))); + } + + public static Simulation createSimpleSucceedingSimulation() { + return new Simulation( + List.of( + new CommonElevator(6, 5, 10), + new CommonElevator(1, 6, 5)), + List.of( + new Human(1, 8))); + } + + public static Simulation createSimpleThreeStepSimulation() { + return new Simulation( + List.of( + new CommonElevator(7, 4, 10), + new CommonElevator(4, 5, 5), + new CommonElevator(1, 5, 3)), + List.of( + new Human(1, 10))); + } + + public static Simulation createSimplePaternosterSimulation() { + return new Simulation( + List.of( + new CommonElevator(6, 5, 10), + new PaternosterElevator(1, 8, 5), + new PaternosterElevator(8, 3, 10)), + List.of( + new Human(1, 7))); + } + + public static AdvancedSimulation createSimpleAdvancedSimulation() { + return new AdvancedSimulation( + List.of( + new CommonElevator(1, 10, 10), + new CommonElevator(1, 10, 8)), + List.of( + new Human(1, 9))); + } + + public static Simulation createMegaSimulation() { + return Simulation.createRandomSimulation(3, 100, 100_000, 100); + } + + public static AdvancedSimulation createMegaAdvancedSimulation() { + return AdvancedSimulation.createRandomSimulation(3, 100, 100_000, 100); + } + + public static Simulation createNotMarkoSimulation() { + return Simulation.createRandomSimulation(2, 4, 1_000, 50); + } + + public static Simulation createGoodPaternosterSimulation() { + return new Simulation( + IntStream.rangeClosed(1, 4).mapToObj(i -> new PaternosterElevator(1, 50, (int) Math.ceil(Math.random() * 50))).toList(), + IntStream.rangeClosed(1, 1_000).mapToObj(i -> new Human((int) Math.ceil(Math.random() * 50), (int) Math.ceil(Math.random() * 50))).toList() + ); + } +} diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/Simulation.java b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/Simulation.java index 6f462c6..387d633 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/Simulation.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/Simulation.java @@ -1,27 +1,34 @@ package org.togetherjava.event.elevator.simulation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.togetherjava.event.elevator.elevators.CommonElevator; import org.togetherjava.event.elevator.elevators.Elevator; import org.togetherjava.event.elevator.elevators.ElevatorSystem; import org.togetherjava.event.elevator.humans.Human; +import org.togetherjava.event.elevator.util.LogUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; import java.util.stream.LongStream; import java.util.stream.Stream; -public final class Simulation { - private final List humans; - private final List elevators; - private final ElevatorSystem elevatorSystem; +public class Simulation { + private static final Logger logger = LogManager.getLogger(); + + protected final List humans; + protected final List elevators; + protected final ElevatorSystem elevatorSystem; private final View view; - private long stepCount; - private final List humanStatistics; + protected long stepCount; + protected final List humanStatistics; public static Simulation createSingleElevatorSingleHumanSimulation() { - return new Simulation(List.of(new Elevator(1, 10, 5)), + return new Simulation(List.of(new CommonElevator(1, 10, 5)), List.of(new Human(1, 10))); } @@ -31,8 +38,8 @@ public static Simulation createSimpleSimulation() { return new Simulation( List.of( - new Elevator(minFloor, floorsServed, 1), - new Elevator(minFloor, floorsServed, 6)), + new CommonElevator(minFloor, floorsServed, 1), + new CommonElevator(minFloor, floorsServed, 6)), List.of( new Human(1, 2), new Human(1, 5), @@ -46,14 +53,18 @@ public static Simulation createRandomSimulation(int amountOfElevators, int amoun } public static Simulation createRandomSimulation(long seed, int amountOfElevators, int amountOfHumans, int floorsServed) { - System.out.println("Seed for random simulation is: " + seed); + return createRandomSimulation(seed, amountOfElevators, amountOfHumans, floorsServed, Simulation::new); + } + + public static S createRandomSimulation(long seed, int amountOfElevators, int amountOfHumans, int floorsServed, SimulationFactory factory) { + logger.info("Seed for random simulation is: " + seed); Random random = new Random(seed); int minFloor = 1; - List elevators = Stream.generate(() -> { + List elevators = Stream.generate(() -> { int currentFloor = minFloor + random.nextInt(floorsServed); - return new Elevator(minFloor, floorsServed, currentFloor); + return new CommonElevator(minFloor, floorsServed, currentFloor); }).limit(amountOfElevators).toList(); List humans = Stream.generate(() -> { @@ -62,10 +73,14 @@ public static Simulation createRandomSimulation(long seed, int amountOfElevators return new Human(startingFloor, destinationFloor); }).limit(amountOfHumans).toList(); - return new Simulation(elevators, humans); + return factory.createSimulation(elevators, humans); } - public Simulation(List elevators, List humans) { + interface SimulationFactory { + S createSimulation(List elevators, List humans); + } + + public Simulation(List elevators, List humans) { this.elevators = new ArrayList<>(elevators); this.humans = new ArrayList<>(humans); @@ -73,7 +88,7 @@ public Simulation(List elevators, List humans) { this.elevators.forEach(elevatorSystem::registerElevator); this.humans.forEach(elevatorSystem::registerElevatorListener); - humanStatistics = this.humans.stream().map(HumanStatistics::new).toList(); + humanStatistics = this.humans.stream().map(HumanStatistics::new).collect(Collectors.toCollection(() -> new ArrayList<>(humans.size()))); view = new View(this); } @@ -91,20 +106,20 @@ public void startAndExecuteUntilDone(int stepLimit) { } public void start() { - elevatorSystem.ready(); + LogUtils.measure("Ready", elevatorSystem::ready); } public void step() { elevatorSystem.moveOneFloor(); - - humanStatistics.forEach(HumanStatistics::step); + LogUtils.measure("Statistics update", () -> humanStatistics.forEach(HumanStatistics::step)); stepCount++; } public boolean isDone() { - return humans.stream() - .map(Human::getCurrentState) - .allMatch(Human.State.ARRIVED::equals); +// return humans.stream() +// .map(Human::getCurrentState) +// .allMatch(Human.State.ARRIVED::equals); + return !elevatorSystem.hasActivePassengers(); } public long getStepCount() { @@ -152,4 +167,17 @@ public int getAverageTimePercentageSpendForState(Human.State state) { long medianPercentage = 100 * medianSteps / stepCount; return (int) medianPercentage; } + + public void printCurrentStatistics() { + logger.trace(() -> humanStatistics.stream() + .collect(Collectors.toMap((HumanStatistics stat) -> stat.getHuman().getCurrentState(), s -> 1, Integer::sum))); + } + + public boolean shouldPrintSummary() { + return elevators.size() <= 100 && humans.size() <= 100; + } + + public boolean shouldPrint() { + return elevatorSystem.getFloorAmount() <= 10 && elevators.size() <= 20; + } } diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/View.java b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/View.java index e61eb63..2f5ed61 100644 --- a/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/View.java +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/simulation/View.java @@ -101,7 +101,7 @@ private List corridorForFloorToLines(int floor) { .filter(Human.State.ARRIVED::equals) .count(); long humansWaiting = simulation.getHumans().stream() - .filter(human -> human.getStartingFloor() == floor) + .filter(human -> human.getCurrentFloor() == floor) .map(Human::getCurrentState) .filter(state -> state.equals(Human.State.IDLE) || state.equals(Human.State.WAITING_FOR_ELEVATOR)) .count(); diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/util/CollectionUtils.java b/Contest/Assignment/src/org/togetherjava/event/elevator/util/CollectionUtils.java new file mode 100644 index 0000000..e5fe80c --- /dev/null +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/util/CollectionUtils.java @@ -0,0 +1,40 @@ +package org.togetherjava.event.elevator.util; + +import java.util.Collection; +import java.util.Objects; + +public final class CollectionUtils { + private CollectionUtils() {} + + /** + * Equality check for collections that {@link java.util.ArrayDeque do not} override {@link #equals(Object) equals} for some reason.
+ * Will fall back to checking {@link #equals(Object) equals} first before doing the manual check, + * so it's the caller responsibility to determine whether they actually need this method + * and avoid potential double iteration.
+ * Not thread-safe, requires external synchronization, + * exception generation is delegated to iterators themselves. + */ + public static boolean equals(Collection c1, Collection c2) { + if (c1 == null || c2 == null) { + return c1 == c2; + } + + if (c1.equals(c2)) { + return true; + } + + if (c1.size() != c2.size()) { + return false; + } + + var i1 = c1.iterator(); + var i2 = c2.iterator(); + while (i1.hasNext()) { + if (!Objects.equals(i1.next(), i2.next())) { + return false; + } + } + + return !i2.hasNext(); + } +} diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/util/ConcurrentUtils.java b/Contest/Assignment/src/org/togetherjava/event/elevator/util/ConcurrentUtils.java new file mode 100644 index 0000000..3f7f721 --- /dev/null +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/util/ConcurrentUtils.java @@ -0,0 +1,25 @@ +package org.togetherjava.event.elevator.util; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.RecursiveAction; +import java.util.function.Consumer; + +public final class ConcurrentUtils { + private ConcurrentUtils() {} + + /** + * Construct a {@link ForkJoinTask} for each member of the specified collection that performs the specified action, + * then submit them to the common {@link java.util.concurrent.ForkJoinPool ForkJoinPool} and wait for their completion. + */ + public static void performTasksInParallel(Collection targets, Consumer action) { + List> tasks = targets.stream().map(target -> new RecursiveAction() { + @Override + protected void compute() { + action.accept(target); + } + }).toList(); + ForkJoinTask.invokeAll(tasks); + } +} diff --git a/Contest/Assignment/src/org/togetherjava/event/elevator/util/LogUtils.java b/Contest/Assignment/src/org/togetherjava/event/elevator/util/LogUtils.java new file mode 100644 index 0000000..72c7cdb --- /dev/null +++ b/Contest/Assignment/src/org/togetherjava/event/elevator/util/LogUtils.java @@ -0,0 +1,37 @@ +package org.togetherjava.event.elevator.util; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class LogUtils { + private static final Logger logger = LogManager.getLogger(); + + private LogUtils() {} + public static void measure(String name, Runnable toLog) { + measure(name, toLog, Level.INFO); + } + + /** + * If the specified log level is enabled, measure the execution time of the specified runnable, + * then output the result at that logging level. Otherwise, just execute the runnable. + */ + public static void measure(String name, Runnable toLog, Level level) { + if (logger.isEnabled(level)) { + long start = System.nanoTime(); + toLog.run(); + long end = System.nanoTime(); + logger.log(level, "%s took %s".formatted(name, formatNanos(end - start))); + } else { + toLog.run(); + } + } + + private static String formatNanos(long nanos) { + if (nanos >= 10_000_000) { + return "%,.3f s".formatted(nanos / 1e9); + } else { + return "%,.3f ms".formatted(nanos / 1e6); + } + } +} diff --git a/Contest/Assignment/test/PreviousElevatorSystemTest.java b/Contest/Assignment/test/PreviousElevatorSystemTest.java index 1d060e6..a09c11b 100644 --- a/Contest/Assignment/test/PreviousElevatorSystemTest.java +++ b/Contest/Assignment/test/PreviousElevatorSystemTest.java @@ -1,5 +1,5 @@ -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.togetherjava.event.elevator.elevators.CommonElevator; import org.togetherjava.event.elevator.elevators.Elevator; import org.togetherjava.event.elevator.elevators.ElevatorSystem; import org.togetherjava.event.elevator.elevators.FloorPanelSystem; @@ -41,7 +41,7 @@ void testReady() { void testMoveOneFloor() { ElevatorSystem system = new ElevatorSystem(); - Supplier createAnyElevator = () -> new Elevator(1, 5, 2); + Supplier createAnyElevator = () -> new CommonElevator(1, 5, 2); List elevators = Stream.generate(createAnyElevator).limit(3).toList(); List listeners = diff --git a/Contest/Assignment/test/PreviousElevatorTest.java b/Contest/Assignment/test/PreviousElevatorTest.java index 28a8b1e..3a87b8a 100644 --- a/Contest/Assignment/test/PreviousElevatorTest.java +++ b/Contest/Assignment/test/PreviousElevatorTest.java @@ -1,5 +1,5 @@ -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.togetherjava.event.elevator.elevators.CommonElevator; import org.togetherjava.event.elevator.elevators.Elevator; import org.togetherjava.event.elevator.elevators.ElevatorPanel; @@ -18,7 +18,7 @@ void testGetFloors() { int expectedFloorsServed = 4; int expectedCurrentFloor = 5; Elevator elevator = - new Elevator(expectedMinFloor, expectedFloorsServed, expectedCurrentFloor); + new CommonElevator(expectedMinFloor, expectedFloorsServed, expectedCurrentFloor); int actualMinFloor = elevator.getMinFloor(); int actualFloorsServed = elevator.getFloorsServed(); @@ -42,7 +42,7 @@ void testGetId() { Set elevatorIds = new HashSet<>(); for (int i = 0; i < 500; i++) { - Elevator elevator = new Elevator(anyMinFloor, anyFloorsServed, anyCurrentFloor); + Elevator elevator = new CommonElevator(anyMinFloor, anyFloorsServed, anyCurrentFloor); int id = elevator.getId(); assertFalse(elevatorIds.contains(id), diff --git a/build.gradle b/build.gradle index 7ed88c7..1ae9424 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,28 @@ subprojects { } } +project(':Contest-Assignment') { + def log4jVersion = '2.18.0' + def jbaVersion = '23.0.0' + + dependencies { + annotationProcessor 'org.projectlombok:lombok:+' + compileOnly 'org.projectlombok:lombok:+' + + annotationProcessor "org.jetbrains:annotations:$jbaVersion" + compileOnly "org.jetbrains:annotations:$jbaVersion" + + implementation "org.apache.logging.log4j:log4j-core:$log4jVersion" + implementation "org.apache.logging.log4j:log4j-api:$log4jVersion" + } + + sourceSets { + main { + resources.srcDir 'resources' + } + } +} + project(':util') { dependencies { testImplementation 'org.mockito:mockito-core:4.0.0'