Towers of hanoi game in javafx - nothing is shown until the games is fully solved

80 Views Asked by At

The renderHanoi() method is supposed to move the disks, by clearing the disks from the VBoxes and then adding them again in the new order after each move is made, but it seems nothing is shown unless it's the last move which makes everything pretty pointless.

I tried different methods of creating delays like Thread.sleep, Platform.runLater, etc. None of them seem to work. How do I solve this?

import java.util.Arrays;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class App extends Application {

    @Override
    public void start(Stage stage) {
        HBox platform = new HBox();
        VBox[] towerBoxes = new VBox[] { new VBox(), new VBox(), new VBox()};
        platform.getChildren().addAll(Arrays.asList(towerBoxes));
        Hanoi testing = new Hanoi(10);
        testing.towerBoxes = towerBoxes;
        var scene = new Scene(platform, 640, 480);
        stage.setScene(scene);
        stage.show();
        testing.solve();
    }

    public static void main(String[] args) {
        launch();
    }
}

class Tower {
    private int sp = 0;
    private Rectangle[] disks;
    
    Tower(int n) {
        disks = new Rectangle[n];
    }
    
    public void push(Rectangle entry) {
        if (sp < disks.length)
            disks[sp++] = entry;
        else
            System.err.println(this + ".push(" + entry + ") failed, stack is full");
    }
    
    public Rectangle pop() {
        if (sp > 0)
            return disks[--sp];
        else {
            System.err.println(this + ".pop() failed, stack is empty");
            return null;
        }
    }
    
    public boolean hasEntry() {
        return sp > 0;
    }
    
    @Override
    public Tower clone() {
        Tower copy = new Tower(disks.length);
        copy.sp = this.sp;
        copy.disks = this.disks.clone();
        return copy;
    }
}

class Hanoi {
    Tower src;
    Tower aux;
    Tower dest;
    int n;
    
    public VBox[] towerBoxes;
    
    public Hanoi(int n) {
        src = new Tower(n);
        aux = new Tower(n);
        dest = new Tower(n);
        
        this.n = n;
        
        for (int i = 0; i < n; i++) {
            Rectangle disk = new Rectangle(30 + 20 * i, 10);
            Color diskColor = generateRandomColor();
            disk.setFill(diskColor);
            disk.setStroke(diskColor);
            src.push(disk);
        }
    }
    
    private static Color generateRandomColor() {
        Random random = new Random();
        double red = random.nextDouble();
        double green = random.nextDouble();
        double blue = random.nextDouble();
        
        return new Color(red, green, blue, 1.0);
    }
    
    private void solve(int n, Tower src, Tower aux, Tower dest) {
        if (n < 1) {
            return;
        }
        solve(n-1, src, dest, aux);
        dest.push(src.pop());
        System.out.println(n);
        solve(n-1, aux, src, dest);
    }
    
    public void solve() {
        renderHanoi();
        timer.start();
        solve(n, src, aux, dest);
    }
    
    AnimationTimer timer = new AnimationTimer() {
        @Override
        public void handle(long now) {
          renderHanoi(); // Update UI after each frame
        }
    };

    
    private void renderHanoi() {
        for (VBox towerBox:towerBoxes)
            towerBox.getChildren().clear();
        Tower[] towersCopy = new Tower[]{src.clone(), aux.clone(), dest.clone()};
        for (int i = 0; i < 3; i++)
            while (towersCopy[i].hasEntry())
                towerBoxes[i].getChildren().add(towersCopy[i].pop());
    }
}
2

There are 2 best solutions below

0
VGR On BEST ANSWER

The problem is that your solve() method returns in a tiny fraction of a second. It happens so fast that there is nothing to animate.

You were on the right track. We want to run the solve method in a different thread, with calls to Thread.sleep, so it doesn’t run too quickly. We cannot and must not call Thread.sleep in the JavaFX application thread (the thread that calls start and all event handlers), because that will cause all processing of events to be delayed, including painting of windows and processing of mouse and keyboard input.

So first, let’s add that Thread.sleep, some calls to Platform.runLater to update the window, and a Thread to do it all safely:

private void solve(int n, Tower src, Tower aux, Tower dest)
throws InterruptedException {
    assert !Platform.isFxApplicationThread() : "Wrong thread!";

    if (n < 1) {
        return;
    }

    solve(n-1, src, dest, aux);
    dest.push(src.pop());
    Platform.runLater(() -> renderHanoi());
    System.out.println(n);
    Thread.sleep(100);

    solve(n-1, aux, src, dest);
    Platform.runLater(() -> renderHanoi());
}

public void solve() {
    renderHanoi();

    Thread solveThread = new Thread(() -> {
        try {
            solve(n, src, aux, dest);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    solveThread.setDaemon(true);
    solveThread.start();
}

We still have one problem: changes to variables or fields made in one thread, are not guaranteed to be seen in other threads. This section of the Java Language Specification explains it succinctly:

For example, in the following (broken) code fragment, assume that this.done is a non-volatile boolean field:

while (!this.done)
    Thread.sleep(1000);

The compiler is free to read the field this.done just once, and reuse the cached value in each execution of the loop. This would mean that the loop would never terminate, even if another thread changed the value of this.done.

Java has many ways to safely handle multi-threaded access. In this case, it is sufficient to use synchronized in the Tower class and final in the Hanoi class.

So we change the method declarations of Tower to look like this:

public synchronized void push(Rectangle entry) {

public synchronized Rectangle pop() {

public synchronized boolean hasEntry() {

@Override
public synchronized Tower clone() {

This guarantees that changes made to a Tower’s fields in the solve method will be visible to the JavaFX application thread.

The Hanoi class itself also references src, aux, and dest in different threads. We could use synchronized here, but it’s simpler to just make those fields final:

final Tower src;
final Tower aux;
final Tower dest;
final int n;

Java can make a lot of safe assumptions about final fields when accessing them from multiple threads, since there is no need to worry about multiple threads failing to see changes to those fields.

0
James_D On

[TLDR: here is a solution that does not require threading]

As pointed out in @VGR's excellent answer, the problem arises because solving the tower problem happens almost instantly. Therefore as soon as you try to render it, it has already been solved. If you try to slow it down by putting pauses between each move, then you have to run the solving code on a separate thread. The answer referenced above shows how to do it this way.

I am a strong believer in not using multiple threads when you don't have to: most importantly because it makes the code a lot more difficult to get right, and also (though this is much less important) because it consumes additional system resources. @VGR's answer correctly implements this with threading, but knowing the code is correct when you are sharing data across multiple threads requires much more advanced programming than single-threaded solutions.

I also believe strongly in separating the data in an application from the presentation (view) of the data.

Here is a mild reworking of your code that separates the data. First, the representation of a tower, which is essentially a list of integers representing which disks are on the tower. There's also an enum to determine which tower it is:

TowerID.java

package org.jamesd.examples.hanoi.model;

public enum TowerID {SRC, DEST, AUX}

Tower.java

package org.jamesd.examples.hanoi.model;

import java.util.*;

public class Tower {

    private final TowerID id;
    private final LinkedList<Integer> disks;
    private final List<Integer> publicDisks;

    Tower(TowerID id, Set<Integer> disks) {
        this.id = id;
        this.disks = new LinkedList<>(disks);
        this.disks.sort(Integer::compare);
        this.publicDisks = Collections.unmodifiableList(this.disks);
    }

    Tower(TowerID id) {
        this(id, Collections.emptySet());
    }

    public int pop() {
        return disks.pop();
    }

    public void push(int i) {
        disks.push(i);
    }

    public List<Integer> getDisks() {
        return publicDisks;
    }

    public TowerID getId() {
        return id;
    }
}

The representation of the puzzle just needs the three towers, and a means to move from one to the other. It turns out it'll be useful to encapsulate a move as a separate object (which is just a simple record here):

Move.java

package org.jamesd.examples.hanoi.model;

public record Move(TowerID fromTower, TowerID toTower) {
    @Override
    public String toString() {
        return fromTower + " -> " + toTower;
    }
}

and then

Hanoi.java

package org.jamesd.examples.hanoi.model;

import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Hanoi {

    private final int n;

    private final Tower src ;
    private final Tower dest ;
    private final Tower aux;

    public Hanoi(int numDisks) {
        this.n = numDisks;
        src = new Tower(TowerID.SRC, IntStream.range(1, n+1).boxed().collect(Collectors.toSet()));
        dest = new Tower(TowerID.DEST);
        aux = new Tower(TowerID.AUX);
    }

    public void makeMove(Move move) {
        Tower fromTower = getTower(move.fromTower());
        Tower toTower = getTower(move.toTower());
        toTower.push(fromTower.pop());
    }

    public Tower getTower(TowerID id) {
        return switch(id) {
            case SRC -> src;
            case DEST -> dest;
            case AUX -> aux;
        };
    }

    public int getNumDisks() {
        return n;
    }
}

Note there is no method here for solving the puzzle. Solving the puzzle just means coming up with a sequence of moves, given the number of disks in the puzzle. Here's the same algorithm as in the OP, factored out into a separate solver class:

HanoiSolver.java

package org.jamesd.examples.hanoi.model;

import java.util.LinkedList;
import java.util.List;

public class HanoiSolver {
    private final int n ;

    public HanoiSolver(int nDisks) {
        this.n = nDisks;
    }

    public List<Move> solve() {
        List<Move> moves = new LinkedList<>();
        solve(moves, n, TowerID.SRC, TowerID.DEST, TowerID.AUX);
        return moves;
    }

    private void solve(List<Move> moves, int n, TowerID fromTower, TowerID destTower, TowerID auxTower) {
        if (n < 1) {
            return ;
        }
        solve(moves, n-1, fromTower, auxTower, destTower);
        moves.add(new Move(fromTower, destTower));
        solve(moves, n-1, auxTower, destTower, fromTower);
    }
}

Note we can now solve a Hanoi tower puzzle without a UI:

package org.jamesd.examples.hanoi.model;

public class Test {
    public static void main(String[] args) {
        HanoiSolver hanoi = new HanoiSolver(4);
        System.out.println(hanoi.solve());
    }
}
[SRC -> AUX, SRC -> DEST, AUX -> DEST, SRC -> AUX, DEST -> SRC, DEST -> AUX, SRC -> AUX, SRC -> DEST, AUX -> DEST, AUX -> SRC, DEST -> SRC, AUX -> DEST, SRC -> AUX, SRC -> DEST, AUX -> DEST]

and the API we have written also lets us check the state after each move in a solution:

package org.jamesd.examples.hanoi.model;

import java.util.List;
import java.util.stream.Stream;

public class Test {
    public static void main(String[] args) {
        HanoiSolver hanoiSolver = new HanoiSolver(4);
        List<Move> moves = hanoiSolver.solve();
        Hanoi hanoi = new Hanoi(4);
        outputHanoi(hanoi);
        for (Move move : moves) {
            hanoi.makeMove(move);
            System.out.println(move);
            outputHanoi(hanoi);
        }
    }

    private static void outputHanoi(Hanoi hanoi) {
        Stream.of(TowerID.values())
                .map(id -> id + " : " + hanoi.getTower(id).getDisks())
                .forEach(System.out::println);
        System.out.println("-----------------");
    }
}
SRC : [1, 2, 3, 4]
DEST : []
AUX : []
-----------------
SRC -> AUX
SRC : [2, 3, 4]
DEST : []
AUX : [1]
-----------------
SRC -> DEST
SRC : [3, 4]
DEST : [2]
AUX : [1]
-----------------
AUX -> DEST
SRC : [3, 4]
DEST : [1, 2]
AUX : []
-----------------
SRC -> AUX
SRC : [4]
DEST : [1, 2]
AUX : [3]
-----------------
DEST -> SRC
SRC : [1, 4]
DEST : [2]
AUX : [3]
-----------------
DEST -> AUX
SRC : [1, 4]
DEST : []
AUX : [2, 3]
-----------------
SRC -> AUX
SRC : [4]
DEST : []
AUX : [1, 2, 3]
-----------------
SRC -> DEST
SRC : []
DEST : [4]
AUX : [1, 2, 3]
-----------------
AUX -> DEST
SRC : []
DEST : [1, 4]
AUX : [2, 3]
-----------------
AUX -> SRC
SRC : [2]
DEST : [1, 4]
AUX : [3]
-----------------
DEST -> SRC
SRC : [1, 2]
DEST : [4]
AUX : [3]
-----------------
AUX -> DEST
SRC : [1, 2]
DEST : [3, 4]
AUX : []
-----------------
SRC -> AUX
SRC : [2]
DEST : [3, 4]
AUX : [1]
-----------------
SRC -> DEST
SRC : []
DEST : [2, 3, 4]
AUX : [1]
-----------------
AUX -> DEST
SRC : []
DEST : [1, 2, 3, 4]
AUX : []
-----------------

To make a UI for the puzzle, we need some UI classes to visualize the towers:

TowerView.java:

package org.jamesd.examples.hanoi.ui;

import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import org.jamesd.examples.hanoi.model.Tower;

import java.util.ArrayList;
import java.util.List;

public class TowerView extends Region {

    private final int totalNumDisks;
    private final Rectangle stem = new Rectangle();
    private final List<Integer> diskIndexes = new ArrayList<>();
    private final List<Rectangle> disks = new ArrayList<>();

    private static final double STEM_WIDTH = 10 ;

    public TowerView(int totalNumDisks) {
        this.totalNumDisks = totalNumDisks;
        stem.setFill(Color.SADDLEBROWN);
        getChildren().add(stem);
    }

    @Override
    protected void layoutChildren() {
        double leftEdge = snappedLeftInset();
        double availableWidth = getWidth() - leftEdge - snappedRightInset();
        double topEdge = snappedTopInset();
        double availableHeight = getHeight() - topEdge - snappedBottomInset();
        stem.setWidth(STEM_WIDTH);
        stem.setX(leftEdge + availableWidth / 2 - STEM_WIDTH / 2);
        stem.setY(topEdge);
        stem.setHeight(availableHeight);

        int nDisks = diskIndexes.size();
        for (int i = 0; i < nDisks; i++)  {
            int relWidth = diskIndexes.get(i);
            Rectangle disk = disks.get(i);
            double diskWidth = availableWidth * relWidth / totalNumDisks;
            disk.setWidth(diskWidth);
            disk.setHeight(availableHeight / totalNumDisks);
            disk.setX(leftEdge + availableWidth / 2 - diskWidth / 2);
            disk.setY(topEdge + availableHeight - (availableHeight * (nDisks - i)) / totalNumDisks);
        }
    }

    public void renderTower(Tower tower) {
        diskIndexes.clear();
        diskIndexes.addAll(tower.getDisks());
        getChildren().setAll(stem);
        disks.clear();
        for (Integer index : diskIndexes) {
            Rectangle disk = new Rectangle();
            disk.setFill(getColor(index));
            double arc = 25.0;
            disk.setArcWidth(arc);
            disk.setArcHeight(arc);
            disks.add(disk);
            getChildren().add(disk);
        }
    }

    private Color getColor(int index) {
        return Color.gray(1.0 - 1.0 * index / totalNumDisks);
    }
}

and for the whole thing:

HanoiView.java:

package org.jamesd.examples.hanoi.ui;

import javafx.scene.layout.Background;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.paint.Color;
import org.jamesd.examples.hanoi.model.Hanoi;
import org.jamesd.examples.hanoi.model.TowerID;

public class HanoiView extends HBox {

    public HanoiView() {
        setSpacing(10);
        setFillHeight(true);
        setBackground(Background.fill(Color.SKYBLUE));
    }

    public void renderHanoi(Hanoi hanoi) {
        int n = hanoi.getNumDisks();
        TowerView src = createTowerView(n);
        TowerView dest = createTowerView(n);
        TowerView aux = createTowerView(n);
        src.renderTower(hanoi.getTower(TowerID.SRC));
        dest.renderTower(hanoi.getTower(TowerID.DEST));
        aux.renderTower(hanoi.getTower(TowerID.AUX));
        getChildren().setAll(src, dest, aux);
    }

    private TowerView createTowerView(int ndisks) {
        TowerView view = new TowerView(ndisks);
        setHgrow(view, Priority.ALWAYS);
        return view;
    }
}

To visualize a solution in an animation, we can get a list of the moves from the solver, create a timeline with a keyframe for each move, and in each keyframe update the model with each move and rerender the view:

            HanoiSolver solver = new HanoiSolver(hanoi.getNumDisks());
            List<Move> moves = solver.solve();
            Duration frameTime = Duration.seconds(0.5);
            Duration current = Duration.ZERO;
            Timeline timeline = new Timeline();
            for (Move move : moves) {
                current = current.add(frameTime);
                KeyFrame keyFrame = new KeyFrame(current, evt -> {
                    hanoi.makeMove(move);
                    hanoiview.renderHanoi(hanoi);
                });
                timeline.getKeyFrames().add(keyFrame);
            }
            timeline.play();

Here is this animation assembled into an application, with a few controls added:

package org.jamesd.examples.hanoi.ui;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jamesd.examples.hanoi.model.Hanoi;
import org.jamesd.examples.hanoi.model.HanoiSolver;
import org.jamesd.examples.hanoi.model.Move;

import java.util.List;

public class App extends Application {

    private Hanoi hanoi ;

    @Override
    public void start(Stage stage) throws Exception {
        HBox controls = new HBox(5);
        controls.getChildren().add(new Label("Number of disks:"));
        Spinner<Integer> numDisksSpinner = new Spinner<>(2, 15, 5);
        Button solve = new Button("Solve");
        controls.getChildren().addAll(numDisksSpinner, solve);

        HanoiView hanoiview = new HanoiView();
        hanoi = new Hanoi(numDisksSpinner.getValue());
        hanoiview.renderHanoi(hanoi);

        numDisksSpinner.valueProperty().addListener((obs, oldValue, newValue) -> {
            hanoi = new Hanoi(newValue);
            hanoiview.renderHanoi(hanoi);
        });

        solve.setOnAction(e -> {
            controls.setDisable(true);
            hanoi = new Hanoi(numDisksSpinner.getValue());
            hanoiview.renderHanoi(hanoi);
            HanoiSolver solver = new HanoiSolver(hanoi.getNumDisks());
            List<Move> moves = solver.solve();
            Duration frameTime = Duration.seconds(0.5);
            Duration current = Duration.ZERO;
            Timeline timeline = new Timeline();
            for (Move move : moves) {
                current = current.add(frameTime);
                KeyFrame keyFrame = new KeyFrame(current, evt -> {
                    hanoi.makeMove(move);
                    hanoiview.renderHanoi(hanoi);
                });
                timeline.getKeyFrames().add(keyFrame);
            }
            timeline.setOnFinished(evt -> controls.setDisable(false));
            timeline.play();
        });

        BorderPane root = new BorderPane();
        root.setTop(controls);
        root.setCenter(hanoiview);
        Scene scene = new Scene(root, 800, 500);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        Application.launch(args);
    }
}

Note we don't use any built-in observability here. We could create the Tower implementation with an ObservableList for the disks and have the view class observe the list, in order to avoid re-rendering the views "by hand" in each frame.