[关闭]
@Darcy 2017-08-11T03:13:44.000000Z 字数 12077 阅读 3925

第七章 Tic-Tac-Toe(井字棋)Applet小程序

JavaForKids

处于安全的考虑,最新版本的JRE已经禁止本地Applet在浏览器上的运行了,所以在以下提到的Applet程序中,你只需通过Eclipse来运行测试结果即可。[1]

当你去浏览一些你喜欢的网站的时候,你可能有碰到过用Java语言写的一些小程序或者游戏,这种技术就是applet。这种特殊的程序可以在网页浏览器上运行。网页浏览器能够解析叫做HTML的简单标签语言,你可以通过这些标签让文本更美观地在浏览器中呈现。除了文本外,你还可以在HTML文件中添加<applet>标签,它会告诉浏览器呈现Java applet的位置和方式。

Java applet 会作为网页的一部分从Internet下载到你的电脑中,浏览器会自动启动Java虚拟机(JVM)来运行这些applet

在这章中你将会学习到如何在你的电脑中创建applet附录C 会告诉你如何发布你的网页到Internet上,这样其他人就可以使用你写的applet程序了。

用户在Internet上浏览网页的时候并不需要知道网页中是否包含有Java applet小程序,但他们想确保自己的电脑不会受到一些恶意的applet的破坏。这也是为什么applet会设计成有下面的一些限制:

为了运行applet,你需要用一种特殊的方式来编写Java 类,要在HTML文件中添加<applet>指向这个类,而且浏览器也必须要支持Java。你可以在Eclipse中或者在一个叫做appletviewer的特殊程序中测试applet。但是,在学习applet之前,让我们先花15分钟来熟悉一下HTML标签吧。

1. 用15分钟学习一下HTML

假设你已经写好并编译完成了TicTacToe这个applet小游戏。现在你需要创建一个HTML文件来展示它的有关信息。那么首先要创建一个文本文件,我们把它命名为TicTacToe.html (顺便说一下,Eclipse也是可以创建文本文件的)。HTML文件的文件名后缀是.html或者.htm, 它们通常包括有headerbody部分。大部分的HTML标签都有相应的带斜杠(/)的结束标签,如<Head></Head> 是一对。TicTacToe.html的内容可能是这样子:

  1. <HTML>
  2. <Head>
  3. <Title>My First Web Page</Title>
  4. </Head>
  5. <BODY>
  6. My Tic-Tac-Toe game is coming soon…
  7. </BODY>
  8. </HTML>

你可以把标签放到同一行,如<Title></Title>,也可以分开不同的行。现在,用浏览器打开这个文件,你就可以看到标题My First Web Page在浏览器的窗口上方,而网页里面你可以看到有My Tic-Tac-Toe game is coming soon…

现在,让我们把Tic-Tac-Toe这个applet添加到这个文件中:

  1. <HTML>
  2. <BODY>
  3. Here is my Tic-Tac-Toe game:
  4. <APPLET code=”TicTacToe.class” width=300 height=250>
  5. </APPLET>
  6. </BODY>
  7. </HTML>

然后刷新或者重新打开 TicTacToe.html ,你会看到下面的内容:

正如我们所料的,浏览器没有找到TicTacToe.class,而只是显示了一个灰色的边框。别急,我们待会就会告诉你怎么创建它。

HTML标签是用尖括号来包裹的,有些标签还会有额外的属性。例如我们用到的<APPLET>就有下面的一些属性:

如果一个Java applet由多个类组成,可以使用jdk自带的jar程序把它们全部压缩到一个.jar文件。如果你这样做了,这个打包文件必须具备名称的属性。你可以在附录A中阅读jars相关的知识点。

2. 使用AWT来编写Applet

如果Swing库表现得更好,那为什么还要使用AWT图形库来编写applet呢? 那我们可以用Swing来编写applet么?答案是肯定的,但是有些事情你是需要了解的。

网页浏览器会自带了它们自己的Java虚拟机(JVM),这个JVM肯定是支持AWT的,但是它有可能不会支持你的AppletSwing 类。当然用户可能会下载最新版本的JVM来支持它,但是你真的想让你的用户去这么做么?当你的网页发布到Internet上之后,你压根就不知道谁会来访问它。想象一下有个在沙漠中的家伙在用着10年前的电脑 - 他宁愿离开你的网页也不愿意这么麻烦地安装软件。如果你想让这个applet帮助你在网上卖游戏,那你肯定不愿意失去这个用户 - 他有可能是我们的潜在用户喔(沙漠中的人们也是有信用卡的)。

如果你不能确定你的用户是用什么浏览器的,那么请用AWT来编写applet吧。

实际上,还有个选择就是强制你的用户去下载新的Java插件,然后配置他们的浏览器去使用这个插件而不是使用浏览器自带的JVM。你可以阅读这个网页来获取更多的相关信息: http://java.sun.com/j2se/1.5.0/docs/guide/plugin/.

3. 怎么用AWT编写 applet?

Java AWT 编写applet必须要继承java.applet.Applet 这个类,例如:

  1. class TicTacToe extends java.applet.Applet {
  2. }

不像Java一般的应用程序,applet不需要main()方法,因为网页浏览器在遇到<applet>标签时,会自动下载和执行这个applet。浏览器也会传递一些信号给applet当一些重要的事件发生时,比如applet开始启动,重新绘制等。为了确保applet能够对这些事件作出反应,你应该覆盖其中的一些特殊回调方法: init() , start(), paint(), stop() ,和destroy()。浏览器的JVM会在下面的情况下调用这些方法:

即便你不需要所有这些回调方法,每个applet也至少应该重写init() 或者paint()方法。下面的applet代码展示了Hello World这句话。这个applet只重写了paint()方法,JVM会通过它返回一个Graphics的对象实例。这个对象提供了非常完整的绘制方法。在下面的例子中我们使用了drawString()来绘制一个字符串Hello World

  1. public class HelloApplet extends java.applet.Applet {
  2. public void paint(java.awt.Graphics graphics) {
  3. graphics.drawString("Hello World!", 70, 40);
  4. }
  5. }

Eclipse中创建这个类并运行它: 选择 Run as-> Java applet就可以了。

为了在网页浏览器中测试它,我们创建一个叫做Hello.html的文件,把它放到和HelloApplet.class相同的文件夹上:

  1. <HTML>
  2. <BODY>
  3. Here is my first applet:<P>
  4. <APPLET code=”HelloApplet.class” width=200 height=100></APPLET>
  5. </BODY>
  6. </HTML>

现在,在你的浏览器上打开Hello.html,就可以看到像下面的结果:

你觉得通过这个简单的例子我们准备好了写一个游戏程序了吗?当然可以啦~系好你的安全带吧...

4. 编写Tic-Tac-Toe游戏

4.1:游戏策略

每个游戏都会使用一些特定的算法 - 根据一系列的规则或者策略来对 玩家的动作做出相关的反馈。同一个游戏的算法可以很简单,也可以非常复杂。你应该听说过国际象棋冠军Gary Kasparov对战计算机的故事吧,他实际上就是和计算机的算法对战。计算机专家团队试图去发明一种智能的算法去打败他。同样地,Tic-Tac-Toe游戏可以使用很多不同的策略,在这里我们会使用相对简单的一种:

  1. 我们将使用3×3的九宫格。
  2. 用户使用符号X,计算机则使用O
  3. 使用同一种符号铺满一行或者一列或者对角线才算赢。
  4. 每下一步,程序就会检查是否有一方赢了。
  5. 赢的一方会把他的结果高亮显示,游戏结束。
  6. 如果没有空格子可以放了,那么游戏结束。
  7. 玩家必须点击New Game的按钮重新开始。
  8. 当计算机在决定下一步在哪里放O时,它必须要试图找到有没有已经出现两个连续的了,如果有,那么它要相应放到第三个连续的位置上。
  9. 如果没有两个连续的O,那么计算机必须要找到有没有两个连续的X出现,如果有,就要放到第三个连续的位置去阻止对手赢。
  10. 如果没有找到可以赢的或者阻止对手赢的位置,那么计算机就会试着放到中间,如果中间没有位置,那么就随机地选择一个空位置。

4.2:游戏代码

我会在这里给你简单地描述一下这个程序,因为在applet的代码中已经有很多注释去帮助你理解了。

这个applet会使用BorderLayout布局,然后窗口的North部分会放置New Game的按钮,Center部分会显示九个按钮代表九宫格,South部分会展示一些信息,程序中会使用一维数组来存储这些按钮,它们的位置对应的下标如图所示:

所有的窗口组件都会在appletinit()方法里面被创建。所有的事件都会通过ActionListener监听器的actionPerformed()方法来处理。方法lookForWinner()会在每移动一步的时候来检查游戏是否已经结束。

我们策略的规则8、9、10会在computerMove()方法里面编码实现,这个方法需要产生的随机数, 随机数的产生我们将会使用Java提供的Math类中的random()方法。

你可能会发现有些比较特殊的语法,比如连续调用多个方法来作为一个表达式:

  1. if(squares[0].getLabel().equals(squares[1].getLabel())){…}

这一长长的表达式是为了让代码更简洁,它的表达结果实际上和下面的一样:

  1. String label0 = squares[0].getLabel();
  2. String label1 = squares[1].getLabel();
  3. if(label0.equals(label1)){…}

在复杂的表达式中,Java会先处理括号里面的代码,然后再处理其他的。在上面简短版本的代码中,首先会计算最里面的括号的结果[也就是squares[1].getLabel()的结果],然后用这个结果和外面的squares[0].getLabel()的结果通过equals()方法进行比较。

尽管下面的代码有几页长,但是它应该不难理解。对照着注释来开始阅读吧。

  1. public class TicTacToe extends JApplet implements ActionListener {
  2. JButton squares[];
  3. JButton newGameButton;
  4. JLabel score;
  5. int emptySquaresLeft = 9;
  6. /**
  7. * applet 的init方法,相当于构造方法
  8. */
  9. @Override
  10. public void init() {
  11. // 获取这个applet的窗口面板
  12. Container appletContent = this.getContentPane();
  13. //设置这个面板的布局管理器,背景颜色
  14. appletContent.setLayout(new BorderLayout());
  15. appletContent.setBackground(Color.CYAN);
  16. // 创建新游戏的按钮,并给它注册点击事件的监听器
  17. newGameButton = new JButton("New Game");
  18. newGameButton.addActionListener(this);
  19. JPanel topPanel = new JPanel();
  20. topPanel.add(newGameButton);
  21. appletContent.add(topPanel, BorderLayout.NORTH);
  22. //中间的面板是个3*3的网格布局类型,当你往上面添加按钮时,就会从左往右,从上往下依次添加到面板中
  23. JPanel centerPanel = new JPanel();
  24. centerPanel.setLayout(new GridLayout(3, 3));
  25. appletContent.add(centerPanel, BorderLayout.CENTER);
  26. score = new JLabel("Your turn!");
  27. appletContent.add(score, BorderLayout.SOUTH);
  28. //创建JButton类型的数组,它负责存放各个按钮的引用
  29. squares = new JButton[9];
  30. // 创建游戏面板上那9个按钮,设置他们的背景为橙色,为它们注册事件,依次添加到面板上。
  31. for (int i = 0; i < 9; i++) {
  32. squares[i] = new JButton();
  33. squares[i].addActionListener(this);
  34. squares[i].setBackground(Color.ORANGE);
  35. centerPanel.add(squares[i]);
  36. }
  37. }
  38. /**
  39. * 这个方法会处理所有的点击事件
  40. *
  41. * @param ActionEvent
  42. * object
  43. */
  44. @Override
  45. public void actionPerformed(ActionEvent e) {
  46. JButton theButton = (JButton) e.getSource();
  47. //点击的是新游戏按钮
  48. if (theButton == newGameButton) {
  49. for (int i = 0; i < 9; i++) {
  50. squares[i].setEnabled(true);
  51. squares[i].setText("");
  52. squares[i].setBackground(Color.ORANGE);
  53. }
  54. emptySquaresLeft = 9;
  55. score.setText("Your turn!");
  56. newGameButton.setEnabled(false);
  57. return; //退出这个方法
  58. }
  59. String winner = "";
  60. //点击的是中间的方格按钮
  61. for (int i = 0; i < 9; i++) {
  62. if (theButton == squares[i]) {
  63. squares[i].setText("X");
  64. winner = lookForWinner();
  65. if (!"".equals(winner)) {
  66. endTheGame();
  67. } else {
  68. computerMove();
  69. winner = lookForWinner();
  70. if (!"".equals(winner)) {
  71. endTheGame();
  72. }
  73. }
  74. break;
  75. }
  76. } //循环结束
  77. if (winner.equals("X")) {
  78. score.setText("You won!");
  79. } else if (winner.equals("O")) {
  80. score.setText("You lost!");
  81. } else if (winner.equals("T")) {
  82. score.setText("It's a tie!"); //平局
  83. }
  84. }
  85. /**
  86. * 这个方法走一步之后都会被调用,它用于检查每行,每列,每个对角线是否出现相同的'O'或者'X'符号。如果有则表示一方赢了,然后高亮显示赢的结果。
  87. *
  88. * @return "X", "O", "T" for tie or "" for no winner ‘X’代表玩家赢了,'O'代表计算机赢了,'T' 代表平局,""代表还没结束
  89. */
  90. String lookForWinner() {
  91. String theWinner = "";
  92. emptySquaresLeft--;
  93. //没有空白格子了,平局
  94. if (emptySquaresLeft == 0) {
  95. return "T";
  96. }
  97. //检查第1行是否是相同的符号
  98. if (!squares[0].getText().equals("")
  99. && squares[0].getText().equals(squares[1].getText())
  100. && squares[0].getText().equals(squares[2].getText())) {
  101. theWinner = squares[0].getText();
  102. highlightWinner(0, 1, 2);
  103. //检查第2行
  104. } else if (!squares[3].getText().equals("")
  105. && squares[3].getText().equals(squares[4].getText())
  106. && squares[3].getText().equals(squares[5].getText())) {
  107. theWinner = squares[3].getText();
  108. highlightWinner(3, 4, 5);
  109. //检查第3行
  110. } else if (!squares[6].getText().equals("")
  111. && squares[6].getText().equals(squares[7].getText())
  112. && squares[6].getText().equals(squares[8].getText())) {
  113. theWinner = squares[6].getText();
  114. highlightWinner(6, 7, 8);
  115. //检查第1列
  116. } else if (!squares[0].getText().equals("")
  117. && squares[0].getText().equals(squares[3].getText())
  118. && squares[0].getText().equals(squares[6].getText())) {
  119. theWinner = squares[0].getText();
  120. highlightWinner(0, 3, 6);
  121. //检查第2列
  122. } else if (!squares[1].getText().equals("")
  123. && squares[1].getText().equals(squares[4].getText())
  124. && squares[1].getText().equals(squares[7].getText())) {
  125. theWinner = squares[1].getText();
  126. highlightWinner(1, 4, 7);
  127. //检查第3列
  128. } else if (!squares[2].getText().equals("")
  129. && squares[2].getText().equals(squares[5].getText())
  130. && squares[2].getText().equals(squares[8].getText())) {
  131. theWinner = squares[2].getText();
  132. highlightWinner(2, 5, 8);
  133. //检查对角线
  134. } else if (!squares[0].getText().equals("")
  135. && squares[0].getText().equals(squares[4].getText())
  136. && squares[0].getText().equals(squares[8].getText())) {
  137. theWinner = squares[0].getText();
  138. highlightWinner(0, 4, 8);
  139. } else if (!squares[2].getText().equals("")
  140. && squares[2].getText().equals(squares[4].getText())
  141. && squares[2].getText().equals(squares[6].getText())) {
  142. theWinner = squares[2].getText();
  143. highlightWinner(2, 4, 6);
  144. }
  145. return theWinner;
  146. }
  147. /**
  148. * 这个方法负责用设定好的规则去找出最合适计算机的一步,如果没有找到,就随机选一个
  149. */
  150. void computerMove() {
  151. int selectedSquare;
  152. // 第一步,试图找到同一条线上是否存在一个空方格连着两个'O',如果有就意味着你输了
  153. selectedSquare = findEmptySquare("O");
  154. // 第二步,如果不存在连个相连的'O',,则找是否有空格之间存在两个连着的'X',有的话就计算机就要去阻止它了。
  155. if (selectedSquare == -1)
  156. selectedSquare = findEmptySquare("X");
  157. //第三步,如果都没有,则看看中间是不是空的,是的话就填这个吧。
  158. if ((selectedSquare == -1) && (squares[4].getText().equals("")))
  159. selectedSquare = 4;
  160. //好吧,中间已经被占领了,那就随便选一个吧。
  161. if (selectedSquare == -1)
  162. selectedSquare = getRandomSquare();
  163. squares[selectedSquare].setText("O");
  164. }
  165. /**
  166. * 这个方法会检查每行,每列,每一条对角线是否存在一个空格在两个和参数player相同符号间,如果有则返回这个空格的位置。
  167. * @param player X是玩家,O是计算机
  168. * @return 返回这个空格的位置,如果不存在,则返回-1
  169. */
  170. int findEmptySquare(String player) {
  171. int weight[] = new int[9];
  172. //'O'的格子填-1, 'X'的格子填1,其他为0
  173. for (int i = 0; i < 9; i++) {
  174. if (squares[i].getText().equals("O"))
  175. weight[i] = -1;
  176. else if (squares[i].getText().equals("X"))
  177. weight[i] = 1;
  178. else
  179. weight[i] = 0;
  180. }
  181. //如果一条线上的值为2则表示有两个'O'在这条直线上,如果有两个'X',则是-2
  182. int twoWeights = player.equals("O") ? -2 : 2;
  183. // 看看第1行是否存在空格连着两个相同符号的
  184. if (weight[0] + weight[1] + weight[2] == twoWeights) {
  185. if (weight[0] == 0)
  186. return 0;
  187. else if (weight[1] == 0)
  188. return 1;
  189. else
  190. return 2;
  191. }
  192. // 看看第2行
  193. if (weight[3] + weight[4] + weight[5] == twoWeights) {
  194. if (weight[3] == 0)
  195. return 3;
  196. else if (weight[4] == 0)
  197. return 4;
  198. else
  199. return 5;
  200. }
  201. // 看看第3行
  202. if (weight[6] + weight[7] + weight[8] == twoWeights) {
  203. if (weight[6] == 0)
  204. return 6;
  205. else if (weight[7] == 0)
  206. return 7;
  207. else
  208. return 8;
  209. }
  210. // 看看第1列
  211. if (weight[0] + weight[3] + weight[6] == twoWeights) {
  212. if (weight[0] == 0)
  213. return 0;
  214. else if (weight[3] == 0)
  215. return 3;
  216. else
  217. return 6;
  218. }
  219. // 看看第2列
  220. if (weight[1] + weight[4] + weight[7] == twoWeights) {
  221. if (weight[1] == 0)
  222. return 1;
  223. else if (weight[4] == 0)
  224. return 4;
  225. else
  226. return 7;
  227. }
  228. // 看看第3列
  229. if (weight[2] + weight[5] + weight[8] == twoWeights) {
  230. if (weight[2] == 0)
  231. return 2;
  232. else if (weight[5] == 0)
  233. return 5;
  234. else
  235. return 8;
  236. }
  237. //看看左上角到右下角的对角线
  238. if (weight[0] + weight[4] + weight[8] == twoWeights) {
  239. if (weight[0] == 0)
  240. return 0;
  241. else if (weight[4] == 0)
  242. return 4;
  243. else
  244. return 8;
  245. }
  246. //看看右上角到左下角的对角线
  247. if (weight[2] + weight[4] + weight[6] == twoWeights) {
  248. if (weight[2] == 0)
  249. return 2;
  250. else if (weight[4] == 0)
  251. return 4;
  252. else
  253. return 6;
  254. }
  255. // 没有就返回-1
  256. return -1;
  257. }
  258. /**
  259. * 随机产生0到9之间的数字, 而且这个数字所对应的方格还必须是空的
  260. */
  261. int getRandomSquare() {
  262. boolean gotEmptySquare = false;
  263. int selectedSquare = -1;
  264. do {
  265. selectedSquare = (int) (Math.random() * 9);
  266. if (squares[selectedSquare].getText().equals("")) {
  267. gotEmptySquare = true; // 循环结束
  268. }
  269. } while (!gotEmptySquare);
  270. return selectedSquare;
  271. }
  272. /**
  273. * 这个方法负责高亮显示赢的那条路径
  274. */
  275. void highlightWinner(int win1, int win2, int win3) {
  276. squares[win1].setBackground(Color.CYAN);
  277. squares[win2].setBackground(Color.CYAN);
  278. squares[win3].setBackground(Color.CYAN);
  279. }
  280. /**
  281. * 禁用方格的所有按钮(点击无效),让新游戏的按钮生效(可以点击)
  282. */
  283. void endTheGame() {
  284. for (int i = 0; i < 9; i++) {
  285. squares[i].setEnabled(false);
  286. }
  287. newGameButton.setEnabled(true);
  288. }
  289. }

恭喜你!你已经完成了你的第一个Java游戏!

你可以直接在Eclipse上运行这个applet,或者通过浏览器打开我们在章节一开始就提到的TicTacToe.html文件,只需要把HTML文件和TicTacToe.class拷贝到同一个文件夹下面就可以了。我们的TicTacToe类还有一个小bug(或许你已经注意到了),但是我肯定在完成接下来的任务之后你能够解决它。

我们的TicTacToe类只是使用了简单的策略,因为我们的目标只是为了学习如何编程,但是如果你想要完善这个游戏,可以去学习极小极大策略去让计算机选择最佳的步骤。极小极大策略不在这本书的讨论范围内,你可以在网上找到它的相关资料。

5. 扩展阅读

  1. Java Applets:
    http://java.sun.com/docs/books/tutorial/applet/

  2. Java 类 Math:
    http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Math.html

6. 练习

  1. 在类TicTacToe的顶部面板上添加两个标签去显示输和赢的数量。声明两个类变量去存储输和赢的次数,每输或者赢一次就相应地加1。每赢一次或者输一次都要立刻刷新标签的显示。

  2. 我们的程序现在是允许在已经有X或者O的格子上点击。这是一个bug! 如果你做了一次有效的移动程序还是会继续运行的。修改代码,让它忽略掉这些点击。

  3. TicTacToe添加一个main()方法,让它作为一个普通的Java 应用程序运行,而不是applet

7. 进一步的练习

重写TicTacToe,用一个二维的3 × 3数组:

  1. JButton squares[][]

来替换原来的一维数组:

  1. JButton squares[]

去存储中间的九个格子按钮。

关于多维数组的内容可以上网了解更多。


創用 CC 授權條款
本著作係採用創用 CC 姓名標示-非商業性-禁止改作 2.5 中國大陸 授權條款授權.


[1] Eclipse上运行applet实际上时调用了JDK工具包中的appletviewer,在Eclipse上可以通过Run as -> Java Applet来执行。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注