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.Map;
import java.util.Optional;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@ -66,6 +68,7 @@ public class ModelRailwaySimulation {
/**
* Remove the specified rail from the rail network.
*
* @param id identifier of the rail to remove
* @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() {
return trainManager.listTrains();
public SortedMap<Integer, String> getTrains() {
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
* @return train collisions, null if no trains are placed
* @throws InvalidInputException if the simulation is not yet ready
*/
public void step(short speed) throws InvalidInputException {
trainManager.step(speed);
public List<SortedSet<Integer>> step(short speed) throws InvalidInputException {
return trainManager.step(speed);
}
}

View File

@ -16,7 +16,7 @@ import java.util.stream.Collectors;
* @author Arne Keller
* @version 1.1
*/
public final class Train {
public final class Train implements Comparable {
/**
* 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,
* just touching the rail is not enough.
* Check whether this train is on a particular rail. Just touching the rail is enough.
*
* @param rail the rail to check
* @return whether this train is on that 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));
}
@ -367,4 +368,9 @@ public final class Train {
}
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;
import edu.kit.informatik.Terminal;
import edu.kit.informatik.ui.InvalidInputException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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.IntStream;
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.
*
@ -32,7 +31,7 @@ public final class TrainManager {
*/
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<>();
@ -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
* rail, simply touching one of the end points is not enough.
* Check whether a train is on the rail with the specified identifier. Trains only touching the end of the rail
* will also be considered.
*
* @param rail the rail to check
* @return whether a train is on that rail
@ -87,19 +86,29 @@ public final class TrainManager {
throw new InvalidInputException("rolling stock already used");
}
final Train train = trains.get(trainId);
if (train != null && train.isPlaced()) {
throw new InvalidInputException("can not add rolling stock to placed train");
}
if (train != null) {
train.add(rollingStock);
} else {
if (train == null) { // create new train
final int correctId = getNextTrainIdentifier();
if (trainId != correctId) {
throw new InvalidInputException("new train identifier must be next free identifier");
}
final Train newTrain = new Train(trainId, rollingStock);
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);
}
/**
@ -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()
.sorted()
.map(trains::get)
.map(Object::toString)
.collect(Collectors.toList());
.collect(Collectors.toMap(Train::getIdentifier, Object::toString, (id1, id2) -> id1, TreeMap::new));
}
/**
* Get the ASCII art representation of a train.
*
* @param trainId identifier of the train to show
* @return ASCII art representation of said train
* @throws InvalidInputException if train not found
@ -179,19 +188,12 @@ public final class TrainManager {
}
/**
* Calculate the next available train identifier.
* @return the next train identifier, or -1 if none available
* Get collisions of trains currently placed. This implements the first collision checking algorithm (1A):
* trains collide if they partially occupy the same rail or touch each other.
*
* @param collisions list of collisions to expand
*/
private int getNextTrainIdentifier() {
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) {
private void getStaticCollisions(List<SortedSet<Train>> collisions) {
final int maxId = trains.keySet().stream().max(Integer::compareTo).orElse(0);
for (int id = 1; id <= maxId; id++) {
final Train train1 = trains.get(id);
@ -200,13 +202,13 @@ public final class TrainManager {
}
final Set<Rail> occupiedRails = train1.getOccupiedRails();
// 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)
.filter(Objects::nonNull)
.filter(Train::isPlaced)
.filter(train -> train1.touches(train)
|| train.getOccupiedRails().stream().anyMatch(occupiedRails::contains))
.collect(Collectors.toSet());
.collect(Collectors.toCollection(TreeSet::new));
if (!collision.isEmpty()) {
collision.add(train1);
addToSetOrAddNew(collisions, collision);
@ -215,15 +217,15 @@ public final class TrainManager {
}
/**
* Implementation of the silly *second* collision checking algorithm.
* Will put two trains in a collision even if they only touch the same rail.
* Implementation of the silly second collision checking algorithm (2B):
* two trains collide if they touch the same rail or position.
*
* @return list of collisions
*/
private List<Set<Train>> getPlacementCollisions() {
final List<Set<Train>> collisions = new ArrayList<>();
private List<SortedSet<Train>> getPlacementCollisions() {
final List<SortedSet<Train>> collisions = new ArrayList<>();
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();
Collections.addAll(occupiedByTrain1, railNetwork.findTouchingRails(train1.getFrontPosition()));
Collections.addAll(occupiedByTrain1, railNetwork.findTouchingRails(train1.getRearPosition()));
@ -233,7 +235,8 @@ public final class TrainManager {
// calculate intersection
occupiedByTrain2.retainAll(occupiedByTrain1);
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);
}
}));
@ -243,7 +246,7 @@ public final class TrainManager {
/**
* 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;
int i = 0;
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.
*
* @return list of collisions (never null, sometimes empty)
*/
private List<Set<Train>> getCollisionsOfOneStep() {
final List<Set<Train>> collisions = new ArrayList<>();
private List<SortedSet<Train>> getCollisionsOfOneStep() {
final List<SortedSet<Train>> collisions = new ArrayList<>();
trains.values().stream().filter(Train::isPlaced).forEach(train -> {
final Vector2D position = train.getFrontPosition();
final Vector2D direction = train.getDirection();
@ -279,7 +292,7 @@ public final class TrainManager {
if (nextPosition == null || nextPosition.equals(position)) {
// train is derailing
train.moveTo(railNetwork, null);
collisions.add(new HashSet<>(Arrays.asList(train)));
collisions.add(Stream.of(train).collect(Collectors.toCollection(TreeSet::new)));
} else {
// train is moving successfully
train.moveTo(railNetwork, nextPosition);
@ -295,8 +308,8 @@ public final class TrainManager {
*
* @return list of collisions (never null, sometimes empty)
*/
private List<Set<Train>> getCollisionsOfOneReverseStep() {
final List<Set<Train>> collisions = new ArrayList<>();
private List<SortedSet<Train>> getCollisionsOfOneReverseStep() {
final List<SortedSet<Train>> collisions = new ArrayList<>();
// perform step
trains.values().stream().filter(Train::isPlaced).forEach(train -> {
final Vector2D position = train.getRearPosition();
@ -305,7 +318,7 @@ public final class TrainManager {
if (nextPosition == null || nextPosition.equals(position)) {
// derailing
train.moveBackTo(railNetwork, nextPosition);
collisions.add(new HashSet<>(Arrays.asList(train)));
collisions.add(Stream.of(train).collect(Collectors.toCollection(TreeSet::new)));
} else {
// train moving successfully
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
* @return train collisions, null if no trains are placed
* @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()) {
throw new InvalidInputException("rail tracks/switches not set up");
}
if (trains.values().stream().noneMatch(Train::isPlaced)) {
Terminal.printLine(OK);
return;
return null;
}
final List<Set<Train>> collisions = IntStream.range(0, Math.abs(speed))
return IntStream.range(0, Math.abs(speed))
.mapToObj(step -> speed >= 0 ? getCollisionsOfOneStep() : getCollisionsOfOneReverseStep())
.flatMap(List::stream).collect(Collectors.toList());
for (final int id : trains.keySet().stream().sorted().collect(Collectors.toList())) {
final Train train = trains.get(id);
final Set<Train> collisionSet = collisions.stream()
.filter(collision -> collision.contains(train))
.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))
// replace train references with identifiers to prevent modification by caller
.flatMap(collisions -> collisions.stream()
.map(collision -> collision.stream().map(Train::getIdentifier)
.collect(Collectors.toCollection(TreeSet::new))))
.sorted(Comparator.comparing(TreeSet::first))
.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.ui.InvalidInputException;
import java.util.List;
import java.util.SortedMap;
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 {
@Override
public void apply(ModelRailwaySimulation simulation) {
final List<String> trains = simulation.listTrains();
final SortedMap<Integer, String> trains = simulation.getTrains();
if (trains.isEmpty()) {
Terminal.printLine("No train exists");
} else {
trains.forEach(Terminal::printLine);
trains.values().forEach(Terminal::printLine);
}
}

View File

@ -1,8 +1,14 @@
package edu.kit.informatik.ui.command;
import edu.kit.informatik.Terminal;
import edu.kit.informatik.model.ModelRailwaySimulation;
import edu.kit.informatik.model.Vector2D;
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.STEP;
@ -20,7 +26,25 @@ public class Step extends Command {
@Override
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