Move step output in UI

This commit is contained in:
Arne Keller 2020-03-06 14:27:03 +01:00
parent 54708c2198
commit 0e298492a3
5 changed files with 131 additions and 88 deletions

View File

@ -9,6 +9,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -66,6 +68,7 @@ public class ModelRailwaySimulation {
/** /**
* Remove the specified rail from the rail network. * Remove the specified rail from the rail network.
*
* @param id identifier of the rail to remove * @param id identifier of the rail to remove
* @return whether the rail could be successfully removed * @return whether the rail could be successfully removed
*/ */
@ -275,12 +278,22 @@ public class ModelRailwaySimulation {
} }
/** /**
* Get a list of trains (their text representation) in the simulation. * Get the trains (their text representation) in the simulation.
* *
* @return list of trains * @return sorted collection of trains
*/ */
public List<String> listTrains() { public SortedMap<Integer, String> getTrains() {
return trainManager.listTrains(); return trainManager.getTrains();
}
/**
* Get the front position of a train.
*
* @param trainId train identifier
* @return position of the train, or null if not found or placed
*/
public Vector2D getTrainPosition(int trainId) {
return trainManager.getPosition(trainId);
} }
/** /**
@ -307,11 +320,13 @@ public class ModelRailwaySimulation {
} }
/** /**
* Move the trains in this simulation, printing their new positions and any collisions. * Move the trains in this simulation.
*
* @param speed amount of steps to move the trains * @param speed amount of steps to move the trains
* @return train collisions, null if no trains are placed
* @throws InvalidInputException if the simulation is not yet ready * @throws InvalidInputException if the simulation is not yet ready
*/ */
public void step(short speed) throws InvalidInputException { public List<SortedSet<Integer>> step(short speed) throws InvalidInputException {
trainManager.step(speed); return trainManager.step(speed);
} }
} }

View File

@ -16,7 +16,7 @@ import java.util.stream.Collectors;
* @author Arne Keller * @author Arne Keller
* @version 1.1 * @version 1.1
*/ */
public final class Train { public final class Train implements Comparable {
/** /**
* Separator between rolling stocks in {@link #show()}. * Separator between rolling stocks in {@link #show()}.
*/ */
@ -85,13 +85,14 @@ public final class Train {
} }
/** /**
* Check whether this train is on a particular rail. Note that at least one rolling stock has to be on the rail, * Check whether this train is on a particular rail. Just touching the rail is enough.
* just touching the rail is not enough. *
* @param rail the rail to check * @param rail the rail to check
* @return whether this train is on that rail * @return whether this train is on that rail
*/ */
public boolean isOnRail(Rail rail) { public boolean isOnRail(Rail rail) {
return isPlaced() && (occupiedRails.stream().anyMatch(blockedRail -> blockedRail == rail) return isPlaced()
&& (occupiedRails.stream().anyMatch(blockedRail -> blockedRail == rail)
|| positions.stream().anyMatch(rail::canConnectTo)); || positions.stream().anyMatch(rail::canConnectTo));
} }
@ -367,4 +368,9 @@ public final class Train {
} }
return stringBuilder.toString(); return stringBuilder.toString();
} }
@Override
public int compareTo(Object o) {
return Integer.compare(this.identifier, ((Train) o).identifier);
}
} }

View File

@ -1,25 +1,24 @@
package edu.kit.informatik.model; package edu.kit.informatik.model;
import edu.kit.informatik.Terminal;
import edu.kit.informatik.ui.InvalidInputException; import edu.kit.informatik.ui.InvalidInputException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.stream.Stream; import java.util.stream.Stream;
import static edu.kit.informatik.ui.CommandLine.OK;
/** /**
* Train manager, used for processing train placements and movement on a rail network. * Train manager, used for processing train placements and movement on a rail network.
* *
@ -32,7 +31,7 @@ public final class TrainManager {
*/ */
private final RailwayNetwork railNetwork; private final RailwayNetwork railNetwork;
/** /**
* Map of trains simulated. * Map of trains simulated. The train identifier is used as key.
*/ */
private final Map<Integer, Train> trains = new HashMap<>(); private final Map<Integer, Train> trains = new HashMap<>();
@ -46,8 +45,8 @@ public final class TrainManager {
} }
/** /**
* Check whether a train is on the rail with the specified identifier. Note that a train must be partially on that * Check whether a train is on the rail with the specified identifier. Trains only touching the end of the rail
* rail, simply touching one of the end points is not enough. * will also be considered.
* *
* @param rail the rail to check * @param rail the rail to check
* @return whether a train is on that rail * @return whether a train is on that rail
@ -87,21 +86,31 @@ public final class TrainManager {
throw new InvalidInputException("rolling stock already used"); throw new InvalidInputException("rolling stock already used");
} }
final Train train = trains.get(trainId); final Train train = trains.get(trainId);
if (train != null && train.isPlaced()) { if (train == null) { // create new train
throw new InvalidInputException("can not add rolling stock to placed train");
}
if (train != null) {
train.add(rollingStock);
} else {
final int correctId = getNextTrainIdentifier(); final int correctId = getNextTrainIdentifier();
if (trainId != correctId) { if (trainId != correctId) {
throw new InvalidInputException("new train identifier must be next free identifier"); throw new InvalidInputException("new train identifier must be next free identifier");
} }
final Train newTrain = new Train(trainId, rollingStock); final Train newTrain = new Train(trainId, rollingStock);
trains.put(trainId, newTrain); trains.put(trainId, newTrain);
} else {
if (train.isPlaced()) {
throw new InvalidInputException("can not add rolling stock to placed train");
}
train.add(rollingStock);
} }
} }
/**
* Calculate the next available train identifier. Will fill 'holes' in the identifier list first.
*
* @return the next train identifier, or -1 if none available
*/
private int getNextTrainIdentifier() {
return IntStream.rangeClosed(1, Integer.MAX_VALUE)
.filter(id -> !trains.containsKey(id)).findFirst().orElse(-1);
}
/** /**
* Delete a train. Will not delete rolling stock contained in that train. * Delete a train. Will not delete rolling stock contained in that train.
* *
@ -118,20 +127,20 @@ public final class TrainManager {
} }
/** /**
* Get a sorted list of trains (their text representation) in the simulation. * Get the trains (their text representation) in the simulation.
* *
* @return list of trains * @return sorted collection of trains
*/ */
public List<String> listTrains() { public SortedMap<Integer, String> getTrains() {
return trains.keySet().stream() return trains.keySet().stream()
.sorted() .sorted()
.map(trains::get) .map(trains::get)
.map(Object::toString) .collect(Collectors.toMap(Train::getIdentifier, Object::toString, (id1, id2) -> id1, TreeMap::new));
.collect(Collectors.toList());
} }
/** /**
* Get the ASCII art representation of a train. * Get the ASCII art representation of a train.
*
* @param trainId identifier of the train to show * @param trainId identifier of the train to show
* @return ASCII art representation of said train * @return ASCII art representation of said train
* @throws InvalidInputException if train not found * @throws InvalidInputException if train not found
@ -179,19 +188,12 @@ public final class TrainManager {
} }
/** /**
* Calculate the next available train identifier. * Get collisions of trains currently placed. This implements the first collision checking algorithm (1A):
* @return the next train identifier, or -1 if none available * trains collide if they partially occupy the same rail or touch each other.
*
* @param collisions list of collisions to expand
*/ */
private int getNextTrainIdentifier() { private void getStaticCollisions(List<SortedSet<Train>> collisions) {
return IntStream.rangeClosed(1, Integer.MAX_VALUE)
.filter(id -> !trains.containsKey(id)).findFirst().orElse(-1);
}
/**
* Get collisions of trains currently placed.
* @param collisions list of collisions (never null, sometimes empty)
*/
private void getStaticCollisions(List<Set<Train>> collisions) {
final int maxId = trains.keySet().stream().max(Integer::compareTo).orElse(0); final int maxId = trains.keySet().stream().max(Integer::compareTo).orElse(0);
for (int id = 1; id <= maxId; id++) { for (int id = 1; id <= maxId; id++) {
final Train train1 = trains.get(id); final Train train1 = trains.get(id);
@ -200,13 +202,13 @@ public final class TrainManager {
} }
final Set<Rail> occupiedRails = train1.getOccupiedRails(); final Set<Rail> occupiedRails = train1.getOccupiedRails();
// check for same position, and rail collisions // check for same position, and rail collisions
final Set<Train> collision = IntStream.rangeClosed(id + 1, maxId) final SortedSet<Train> collision = IntStream.rangeClosed(id + 1, maxId)
.mapToObj(trains::get) .mapToObj(trains::get)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(Train::isPlaced) .filter(Train::isPlaced)
.filter(train -> train1.touches(train) .filter(train -> train1.touches(train)
|| train.getOccupiedRails().stream().anyMatch(occupiedRails::contains)) || train.getOccupiedRails().stream().anyMatch(occupiedRails::contains))
.collect(Collectors.toSet()); .collect(Collectors.toCollection(TreeSet::new));
if (!collision.isEmpty()) { if (!collision.isEmpty()) {
collision.add(train1); collision.add(train1);
addToSetOrAddNew(collisions, collision); addToSetOrAddNew(collisions, collision);
@ -215,15 +217,15 @@ public final class TrainManager {
} }
/** /**
* Implementation of the silly *second* collision checking algorithm. * Implementation of the silly second collision checking algorithm (2B):
* Will put two trains in a collision even if they only touch the same rail. * two trains collide if they touch the same rail or position.
* *
* @return list of collisions * @return list of collisions
*/ */
private List<Set<Train>> getPlacementCollisions() { private List<SortedSet<Train>> getPlacementCollisions() {
final List<Set<Train>> collisions = new ArrayList<>(); final List<SortedSet<Train>> collisions = new ArrayList<>();
trains.values().stream().filter(Train::isPlaced).forEach(train1 -> trains.values().stream().filter(Train::isPlaced).forEach(train1 ->
trains.values().stream().filter(Train::isPlaced).forEach(train2 -> { trains.values().stream().filter(train -> train != train1).filter(Train::isPlaced).forEach(train2 -> {
final Set<Rail> occupiedByTrain1 = train1.getOccupiedRails(); final Set<Rail> occupiedByTrain1 = train1.getOccupiedRails();
Collections.addAll(occupiedByTrain1, railNetwork.findTouchingRails(train1.getFrontPosition())); Collections.addAll(occupiedByTrain1, railNetwork.findTouchingRails(train1.getFrontPosition()));
Collections.addAll(occupiedByTrain1, railNetwork.findTouchingRails(train1.getRearPosition())); Collections.addAll(occupiedByTrain1, railNetwork.findTouchingRails(train1.getRearPosition()));
@ -233,7 +235,8 @@ public final class TrainManager {
// calculate intersection // calculate intersection
occupiedByTrain2.retainAll(occupiedByTrain1); occupiedByTrain2.retainAll(occupiedByTrain1);
if (!occupiedByTrain2.isEmpty()) { if (!occupiedByTrain2.isEmpty()) {
final Set<Train> collision = Stream.of(train1, train2).collect(Collectors.toSet()); final SortedSet<Train> collision
= Stream.of(train1, train2).collect(Collectors.toCollection(TreeSet::new));
addToSetOrAddNew(collisions, collision); addToSetOrAddNew(collisions, collision);
} }
})); }));
@ -243,7 +246,7 @@ public final class TrainManager {
/** /**
* Add the set to an existing set (and merge sets), or add it to the list. * Add the set to an existing set (and merge sets), or add it to the list.
*/ */
private void addToSetOrAddNew(List<Set<Train>> setList, Set<Train> newSet) { private void addToSetOrAddNew(List<SortedSet<Train>> setList, SortedSet<Train> newSet) {
Set<Train> existing = null; Set<Train> existing = null;
int i = 0; int i = 0;
while (i < setList.size()) { while (i < setList.size()) {
@ -265,13 +268,23 @@ public final class TrainManager {
} }
} }
/**
* Get the front position of the specified train.
*
* @param trainId train identifier
* @return front position of that train
*/
public Vector2D getPosition(int trainId) {
return trains.get(trainId).getFrontPosition();
}
/** /**
* Get collisions of moving the trains one step forward, removing crashing trains from the rails in the process. * Get collisions of moving the trains one step forward, removing crashing trains from the rails in the process.
* *
* @return list of collisions (never null, sometimes empty) * @return list of collisions (never null, sometimes empty)
*/ */
private List<Set<Train>> getCollisionsOfOneStep() { private List<SortedSet<Train>> getCollisionsOfOneStep() {
final List<Set<Train>> collisions = new ArrayList<>(); final List<SortedSet<Train>> collisions = new ArrayList<>();
trains.values().stream().filter(Train::isPlaced).forEach(train -> { trains.values().stream().filter(Train::isPlaced).forEach(train -> {
final Vector2D position = train.getFrontPosition(); final Vector2D position = train.getFrontPosition();
final Vector2D direction = train.getDirection(); final Vector2D direction = train.getDirection();
@ -279,7 +292,7 @@ public final class TrainManager {
if (nextPosition == null || nextPosition.equals(position)) { if (nextPosition == null || nextPosition.equals(position)) {
// train is derailing // train is derailing
train.moveTo(railNetwork, null); train.moveTo(railNetwork, null);
collisions.add(new HashSet<>(Arrays.asList(train))); collisions.add(Stream.of(train).collect(Collectors.toCollection(TreeSet::new)));
} else { } else {
// train is moving successfully // train is moving successfully
train.moveTo(railNetwork, nextPosition); train.moveTo(railNetwork, nextPosition);
@ -295,8 +308,8 @@ public final class TrainManager {
* *
* @return list of collisions (never null, sometimes empty) * @return list of collisions (never null, sometimes empty)
*/ */
private List<Set<Train>> getCollisionsOfOneReverseStep() { private List<SortedSet<Train>> getCollisionsOfOneReverseStep() {
final List<Set<Train>> collisions = new ArrayList<>(); final List<SortedSet<Train>> collisions = new ArrayList<>();
// perform step // perform step
trains.values().stream().filter(Train::isPlaced).forEach(train -> { trains.values().stream().filter(Train::isPlaced).forEach(train -> {
final Vector2D position = train.getRearPosition(); final Vector2D position = train.getRearPosition();
@ -305,7 +318,7 @@ public final class TrainManager {
if (nextPosition == null || nextPosition.equals(position)) { if (nextPosition == null || nextPosition.equals(position)) {
// derailing // derailing
train.moveBackTo(railNetwork, nextPosition); train.moveBackTo(railNetwork, nextPosition);
collisions.add(new HashSet<>(Arrays.asList(train))); collisions.add(Stream.of(train).collect(Collectors.toCollection(TreeSet::new)));
} else { } else {
// train moving successfully // train moving successfully
train.moveBackTo(railNetwork, nextPosition); train.moveBackTo(railNetwork, nextPosition);
@ -317,42 +330,27 @@ public final class TrainManager {
} }
/** /**
* Move the trains in this simulation, printing their new positions and any collisions. * Move the trains in this simulation.
*
* @param speed amount of steps to move the trains * @param speed amount of steps to move the trains
* @return train collisions, null if no trains are placed
* @throws InvalidInputException if simulation is not yet ready * @throws InvalidInputException if simulation is not yet ready
*/ */
public void step(short speed) throws InvalidInputException { public List<SortedSet<Integer>> step(short speed) throws InvalidInputException {
if (!railNetwork.isReadyForTrains()) { if (!railNetwork.isReadyForTrains()) {
throw new InvalidInputException("rail tracks/switches not set up"); throw new InvalidInputException("rail tracks/switches not set up");
} }
if (trains.values().stream().noneMatch(Train::isPlaced)) { if (trains.values().stream().noneMatch(Train::isPlaced)) {
Terminal.printLine(OK); return null;
return;
} }
final List<Set<Train>> collisions = IntStream.range(0, Math.abs(speed)) return IntStream.range(0, Math.abs(speed))
.mapToObj(step -> speed >= 0 ? getCollisionsOfOneStep() : getCollisionsOfOneReverseStep()) .mapToObj(step -> speed >= 0 ? getCollisionsOfOneStep() : getCollisionsOfOneReverseStep())
.flatMap(List::stream).collect(Collectors.toList()); // replace train references with identifiers to prevent modification by caller
.flatMap(collisions -> collisions.stream()
for (final int id : trains.keySet().stream().sorted().collect(Collectors.toList())) { .map(collision -> collision.stream().map(Train::getIdentifier)
final Train train = trains.get(id); .collect(Collectors.toCollection(TreeSet::new))))
final Set<Train> collisionSet = collisions.stream() .sorted(Comparator.comparing(TreeSet::first))
.filter(collision -> collision.contains(train)) .collect(Collectors.toList());
.findFirst().orElse(null);
if (collisionSet != null) { // print collision
final int first = collisionSet.stream()
.min(Comparator.comparing(Train::getIdentifier)).get().getIdentifier();
if (train.getIdentifier() == first) { // only print each collision once
final List<Train> collision = collisionSet.stream()
.sorted(Comparator.comparing(Train::getIdentifier))
.collect(Collectors.toList());
Terminal.printLine("Crash of train " + String.join(",", collision.stream()
.map(crashedTrain -> Integer.toString(crashedTrain.getIdentifier()))
.toArray(String[]::new)));
}
} else if (train.isPlaced()) { // or position, if not in collision
Terminal.printLine(String.format("Train %d at %s", train.getIdentifier(), train.getFrontPosition()));
}
}
} }
} }

View File

@ -4,7 +4,7 @@ import edu.kit.informatik.Terminal;
import edu.kit.informatik.model.ModelRailwaySimulation; import edu.kit.informatik.model.ModelRailwaySimulation;
import edu.kit.informatik.ui.InvalidInputException; import edu.kit.informatik.ui.InvalidInputException;
import java.util.List; import java.util.SortedMap;
import static edu.kit.informatik.ui.command.CommandFactory.LIST_TRAINS; import static edu.kit.informatik.ui.command.CommandFactory.LIST_TRAINS;
@ -17,11 +17,11 @@ import static edu.kit.informatik.ui.command.CommandFactory.LIST_TRAINS;
public class ListTrains extends Command { public class ListTrains extends Command {
@Override @Override
public void apply(ModelRailwaySimulation simulation) { public void apply(ModelRailwaySimulation simulation) {
final List<String> trains = simulation.listTrains(); final SortedMap<Integer, String> trains = simulation.getTrains();
if (trains.isEmpty()) { if (trains.isEmpty()) {
Terminal.printLine("No train exists"); Terminal.printLine("No train exists");
} else { } else {
trains.forEach(Terminal::printLine); trains.values().forEach(Terminal::printLine);
} }
} }

View File

@ -1,8 +1,14 @@
package edu.kit.informatik.ui.command; package edu.kit.informatik.ui.command;
import edu.kit.informatik.Terminal;
import edu.kit.informatik.model.ModelRailwaySimulation; import edu.kit.informatik.model.ModelRailwaySimulation;
import edu.kit.informatik.model.Vector2D;
import edu.kit.informatik.ui.InvalidInputException; import edu.kit.informatik.ui.InvalidInputException;
import java.util.List;
import java.util.SortedSet;
import static edu.kit.informatik.ui.CommandLine.OK;
import static edu.kit.informatik.ui.command.CommandFactory.NUMBER; import static edu.kit.informatik.ui.command.CommandFactory.NUMBER;
import static edu.kit.informatik.ui.command.CommandFactory.STEP; import static edu.kit.informatik.ui.command.CommandFactory.STEP;
@ -20,7 +26,25 @@ public class Step extends Command {
@Override @Override
public void apply(ModelRailwaySimulation simulation) throws InvalidInputException { public void apply(ModelRailwaySimulation simulation) throws InvalidInputException {
simulation.step(speed); final List<SortedSet<Integer>> collisions = simulation.step(speed);
if (collisions == null) {
Terminal.printLine(OK);
} else {
for (final int id : simulation.getTrains().keySet()) {
final SortedSet<Integer> collisionSet = collisions.stream()
.filter(collision -> collision.first() == id)
.findFirst().orElse(null);
if (collisionSet != null) { // print collision
Terminal.printLine("Crash of train " + String.join(",",
collisionSet.stream().map(Object::toString).toArray(String[]::new)));
} else { // or position, if not in collision
final Vector2D position = simulation.getTrainPosition(id);
if (position != null) { // only print placed trains
Terminal.printLine(String.format("Train %d at %s", id, position));
}
}
}
}
} }
@Override @Override