汉诺塔(Tower of Hanoi),是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。本文使用JavaFX模拟汉诺塔,使用了Java面向对象,Java stream和Optional, JavaFX图形技术, 包含Stage、Scene、事件处理、Pane、StackPane等,要求对Java8以上版本的特性有了解。一起来实现这个小游戏吧!
效果图
游戏逻辑
创建三个塔,使用三个矩形模拟三个塔
在第一个矩形周围绘制多个圆形表示圆盘
点击塔中的圆盘选中后,再点击其它塔,移动圆盘到其它塔
直到将所有圆盘按从小到大的顺序显示在第三个塔上
一、创建一个Application
- Stage 舞台
- Scene 场景
- 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塔类,模拟矩形塔
- 类名定义为Tower, 属性有哪些? 如果是绘制图形,则需要坐标位置信息,据此在构造方法中传入坐标值
- Tower类中需要绘制一个矩形,这个矩形可称为塔, 在它的边上需要绘制多个圆圈,从小到大顺序
- Tower类须继承 StackPane,因为同一个位置的其它圆的绘制有先后顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private class Tower extends StackPane { Tower(int x, int y) { setTranslateX(x); setTranslateY(y); setPrefSize(400,400); Rectangle rectTower = new Rectangle(35,35); 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++) { Tower tower = new Tower(i*400, 0); 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++) { 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; } }
|
五、获取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) .map(element->(Circle)element) .min(Comparator.comparingDouble(Circle::getRadius)) .orElse(null); return top; }
|
六、定义方法添加圆盘到Tower塔 面板容器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void addCircle(Circle circle) { Circle topMost = getTop(); if(topMost==null) { getChildren().add(circle); } else { if(circle.getRadius() < topMost.getRadius()) { 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
| 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(element->element instanceof Circle) .map(element->(Circle)element) .min(Comparator.comparingDouble(Circle::getRadius)) .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) { Circle topCircle = getTop(); if(topCircle==null) { getChildren().add(circle); } else { 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表达式等技术实现了一个简易版的汉诺塔小游戏,借此练习如何使用这些新特性,提升技能水平。