O método renderHanoi() deve mover os discos, limpando-os dos VBoxes e adicionando-os novamente na nova ordem após cada movimento ser feito, mas parece que nada é mostrado a menos que seja o último movimento, o que torna tudo bastante inútil .
Tentei diferentes métodos de criação de atrasos, como Thread.sleep, Platform.runLater, etc. Nenhum deles parece funcionar. Como faço para resolver isso?
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());
}
}
O problema é que seu método solve() retorna em uma pequena fração de segundo. Acontece tão rápido que não há nada para animar.
Você estava no caminho certo. Queremos executar o
solve
método em uma thread diferente, com chamadas para Thread.sleep, para que ele não seja executado muito rapidamente. Não podemos e não devemos chamar Thread.sleep no thread do aplicativo JavaFX (o thread que chamastart
todos os manipuladores de eventos), porque isso atrasará todo o processamento de eventos, incluindo a pintura de janelas e o processamento de entradas de mouse e teclado.Então, primeiro, vamos adicionar Thread.sleep, algumas chamadas para Platform.runLater para atualizar a janela e um Thread para fazer tudo com segurança:
Ainda temos um problema: alterações em variáveis ou campos feitas em um thread não têm garantia de serem vistas em outros threads. Esta seção da especificação da linguagem Java explica sucintamente:
Java tem muitas maneiras de lidar com acesso multithread com segurança. Neste caso, basta utilizar
synchronized
na classe Tower efinal
na classe Hanói.Então, alteramos as declarações dos métodos de Tower para ficarem assim:
Isso garante que as alterações feitas nos campos de uma Torre no
solve
método ficarão visíveis para o thread da aplicação JavaFX.A própria classe Hanoi também faz referência a src, aux e dest em diferentes threads. Poderíamos usar
synchronized
aqui, mas é mais simples tornar esses campos finais:Java pode fazer muitas suposições seguras sobre
final
campos ao acessá-los a partir de vários threads, já que não há necessidade de se preocupar com a falha de vários threads em ver alterações nesses campos.[TLDR: aqui está uma solução que não requer threading]
Conforme apontado na excelente resposta do @VGR , o problema surge porque a solução do problema da torre acontece quase que instantaneamente. Portanto, assim que você tentar renderizá-lo, já estará resolvido. Se você tentar desacelerar colocando pausas entre cada movimento, será necessário executar o código de solução em um thread separado. A resposta mencionada acima mostra como fazer dessa maneira.
Acredito firmemente em não usar vários threads quando não é necessário: o mais importante é que isso torna o código muito mais difícil de acertar e também (embora isso seja muito menos importante) porque consome recursos adicionais do sistema. A resposta do @VGR implementa isso corretamente com threading, mas saber que o código está correto quando você compartilha dados entre vários threads requer uma programação muito mais avançada do que soluções de thread único.
Também acredito fortemente na separação dos dados em um aplicativo da apresentação (visualização) dos dados.
Aqui está uma leve reformulação do seu código que separa os dados. Primeiro, a representação de uma torre, que é essencialmente uma lista de números inteiros que representam quais discos estão na torre. Há também um enum para determinar qual torre é:
TowerID.java
Torre.java
A representação do puzzle necessita apenas das três torres e de um meio de passar de uma para a outra. Acontece que será útil encapsular um movimento como um objeto separado (que é apenas um registro simples aqui):
Mover.java
e então
Hanói.java
Observe que não há método aqui para resolver o quebra-cabeça. Resolver o quebra-cabeça significa apenas criar uma sequência de movimentos, dado o número de discos do quebra-cabeça. Aqui está o mesmo algoritmo do OP, fatorado em uma classe de solucionador separada:
HanoiSolver.java
Observe que agora podemos resolver um quebra-cabeça da torre de Hanói sem uma IU:
e a API que escrevemos também nos permite verificar o estado após cada movimentação em uma solução:
Para fazer uma UI para o quebra-cabeça, precisamos de algumas classes de UI para visualizar as torres:
TorreView.java:
e para tudo:
HanóiView.java:
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:
Here is this animation assembled into an application, with a few controls added:
Note we don't use any built-in observability here. We could create the
Tower
implementation with anObservableList
for the disks and have the view class observe the list, in order to avoid re-rendering the views "by hand" in each frame.