JavaFX汉诺塔

汉诺塔(Tower of Hanoi),是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。本文使用JavaFX模拟汉诺塔,使用了Java面向对象,Java stream和Optional, JavaFX图形技术, 包含Stage、Scene、事件处理、Pane、StackPane等,要求对Java8以上版本的特性有了解。一起来实现这个小游戏吧!

效果图

游戏逻辑

  1. 创建三个塔,使用三个矩形模拟三个塔

  2. 在第一个矩形周围绘制多个圆形表示圆盘

  3. 点击塔中的圆盘选中后,再点击其它塔,移动圆盘到其它塔

  4. 直到将所有圆盘按从小到大的顺序显示在第三个塔上

一、创建一个Application

  1. Stage 舞台
  2. Scene 场景
  3. Pane容器用于存放绘制的形状
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TowerHanoi extends Application {

@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
private Parent createContent() {

Pane root = new Pane();
root.setPrefSize(1000,400);

return root;
}
}

二、新建Tower塔类,模拟矩形塔

  1. 类名定义为Tower, 属性有哪些? 如果是绘制图形,则需要坐标位置信息,据此在构造方法中传入坐标值
  2. Tower类中需要绘制一个矩形,这个矩形可称为塔, 在它的边上需要绘制多个圆圈,从小到大顺序
  3. Tower类须继承 StackPane,因为同一个位置的其它圆的绘制有先后顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Tower是一个StackPane
private class Tower extends StackPane {
Tower(int x, int y) {
//当前Tower的坐标
setTranslateX(x);
setTranslateY(y);
//当前Tower容器的大小
setPrefSize(400,400);
//定义一个矩形
Rectangle rectTower = new Rectangle(35,35);
//将这个矩形添加到当前的Tower容器中
getChildren().add(rectTower);
}
}

三、创建三个Tower对象

  • 此步骤创建三个Tower对象,并添加到容器中,同时设定到Scene场景对象中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TowerHanoi extends Application {

@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
private Parent createContent() {

Pane root = new Pane();
root.setPrefSize(1000,400);
for (int i = 0; i < 3; i++) {
//纵坐标不变, 横坐标每根据循环变量 i 变化
Tower tower = new Tower(i*400, 0);
//将三个Tower对象添加到root面板容器中
root.getChildren().add(tower);
}
return root;
}
}

四、在第一个Tower中绘制N个圆

  • 绘制的圆表示放在Tower上的圆盘, 此处我们使用一个常量 NUM_CIRCLES 表示, 设定为4, 也可随意设置
1
private static final int NUM_CIRCLES = 4;
  • 在创建第一个Tower时,将NUM_CIRCLES个圆盘绘制在 Tower上面
  • 绘制时使用Circle类创建圆
  • 循环时从最大的开始,在计算半径时保证最大的圆盘先添加到Tower面板容器中,最后绘制的在Tower面板容器的最上面, 使用的是栈结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class TowerHanoi extends Application {

@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
private Parent createContent() {

Pane root = new Pane();
root.setPrefSize(1000,400);
for (int i = 0; i < 3; i++) {
//纵坐标不变, 横坐标每根据循环变量 i 变化
Tower tower = new Tower(i*400, 0);

//i=0的时候,画三个圆盘
if(i==0) {
//循环时从最大的开始
for (int j = NUM_CIRCLES; j > 0; j--) {
Circle circle = new Circle(30+j*20, null);
//设置边线颜色和宽度
circle.setStroke(Color.BLUE);
circle.setStrokeWidth(circle.getRadius()/20);
//添加到面板容器
tower.addCircle(circle);
}
}

//将三个Tower对象添加到root面板容器中
root.getChildren().add(tower);
}
return root;
}
}

五、获取Tower塔上半径最小的圆盘

  • 在Tower类中定义一个方法,用于获取Tower面板容器中最上面的那个圆盘
  • 这个方法采用getChildren()获取当前Tower 面板容器对象上的所有元素, 转换成stream, 再通过filter和map以及min这些中间操作后获得半径最小的圆盘
  • stream 是java8之后的重要特性,此处还用到 lambda 表达式, 在集合的内部迭代中经常使用
1
2
3
4
5
6
7
8
9
private Circle getTop() {
var top = getChildren()
.stream()
.filter(element->element instanceof Circle) //返回元素类型为Circle的stream
.map(element->(Circle)element) //将过滤后的类型转换为Circle类型
.min(Comparator.comparingDouble(Circle::getRadius)) //比较一下所有的Circle,并得到半径最小的那个圆盘
.orElse(null);
return top;
}

六、定义方法添加圆盘到Tower塔 面板容器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void addCircle(Circle circle) {
//获取画在StackPane也就是Tower上最上面的那个Circle
Circle topMost = getTop();
//如果为null则表示Tower上面没有圆
if(topMost==null) {
getChildren().add(circle);
}
else {
//如果不为空,则添加到Tower上的圆要和最上层的圆进行比较,小于最小层时才能添加
if(circle.getRadius() < topMost.getRadius()) {
getChildren().add(circle);
}
}
//getChildren().add(circle);
}

七、点击Tower时获得最小半径的圆盘并改变颜色

  • 定义一个Optional, 用于解决可能出现的NullPointException
  • 变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的Optional对象,由方法Optional.empty()返回
  • Optional.empty()方法是一个静态工厂方法,它返回Optional类的特定单一实例
1
private Optional<Circle> selectedCircle = Optional.empty();
  • 为Tower上的矩形定义一个鼠标点击事件,使用setOnMouseClicked
  • Optional类提供了一个isPresent方法,如果Optional对象包含值,该方法就返回true, 如果不包含则返回false
  • addCircle方法是在Tower类中定义的,目的是将圆添加到Tower中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//Tower是一个StackPane
private class Tower extends StackPane {
Tower(int x, int y) {
//当前Tower的坐标
setTranslateX(x);
setTranslateY(y);
//当前Tower容器的大小
setPrefSize(400,400);
//定义一个矩形
Rectangle rectTower = new Rectangle(35,35);

//监听每一个矩形塔的点击事件
rectTower.setOnMouseClicked(e->{
//判断Optional对象是否包含值,如果包含则返回true,否则返回false
if(selectedCircle.isPresent()) {
//包含值则获取其中的对象
Circle c = selectedCircle.get();
//设置边框颜色
c.setStroke(Color.BLUE);
//添加圆盘到当前Tower中
addCircle(c);
//模拟一个空的Optional对象,重新赋值
selectedCircle = Optional.empty();
}
else {
selectedCircle = Optional.ofNullable(getTop());
//如果选中了则将边框颜色设置为红色
selectedCircle.get().setStroke(Color.RED);
}
});

//将这个矩形添加到当前的Tower容器中
getChildren().add(rectTower);
}

private Circle getTop() {
var top = getChildren()
.stream()
.filter(element->element instanceof Circle) //返回元素类型为Circle的stream
.map(element->(Circle)element) //将过滤后的类型转换为Circle类型
.min(Comparator.comparingDouble(Circle::getRadius)) //比较一下所有的Circle,并得到半径最小的那个圆盘
.orElse(null);
return top;
}

private void addCircle(Circle circle) {


}
}


八、addCircle方法实现

  • addCircle方法是将选中的Circle添加到不同的Tower上面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void addCircle(Circle circle) {
//获取画在StackPane也就是Tower上最小的Circle
Circle topCircle = getTop();
//如果为null则表示Tower上面没有圆, 没有圆盘则可将圆盘直接添加到Tower上
if(topCircle==null) {
getChildren().add(circle);
}
else {

//如果Tower上有圆盘,则添加到Tower上的圆盘要和最上层的圆盘进行比较,小于最上面那个圆盘时才能添加
if(circle.getRadius() < topCircle.getRadius()) {
getChildren().add(circle);
}
}

}

九、完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package top.codecool.game;

import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

import java.util.Comparator;
import java.util.Optional;

public class TowerHanoiApp extends Application {
private static final int NUM_CIRCLES = 4;
private Optional<Circle> selectedCircle = Optional.empty();

@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.setTitle("汉诺塔游戏");
stage.show();
}
private Parent createContent() {
Pane root = new Pane();
root.setPrefSize(400*3,400);
for (int i = 0; i < 3; i++) {
Tower tower = new Tower(i*400, 0);
if(i==0) {
for (int j = NUM_CIRCLES; j > 0; j--) {
Circle circle = new Circle(30+j*20, null);
circle.setStroke(Color.BLUE);
circle.setStrokeWidth(circle.getRadius()/20);
tower.addCircle(circle);

}
}
root.getChildren().add(tower);
}
return root;
}



private class Tower extends StackPane {
Tower(int x, int y) {
setTranslateX(x);
setTranslateY(y);
setPrefSize(400,400);
Rectangle rectTower = new Rectangle(35,35);
rectTower.setOnMouseClicked(e->{
if(selectedCircle.isPresent()) {
Circle c = selectedCircle.get();
c.setStroke(Color.BLUE);
addCircle(c);
selectedCircle = Optional.empty();
}
else {
selectedCircle = Optional.ofNullable(getTop());
selectedCircle.get().setStroke(Color.RED);
}

});
getChildren().add(rectTower);

}

private Circle getTop() {
var top = getChildren()
.stream()
.filter(n->n instanceof Circle)
.map(n->(Circle)n)
.min(Comparator.comparingDouble(Circle::getRadius))
.orElse(null);
return top;
}

private void addCircle(Circle circle) {
Circle topCircle = getTop();
if(topCircle==null) {
getChildren().add(circle);
}
else {
if(circle.getRadius() < topCircle.getRadius()) {
getChildren().add(circle);
}
}

}
}
}

总结

本文使用JavaFX和Java8中的stream、Optional、Lambda表达式等技术实现了一个简易版的汉诺塔小游戏,借此练习如何使用这些新特性,提升技能水平。