Building on top of Simon's excellent answer, why stop at enum
s for Direction
s when you can do one to represent Border
s too?
Looking at it from that perspective, if we have just moved Left
, we cannot go Left
again if there is a border on the Left
. If we just moved Up
, we cannot go Up
again if there is a border on the top (i.e. Up
, for naming consistency). You should get the idea by now.
Let's create our Border
enum
:
import java.util.EnumSet;
import java.util.Set;
public enum Border {
LEFT, UP, RIGHT, BOTTOM;
public static Set<Border> getBorders(final int y, final int x, final int rowLimit, final int columnLimit) {
Set<Border> result = EnumSet.noneOf(Border.class);
if (x == 0) {
result.add(LEFT);
} else if (x == rowLimit) {
result.add(RIGHT);
}
if (y == 0) {
result.add(BOTTOM);
} else if (y == columnLimit) {
result.add(UP);
}
return result;
}
}
We have a static
method to generate the Set
of Border
s for us given the current y, x
(side note, I think it's easier to read as x, y
instead) positions and the limits of our plane.
Our Direction
enum
can then be enhanced with two helper methods to determine what are the valid moves (I'm borrowing Stream
s from Java 8, but I don't think it's that hard to replace it with loops for a < Java 8 implementation):
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.Collectors;
public enum Direction {
LEFT, UP, RIGHT, BOTTOM;
public Direction opposite() {
switch (this) {
case LEFT: return RIGHT;
case RIGHT: return LEFT;
case UP: return BOTTOM;
case BOTTOM: return UP;
default: throw new IllegalStateException();
}
}
public boolean isValidMove(final Set<Border> borders) {
return borders.stream().noneMatch(v -> v.name().equals(name()));
}
public List<Direction> getValidMoves(final Set<Border> borders) {
return Stream.of(values())
.filter(v -> v != opposite() && v.isValidMove(borders))
.collect(Collectors.toList());
}
}
The trick we employ in isValidMove
assumes that the Border
enum
s have the same names as our Direction
enum
s to simplify the logic (returning false
when the name()
s match), alternatively, you can replace it with a more full-blown switch
logic. getValidMoves
calls isValidMove
for the Direction
it is evaluating, on top of making sure it is not in the opposite direction of the current Direction
(v != opposite()
) to give us the List
of valid moves.
For testing and illustration purpose, I have the Game
class below that shows all the possible outcomes for a given Direction
, i.e. the previous
argument in your original code for nextDirection()
:
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
public class Game {
public static void main(String[] args) {
final int limit = 10;
final Set<Border> leftBorder = Border.getBorders(1, 0, limit, limit);
final Set<Border> rightBorder = Border.getBorders(0, 1, limit, limit);
final Set<Border> topBorder = Border.getBorders(limit, 1, limit, limit);
final Set<Border> bottomBorder = Border.getBorders(1, limit, limit, limit);
final Set<Border> topLeftCorner = Border.getBorders(limit, 0, limit, limit);
final Set<Border> topRightCorner = Border.getBorders(limit, limit, limit, limit);
final Set<Border> bottomRightCorner = Border.getBorders(0, limit, limit, limit);
final Set<Border> bottomLeftCorner = Border.getBorders(0, 0, limit, limit);
final Set<Border> free = Border.getBorders(1, 1, limit, limit);
final List<Set<Border>> combinations = Arrays.asList(leftBorder, rightBorder, topBorder, bottomBorder,
topLeftCorner, topRightCorner, bottomRightCorner, bottomLeftCorner, free);
Stream.of(Direction.values())
.flatMap(d -> combinations.stream()
.filter(b -> b.stream().noneMatch(v -> v.name().equals(d.opposite().name())))
.peek(b -> System.out.println("Generating moves for " + d + " given borders " + b))
.map(b -> d.getValidMoves(b)))
.forEach(System.out::println);
}
}
What the final Stream.of()
statement does is this:
- Given an element from a
Stream
of Direction.values()
, d
,
- And an element from a
Stream
of our test Set<Border>
cases, b
,
- We filter any
b
where it is impossible to make the move d
(e.g. we can't come from the LEFT
if we are along the RIGHT
edge),
- And then call
d.getValidMoves()
with each b
to map()
the results as the resulting Stream
,
- finally calling
println
to the console for each result.
The console output:
Generating moves for LEFT given borders [LEFT]
[UP, BOTTOM]
Generating moves for LEFT given borders [BOTTOM]
[LEFT, UP]
Generating moves for LEFT given borders [UP]
[LEFT, BOTTOM]
Generating moves for LEFT given borders [LEFT, UP]
[BOTTOM]
Generating moves for LEFT given borders [LEFT, BOTTOM]
[UP]
Generating moves for LEFT given borders []
[LEFT, UP, BOTTOM]
Generating moves for UP given borders [LEFT]
[UP, RIGHT]
Generating moves for UP given borders [UP]
[LEFT, RIGHT]
Generating moves for UP given borders [RIGHT]
[LEFT, UP]
Generating moves for UP given borders [LEFT, UP]
[RIGHT]
Generating moves for UP given borders [UP, RIGHT]
[LEFT]
Generating moves for UP given borders []
[LEFT, UP, RIGHT]
Generating moves for RIGHT given borders [BOTTOM]
[UP, RIGHT]
Generating moves for RIGHT given borders [UP]
[RIGHT, BOTTOM]
Generating moves for RIGHT given borders [RIGHT]
[UP, BOTTOM]
Generating moves for RIGHT given borders [UP, RIGHT]
[BOTTOM]
Generating moves for RIGHT given borders [RIGHT, BOTTOM]
[UP]
Generating moves for RIGHT given borders []
[UP, RIGHT, BOTTOM]
Generating moves for BOTTOM given borders [LEFT]
[RIGHT, BOTTOM]
Generating moves for BOTTOM given borders [BOTTOM]
[LEFT, RIGHT]
Generating moves for BOTTOM given borders [RIGHT]
[LEFT, BOTTOM]
Generating moves for BOTTOM given borders [RIGHT, BOTTOM]
[LEFT]
Generating moves for BOTTOM given borders [LEFT, BOTTOM]
[RIGHT]
Generating moves for BOTTOM given borders []
[LEFT, RIGHT, BOTTOM]
In conclusion
Retrofitting everything together...
private Direction nextDirection(final int y, final int x, final int rowLimit, final int columnLimit, final Direction previous) {
List<Direction> results = previous.getValidMoves(Border.getBorders(y, x, rowLimit, columnLimit));
return results.get(random.nextInt(results.size()));
}
Caveats
This solution can't claim to be the speediest, and I still think Simon's answer provides the ideal balance between performance and an 'OO'-approach. JS1's approach is the bit mask approach you ask for... Consider this if it turns out you do have a case to determine where your Border
s are, such that the comparison logic between your Direction
s and said Border
s can be abstracted away.