FX My Life 3.3: Monsters and Bullies

FX My Life 3.3: Monsters and Bullies

·

20 min read

I write these in odd moments between other, more "serious" work and sometimes, if I get busy, it can take weeks to come out with a new installment. And then when I get to the end of the post, I'm not sure where I started from! (This is another advantage of an edited, polished book: Things can be more cohesive. But ain't nobody got time for that!)

Last time, for example, I thought we'd do more with the designer (we didn't), set up something to actually play the level (we didn't), thought we'd have to make player and exit into things (nope), and we ended up spending half the time figuring out an issue that required us to delve into game state.

The main thing, though, is that we put in the treasure object. That aspect of it was pretty easy.

This time, we might actually fill in some of the spots we missed from the last post, but our main focus will be on creating a monster. Designations of "treasure", "monster", "trap" and so on are rather arbitrary, so we'll think more in terms of what kind of behavior we want, and how we'll express that.

Flee!

The first behavior will be to flee. If a thing flees, that means at the end of the player's move, if the player is next to the thing and not blocked, the thing will move away from the player.

So if we add a flees field:

public record Thing(
            GamePiece id,
            Direction blocks,
            Boolean occupies,
            Boolean removeOnTouch,
            Boolean flees
    ) {
    }

This will break our constructors unfortunately. We have to add false to our wall constructor:

public static Thing Wall(Direction dir) {
        return new Thing(GamePiece.wall, dir, false, false, false);
    }

To the Dunslip constructor:

for (Map.Entry<Coord, List<Thing>> entry : d.things.entrySet()) {
            for (Thing t : entry.getValue()) {
                this.add(entry.getKey().x, entry.getKey().y, new Thing(t.id, t.blocks, t.occupies, t.removeOnTouch, t.flees));
            }
        }

And to copy_history:

        for (Map.Entry<Coord, List<Thing>> entry : d.things.entrySet()) {
            for (Thing t : entry.getValue()) {
                this.add(entry.getKey().x, entry.getKey().y, new Thing(t.id, t.blocks, t.occupies, t.removeOnTouch, t.flees));
            }
        }

In our controller, with our little minimal setup:

        game.add(2,2,  new Thing(wall, DOWN, false, false, false));
        game.add(2, 0, new Thing(wall, LEFT, false, false, false));
        game.add(2, 0, new Thing(treasure, null, true, true, false));

Aaaand with the treasure constructor:

case "Treasure" -> dvm.thing = new Thing(treasure, null, true, true, false);

Sheesh. That's gonna get old fast.

###Sidebar: Builder Pattern

There is a thing called the Builder pattern which allows us to get around this. The gag is to create an inner class with all the same fields as the record, and then specific methods to set the fields:

        public static class ThingBuilder {
            private GamePiece id;
            private Direction blocks;
            private Boolean occupies;
            private Boolean removeOnTouch;
            private Boolean flees;

            public ThingBuilder() {}

            public ThingBuilder id(GamePiece field) {
                this.id = field;
                return this;
            }
... //et cetera

You then give the class a build() that your record calls in its constructor:

        private Thing(ThingBuilder thingBuilder) {
            this(thingBuilder.id, thingBuilder.blocks, thingBuilder.occupies, thingBuilder.removeOnTouch, thingBuilder.flees);
        }

And you can now create records with selected fields, so for a wall, we go from this:

        game.add(8, 8,  new Thing(wall, RIGHT, false, false, false));
        game.add(1, 0, new Thing(wall, LEFT, false, false, false));
        game.add(4, 9, new Thing(treasure, null, true, true, false));

to:

        game.add(8, 8,  new Thing.ThingBuilder().id(wall).blocks(RIGHT);
        game.add(8, 8,  new Thing.ThingBuilder().id(wall).blocks(LEFT);
        game.add(8, 8,  new Thing.ThingBuilder().id(treasure).occupies(TRUE);

While that is clearer, it strikes me as a staggering amount of boilerplate code to write for code that will ultimately only be called in a couple of places, in DunslipController:

        complist.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
            switch(newValue) {
                case "Wall (left)" -> dvm.thing = Dunslip.Wall(LEFT);
                case "Wall (right)" -> dvm.thing = Dunslip.Wall(RIGHT);
                case "Wall (up)" -> dvm.thing = Dunslip.Wall(UP);
                case "Wall (down)" -> dvm.thing = Dunslip.Wall(DOWN);
                case "Treasure" -> dvm.thing = new Thing(treasure, null, true, true, false);
                default -> dvm.thing = null;
            }

And in helper methods, like Wall:

    public static Thing Wall(Direction dir) {
        return new Thing(GamePiece.wall, dir, false, false, false);
    }

So I judge this as "not worth it". However, I will update the "start-up" puzzle code to use Wall:

        game.add(8, 8, Dunslip.Wall(RIGHT));
        game.add(1, 0, Dunslip.Wall(LEFT));
        game.add(4, 9, new Thing(treasure, null, true, true, false));

And, while we're at it, GamePiece members should be all-caps, but we'll fix that when add the next piece.

Back To Our Setup Code

Let's change the minimal setup so that it gives us a good test for our fleeing monster.

        game.add(8, 8,  new Thing(wall, RIGHT, false, false, false));
        game.add(1, 0, new Thing(wall, LEFT, false, false, false));
        game.add(4, 9, new Thing(treasure, null, true, true, false));

This produces:

image.png

We'll put a monster in the square immediately above the wall in the lower-right. The exit strategy will be: down, right (monster flees north), down, right, up (monster flees left), left, down.

image.png

The blue line shows the player's move, the white line shows the monster fleeing. This is a genuine puzzle, right? Because the only path out is the one shown. If the player moves left or right first, or up followed by anything other than down, he's lost. (But of course, he can rewind.)

Those Darn Goblins Are Everywhere

Once a feared and respected mythological creature spanning multiple cultures, the world of RPGs has turned the goblin into the whipping boy of fantasy: Cowardly, dumb, and vicious. So, naturally, we'll make our first monsters goblins.

public enum GamePiece {WALL, TREASURE, GOBLIN}

Placed on the minimal set-up map, and note the case shift above and below:

game.add(8, 8, Dunslip.Wall(RIGHT));
game.add(1, 0, Dunslip.Wall(LEFT));
game.add(4, 9, new Thing(TREASURE, null, true, true, false));
game.add(8, 7, new Thing(GOBLIN, null, true, true, true));

Drawn in the draw routine:

                    case GOBLIN -> {
                        var t = new Circle();
                        setToken(t, tokenCalc(t, new Coord(entry.getKey().x, entry.getKey().y)));
                        t.setFill(Color.WHITE);
                        board.piecePane.getChildren().add(t);
                    }

image.png

Sweet. But not quite sweet enough. The goblin case is awful close to the treasure case (and probably will continue to be so even when we swap out for real graphics), so let's refactor.

    public void addPiece(Coord loc, Color c) {
        var t = new Circle();
        setToken(t, tokenCalc(t, new Coord(loc.x, loc.y)));
        t.setFill(c);
        board.piecePane.getChildren().add(t);
    }

Now, our loop to draw things looks like:

        for (var entry : game.things.entrySet()) {
            for (var thing : entry.getValue()) {
                switch (thing.id()) {
                    case wall -> addWall(entry.getKey().x, entry.getKey().y, thing.blocks());
                    case treasure -> addPiece(entry.getKey(), Color.GOLD);
                    case goblin -> addPiece(entry.getKey(), Color.WHITE);
                }
            }

Originally I had addPiece take the x and y value separately, matching AddWall, but I think passing the Coord is cleaner and will probably change addWall to match.

If you run through this, you'll see that our "goblin" behaves just like treasure. It's time to implement fleeing.

But how?

Run, Run, Runaway!

The way the effects work—of which we currently only have removeOnTouch—is that the player moves along and creates effects as he goes. In theory that would mean that if he ran over a bunch of removeOnTouch items, they'd be added one-by-one. (Right now because our only such item is treasure and it occupies the square, it also stops the player's motion, but that's not necessarily the case in the future.)

Flee is different: Fleeing is triggered by the player's location after his move is over. Moving past a creature doesn't do anything: He must land adjacent. In our movePlayer, we'll add a new routine after all the moving and effects are done:

        processEffects();
        afterEffects(); //ADD

And for afterEffects, we'll fall back on using streams:

    private void afterEffects() {
        var co = new Coord(player.x, player.y);
        for (Direction dir : Direction.values()) {
            Coord delta = dirToDelta(dir);
            Coord target = new Coord(co.x + delta.x, co.y + delta.y);
            var ts = things.get(target);
            if (ts != null)
                ts.stream()
                        .filter(thing -> thing.flees)
                        .forEach(thing -> {
                            flee(thing, dir);
                        });
        }
    }

For now, flee can just be:

    private void flee(Thing thing, Direction dir) {
        System.out.println(thing.toString() + "is fleeing " + dir.toString() +"!");
    }

And if we move our player under the goblin, we see:

Thing[id=goblin, blocks=null, occupies=true, removeOnTouch=true, flees=true]is fleeing UP!

Now it's time to pay the piper, however. We built our movePlayer and movePlayerOneSpace methods specifically to, y'know, move the player but now we gotta move something NOT the player.

We shouldn't be too bad off, however. The flow is always going to be: player moves, other pieces react, so we shouldn't have to worry too much about effects getting tangled. (I hope.)

Let's refactor movePlayer:

    public boolean movePlayer(Direction d) {
        if (gameState != GameState.inGame) return false;
        if (history.size() == 0) recordState();
        effectList = new ArrayList<>();
        var moved = moveThing(player, d);
        processEffects();
        afterEffects();
        recordState();
        return moved;
    }

We've taken the actual movement code to moveThing:

    private boolean moveThing(Coord thing, Direction d) {
        var moved = false;
        while (movePlayerOneSpace(d)) {
            var delta = dirToDelta(d);
            thing.x += delta.x;
            thing.y += delta.y;
            moved = true;
            if (thing.equals(exit)) {
                gameState = postGame;
                break;
            }
        }
        return moved;
    }

Everything still works, so let's now change movePlayerOneSpace to moveThingOneSpace:

        while (moveThingOneSpace(d)) {

Just replace player with the passed in coordinate:

    public boolean moveThingOneSpace(Coord it, Direction d) {
        Coord c = it.add(dirToDelta(d));
        if (c.x < 0 || c.y < 0 || c.x >= width || c.y >= height) return false;
        for (var entry : things.entrySet()) {
            if (entry.getKey().equals(it) &&
                    entry.getValue().stream().anyMatch(thing -> thing.blocks == d)) return false;
            if (entry.getKey().equals(c) &&
                    entry.getValue().stream().anyMatch(thing -> thing.blocks == directionComplement(d))) return false;
            entry.getValue().stream()
                    .filter(thing -> entry.getKey().equals(c) && thing.removeOnTouch)
                    .forEach(thing -> {
                        effectList.add(new Effect(c, thing, it, null));
                    });
            if (entry.getKey().equals(c) &&
                    entry.getValue().stream().anyMatch(thing -> thing.occupies)) return false;
        }
        return true;
    }

I already had a thing in the code, so I called the passed-in parameter it. At some point, we're probably going to want to pass in an actual Thing rather than just a coordinate, because we probably care about the characteristics of the Thing being moved. Like, maybe we don't want monsters (e.g.) to pick up treasure

But this was no big deal and incremental changes are good. So let's see if we can now get our goblins to flee.

private void flee(Thing thing, Direction dir) {
   moveThing(thing.???)

Whoops. We're not passing in the coordinate. In fact, Thing has no coordinate. All things are stored in the things list, keyed by coordinate. The coordinate we pass in is shared by all items on that location!

So, really, what we want to do is to remove the item from the particular coordinate in the things list and then add it in the new place for each step it takes. I guess it's not a huge deal but let's take a step back.

Why did we use the Coord=>List-Of-Things hashmap in the first place? Well, we were just getting away from a 2D array and we knew we'd need more than one item on a square. Well, what if we just had a list of things and included the Coord as part of the Thing?

Not only would that work, I think, if we had a function for collecting all the items at a given coordinate, we could probably use our code mostly unchanged.

We Don't HAVE To Recode It; We GET To Recode It!

OK, let's just switch it over and see what happens:

public List<Thing> things = new ArrayList<>();

IJ says there's one related problem, which seems way too few, but I think it means "one problem outside the classe" which is, after all, the stuff that's the most important to protect.

It's the code in the view-model that does the drawing. Looking at it, it should be easier without the hash-map:

        for (var entry : game.things.entrySet()) {
            for (var thing : entry.getValue()) {
                switch (thing.id()) {
                    case WALL -> addWall(entry.getKey().x, entry.getKey().y, thing.blocks());
                    case TREASURE -> addPiece(entry.getKey(), Color.GOLD);
                    case GOBLIN -> addPiece(entry.getKey(), Color.WHITE);
                }
            }
        }

We do need to add a location field to Thing, as discussed:

    public record Thing(
            GamePiece id,
            Coord location,
. . .

And now the loop is simplified:

        for (var thing : game.things) {
            switch (thing.id()) {
                case WALL -> addWall(thing.location().x, thing.location().y, thing.blocks());
                case TREASURE -> addPiece(thing.location(), Color.GOLD);
                case GOBLIN -> addPiece(thing.location(), Color.WHITE);
            }
        }

So that's nice. Now IJ is flagging issues in the controller, and predictably we can't add things any more the same way:

        game.add(4, 9, new Thing(TREASURE, null, true, true, false));
        game.add(8, 7, new Thing(GOBLIN, null, true, true, true));
. . .
    case "Treasure" -> dvm.thing = new Thing(TREASURE, null, true, true, false);

Let's change the Dunslip add() first to remove the coordinates:

public boolean add(Thing thing) {

Hmmm. This also breaks our code for adding walls:

        game.add(8, 8, Dunslip.Wall(RIGHT));
        game.add(1, 0, Dunslip.Wall(LEFT));

All right, well, let's fix Wall() first:

    public static Thing Wall(Coord c, Direction dir) {
        return new Thing(GamePiece.WALL, c, dir, false, false, false);
    }

Adding a wall now looks like:

        game.add(Dunslip.Wall(new Coord(8, 8), RIGHT));
        game.add(Dunslip.Wall(new Coord(1, 0), LEFT));

Actually, since this is kind of a "sugar" method, let's do it this way:

    public static Thing Wall(int x, int y, Direction dir) {
        return new Thing(GamePiece.WALL, new Coord(x, y), dir, false, false, false);
    }

And now adding a wall is:

        game.add(Dunslip.Wall(8, 8, RIGHT));
        game.add(Dunslip.Wall(1, 0, LEFT));

I really don't like the idea of the client passing in coordinate objects. What if he mistakenly passes the same Coord to do different objects? I mean, we're talking about US in this case, but we don't want to shoot ourselves in the foot.

All right, I'm going to flip the coin on this and revise Thing even further:

    public record Thing(
            GamePiece id,
            int x,
            int y,
            Direction blocks,
            boolean occupies,
            boolean removeOnTouch,
            boolean flees
    ) {
    }

Ideally, our record would just be a store of values, not objects. So I've also changed the Booleans to boolean. Enums are still objects, which kind of sucks since we really just need GamePiece and Direction to be simple values, but we won't mess with that right now.

So while our pre-fab puzzle now looks like this:

        game.add(Dunslip.Wall(8, 8, RIGHT));
        game.add(Dunslip.Wall(1, 0, LEFT));
        game.add(new Thing(TREASURE, 4, 9, null, true, true, false));
        game.add(new Thing(GOBLIN, 8, 7, null, true, true, true));

we have a problem with our user-interface:

                case "Wall (left)" -> dvm.thing = Dunslip.Wall(LEFT);
                case "Wall (right)" -> dvm.thing = Dunslip.Wall(RIGHT);
                case "Wall (up)" -> dvm.thing = Dunslip.Wall(UP);
                case "Wall (down)" -> dvm.thing = Dunslip.Wall(DOWN);
                case "Treasure" -> dvm.thing = new Thing(TREASURE, null, true, true, false);

Because they all need locations. I guess that's the downside of having the coordinate NOT separate from the thing: Objects can't be nowhere. And if we'd kept the Coord location, we could have allowed nulls. So that's a strike against our current "primitive" approach:

        complist.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
            switch(newValue) {
                case "Wall (left)" -> dvm.thing = Dunslip.Wall(-1, -1, LEFT);
                case "Wall (right)" -> dvm.thing = Dunslip.Wall(-1, -1, RIGHT);
                case "Wall (up)" -> dvm.thing = Dunslip.Wall(-1, -1, UP);
                case "Wall (down)" -> dvm.thing = Dunslip.Wall(-1, -1, DOWN);
                case "Treasure" -> dvm.thing = new Thing(TREASURE, -1, -1, null, true, true, false);
                default -> dvm.thing = null;
            }

So we'll put "unplaced" objects at -1, -1. Note that, since these haven't been added to the master Things list, it doesn't really matter.

OK, IJ has stopped complaining about Thing's definition but we still gots errors all over the damn place. (Well, in Dunslip and DsViewModel, anyway.) Let's fix 'em as we go, starting in Dunslip, we have our Wall sugar:

    public static Thing Wall(int x, int y, Direction dir) {
        return new Thing(GamePiece.WALL, x, y, dir, false, false, false);
    }

Then we have the method that creates a Dunslip from an existing Dunslip, specifically the part that copies the Things:

    public Dunslip(Dunslip d) {
. . .
        for (Map.Entry<Coord, List<Thing>> entry : d.things.entrySet()) {
            for (Thing t : entry.getValue()) {
                this.add(entry.getKey().x, entry.getKey().y, new Thing(t.id, t.blocks, t.occupies, t.removeOnTouch, t.flees));
            }
        }
. . .
    }

This is simplified:

        for (Thing t : d.things) {
            this.add(new Thing(t.id, t.x, t.y, t.blocks, t.occupies, t.removeOnTouch, t.flees));
        }

And we use the exact same code in copy_history().

Now we have add(), which before required us to check to see if an entry already existed:

    public boolean add(Thing thing) {
        var c = new Coord(x, y);
        things.computeIfAbsent(c, k -> new ArrayList<>());
        var l = things.get(new Coord(x, y));
        if (l.contains(thing)) return false;
        l.add(thing);
        return true;
    }

I think we can really simplify this:

    public boolean add(Thing thing) {
        if (things.contains(thing)) return false;
        things.add(thing);
        return true;
    }

The thing (as it were) to keep in mind is whether contains() will work for an object which has all the same values but isn't the same reference, e.g., you could have two LEFT walls with the same coordinates that were different objects.

I think this is a point for using x and y rather than Coord, since we don't have to worry about the object being the same reference AND the Coord being the same reference, too.

Now for remove():

    public boolean remove(Thing thing) {
        var l = things.indexOf(thing);
        if (l == -1) return false;
        things.remove(l);
        return true;
    }

Again, I'm not 100% sure this will work, but the mechanic isn't much different from before, so it should.

Both these changes will make trouble in the view-model, where we add and remove things, but let's keep going in Dunslip. moveThingOneSpace() is up next:

    public boolean moveThingOneSpace(Coord it, Direction d) {
        Coord c = it.add(dirToDelta(d));
        if (c.x < 0 || c.y < 0 || c.x >= width || c.y >= height) return false;
        for (var entry : things.entrySet()) {
            if (entry.getKey().equals(it) &&
                    entry.getValue().stream().anyMatch(thing -> thing.blocks == d)) return false;
            if (entry.getKey().equals(c) &&
                    entry.getValue().stream().anyMatch(thing -> thing.blocks == directionComplement(d))) return false;
            entry.getValue().stream()
                    .filter(thing -> entry.getKey().equals(c) && thing.removeOnTouch)
                    .forEach(thing -> {
                        effectList.add(new Effect(c, thing, it, null));
                    });
            if (entry.getKey().equals(c) &&
                    entry.getValue().stream().anyMatch(thing -> thing.occupies)) return false;
        }
        return true;
    }

This is a little tricky to read: Streams, while very functional, are hardly Java-like at all. But we're just looking for two things really: Something that blocks exit from the current location, and something that block entry from the current location (or any location, in the case of occupies).

The result is:

    public boolean moveThingOneSpace(Coord it, Direction d) {
        Coord dest = it.add(dirToDelta(d));
        if (dest.x < 0 || dest.y < 0 || dest.x >= width || dest.y >= height) return false;
        for (var thing : things) {
            var thingLoc = new Coord(thing.x, thing.y);
            if (it.equals(thingLoc) && (thing.blocks == d)) return false;
            if (dest.equals(thingLoc)) {
                if (thing.removeOnTouch) effectList.add(new Effect(dest, thing, it, null));
                if (thing.occupies || (thing.blocks == directionComplement(d))) return false;
            }
        }
        return true;
    }
```java
That's...so much easier. To read and to write. I cleaned it up a little by changing `c` to `dest` for the destination coordinate. 

If this actually works, we'll be in great shape. We should also take a moment to reflect on how those tests we didn't write would've been really useful about now.

Next up is `processEffects()`:
```java
    public void processEffects() {
        effectList.forEach(e -> {
            if (e.effect.removeOnTouch)
                remove(e.effectLoc.x, e.effectLoc.y, e.effect);
        });
    }

Hmmm.

    public void processEffects() {
        effectList.forEach(e -> {
            if (e.effect.removeOnTouch)
                remove(e.effect);
        });
    }

And the last item is afterEffects(), which we're going to comment out since the whole point was to be able to call flee()—the very thing we're implementing.

Now we just have to fix the view-model drawing code:

        for (var thing : game.things) {
            switch (thing.id()) {
                case WALL -> addWall(thing.x(), thing.y(), thing.blocks());
                case TREASURE -> addPiece(new Coord(thing.x(), thing.y()), Color.GOLD);
                case GOBLIN -> addPiece(new Coord(thing.x(), thing.y()), Color.WHITE);
            }
        }

I feel like we should make addPiece handle x and y rather than Coord but we can do that later. The last bit is placing the pieces when the user clicks:

        board.setOnGridMouseClicked((var1, x, y) -> {
                    System.out.println(thing);
                    if (thing != null) {
                        if (!game.remove(x, y, thing))
                            game.add(x, y, thing);
                        draw();
                    }
                }

So, the thing about the Java record type is that it's immutable, sorta. We can't change x or y. If we had a Coord, we could change that particular object—though we could never set it to null. It would have to be an object with values that we changed.

Let's just change this to:

board.setOnGridMouseClicked((var1, x, y) -> {
                    System.out.println(thing);
                    if (thing != null) {
                        if (!game.remove(thing))
                            game.add(thing);
                        draw();
                    }
                }
        );

For now and run the code we have. Perhaps shockingly, it all works.

I don't normally redesign my program without even checking for syntax errors, but when I do...

Now What?

Even though the code works as before (minus the placing of objects), it doesn't actually help our cause much. It works because we're still only passing Player to the move routines and Player isn't a Thing: It's just a Coord!

Well, in the interest of minimizing disruption, let's change Player to a Thing and see where that leads us:

    Thing player; //in DUNSLIP
. . .
    public enum GamePiece {PLAYER, WALL, TREASURE, GOBLIN}
... //IN THE CONTROLLER
game.player = new Thing(PLAYER, 4, 4, null, true, false, false);

In Dunslip() and copy_history, we're treating player as a Coord, so let's treat them as Things.

this.player = Thing.copy(d.player);

(This will cause problems, as we'll see later.) Now, we need to make the moveThing* methods take a Thing instead of aCoord, but the only thing that changes is that we have to set the dest Coord differently:

public boolean moveThingOneSpace(Thing it, Direction d) {
        Coord dest = new Coord(it.x, it.y).add(dirToDelta(d));

moveThing has to change a bit more, but still not drastically:

    private boolean moveThing(Thing thing, Direction d) {
        var moved = false;
        while (moveThingOneSpace(thing, d)) {
            var delta = dirToDelta(d);
            var t = Thing.move(thing, thing.x + delta.x, thing.y + delta.y);
            remove(thing);
            add(t);
            if (player == thing) player = t;
            thing = t;
            moved = true;
            if (thing.x == exit.x && thing.y == exit.y) {
                gameState = postGame;
                break;
            }
        }
        return moved;
    }

We "move" a thing by calling Thing.move which just creates a new record with the new Coord, then we remove the old thing, add the new thing, set the player field because we now have two references to the player—the field and the item in the things list—and generally we have to use x and y rather than just a passed-in Coord.

Now we can flesh out afterEffects() again, and it'll be pretty easy:

    private void afterEffects() {
        for(int i = things.size()-1; i >= 0; i--) {
            var thing = things.get(i);
            var dx = thing.x - player.x;
            var dy = thing.y - player.y;
            if (Math.abs(dx) + Math.abs(dy) == 1) {
                if (thing.flees) flee(thing, deltaToDir(dx, dy));
            }
        }

We just look through all the things and if they adjacent (not diagonal) to the player, the thing flees (if they're the fleeing kind). flee becomes super-simple:

    private void flee(Thing thing, Direction dir) {
        moveThing(thing, dir);
    }

That's nice. It's not animating nice, but it's moving the piece correctly. Before we get to that, though, we should fix our rewind.

Rewinding rewind

Rewind is still recording, but we've got an issue because we're treating player differently from other pieces. We've got a reference and we're assigning it to field variable and...blah. Let's remove the player field completely.

Let's instead have a function that returns the player object:

    public Thing player() {
        return things.stream().filter(thing -> thing.id == PLAYER).findFirst().get();
    }

This will create some issues. In the view-model, they're all resolved the same way, by adding ():

addTransition(new DsViewModel.Transition(playerToken, new Coord(game.player().x(), game.player().y())));
. . .etc

In the Controller, we were setting the field directly—oh, and not adding it to the things list, so that could've been a source of problems:

game.add(new Thing(PLAYER, 4, 4, null, true, false, false));

In Dunslip itself, we need to take out these lines:

this.player = Thing.copy(d.player);//two of these!
. . .
if (player == thing) player = t;

And change these:

var moved = moveThing(player(), d);
. . .
            var dx = thing.x - player().x;
            var dy = thing.y - player().y;

Ok, that gets us—just where we were, but with one less variable. If we look at the things list of our game that won't rewind we'll discover we have two players, but at different coordinates. This is because we're not clearing our things in copy_history:

        things.clear();
        for (Thing t : d.things) this.add(Thing.copy(t));

(Might as well use Thing.copy() rather than the long form, too.)

Now we'll discover that we do get a player rewind, but if we move south to bump on the treasure, the rewind won't put it back!

Well, it does, actually. But then this:

processEffects();

Erases it. Because we save the effects—but the effects are caused by the thing we're rewinding! So let's just leave them out:

    public Dunslip(Dunslip d) {
        this.width = d.width;
        this.height = d.height;
        this.exit = new Coord(d.exit.x, d.exit.y);
        this.gameState = d.gameState;
        for (Thing t : d.things)  this.add(Thing.copy(t));
        d.consumers = this.consumers;
    }

    public void copy_history(int hp) {
        var d = history.get(hp);
        this.width = d.width;
        this.height = d.height;
        this.exit = new Coord(d.exit.x, d.exit.y);
        this.gameState = d.gameState;
        things.clear();
        for (Thing t : d.things) this.add(Thing.copy(t));
        this.consumers = d.consumers;
    }

Now rewinds work! And fast-forwards, which is nice.

Wrapping Up

The animation for non-player pieces needs work but otherwise, we are done with the goblin. Note that the goblin itself wasn't hard to code—once we had straightened out our design.

Next time we'll figure out how to animate non-player pieces by adding a behavior that creatures "flee" not just from the player but when adjacent creatures are killed by the player. Next we'll add a trap, let's say a pit, and figure out how games will be played. Once we have that worked out, we can add a turn limit, and also somewhere in there make the exit only open on certain conditions.

With that, I think the next steps will be refactoring to clean up some code, adding some nice resources and sound effects, and finally packaging to various platforms.

Source code here .