javajavafxtowers-of-hanoi

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


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());
    }
}

Solution

  • 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.